@starklab/stark-mcp 0.2.0 → 0.3.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.
- package/README.md +42 -4
- package/package.json +12 -1
- package/src/adopt/a11yPass.js +397 -0
- package/src/adopt/adoptGate.js +471 -0
- package/src/adopt/adoptScanReport.js +9 -0
- package/src/adopt/componentPropApi.js +200 -0
- package/src/adopt/findingSnippet.js +386 -0
- package/src/adopt/foreignDiscoveryResolver.js +20 -3
- package/src/adopt/foreignScoringResolver.js +163 -22
- package/src/adopt/moduleGraph.js +44 -2
- package/src/adopt/prCheckReport.js +274 -0
- package/src/adopt/propApiResolver.js +2 -2
- package/src/adopt/referenceResolver.js +2 -2
- package/src/adopt/scanRollup.js +192 -0
- package/src/adopt/tailwindResolver.js +6 -1
- package/src/adopt/targetDiscovery.js +31 -3
- package/src/adopt/tokenAliasResolver.js +45 -2
- package/src/adopt/usageRulesResolver.js +299 -14
- package/src/adopt/vecnaMaterializer.js +45 -5
- package/src/adopt/vecnaVerifier.js +20 -9
- package/src/adopt/wrapperResolver.js +3 -3
- package/src/cli.js +343 -8
- package/src/data.js +105 -11
- package/src/server.js +69 -14
- package/src/whisperer.d.ts +106 -0
- package/src/whisperer.js +814 -0
package/src/whisperer.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whisperer — the design-system facts an answer is allowed to be built on,
|
|
3
|
+
* and the instructions they are handed to a model under.
|
|
4
|
+
*
|
|
5
|
+
* Whisperer answers questions about *this* design system, so the model is
|
|
6
|
+
* never asked to recall one. Everything it may assert is assembled here from
|
|
7
|
+
* the same generated files the rest of the toolchain validates against. This
|
|
8
|
+
* module is pure: it takes those files as data and returns the grounding
|
|
9
|
+
* functions, so the one code path serves every surface — the MCP tool and the
|
|
10
|
+
* CLI load the files from the installed `@starklab/stk` (see data.js), and
|
|
11
|
+
* Dominion's chat panel hands in the JSON its bundler already carries.
|
|
12
|
+
*
|
|
13
|
+
* - `catalog.json` — *which symbols exist*, on which platform, and
|
|
14
|
+
* what kind each one is. This is the existence
|
|
15
|
+
* authority; the manifest is built from it too,
|
|
16
|
+
* and the union below is kept so that a component
|
|
17
|
+
* the manifest has not yet been regenerated for
|
|
18
|
+
* still exists here.
|
|
19
|
+
* - `manifest.json` — the prose usage docs (dos/donts), per platform:
|
|
20
|
+
* `usage` is the web file, and `nativeUsage` the
|
|
21
|
+
* React Native one, which is its own document —
|
|
22
|
+
* its own rules, a typed prop table and a code
|
|
23
|
+
* example — because the RN port's props differ
|
|
24
|
+
* from web's for most components.
|
|
25
|
+
* - `component-props.json` — the *real* React signatures, generated from the
|
|
26
|
+
* component sources; the prop mapping files are a
|
|
27
|
+
* Figma contract and drift from the signature on
|
|
28
|
+
* purpose.
|
|
29
|
+
* - `json-meta` — every token: its resolved value, the layer its
|
|
30
|
+
* source file sits in, the token it aliases, the
|
|
31
|
+
* sentence written beside it and the name React
|
|
32
|
+
* Native exports it under.
|
|
33
|
+
* - `json-dark` — the value a token takes in dark mode, for the
|
|
34
|
+
* tokens the dark overlay redefines.
|
|
35
|
+
*
|
|
36
|
+
* Three rules hold this together, and all three are load-bearing:
|
|
37
|
+
*
|
|
38
|
+
* 1. **The full catalog always goes in, the detail never does.** Every name,
|
|
39
|
+
* its one-line description and its prop names cost ~20 KB and are what
|
|
40
|
+
* stop the model inventing a component or a prop that does not exist — or
|
|
41
|
+
* denying one that does. The per-component detail — usage, props, tokens
|
|
42
|
+
* — averages 4.5 KB each, so only the components the conversation is
|
|
43
|
+
* about are expanded — the ones the question names, then the most
|
|
44
|
+
* recently discussed — capped at MAX_DETAIL.
|
|
45
|
+
* 2. **The semantic layer and the scales are listed; the colour primitives
|
|
46
|
+
* are not.** The layer comes from the token's source directory, not from
|
|
47
|
+
* a list of family names kept here — a family added to `tokens/semantic/`
|
|
48
|
+
* is in the next build's list without anyone remembering this file (the
|
|
49
|
+
* chart palette was missing for exactly that reason). The 192
|
|
50
|
+
* `color-shade-*` primitives are deliberately withheld: CLAUDE.md's layer
|
|
51
|
+
* hierarchy says a component token must alias a semantic token and never
|
|
52
|
+
* a primitive, and a list the model can read from is a list it will
|
|
53
|
+
* suggest from. The twelve `color-brand-palette-*` primitives are the
|
|
54
|
+
* exception, listed under their own heading with the same warning,
|
|
55
|
+
* because "what is the exact brand colour?" deserves its real answer.
|
|
56
|
+
* Every primitive is still *resolved* when a question names it outright,
|
|
57
|
+
* so "what is `--stk-color-shade-palette-1-8`?" gets one too.
|
|
58
|
+
* 3. **Nothing here reads the questioner's code.** This module is the design
|
|
59
|
+
* system, not the workspace; what a consumer knows about its own code is
|
|
60
|
+
* its own business (Dominion's Whisperer knows nothing of it, by decision
|
|
61
|
+
* #23; an editor agent holding the MCP tool knows all of it).
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {{ dos?: string[], donts?: string[], codeExample?: string }} UsageDoc
|
|
66
|
+
*
|
|
67
|
+
* @typedef {object} NativeUsageProp
|
|
68
|
+
* One row of a React Native usage file's prop table. The RN files enumerate
|
|
69
|
+
* their props (the web files do not — web's values come from the mapping
|
|
70
|
+
* file), and `type` there is the TypeScript-ish shape the author wrote:
|
|
71
|
+
* `'sm' | 'md' | 'lg'`, `ReactNode`, `(id: string) => void`.
|
|
72
|
+
* @property {string} name
|
|
73
|
+
* @property {string} type
|
|
74
|
+
* @property {string} [default]
|
|
75
|
+
* @property {boolean | string} [required] `true`, or the condition under which it is required.
|
|
76
|
+
* @property {string} [description]
|
|
77
|
+
*
|
|
78
|
+
* @typedef {UsageDoc & { props?: NativeUsageProp[] }} NativeUsageDoc
|
|
79
|
+
*
|
|
80
|
+
* @typedef {object} PropMapEntry
|
|
81
|
+
* One entry of a prop-mapping file's `propMap`, as the manifest republishes
|
|
82
|
+
* it. `values` is the whole point: for an enum it is every value the prop
|
|
83
|
+
* accepts, and it exists nowhere else the grounding reads — the React
|
|
84
|
+
* signature only carries a name and a default.
|
|
85
|
+
* @property {string} react
|
|
86
|
+
* @property {string} [type]
|
|
87
|
+
* @property {Record<string, string>} [values]
|
|
88
|
+
* @property {string} [transform] `presence`: the Figma boolean is derived from a React node being passed.
|
|
89
|
+
* @property {string} [description]
|
|
90
|
+
* @property {boolean} [componentProp] `false` when the Figma property has no React prop of that name.
|
|
91
|
+
*
|
|
92
|
+
* @typedef {object} ManifestComponent
|
|
93
|
+
* @property {string} name
|
|
94
|
+
* @property {string} slug
|
|
95
|
+
* @property {string} status
|
|
96
|
+
* @property {string} description
|
|
97
|
+
* @property {string[]} [platforms]
|
|
98
|
+
* @property {UsageDoc | null} [usage]
|
|
99
|
+
* @property {NativeUsageDoc | null} [nativeUsage]
|
|
100
|
+
* @property {{ web?: { propMap?: PropMapEntry[] } | null } | null} [props]
|
|
101
|
+
* @property {Record<string, unknown> | null} [tokens]
|
|
102
|
+
*
|
|
103
|
+
* @typedef {{ name: string, hasDefault?: boolean, defaultSource?: string, rest?: boolean }} PropEntry
|
|
104
|
+
* @typedef {{ package?: string, components?: Record<string, { props?: PropEntry[] }> }} SignaturePlatform
|
|
105
|
+
*
|
|
106
|
+
* @typedef {object} TokenMeta
|
|
107
|
+
* @property {unknown} value
|
|
108
|
+
* @property {string} type
|
|
109
|
+
* @property {'primitive' | 'semantic' | 'component' | null} layer
|
|
110
|
+
* @property {string | null} alias
|
|
111
|
+
* @property {string | null} description
|
|
112
|
+
* @property {{ name: string, module: string } | null} rn
|
|
113
|
+
*
|
|
114
|
+
* @typedef {object} WhispererData
|
|
115
|
+
* @property {{ components: ManifestComponent[] }} manifest
|
|
116
|
+
* @property {{ platforms: Record<string, { exports: Record<string, { kind: string }> }> }} catalog
|
|
117
|
+
* @property {{ platforms: { web?: SignaturePlatform, native?: SignaturePlatform } }} componentProps
|
|
118
|
+
* @property {Record<string, TokenMeta>} tokenMeta
|
|
119
|
+
* @property {Record<string, unknown>} tokenDark
|
|
120
|
+
*
|
|
121
|
+
* @typedef {object} Known
|
|
122
|
+
* @property {string} name
|
|
123
|
+
* @property {ManifestComponent | null} doc
|
|
124
|
+
* @property {string} kind
|
|
125
|
+
* @property {string} status
|
|
126
|
+
* @property {string[]} platforms
|
|
127
|
+
* @property {string | null} description
|
|
128
|
+
*
|
|
129
|
+
* @typedef {object} Grounding
|
|
130
|
+
* @property {string} text The grounding block, ready to sit under the system prompt.
|
|
131
|
+
* @property {string[]} components The components whose full detail the block carries.
|
|
132
|
+
* @property {string[]} named The subset the question itself named.
|
|
133
|
+
* @property {string[]} tokens The tokens the question named outright.
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
/** Kinds a question can legitimately be about. `hook` is not one of them. */
|
|
137
|
+
const LISTED_KINDS = new Set(['component', 'primitive', 'subpart']);
|
|
138
|
+
|
|
139
|
+
/** At most this many components get their full detail block. */
|
|
140
|
+
const MAX_DETAIL = 4;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A token value as it is written in an answer: a string as-is, a composite
|
|
144
|
+
* (the text styles) as its parts.
|
|
145
|
+
* @param {unknown} v
|
|
146
|
+
*/
|
|
147
|
+
function valueText(v) {
|
|
148
|
+
if (v && typeof v === 'object') {
|
|
149
|
+
return Object.entries(v)
|
|
150
|
+
.map(([k, x]) => `${k} ${String(x)}`)
|
|
151
|
+
.join(', ');
|
|
152
|
+
}
|
|
153
|
+
return String(v);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The first sentence of a description, for the always-on list. A few of them
|
|
158
|
+
* run to a paragraph — the chart palette explains its CVD checks,
|
|
159
|
+
* `text.subtle` its contrast ratios — and that text is worth having, but
|
|
160
|
+
* once, next to the token when a question names it, not on every turn.
|
|
161
|
+
* @param {string} description
|
|
162
|
+
*/
|
|
163
|
+
function brief(description) {
|
|
164
|
+
const first = description.split(/(?<=\.)\s+/)[0] ?? description;
|
|
165
|
+
return first.length > 140 ? `${first.slice(0, 137).trimEnd()}…` : first;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {string | undefined} ch */
|
|
169
|
+
const isWordChar = (ch) => !!ch && /[a-z0-9]/.test(ch);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Where a name first appears in the text as a whole word, or -1. Every
|
|
173
|
+
* occurrence is tried rather than only the first, because the first is often
|
|
174
|
+
* the one inside a longer name: in "SelectionCard and Card", `Card`'s first
|
|
175
|
+
* hit sits inside `SelectionCard` and is rejected, and stopping there would
|
|
176
|
+
* lose the standalone mention that follows it.
|
|
177
|
+
* @param {string} hay
|
|
178
|
+
* @param {string} needle
|
|
179
|
+
*/
|
|
180
|
+
function wordIndexOf(hay, needle) {
|
|
181
|
+
let at = hay.indexOf(needle);
|
|
182
|
+
while (at !== -1) {
|
|
183
|
+
if (!isWordChar(hay[at - 1]) && !isWordChar(hay[at + needle.length])) return at;
|
|
184
|
+
at = hay.indexOf(needle, at + 1);
|
|
185
|
+
}
|
|
186
|
+
return -1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Whether anything placed between the component's tags is rendered. Derived
|
|
191
|
+
* from the signature, never assumed: 8 of the 75 web components accept
|
|
192
|
+
* `children`, and the other 67 render nothing from it — a
|
|
193
|
+
* `<Button>Save</Button>` is an empty button, because the text goes through
|
|
194
|
+
* `label`.
|
|
195
|
+
* @param {string} name
|
|
196
|
+
* @param {PropEntry[]} props
|
|
197
|
+
*/
|
|
198
|
+
function childrenRule(name, props) {
|
|
199
|
+
if (props.some((p) => p.name === 'children')) {
|
|
200
|
+
return `${name} accepts \`children\`: content between its tags is rendered.`;
|
|
201
|
+
}
|
|
202
|
+
const slots = props
|
|
203
|
+
.filter((p) => /^(label|title|text|content|description|value)$/.test(p.name))
|
|
204
|
+
.map((p) => `\`${p.name}\``);
|
|
205
|
+
const via = slots.length ? ` Its text goes through ${slots.join(', ')}.` : '';
|
|
206
|
+
return (
|
|
207
|
+
`${name} does not accept \`children\`: anything placed between its tags is not rendered, ` +
|
|
208
|
+
`and no prop outside the list above exists.${via}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The grounding functions, built over one set of generated files.
|
|
214
|
+
* @param {WhispererData} data
|
|
215
|
+
*/
|
|
216
|
+
export function createWhisperer({ manifest, catalog, componentProps, tokenMeta, tokenDark }) {
|
|
217
|
+
const DOCUMENTED = manifest.components;
|
|
218
|
+
const META = tokenMeta;
|
|
219
|
+
const DARK = tokenDark;
|
|
220
|
+
|
|
221
|
+
/** @param {string} platform */
|
|
222
|
+
function catalogNames(platform) {
|
|
223
|
+
const exports = catalog.platforms[platform]?.exports ?? {};
|
|
224
|
+
return new Set(
|
|
225
|
+
Object.entries(exports)
|
|
226
|
+
.filter(([, entry]) => LISTED_KINDS.has(entry?.kind))
|
|
227
|
+
.map(([name]) => name),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Everything a question may name, and what is known about it. Names are
|
|
233
|
+
* the union of the catalog and the manifest, and so are the platforms: the
|
|
234
|
+
* catalog says where a component is exported, the manifest repeats the
|
|
235
|
+
* mapping file's claim, and a mapping file that says `web` for a component
|
|
236
|
+
* the RN barrel also exports (DropdownMenu, once) must not turn into
|
|
237
|
+
* "web-only" in the index — that tag is what the answer to "does it exist
|
|
238
|
+
* on React Native?" is read from.
|
|
239
|
+
* @type {Known[]}
|
|
240
|
+
*/
|
|
241
|
+
const KNOWN = (() => {
|
|
242
|
+
const web = catalogNames('web');
|
|
243
|
+
const rn = catalogNames('rn');
|
|
244
|
+
const documented = new Map(DOCUMENTED.map((c) => [c.name, c]));
|
|
245
|
+
const names = [...new Set([...web, ...rn, ...documented.keys()])].sort();
|
|
246
|
+
return names.map((name) => {
|
|
247
|
+
const doc = documented.get(name) ?? null;
|
|
248
|
+
const declared = new Set(doc?.platforms ?? []);
|
|
249
|
+
const platforms = /** @type {string[]} */ (
|
|
250
|
+
[
|
|
251
|
+
web.has(name) || declared.has('web') ? 'web' : null,
|
|
252
|
+
rn.has(name) || declared.has('native') ? 'native' : null,
|
|
253
|
+
].filter(Boolean)
|
|
254
|
+
);
|
|
255
|
+
const kind =
|
|
256
|
+
catalog.platforms.web?.exports?.[name]?.kind ??
|
|
257
|
+
catalog.platforms.rn?.exports?.[name]?.kind ??
|
|
258
|
+
'component';
|
|
259
|
+
// The manifest writes `status: "unknown"` for the layout primitives,
|
|
260
|
+
// which is a fact about the manifest, not about the component. Say what
|
|
261
|
+
// it is instead of quoting a status nobody set.
|
|
262
|
+
const status = doc?.status && doc.status !== 'unknown' ? doc.status : kind;
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
doc,
|
|
266
|
+
kind,
|
|
267
|
+
status,
|
|
268
|
+
platforms: platforms.length ? platforms : ['web'],
|
|
269
|
+
description: doc?.description ?? null,
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
})();
|
|
273
|
+
|
|
274
|
+
const SIGNATURES = componentProps.platforms;
|
|
275
|
+
const REACT_PROPS = SIGNATURES.web?.components ?? {};
|
|
276
|
+
const NATIVE_PROPS = SIGNATURES.native?.components ?? {};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Where each platform's components are imported from. Read from the
|
|
280
|
+
* signature file, which records the package it was generated from, because
|
|
281
|
+
* the manifest's own `package` field names the token package — and an
|
|
282
|
+
* import line is the one fact a model will most confidently make up when
|
|
283
|
+
* it is not in front of it.
|
|
284
|
+
*/
|
|
285
|
+
const PACKAGES = {
|
|
286
|
+
web: SIGNATURES.web?.package ?? null,
|
|
287
|
+
native: SIGNATURES.native?.package ?? null,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** Every token, the same order the build writes them in. */
|
|
291
|
+
const TOKEN_NAMES = Object.keys(META);
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The tokens the always-on list carries: the whole semantic layer and every
|
|
295
|
+
* primitive scale that is not a colour — spacing, radius, type, opacity,
|
|
296
|
+
* motion, elevation, breakpoints. The colour primitives are the shades
|
|
297
|
+
* (withheld, rule 2 above) and the brand palette (listed apart). Component
|
|
298
|
+
* tokens reach the model through their own component's detail block.
|
|
299
|
+
* @param {string} name
|
|
300
|
+
*/
|
|
301
|
+
function inScaleList(name) {
|
|
302
|
+
const t = META[name];
|
|
303
|
+
return t.layer === 'semantic' || (t.layer === 'primitive' && t.type !== 'color');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @param {string} name */
|
|
307
|
+
function isBrandPrimitive(name) {
|
|
308
|
+
return name.startsWith('stk-color-brand-');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* One token, one line. Always: the name, the light value, the dark value
|
|
313
|
+
* when the dark overlay changes it, and `→` the token it aliases — that
|
|
314
|
+
* arrow is what answers "what does the button's background *really* point
|
|
315
|
+
* at". The description follows. `full` gives the whole description and the
|
|
316
|
+
* React Native export; the list gives the first sentence and leaves RN to
|
|
317
|
+
* the rule under its heading.
|
|
318
|
+
* @param {string} name
|
|
319
|
+
* @param {boolean} [full]
|
|
320
|
+
*/
|
|
321
|
+
function tokenLine(name, full = false) {
|
|
322
|
+
const t = META[name];
|
|
323
|
+
let line = `--${name}: ${valueText(t.value)}`;
|
|
324
|
+
if (DARK[name] !== undefined && valueText(DARK[name]) !== valueText(t.value)) {
|
|
325
|
+
line += ` (dark: ${valueText(DARK[name])})`;
|
|
326
|
+
}
|
|
327
|
+
if (t.alias) line += ` → --${t.alias}`;
|
|
328
|
+
if (t.description) line += ` — ${full ? t.description : brief(t.description)}`;
|
|
329
|
+
if (full) {
|
|
330
|
+
line += t.rn
|
|
331
|
+
? `\n React Native: \`${t.rn.name}\` from \`${t.rn.module}\``
|
|
332
|
+
: '\n React Native: not exported — web only';
|
|
333
|
+
}
|
|
334
|
+
return line;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @type {string | null} */
|
|
338
|
+
let catalogCache = null;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Every component, one line each — the answer to "does this exist?" — and,
|
|
342
|
+
* under it, the names of its web props. The names cost ~9 KB for the whole
|
|
343
|
+
* catalog and buy the closed list for components an answer *composes with*
|
|
344
|
+
* rather than asks about: a question about `Card` expands `Card` alone, and
|
|
345
|
+
* the example then reaches for `Stack` — which, without this line, got a
|
|
346
|
+
* `padding` prop it has never had. Values stay in the detail blocks.
|
|
347
|
+
*/
|
|
348
|
+
function catalogIndex() {
|
|
349
|
+
if (catalogCache) return catalogCache;
|
|
350
|
+
catalogCache = KNOWN.map((k) => {
|
|
351
|
+
const description = k.description ?? 'Exported and usable; no description reached this index.';
|
|
352
|
+
const names = (REACT_PROPS[k.name]?.props ?? []).filter((p) => !p.rest).map((p) => p.name);
|
|
353
|
+
const props = names.length ? `\n props: ${names.join(', ')}` : '';
|
|
354
|
+
return `${k.name} [${k.status}, ${k.platforms.join('/')}] — ${description}${props}`;
|
|
355
|
+
}).join('\n');
|
|
356
|
+
return catalogCache;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @type {string | null} */
|
|
360
|
+
let tokenListCache = null;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The always-on token list, with the rules for reading it said once at the
|
|
364
|
+
* top rather than on every line: what the arrow means, what dark means, and
|
|
365
|
+
* how a name becomes its React Native export. The RN rule is stated instead
|
|
366
|
+
* of listed because it holds for every line here — the exceptions are the
|
|
367
|
+
* colour primitives, which are not exported on RN and are not in this list.
|
|
368
|
+
*/
|
|
369
|
+
function tokenList() {
|
|
370
|
+
if (tokenListCache) return tokenListCache;
|
|
371
|
+
const scale = TOKEN_NAMES.filter(inScaleList).map((n) => tokenLine(n));
|
|
372
|
+
const brand = TOKEN_NAMES.filter(isBrandPrimitive).map((n) => tokenLine(n));
|
|
373
|
+
tokenListCache = [
|
|
374
|
+
'How to read a line: `--name: light value (dark: value in dark mode) → the token it aliases — what it is for`.',
|
|
375
|
+
'A token with no `(dark: …)` keeps its light value in dark mode. A token with no `→` holds a raw value: it is',
|
|
376
|
+
'the end of the chain. Component tokens (listed under their component) alias these; these alias the primitives.',
|
|
377
|
+
'React Native exports every token below under its camelCase name, from `@starklab/stk/rn`:',
|
|
378
|
+
'`--stk-border-default` is `stkBorderDefault`, `--stk-spacing-md` is `stkSpacingMd`. Easing tokens come from',
|
|
379
|
+
'`@starklab/stk/rn-easing` instead, and the per-platform spacing, type-size and shadow steps are folded into one',
|
|
380
|
+
'Platform.select() export each (`stkSpacingMd` from `@starklab/stk/rn-spacing`, `stkTypographySizeMd` from',
|
|
381
|
+
'`@starklab/stk/rn-typography`, `stkShadowMd` from `@starklab/stk/rn-shadows`).',
|
|
382
|
+
'',
|
|
383
|
+
`### Semantic tokens and scales (${scale.length})`,
|
|
384
|
+
'',
|
|
385
|
+
...scale,
|
|
386
|
+
'',
|
|
387
|
+
`### Brand colour primitives (${brand.length})`,
|
|
388
|
+
'',
|
|
389
|
+
'The exact brand colours. Reference only: a component or a screen takes its colour from a semantic surface,',
|
|
390
|
+
'text, border or icon token above, never from these directly, and they are not exported on React Native.',
|
|
391
|
+
'',
|
|
392
|
+
...brand,
|
|
393
|
+
].join('\n');
|
|
394
|
+
return tokenListCache;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Which components the question names, in the order it names them. Matched
|
|
399
|
+
* on word boundaries against the catalog's own names, so "card" hits `Card`
|
|
400
|
+
* but "discard" does not.
|
|
401
|
+
* @param {string} text
|
|
402
|
+
* @returns {{ entry: Known, at: number }[]}
|
|
403
|
+
*/
|
|
404
|
+
function namedInQuestion(text) {
|
|
405
|
+
const hay = text.toLowerCase();
|
|
406
|
+
return KNOWN.map((entry) => ({ entry, at: wordIndexOf(hay, entry.name.toLowerCase()) }))
|
|
407
|
+
.filter((hit) => hit.at !== -1)
|
|
408
|
+
.sort((a, b) => a.at - b.at);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The components whose full detail block goes into the grounding. Longer
|
|
413
|
+
* names win first (`DataTable` before `Table`-like substrings) because a
|
|
414
|
+
* longer name is the more specific thing to expand when the cap bites.
|
|
415
|
+
* @param {string} text
|
|
416
|
+
*/
|
|
417
|
+
function componentsInQuestion(text) {
|
|
418
|
+
const hits = namedInQuestion(text).map((h) => h.entry);
|
|
419
|
+
hits.sort((a, b) => b.name.length - a.name.length);
|
|
420
|
+
return hits.slice(0, MAX_DETAIL);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The question's own components first, then — while there is room under
|
|
425
|
+
* MAX_DETAIL — the ones the conversation was most recently about. A
|
|
426
|
+
* follow-up rarely repeats the name: "tell me the sizes" names nothing, and
|
|
427
|
+
* grounded on itself alone it carried no `Button` block at all, so the
|
|
428
|
+
* answer to it came from the model's memory of some other design system.
|
|
429
|
+
* `context` is the prior turns, newest first; the newest mention wins the
|
|
430
|
+
* last slot.
|
|
431
|
+
* @param {string} question
|
|
432
|
+
* @param {string[]} context
|
|
433
|
+
*/
|
|
434
|
+
function componentsToExpand(question, context) {
|
|
435
|
+
const picked = componentsInQuestion(question);
|
|
436
|
+
for (const text of context) {
|
|
437
|
+
if (picked.length >= MAX_DETAIL) break;
|
|
438
|
+
for (const hit of componentsInQuestion(text)) {
|
|
439
|
+
if (picked.length >= MAX_DETAIL) break;
|
|
440
|
+
if (!picked.includes(hit)) picked.push(hit);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return picked;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The same matches, in reading order and uncapped — what "which component
|
|
448
|
+
* is this question about?" means to a reader, and what a consumer that
|
|
449
|
+
* counts asks per component classifies against. Exposed so such a count is
|
|
450
|
+
* built from the same catalog the answer was grounded in, never from a
|
|
451
|
+
* second list.
|
|
452
|
+
* @param {string} text
|
|
453
|
+
* @returns {{ name: string, platforms: string[] }[]}
|
|
454
|
+
*/
|
|
455
|
+
function componentsNamedIn(text) {
|
|
456
|
+
return namedInQuestion(text).map((h) => ({ name: h.entry.name, platforms: h.entry.platforms }));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Every component name the catalog knows — the closed set, for validation.
|
|
461
|
+
* @param {string} name
|
|
462
|
+
*/
|
|
463
|
+
function isKnownComponent(name) {
|
|
464
|
+
return KNOWN.some((k) => k.name === name);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Whether a token name (without its leading `--`) exists in the token build.
|
|
469
|
+
* @param {string} name
|
|
470
|
+
*/
|
|
471
|
+
function isKnownToken(name) {
|
|
472
|
+
return META[name.replace(/^--/, '').toLowerCase()] !== undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* React Native export name → token name, for questions asked in RN terms. A
|
|
477
|
+
* Platform.select() export stands for several tokens; the first wins.
|
|
478
|
+
*/
|
|
479
|
+
const RN_NAMES = (() => {
|
|
480
|
+
/** @type {Map<string, string>} */
|
|
481
|
+
const map = new Map();
|
|
482
|
+
for (const name of TOKEN_NAMES) {
|
|
483
|
+
const rn = META[name].rn;
|
|
484
|
+
if (rn && !map.has(rn.name)) map.set(rn.name, name);
|
|
485
|
+
}
|
|
486
|
+
return map;
|
|
487
|
+
})();
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Any token the question names outright, primitives included, in either
|
|
491
|
+
* spelling: the CSS name (`--stk-border-default`, with or without the
|
|
492
|
+
* dashes) or the React Native export (`stkBorderDefault`).
|
|
493
|
+
* @param {string} text
|
|
494
|
+
* @returns {string[]}
|
|
495
|
+
*/
|
|
496
|
+
function tokensInQuestion(text) {
|
|
497
|
+
/** @type {Set<string>} */
|
|
498
|
+
const seen = new Set();
|
|
499
|
+
for (const raw of text.match(/(?:--)?\bstk-[a-z0-9-]+/gi) ?? []) {
|
|
500
|
+
const name = raw.replace(/^--/, '').toLowerCase();
|
|
501
|
+
if (META[name] !== undefined) seen.add(name);
|
|
502
|
+
}
|
|
503
|
+
for (const raw of text.match(/\bstk[A-Z][A-Za-z0-9]*/g) ?? []) {
|
|
504
|
+
const name = RN_NAMES.get(raw);
|
|
505
|
+
if (name) seen.add(name);
|
|
506
|
+
}
|
|
507
|
+
return [...seen];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* One line per prop, and every enum's values on it. The list itself comes
|
|
512
|
+
* from the React signature — that is the closed set, and a prop the source
|
|
513
|
+
* never destructures is not a prop however the mapping file describes it.
|
|
514
|
+
* The mapping file adds what the signature cannot say: the values an enum
|
|
515
|
+
* accepts, whether a prop is a slot, and the sentence the author wrote
|
|
516
|
+
* about it.
|
|
517
|
+
*
|
|
518
|
+
* The values matter more than anything else here. "Button has 3 sizes" is
|
|
519
|
+
* in the usage prose; `sm | md | lg` is in the mapping file and nowhere
|
|
520
|
+
* else, and a model told the count but not the names answers "what sizes
|
|
521
|
+
* are there?" with a guess dressed as a fact.
|
|
522
|
+
* @param {Known} k
|
|
523
|
+
* @param {PropEntry[]} props
|
|
524
|
+
*/
|
|
525
|
+
function propLines(k, props) {
|
|
526
|
+
/** @type {Map<string, PropMapEntry>} */
|
|
527
|
+
const mapped = new Map();
|
|
528
|
+
for (const entry of k.doc?.props?.web?.propMap ?? []) {
|
|
529
|
+
if (entry.componentProp !== false) mapped.set(entry.react, entry);
|
|
530
|
+
}
|
|
531
|
+
const lines = props
|
|
532
|
+
.filter((p) => !p.rest)
|
|
533
|
+
.map((p) => {
|
|
534
|
+
const m = mapped.get(p.name);
|
|
535
|
+
const values = m?.values ? Object.keys(m.values) : [];
|
|
536
|
+
const isBool = values.length === 2 && values.includes('true') && values.includes('false');
|
|
537
|
+
let shape;
|
|
538
|
+
if (values.length && !isBool) shape = values.join(' | ');
|
|
539
|
+
else if (m?.type === 'instance' || m?.transform === 'presence') shape = 'a React element (slot)';
|
|
540
|
+
else if (m?.type === 'function') shape = 'callback';
|
|
541
|
+
else if (m?.type === 'boolean' || isBool) shape = 'boolean';
|
|
542
|
+
else if (m?.type === 'text') shape = 'text';
|
|
543
|
+
else shape = '';
|
|
544
|
+
const dflt = p.hasDefault ? `default ${p.defaultSource}` : 'no default';
|
|
545
|
+
const note = m?.description ? ` — ${m.description}` : '';
|
|
546
|
+
return `- ${p.name}${shape ? `: ${shape}` : ''} (${dflt})${note}`;
|
|
547
|
+
});
|
|
548
|
+
if (props.some((p) => p.rest)) {
|
|
549
|
+
lines.push(
|
|
550
|
+
'- …rest: any other attribute (aria-*, data-*, id, event handlers) is passed through to the underlying element',
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return lines.join('\n');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The React Native half of a component's detail, or the one line that says
|
|
558
|
+
* there is none. The closed list is the RN signature (component-props.json,
|
|
559
|
+
* generated from `packages/stk-react-native/src`); the RN usage file adds
|
|
560
|
+
* what a signature cannot say — the type, whether it is required, and the
|
|
561
|
+
* sentence the author wrote — the way the mapping file does for web. Then
|
|
562
|
+
* the RN children rule, the RN Do/Don't and the RN code example, each its
|
|
563
|
+
* own set: `Button` on RN has no `href`, `Card` has `onPress`, and a rule
|
|
564
|
+
* written for the DOM ("pair it with a focus ring") is not a rule on a
|
|
565
|
+
* phone.
|
|
566
|
+
* @param {Known} k
|
|
567
|
+
* @returns {string[]}
|
|
568
|
+
*/
|
|
569
|
+
function nativeSection(k) {
|
|
570
|
+
/** @type {string[]} */
|
|
571
|
+
const parts = [];
|
|
572
|
+
const sig = NATIVE_PROPS[k.name]?.props ?? [];
|
|
573
|
+
const nu = k.doc?.nativeUsage ?? null;
|
|
574
|
+
if (!k.platforms.includes('native') && !sig.length && !nu) {
|
|
575
|
+
parts.push(`${k.name} has no React Native version: it exists on web only.`);
|
|
576
|
+
return parts;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** @type {Map<string, NativeUsageProp>} */
|
|
580
|
+
const documented = new Map();
|
|
581
|
+
for (const p of nu?.props ?? []) documented.set(p.name, p);
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @param {string} name
|
|
585
|
+
* @param {NativeUsageProp | undefined} p
|
|
586
|
+
* @param {string | null} sigDefault
|
|
587
|
+
*/
|
|
588
|
+
const describe = (name, p, sigDefault) => {
|
|
589
|
+
const shape = p?.type ? `: ${p.type}` : '';
|
|
590
|
+
const dflt = sigDefault ?? (p?.default ? p.default : null);
|
|
591
|
+
const required =
|
|
592
|
+
p?.required === true ? 'required' : typeof p?.required === 'string' ? `required ${p.required}` : null;
|
|
593
|
+
const state = [dflt ? `default ${dflt}` : null, required].filter(Boolean).join(', ') || 'no default';
|
|
594
|
+
const note = p?.description ? ` — ${p.description}` : '';
|
|
595
|
+
return `- ${name}${shape} (${state})${note}`;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/** @type {string[]} */
|
|
599
|
+
const lines = [];
|
|
600
|
+
/** @type {Set<string>} */
|
|
601
|
+
const listed = new Set();
|
|
602
|
+
for (const p of sig) {
|
|
603
|
+
if (p.rest) continue;
|
|
604
|
+
listed.add(p.name);
|
|
605
|
+
lines.push(describe(p.name, documented.get(p.name), p.hasDefault ? (p.defaultSource ?? null) : null));
|
|
606
|
+
}
|
|
607
|
+
if (sig.some((p) => p.rest)) {
|
|
608
|
+
lines.push('- …rest: any other prop (style, testID, accessibility*) is passed through to the underlying view');
|
|
609
|
+
}
|
|
610
|
+
// What the RN usage file documents beyond the signature is a compound
|
|
611
|
+
// part's prop (`DropdownMenu.Item.label`), never a prop of the component
|
|
612
|
+
// itself.
|
|
613
|
+
const extra = [...documented.values()].filter((p) => !listed.has(p.name));
|
|
614
|
+
for (const p of extra) lines.push(describe(p.name, p, null));
|
|
615
|
+
|
|
616
|
+
if (lines.length) {
|
|
617
|
+
const differs = (REACT_PROPS[k.name]?.props ?? []).length
|
|
618
|
+
? ' (a different list from web — answer for the platform asked)'
|
|
619
|
+
: '';
|
|
620
|
+
parts.push(`React Native props — the complete list for that platform${differs}:\n${lines.join('\n')}`);
|
|
621
|
+
if (sig.length) parts.push(`On React Native, ${childrenRule(k.name, sig)}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (nu?.dos?.length) parts.push(`React Native — Do:\n${nu.dos.map((d) => `- ${d}`).join('\n')}`);
|
|
625
|
+
if (nu?.donts?.length) parts.push(`React Native — Don't:\n${nu.donts.map((d) => `- ${d}`).join('\n')}`);
|
|
626
|
+
if (nu?.codeExample) {
|
|
627
|
+
parts.push(`React Native example (from the usage docs):\n\`\`\`jsx\n${nu.codeExample}\n\`\`\``);
|
|
628
|
+
}
|
|
629
|
+
return parts;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** @param {Known} k */
|
|
633
|
+
function detailFor(k) {
|
|
634
|
+
const c = k.doc;
|
|
635
|
+
/** @type {string[]} */
|
|
636
|
+
const parts = [`### ${k.name} (${k.status})`];
|
|
637
|
+
if (k.description) parts.push(k.description);
|
|
638
|
+
else
|
|
639
|
+
parts.push(
|
|
640
|
+
'Exported and usable, but no usage guidance reached this index. Answer from its props, ' +
|
|
641
|
+
'and say that the written guidance for it is not in front of you rather than inventing any.',
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const imports = [
|
|
645
|
+
PACKAGES.web && REACT_PROPS[k.name] ? `import { ${k.name} } from '${PACKAGES.web}'` : null,
|
|
646
|
+
PACKAGES.native && NATIVE_PROPS[k.name]
|
|
647
|
+
? `import { ${k.name} } from '${PACKAGES.native}' (React Native)`
|
|
648
|
+
: null,
|
|
649
|
+
].filter(Boolean);
|
|
650
|
+
if (imports.length) parts.push(`Import:\n${imports.join('\n')}`);
|
|
651
|
+
|
|
652
|
+
const props = REACT_PROPS[k.name]?.props ?? [];
|
|
653
|
+
if (props.length) {
|
|
654
|
+
parts.push(`Web props — the complete list (from the component source):\n${propLines(k, props)}`);
|
|
655
|
+
parts.push(childrenRule(k.name, props));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (c?.usage?.codeExample) {
|
|
659
|
+
parts.push(`Web example (from the usage docs):\n\`\`\`jsx\n${c.usage.codeExample}\n\`\`\``);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// The usage files are written for layout authoring as much as for React,
|
|
663
|
+
// and a dozen of them speak in that vocabulary — `heading(title-4)`,
|
|
664
|
+
// `Stack(padding:'sm')` — which names nodes and keys the React components
|
|
665
|
+
// do not have. Said once, next to the lines that do it, rather than left
|
|
666
|
+
// for the model to reconcile against the prop list on its own.
|
|
667
|
+
if (c?.usage?.dos?.length || c?.usage?.donts?.length) {
|
|
668
|
+
parts.push(
|
|
669
|
+
'Usage guidance (written for layout authoring too: a `heading`, `text` or `spacer` it names is ' +
|
|
670
|
+
'a layout node, not a component, and a key like `padding` on another component is a layout ' +
|
|
671
|
+
'key, not a prop — the prop lists are the only source of props):',
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
if (c?.usage?.dos?.length) {
|
|
675
|
+
parts.push(`Web — Do:\n${c.usage.dos.map((d) => `- ${d}`).join('\n')}`);
|
|
676
|
+
}
|
|
677
|
+
if (c?.usage?.donts?.length) {
|
|
678
|
+
parts.push(`Web — Don't:\n${c.usage.donts.map((d) => `- ${d}`).join('\n')}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
parts.push(...nativeSection(k));
|
|
682
|
+
|
|
683
|
+
const tokenNames = TOKEN_NAMES.filter((name) =>
|
|
684
|
+
name.startsWith(`stk-${k.name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()}-`),
|
|
685
|
+
);
|
|
686
|
+
if (tokenNames.length) {
|
|
687
|
+
parts.push(
|
|
688
|
+
`Its own component tokens — each one aliases a semantic token (→), and that is the token to name when ` +
|
|
689
|
+
`asked what colour, size or radius the component uses; override the component token to change it:\n` +
|
|
690
|
+
tokenNames.map((n) => tokenLine(n, true)).join('\n'),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return parts.join('\n\n');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** @type {string | null} */
|
|
698
|
+
let packagesCache = null;
|
|
699
|
+
|
|
700
|
+
/** The one import fact per platform, so no answer has to guess a package name. */
|
|
701
|
+
function packagesSection() {
|
|
702
|
+
if (packagesCache) return packagesCache;
|
|
703
|
+
packagesCache = [
|
|
704
|
+
PACKAGES.web ? `Web (React): components are imported from \`${PACKAGES.web}\`.` : null,
|
|
705
|
+
PACKAGES.native ? `React Native: components are imported from \`${PACKAGES.native}\`.` : null,
|
|
706
|
+
]
|
|
707
|
+
.filter(Boolean)
|
|
708
|
+
.join('\n');
|
|
709
|
+
return packagesCache;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The grounding block for one question. `context` is the prior turns of the
|
|
714
|
+
* conversation, newest first, so a follow-up that names no component keeps
|
|
715
|
+
* the detail of the one it is about. Returns the text, the components it
|
|
716
|
+
* expanded, the subset the question itself named and the tokens it named,
|
|
717
|
+
* so the caller can say what the answer was built on without re-deriving
|
|
718
|
+
* it.
|
|
719
|
+
* @param {string} question
|
|
720
|
+
* @param {string[]} [context]
|
|
721
|
+
* @returns {Grounding}
|
|
722
|
+
*/
|
|
723
|
+
function buildGrounding(question, context = []) {
|
|
724
|
+
const named = componentsInQuestion(question);
|
|
725
|
+
const picked = componentsToExpand(question, context);
|
|
726
|
+
const namedTokens = tokensInQuestion(question);
|
|
727
|
+
|
|
728
|
+
const sections = [
|
|
729
|
+
`## Every component in the system (${KNOWN.length})\n\n${catalogIndex()}`,
|
|
730
|
+
`## Packages\n\n${packagesSection()}`,
|
|
731
|
+
`## Tokens\n\n${tokenList()}`,
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
if (picked.length) {
|
|
735
|
+
sections.push(`## The components this conversation is about\n\n${picked.map(detailFor).join('\n\n')}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (namedTokens.length) {
|
|
739
|
+
sections.push(
|
|
740
|
+
`## The tokens this question names\n\n${namedTokens.map((n) => tokenLine(n, true)).join('\n')}`,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
text: sections.join('\n\n'),
|
|
746
|
+
components: picked.map((k) => k.name),
|
|
747
|
+
named: named.map((k) => k.name),
|
|
748
|
+
tokens: namedTokens,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return { PACKAGES, buildGrounding, componentsNamedIn, isKnownComponent, isKnownToken, tokensInQuestion };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* The instructions the grounding is handed to the model under. Kept beside
|
|
757
|
+
* the grounding because the two only make sense together: the "every name
|
|
758
|
+
* must appear in the facts" rule is only honest while `buildGrounding` really
|
|
759
|
+
* does put the whole catalog in. These are the rules about the design system;
|
|
760
|
+
* a surface adds its own — how its panel renders, what it can and cannot see.
|
|
761
|
+
*/
|
|
762
|
+
export const WHISPERER_SYSTEM_PROMPT = [
|
|
763
|
+
'You are Whisperer, the customization assistant for the Stark design system.',
|
|
764
|
+
'You answer questions about what a component can express, which token to reach for,',
|
|
765
|
+
'and what to do when neither fits.',
|
|
766
|
+
'',
|
|
767
|
+
'Rules:',
|
|
768
|
+
'- Answer only from the design system facts below. They are generated from the',
|
|
769
|
+
' system itself and are current; your own recollection of any design system is not.',
|
|
770
|
+
'- Every component, prop and token name you write must appear in those facts.',
|
|
771
|
+
' If the thing being asked for does not exist, say so plainly and name the',
|
|
772
|
+
' closest thing that does — that gap is the most useful answer you can give.',
|
|
773
|
+
"- A component's prop list in the facts is complete. A prop that is not on it",
|
|
774
|
+
' does not exist, `children` included: when the list says a component does',
|
|
775
|
+
' not accept `children`, nothing between its tags is rendered and its text',
|
|
776
|
+
' goes through the prop the list names. Never write a code example that',
|
|
777
|
+
' passes a prop, or places content, the list does not allow.',
|
|
778
|
+
'- Where a prop lists its values, those are all of them. Name them; never',
|
|
779
|
+
' answer with a count when the values are in front of you.',
|
|
780
|
+
'- The system has two platforms, web (React) and React Native, and the tag on',
|
|
781
|
+
' each index line says where a component exists: `web/native` is both,',
|
|
782
|
+
' `web` alone means there is no React Native version — say so plainly when',
|
|
783
|
+
' asked, and name the closest thing that does exist there. A component that',
|
|
784
|
+
' exists on both has two prop lists, and they differ: answer for the platform',
|
|
785
|
+
" the question asks about, from that platform's list and that platform's",
|
|
786
|
+
" Do/Don't. When the question does not say, answer for web and say that you",
|
|
787
|
+
' did.',
|
|
788
|
+
'- Import lines come from the facts too: the package under "Packages", the',
|
|
789
|
+
' import under the component. Never write a package name from memory.',
|
|
790
|
+
'- A code example may only use components from the index, with props from',
|
|
791
|
+
' their lists — never a placeholder component, not even with a comment',
|
|
792
|
+
' saying to assume it. For plain text inside a slot, use an HTML element',
|
|
793
|
+
' and say the system has no component for it.',
|
|
794
|
+
'- If the question uses a word you do not find in the facts — a product, a',
|
|
795
|
+
' platform, a tool, a component — do not guess what it means and do not answer',
|
|
796
|
+
' around it. Say you do not recognise it and ask what it refers to.',
|
|
797
|
+
'- Tokens are three layers: primitives (shades, the spacing and radius scales),',
|
|
798
|
+
' semantic tokens that alias them (`surface.*`, `text.*`, `border.*`, `icon.*`,',
|
|
799
|
+
' `state.*`, `chart.*`), and component tokens that alias the semantic ones.',
|
|
800
|
+
' Component tokens alias semantic tokens, never primitive shades. Never suggest',
|
|
801
|
+
' a raw hex, px value or a `color-shade-*` token where a semantic one exists.',
|
|
802
|
+
' When asked what a token "is", give the chain the facts show: its value, the',
|
|
803
|
+
' token it aliases (the `→` on its line), and what it is for.',
|
|
804
|
+
"- A token's line carries its dark-mode value when it has one; a line without",
|
|
805
|
+
' `(dark: …)` keeps the same value in both themes. Say which when asked.',
|
|
806
|
+
'- On React Native a token is the camelCase export named on its line or by the',
|
|
807
|
+
' rule at the top of the token list, imported from the `@starklab/stk` module',
|
|
808
|
+
' the line names — never a CSS custom property. Colour primitives have no',
|
|
809
|
+
' React Native export.',
|
|
810
|
+
'- Chart colours are the `chart.*` tokens — categorical, sequential, diverging',
|
|
811
|
+
' and de-emphasis, each validated for colour-vision deficiency. Never build a',
|
|
812
|
+
' chart palette from surface or brand tokens.',
|
|
813
|
+
'- Be short. Two or three sentences and a small code example beat an essay.',
|
|
814
|
+
].join('\n');
|