@starklab/stark-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/package.json +31 -0
  4. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +21 -0
  5. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +13 -0
  6. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +11 -0
  7. package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +34 -0
  8. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +8 -0
  9. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +9 -0
  10. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +9 -0
  11. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +7 -0
  12. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +12 -0
  13. package/src/adopt/catalog.js +88 -0
  14. package/src/adopt/dominionFixture.test.js +165 -0
  15. package/src/adopt/moduleGraph.js +232 -0
  16. package/src/adopt/parseSource.js +25 -0
  17. package/src/adopt/propApiResolver.js +278 -0
  18. package/src/adopt/propApiResolver.test.js +229 -0
  19. package/src/adopt/referenceResolver.js +151 -0
  20. package/src/adopt/referenceResolver.test.js +213 -0
  21. package/src/adopt/rnTailwindResolver.js +347 -0
  22. package/src/adopt/rnTailwindResolver.test.js +263 -0
  23. package/src/adopt/rnTokenAliasResolver.js +474 -0
  24. package/src/adopt/rnTokenAliasResolver.test.js +260 -0
  25. package/src/adopt/tailwindResolver.js +512 -0
  26. package/src/adopt/tailwindResolver.test.js +178 -0
  27. package/src/adopt/targetDiscovery.js +237 -0
  28. package/src/adopt/targetDiscovery.test.js +227 -0
  29. package/src/adopt/tokenAliasResolver.js +513 -0
  30. package/src/adopt/tokenAliasResolver.test.js +319 -0
  31. package/src/adopt/wrapperResolver.js +874 -0
  32. package/src/adopt/wrapperResolver.test.js +324 -0
  33. package/src/cli.js +376 -0
  34. package/src/data.js +267 -0
  35. package/src/data.test.js +231 -0
  36. package/src/index.js +8 -0
  37. package/src/server.js +149 -0
@@ -0,0 +1,474 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import fg from 'fast-glob';
5
+ import traverseModule from '@babel/traverse';
6
+
7
+ import { stkRoot } from '../data.js';
8
+ import { buildModuleGraph, resolveOriginDeep } from './moduleGraph.js';
9
+
10
+ const traverse = traverseModule.default ?? traverseModule;
11
+
12
+ // "5 is governance, 20 is safety" — see ADOPTION_APP_PLAN.md §4/§5a and the
13
+ // same pattern in moduleGraph.js's resolveOrigin/resolveOriginDeep and
14
+ // tokenAliasResolver.js's own resolveTerminal.
15
+ const WARN_DEPTH = 3;
16
+ const MAX_DEPTH = 20;
17
+
18
+ // The RN counterpart of CSS custom properties: no var(), so a Stark RN token
19
+ // constant is always reached through one of these package subpaths
20
+ // (packages/stk/package.json "exports"). Importing the bare web build
21
+ // (`@starklab/stk`) on RN is out of scope — it has no Platform.select
22
+ // values and isn't the documented RN entry point (see the port-to-rn skill).
23
+ const RN_TOKEN_PACKAGES = new Set([
24
+ '@starklab/stk/rn',
25
+ '@starklab/stk/rn-spacing',
26
+ '@starklab/stk/rn-typography',
27
+ '@starklab/stk/rn-easing',
28
+ '@starklab/stk/rn-shadows',
29
+ '@starklab/stk/rn-dark',
30
+ ]);
31
+
32
+ const RN_BUILD_FILES = [
33
+ 'build/rn/tokens.rn.js',
34
+ 'build/rn/tokens.dark.rn.js',
35
+ 'build/rn/spacing.rn.js',
36
+ 'build/rn/typography.rn.js',
37
+ 'build/rn/easing.rn.js',
38
+ 'build/rn/shadows.rn.js',
39
+ ];
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Inventory + layer classification
43
+ //
44
+ // Same DTCG source as tokenAliasResolver.js's loadTokenLayers, but flattened
45
+ // to RN token names (packages/stk's own strip-"--stk-"/split-"-"/camelCase/
46
+ // prefix-"stk" convention, documented in the port-to-rn skill) instead of CSS
47
+ // custom-property names, so a layer-violation finding (aliasing a primitive
48
+ // token) works the same way it does on web.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function cssVarToRnToken(cssVarName) {
52
+ const rest = cssVarName.replace(/^--stk-/, '');
53
+ const camel = rest
54
+ .split('-')
55
+ .filter(Boolean)
56
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
57
+ .join('');
58
+ return `stk${camel}`;
59
+ }
60
+
61
+ function flattenDtcgRn(node, prefix, layer, out) {
62
+ if (node == null || typeof node !== 'object') return;
63
+ if ('$value' in node) {
64
+ out.set(cssVarToRnToken(`--stk-${prefix.join('-')}`), layer);
65
+ return;
66
+ }
67
+ for (const [key, child] of Object.entries(node)) {
68
+ if (key.startsWith('$')) continue;
69
+ flattenDtcgRn(child, [...prefix, key], layer, out);
70
+ }
71
+ }
72
+
73
+ function loadRnTokenLayers(stkPkgRoot) {
74
+ const layers = new Map();
75
+ const dirs = [
76
+ ['base', 'primitive'],
77
+ ['semantic', 'semantic'],
78
+ ['components', 'component'],
79
+ ];
80
+ for (const [dir, layer] of dirs) {
81
+ const full = path.join(stkPkgRoot, 'tokens', dir);
82
+ if (!existsSync(full)) continue;
83
+ for (const file of fg.sync('**/*.json', { cwd: full, absolute: true })) {
84
+ let json;
85
+ try {
86
+ json = JSON.parse(readFileSync(file, 'utf-8'));
87
+ } catch {
88
+ continue;
89
+ }
90
+ flattenDtcgRn(json, [], layer, layers);
91
+ }
92
+ }
93
+ return layers;
94
+ }
95
+
96
+ export function loadRnTokenInventory(stkPkgRoot) {
97
+ const inventory = new Set();
98
+ for (const rel of RN_BUILD_FILES) {
99
+ const full = path.join(stkPkgRoot, rel);
100
+ if (!existsSync(full)) continue;
101
+ const src = readFileSync(full, 'utf-8');
102
+ for (const m of src.matchAll(/export const (stk[A-Za-z0-9]+)\s*=/g)) {
103
+ inventory.add(m[1]);
104
+ }
105
+ }
106
+ if (inventory.size === 0) {
107
+ throw new Error(`RN token build not found under ${stkPkgRoot}/build/rn — run "npm run build" in packages/stk first.`);
108
+ }
109
+ const layers = loadRnTokenLayers(stkPkgRoot);
110
+ return { inventory, layers };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Pass 1 — the consumer's own JS-object alias graph
115
+ //
116
+ // The RN equivalent of tokenAliasResolver.js's propGraph: instead of
117
+ // `--app-primary: var(--stk-…)`, the indirection is a local `const` whose
118
+ // initializer is either a scalar alias (`const primary = stkSurfaceBrand1Strong`)
119
+ // or an object literal whose properties alias tokens
120
+ // (`const theme = { primary: stkSurfaceBrand1Strong }`). Each alias is keyed
121
+ // per-file (JS module scope, unlike CSS custom properties which are
122
+ // effectively global) — `localName` for a scalar const, `localName.propName`
123
+ // for an object property.
124
+ //
125
+ // Known simplifications (documented, not oversights — this is the first RN
126
+ // resolver and ADOPTION_APP_PLAN.md §5a explicitly argues for building it
127
+ // only after the web one is validated, since there is no getComputedStyle()
128
+ // oracle to reconcile against on RN):
129
+ // - Declarations are captured file-wide (any scope depth), not just
130
+ // module-top-level; if the same name is `const`-declared twice in one
131
+ // file (e.g. two components each with their own local `theme`), the
132
+ // later declaration wins. Real component files define one theme/style
133
+ // object per file, so this rarely matters in practice.
134
+ // - Only single-hop member access (`obj.prop`) is resolved — `a.b.c` is out
135
+ // of scope, mirroring tailwindResolver.js's non-theme-utility carve-out
136
+ // for what static analysis doesn't reach.
137
+ // - A property whose value isn't one of {Identifier, `obj.prop` member
138
+ // access, plain literal} (a call, a ternary, a spread, a nested object…)
139
+ // is not captured as an alias at all — never scored, same "dynamic
140
+ // expressions are skipped" rule tailwindResolver.js applies to
141
+ // clsx()/ternary className values.
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function isPlainLiteral(node) {
145
+ if (node.type === 'StringLiteral' || node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') return true;
146
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) return true;
147
+ return false;
148
+ }
149
+
150
+ export function isAnalyzableValueNode(node) {
151
+ if (!node) return false;
152
+ if (node.type === 'Identifier') return true;
153
+ if (node.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.type === 'Identifier') return true;
154
+ return isPlainLiteral(node);
155
+ }
156
+
157
+ function setAlias(aliasGraph, file, key, node, line) {
158
+ let byFile = aliasGraph.get(file);
159
+ if (!byFile) {
160
+ byFile = new Map();
161
+ aliasGraph.set(file, byFile);
162
+ }
163
+ byFile.set(key, { node, line, file });
164
+ }
165
+
166
+ export function buildAliasGraph(moduleGraph) {
167
+ const aliasGraph = new Map(); // file -> Map(key -> {node, line, file})
168
+ const definitionNodes = new Set(); // value nodes that are alias-graph edges, not Pass-2 usage sites
169
+
170
+ for (const file of moduleGraph.files) {
171
+ const entry = moduleGraph.graph.get(file);
172
+ if (!entry?.ast) continue;
173
+
174
+ traverse(entry.ast, {
175
+ VariableDeclarator(nodePath) {
176
+ const { id, init } = nodePath.node;
177
+ if (!init || id.type !== 'Identifier') return;
178
+ const localName = id.name;
179
+
180
+ if (init.type === 'ObjectExpression') {
181
+ for (const prop of init.properties) {
182
+ if (prop.type !== 'ObjectProperty' || prop.computed) continue;
183
+ const propName =
184
+ prop.key.type === 'Identifier' ? prop.key.name : prop.key.type === 'StringLiteral' ? prop.key.value : null;
185
+ if (!propName || !isAnalyzableValueNode(prop.value)) continue;
186
+ setAlias(aliasGraph, file, `${localName}.${propName}`, prop.value, prop.loc?.start?.line ?? null);
187
+ definitionNodes.add(prop.value);
188
+ }
189
+ return;
190
+ }
191
+
192
+ if (!isAnalyzableValueNode(init)) return;
193
+ setAlias(aliasGraph, file, localName, init, nodePath.node.loc?.start?.line ?? null);
194
+ definitionNodes.add(init);
195
+ },
196
+ });
197
+ }
198
+
199
+ return { aliasGraph, definitionNodes };
200
+ }
201
+
202
+ export function classifyTerminalState(kind) {
203
+ if (kind === 'stk') return 'conformant';
204
+ if (kind === 'raw') return 'drift';
205
+ return 'broken'; // undefined | cycle | depth-limit | unsupported
206
+ }
207
+
208
+ function classifyOrigin(origin, propName, ctx, visited, depth) {
209
+ if (!origin) return { kind: 'undefined', depth };
210
+ if (origin.cycle) return { kind: 'cycle', depth };
211
+
212
+ if (origin.type === 'package') {
213
+ if (!RN_TOKEN_PACKAGES.has(origin.pkg)) return { kind: 'undefined', depth };
214
+ const tokenName = origin.name === '*' ? propName : origin.name;
215
+ if (!tokenName || !ctx.inventory.has(tokenName)) return { kind: 'undefined', depth };
216
+ return { kind: 'stk', depth: depth + 1, stkToken: tokenName, layer: ctx.layers.get(tokenName) ?? 'unknown' };
217
+ }
218
+
219
+ if (origin.type === 'local') {
220
+ const key = propName ? `${origin.name}.${propName}` : origin.name;
221
+ if (ctx.aliasGraph.get(origin.file)?.has(key)) {
222
+ return resolveAliasKey(origin.file, key, ctx, visited, depth + 1);
223
+ }
224
+ return { kind: 'undefined', depth };
225
+ }
226
+
227
+ return { kind: 'undefined', depth };
228
+ }
229
+
230
+ function resolveAliasKey(file, key, ctx, visited, depth) {
231
+ const visitKey = `${file}::${key}`;
232
+ if (visited.has(visitKey)) return { kind: 'cycle', depth };
233
+ const entry = ctx.aliasGraph.get(file)?.get(key);
234
+ if (!entry) return { kind: 'undefined', depth };
235
+ const nextVisited = new Set(visited).add(visitKey);
236
+ return classifyNode(file, entry.node, ctx, nextVisited, depth + 1);
237
+ }
238
+
239
+ function classifyIdentifierRef(file, name, ctx, visited, depth) {
240
+ if (ctx.aliasGraph.get(file)?.has(name)) {
241
+ return resolveAliasKey(file, name, ctx, visited, depth);
242
+ }
243
+ const origin = resolveOriginDeep(ctx.moduleGraph, file, name);
244
+ return classifyOrigin(origin, null, ctx, visited, depth);
245
+ }
246
+
247
+ function classifyMemberRef(file, objName, propName, ctx, visited, depth) {
248
+ const compoundKey = `${objName}.${propName}`;
249
+ if (ctx.aliasGraph.get(file)?.has(compoundKey)) {
250
+ return resolveAliasKey(file, compoundKey, ctx, visited, depth);
251
+ }
252
+ const origin = resolveOriginDeep(ctx.moduleGraph, file, objName);
253
+ return classifyOrigin(origin, propName, ctx, visited, depth);
254
+ }
255
+
256
+ export function classifyNode(file, node, ctx, visited, depth) {
257
+ if (depth > MAX_DEPTH) return { kind: 'depth-limit', depth };
258
+ if (node.type === 'Identifier') return classifyIdentifierRef(file, node.name, ctx, visited, depth);
259
+ if (node.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.type === 'Identifier') {
260
+ return classifyMemberRef(file, node.object.name, node.property.name, ctx, visited, depth);
261
+ }
262
+ if (isPlainLiteral(node)) return { kind: 'raw', depth };
263
+ return { kind: 'unsupported', depth };
264
+ }
265
+
266
+ function classifyAliasGraph(aliasGraph, ctx) {
267
+ const properties = [];
268
+ const findings = [];
269
+
270
+ for (const [file, byKey] of aliasGraph) {
271
+ for (const [key, entry] of byKey) {
272
+ const terminal = classifyNode(file, entry.node, ctx, new Set(), 0);
273
+ const state = classifyTerminalState(terminal.kind);
274
+
275
+ if (terminal.kind === 'stk' && terminal.layer === 'primitive') {
276
+ findings.push({ rule: 'layer-violation', severity: 'critical', property: key, file, line: entry.line, stkToken: terminal.stkToken });
277
+ }
278
+ if (terminal.kind === 'stk' && terminal.depth > WARN_DEPTH) {
279
+ findings.push({ rule: 'deep-alias-chain', severity: 'warning', property: key, file, line: entry.line, depth: terminal.depth });
280
+ }
281
+ if (state === 'drift') {
282
+ findings.push({ rule: 'drift-behind-alias', severity: 'critical', property: key, file, line: entry.line });
283
+ }
284
+ if (state === 'broken') {
285
+ findings.push({ rule: 'broken-alias', severity: 'critical', property: key, file, line: entry.line, reason: terminal.kind });
286
+ }
287
+
288
+ properties.push({ name: key, file, line: entry.line, state, terminal });
289
+ }
290
+ }
291
+
292
+ return { properties, findings };
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Pass 2 — usage classification
297
+ //
298
+ // A usage is `obj.prop` or a bare `name` reference outside of the Pass-1
299
+ // definition sites themselves. Three ways a usage resolves:
300
+ // 'direct' — the reference reaches a Stark RN token constant with no
301
+ // consumer indirection at all (a namespace-import property
302
+ // access, a directly-imported token identifier, or — the
303
+ // `useTokens()` case — an object whose origin isn't statically
304
+ // traceable but whose property name is an unambiguous "stk"-
305
+ // prefixed inventory token; this matches the reference's
306
+ // static *name*, never a resolved *value*, which is the
307
+ // distinction ADOPTION_APP_PLAN.md §5a's anti-pattern warning
308
+ // draws — "colour values collide constantly, a metric built on
309
+ // coincidence is worse than a missing metric").
310
+ // 'aliased' — the reference resolves through a Pass-1 alias-graph entry to
311
+ // a conformant stk terminal.
312
+ // (skipped) — anything else (an unrelated object/property pair, e.g.
313
+ // `props.variant` or `StyleSheet.create`) is out of scope, not
314
+ // a violation — same "not every property is ours to judge"
315
+ // rule tailwindResolver.js applies to Tailwind's own default
316
+ // palette classes.
317
+ // ---------------------------------------------------------------------------
318
+
319
+ function pushResolvedUsage(usages, { file, line, ref, terminal }) {
320
+ const state = classifyTerminalState(terminal.kind);
321
+ const classification = state === 'conformant' ? 'aliased' : state;
322
+ usages.push({ file, line, ref, classification, stkToken: terminal.stkToken, depth: terminal.depth });
323
+ }
324
+
325
+ function collectUsages(moduleGraph, definitionNodes, ctx) {
326
+ const usages = [];
327
+
328
+ for (const file of moduleGraph.files) {
329
+ const entry = moduleGraph.graph.get(file);
330
+ if (!entry?.ast) continue;
331
+
332
+ traverse(entry.ast, {
333
+ MemberExpression(nodePath) {
334
+ const node = nodePath.node;
335
+ if (definitionNodes.has(node)) return; // Pass-1 alias-graph edge, not a usage site
336
+ if (node.computed) return;
337
+ if (node.object.type !== 'Identifier' || node.property.type !== 'Identifier') return;
338
+
339
+ const objName = node.object.name;
340
+ const propName = node.property.name;
341
+ const line = node.loc?.start?.line ?? null;
342
+ const compoundKey = `${objName}.${propName}`;
343
+
344
+ if (ctx.aliasGraph.get(file)?.has(compoundKey)) {
345
+ pushResolvedUsage(usages, { file, line, ref: compoundKey, terminal: resolveAliasKey(file, compoundKey, ctx, new Set(), 0) });
346
+ return;
347
+ }
348
+
349
+ const origin = resolveOriginDeep(ctx.moduleGraph, file, objName);
350
+ if (origin?.type === 'package' && origin.name === '*' && RN_TOKEN_PACKAGES.has(origin.pkg) && ctx.inventory.has(propName)) {
351
+ usages.push({ file, line, ref: compoundKey, classification: 'direct', stkToken: propName });
352
+ return;
353
+ }
354
+ if (origin?.type === 'local') {
355
+ const key = `${origin.name}.${propName}`;
356
+ if (ctx.aliasGraph.get(origin.file)?.has(key)) {
357
+ pushResolvedUsage(usages, { file, line, ref: compoundKey, terminal: resolveAliasKey(origin.file, key, ctx, new Set(), 0) });
358
+ return;
359
+ }
360
+ }
361
+
362
+ if (propName.startsWith('stk') && ctx.inventory.has(propName)) {
363
+ usages.push({ file, line, ref: compoundKey, classification: 'direct', stkToken: propName, heuristic: true });
364
+ }
365
+ },
366
+
367
+ Identifier(nodePath) {
368
+ if (!nodePath.isReferencedIdentifier()) return;
369
+ const node = nodePath.node;
370
+ if (definitionNodes.has(node)) return;
371
+ const name = node.name;
372
+ const line = node.loc?.start?.line ?? null;
373
+
374
+ if (ctx.aliasGraph.get(file)?.has(name)) {
375
+ pushResolvedUsage(usages, { file, line, ref: name, terminal: resolveAliasKey(file, name, ctx, new Set(), 0) });
376
+ return;
377
+ }
378
+
379
+ const origin = resolveOriginDeep(ctx.moduleGraph, file, name);
380
+ if (origin?.type === 'package' && origin.name !== '*' && RN_TOKEN_PACKAGES.has(origin.pkg) && ctx.inventory.has(origin.name)) {
381
+ usages.push({ file, line, ref: name, classification: 'direct', stkToken: origin.name });
382
+ }
383
+ },
384
+ });
385
+ }
386
+
387
+ return usages;
388
+ }
389
+
390
+ // Same three-number report as tokenAliasResolver.js/tailwindResolver.js.
391
+ function summarizeReport(usages) {
392
+ const counts = { direct: 0, aliased: 0, unresolved: 0 };
393
+ for (const u of usages) {
394
+ if (u.classification === 'direct') counts.direct++;
395
+ else if (u.classification === 'aliased') counts.aliased++;
396
+ else counts.unresolved++;
397
+ }
398
+ const total = counts.direct + counts.aliased + counts.unresolved;
399
+ const pct = (n) => (total === 0 ? 0 : Math.round((n / total) * 1000) / 10);
400
+ return {
401
+ total,
402
+ direct: counts.direct,
403
+ directPct: pct(counts.direct),
404
+ aliased: counts.aliased,
405
+ aliasedPct: pct(counts.aliased),
406
+ unresolved: counts.unresolved,
407
+ unresolvedPct: pct(counts.unresolved),
408
+ conformancePct: pct(counts.direct + counts.aliased),
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Resolves a consumer's own JS-object token indirection on React Native —
414
+ * the RN analog of tokenAliasResolver.js's CSS custom-property alias graph
415
+ * (ADOPTION_APP_PLAN.md §5a: "no CSS custom properties, so the indirection is
416
+ * a JS object… same graph, resolved over imports and property access instead
417
+ * of var()"). Native-only: CSS var()/Tailwind utility classes have no RN
418
+ * equivalent — those are the two web-only resolvers.
419
+ *
420
+ * Static-only by construction (§5a/§3d): there is no getComputedStyle()-style
421
+ * runtime oracle on RN to reconcile against, so `confidence: 'uncertain'`
422
+ * always accompanies a `detected: true` result — never treat this result as
423
+ * equal-confidence with the web resolver's CSS-based ground truth.
424
+ */
425
+ export function resolveRnTokenAliases(root, { platform = 'native', ignore = [] } = {}) {
426
+ if (platform !== 'native') {
427
+ throw new Error(
428
+ `resolveRnTokenAliases only supports platform "native" (got "${platform}") — CSS custom properties and Tailwind utility classes are the web analog, see tokenAliasResolver.js/tailwindResolver.js.`
429
+ );
430
+ }
431
+
432
+ const stkPkgRoot = stkRoot();
433
+ const { inventory, layers } = loadRnTokenInventory(stkPkgRoot);
434
+ const moduleGraph = buildModuleGraph(root, { ignore });
435
+
436
+ const anyStkImport = [...moduleGraph.graph.values()].some((entry) =>
437
+ [...entry.imports.values()].some((b) => RN_TOKEN_PACKAGES.has(b.source))
438
+ );
439
+ if (!anyStkImport) {
440
+ // Detection is always cheaper than resolution: no import from any Stark
441
+ // RN token package anywhere means there's nothing to measure, not a 0%
442
+ // score — same refusal tailwindResolver.js makes for "no config found".
443
+ return {
444
+ platform,
445
+ root,
446
+ detected: false,
447
+ reason:
448
+ 'No import from a @starklab/stk RN token package (rn, rn-spacing, rn-typography, rn-easing, rn-shadows, rn-dark) found anywhere in the scanned files.',
449
+ report: null,
450
+ };
451
+ }
452
+
453
+ const { aliasGraph, definitionNodes } = buildAliasGraph(moduleGraph);
454
+ const ctx = { aliasGraph, moduleGraph, inventory, layers };
455
+ const { properties, findings } = classifyAliasGraph(aliasGraph, ctx);
456
+ const usages = collectUsages(moduleGraph, definitionNodes, ctx);
457
+ const report = summarizeReport(usages);
458
+
459
+ return {
460
+ platform,
461
+ root,
462
+ detected: true,
463
+ scannedFiles: moduleGraph.files.length,
464
+ tokenInventoryTotal: inventory.size,
465
+ aliasCount: [...aliasGraph.values()].reduce((n, byKey) => n + byKey.size, 0),
466
+ properties,
467
+ usages,
468
+ findings,
469
+ report,
470
+ confidence: 'uncertain',
471
+ confidenceReason:
472
+ 'Static-only (ADOPTION_APP_PLAN.md §5a): no getComputedStyle()-equivalent runtime oracle exists on React Native to reconcile against, so this result has to be right by construction rather than cross-checked.',
473
+ };
474
+ }