@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,237 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import fg from 'fast-glob';
5
+
6
+ const STK_PREFIX = '@starklab/stk';
7
+ const UI_FRAMEWORK_DEPS = ['react', 'react-dom', 'react-native', 'expo', 'preact', 'vue', 'svelte'];
8
+ const DEFAULT_IGNORE = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**'];
9
+
10
+ function readJson(file) {
11
+ try {
12
+ return JSON.parse(readFileSync(file, 'utf-8'));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * `dominion.config.json#targets` is the asymmetric-override surface
20
+ * (ADOPTION_APP_PLAN.md §3e): force-include is unrestricted (a team wants
21
+ * adoption tracked from day zero even before it depends on stk*);
22
+ * force-exclude requires a recorded reason, shown beside the excluded count
23
+ * — "the gaming direction" the plan calls out explicitly. Reuses the same
24
+ * config file wrapperResolver.js already reads for declared wrappers.
25
+ */
26
+ function readTargetOverrides(root) {
27
+ const configPath = path.join(root, 'dominion.config.json');
28
+ const raw = existsSync(configPath) ? readJson(configPath) : null;
29
+ const targets = raw?.targets && typeof raw.targets === 'object' ? raw.targets : {};
30
+ const forceInclude = Array.isArray(targets.forceInclude) ? targets.forceInclude : [];
31
+ const forceExclude = targets.forceExclude && typeof targets.forceExclude === 'object' ? targets.forceExclude : {};
32
+ return { forceInclude, forceExclude };
33
+ }
34
+
35
+ /**
36
+ * Resolves the workspace glob patterns a repo declares: npm/yarn's
37
+ * package.json#workspaces first (array form, or the yarn {packages:[...]}
38
+ * form), falling back to pnpm-workspace.yaml's `packages:` list. The
39
+ * pnpm-workspace.yaml parser is a hand-rolled flat-list reader, not a YAML
40
+ * parser — this package has no YAML dependency and the only shape pnpm
41
+ * actually emits for this key is a flat list of quoted globs, so a full
42
+ * parser would be unneeded weight.
43
+ */
44
+ function resolveWorkspaceGlobs(root) {
45
+ const pkgPath = path.join(root, 'package.json');
46
+ const pkg = existsSync(pkgPath) ? readJson(pkgPath) : null;
47
+ if (pkg?.workspaces) {
48
+ const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
49
+ if (Array.isArray(globs) && globs.length) return { source: 'package.json#workspaces', globs };
50
+ }
51
+
52
+ const pnpmPath = path.join(root, 'pnpm-workspace.yaml');
53
+ if (existsSync(pnpmPath)) {
54
+ const lines = readFileSync(pnpmPath, 'utf-8').split('\n');
55
+ const globs = [];
56
+ let inPackages = false;
57
+ for (const line of lines) {
58
+ if (/^packages:\s*$/.test(line)) {
59
+ inPackages = true;
60
+ continue;
61
+ }
62
+ if (inPackages) {
63
+ const m = line.match(/^\s+-\s*['"]?([^'"]+)['"]?\s*$/);
64
+ if (m) {
65
+ globs.push(m[1]);
66
+ continue;
67
+ }
68
+ if (line.trim() === '') continue;
69
+ inPackages = false; // dedented to a new top-level key — packages: list ended
70
+ }
71
+ }
72
+ if (globs.length) return { source: 'pnpm-workspace.yaml', globs };
73
+ }
74
+
75
+ return { source: null, globs: [] };
76
+ }
77
+
78
+ function monorepoToolingSignals(root) {
79
+ return {
80
+ turbo: existsSync(path.join(root, 'turbo.json')),
81
+ nx: existsSync(path.join(root, 'nx.json')),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Expands workspace globs to every directory containing its own
87
+ * package.json. When no manifest declares any globs at all, falls back to
88
+ * scanning the whole repo for package.json files — "never ask someone to
89
+ * type 20 paths" (ADOPTION_APP_PLAN.md §3e). A repo with no nested
90
+ * package.json anywhere is itself the one and only target.
91
+ */
92
+ function expandTargetDirs(root, globs) {
93
+ if (globs.length === 0) {
94
+ const found = fg.sync(['**/package.json'], { cwd: root, ignore: DEFAULT_IGNORE, absolute: true });
95
+ const dirs = found.map((f) => path.dirname(f)).filter((d) => d !== root);
96
+ return dirs.length ? dirs : [root];
97
+ }
98
+ const patterns = globs.map((g) => `${g.replace(/\/$/, '')}/package.json`);
99
+ const found = fg.sync(patterns, { cwd: root, ignore: DEFAULT_IGNORE, absolute: true });
100
+ return found.map((f) => path.dirname(f));
101
+ }
102
+
103
+ function depNames(pkg) {
104
+ return new Set([
105
+ ...Object.keys(pkg?.dependencies || {}),
106
+ ...Object.keys(pkg?.devDependencies || {}),
107
+ ...Object.keys(pkg?.peerDependencies || {}),
108
+ ]);
109
+ }
110
+
111
+ function hasDirectStkDep(pkg) {
112
+ for (const name of depNames(pkg)) {
113
+ if (name.startsWith(STK_PREFIX)) return true;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ function hasUiFrameworkDep(pkg) {
119
+ const deps = depNames(pkg);
120
+ return UI_FRAMEWORK_DEPS.some((d) => deps.has(d));
121
+ }
122
+
123
+ /**
124
+ * Discovers every workspace target in a repo and classifies each in/out of
125
+ * scope by direct-or-transitive dependency on @starklab/stk*
126
+ * (ADOPTION_APP_PLAN.md §3e — "a workspace with no dependency, direct or
127
+ * transitive, on stk* is out of scope, not 0%"). "Transitive" here means
128
+ * transitive through the workspace's own internal dependency graph (one
129
+ * target depending on another target that itself depends on stk*) — the
130
+ * exact shape this repo has (apps/storybook-native → packages/stk-react-
131
+ * native → packages/stk). There is no node_modules resolution step, so a
132
+ * transitive dependency reached only through a *non-workspace* npm package
133
+ * is not detected; that's an acceptable gap since a non-workspace package
134
+ * can't be a scan target of its own anyway.
135
+ *
136
+ * A second, independent signal — `rendersUi` — flags whether an excluded
137
+ * target still has a UI framework dependency (React/RN/etc.) despite having
138
+ * no stk* dependency. These are the "opportunity list": excluded rows that
139
+ * must never read as zeros inside a score, because they're the most
140
+ * valuable rows to bring into scope next.
141
+ */
142
+ export function discoverTargets(root) {
143
+ const { source, globs } = resolveWorkspaceGlobs(root);
144
+ const monorepoTooling = monorepoToolingSignals(root);
145
+ const { forceInclude, forceExclude } = readTargetOverrides(root);
146
+
147
+ const dirs = expandTargetDirs(root, globs);
148
+
149
+ const rawTargets = dirs.map((dir) => {
150
+ const pkgPath = path.join(dir, 'package.json');
151
+ const pkg = existsSync(pkgPath) ? readJson(pkgPath) : null;
152
+ return {
153
+ dir,
154
+ relDir: path.relative(root, dir) || '.',
155
+ name: pkg?.name || path.basename(dir),
156
+ pkg,
157
+ };
158
+ });
159
+
160
+ const byPackageName = new Map(rawTargets.filter((t) => t.pkg?.name).map((t) => [t.pkg.name, t]));
161
+
162
+ function transitivelyDependsOnStk(target, visited) {
163
+ if (visited.has(target.relDir)) return false;
164
+ visited.add(target.relDir);
165
+ if (!target.pkg) return false;
166
+ if (hasDirectStkDep(target.pkg)) return true;
167
+ for (const dep of depNames(target.pkg)) {
168
+ const sibling = byPackageName.get(dep);
169
+ if (sibling && transitivelyDependsOnStk(sibling, visited)) return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ const targets = rawTargets.map((t) => {
175
+ const relPath = t.relDir;
176
+ const isForceIncluded = forceInclude.includes(relPath);
177
+ const forceExcludeReason = forceExclude[relPath];
178
+ const dependsOnStk = transitivelyDependsOnStk(t, new Set());
179
+ const rendersUi = t.pkg ? hasUiFrameworkDep(t.pkg) : false;
180
+
181
+ let inScope;
182
+ let scopeReason;
183
+ if (forceExcludeReason) {
184
+ inScope = false;
185
+ scopeReason = `force-excluded: ${forceExcludeReason}`;
186
+ } else if (isForceIncluded) {
187
+ inScope = true;
188
+ scopeReason = 'force-included';
189
+ } else if (dependsOnStk) {
190
+ inScope = true;
191
+ scopeReason = 'depends on @starklab/stk*, directly or via a workspace sibling';
192
+ } else {
193
+ inScope = false;
194
+ scopeReason = 'no @starklab/stk* dependency, direct or transitive';
195
+ }
196
+
197
+ return {
198
+ name: t.name,
199
+ dir: relPath,
200
+ packageName: t.pkg?.name || null,
201
+ inScope,
202
+ scopeReason,
203
+ rendersUi,
204
+ forceIncluded: isForceIncluded,
205
+ forceExcluded: Boolean(forceExcludeReason),
206
+ };
207
+ });
208
+
209
+ const inScope = targets.filter((t) => t.inScope);
210
+ const excluded = targets.filter((t) => !t.inScope);
211
+ const opportunities = excluded.filter((t) => t.rendersUi);
212
+
213
+ return {
214
+ root,
215
+ manifestSource: source,
216
+ monorepoTooling,
217
+ targets,
218
+ inScope,
219
+ excluded,
220
+ opportunities,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Builds the `Map<packageName, absoluteDir>` resolveWrappers()'s
226
+ * `workspacePackages` option expects, from a discovery result — every
227
+ * target with a real package.json#name, in or out of scope alike (a
228
+ * cross-workspace wrapper chain can legitimately pass through an
229
+ * out-of-scope package on its way to a DS component).
230
+ */
231
+ export function workspacePackageMap(discovery) {
232
+ const map = new Map();
233
+ for (const t of discovery.targets) {
234
+ if (t.packageName) map.set(t.packageName, path.join(discovery.root, t.dir));
235
+ }
236
+ return map;
237
+ }
@@ -0,0 +1,227 @@
1
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { describe, it, expect, afterEach } from 'vitest';
6
+
7
+ import { discoverTargets, workspacePackageMap } from './targetDiscovery.js';
8
+
9
+ let tmpDirs = [];
10
+
11
+ afterEach(() => {
12
+ for (const dir of tmpDirs) rmSync(dir, { recursive: true, force: true });
13
+ tmpDirs = [];
14
+ });
15
+
16
+ function fixture(files) {
17
+ const root = mkdtempSync(path.join(os.tmpdir(), 'stark-adopt-targets-'));
18
+ tmpDirs.push(root);
19
+ for (const [rel, content] of Object.entries(files)) {
20
+ const full = path.join(root, rel);
21
+ mkdirSync(path.dirname(full), { recursive: true });
22
+ writeFileSync(full, content);
23
+ }
24
+ return root;
25
+ }
26
+
27
+ function target(discovery, dir) {
28
+ const found = discovery.targets.find((t) => t.dir === dir);
29
+ if (!found) throw new Error(`No target "${dir}". Found: ${discovery.targets.map((t) => t.dir).join(', ')}`);
30
+ return found;
31
+ }
32
+
33
+ describe('discoverTargets — manifest source resolution', () => {
34
+ it('reads npm/yarn array-form package.json#workspaces', () => {
35
+ const root = fixture({
36
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
37
+ 'packages/a/package.json': JSON.stringify({ name: 'a' }),
38
+ 'packages/b/package.json': JSON.stringify({ name: 'b' }),
39
+ });
40
+ const d = discoverTargets(root);
41
+ expect(d.manifestSource).toBe('package.json#workspaces');
42
+ expect(d.targets.map((t) => t.dir).sort()).toEqual(['packages/a', 'packages/b']);
43
+ });
44
+
45
+ it('reads yarn object-form package.json#workspaces.packages', () => {
46
+ const root = fixture({
47
+ 'package.json': JSON.stringify({ name: 'root', workspaces: { packages: ['apps/*'] } }),
48
+ 'apps/one/package.json': JSON.stringify({ name: 'one' }),
49
+ });
50
+ const d = discoverTargets(root);
51
+ expect(d.manifestSource).toBe('package.json#workspaces');
52
+ expect(d.targets.map((t) => t.dir)).toEqual(['apps/one']);
53
+ });
54
+
55
+ it('falls back to pnpm-workspace.yaml when package.json has no workspaces field', () => {
56
+ const root = fixture({
57
+ 'package.json': JSON.stringify({ name: 'root' }),
58
+ 'pnpm-workspace.yaml': "packages:\n - 'packages/*'\n - 'apps/*'\n",
59
+ 'packages/x/package.json': JSON.stringify({ name: 'x' }),
60
+ 'apps/y/package.json': JSON.stringify({ name: 'y' }),
61
+ });
62
+ const d = discoverTargets(root);
63
+ expect(d.manifestSource).toBe('pnpm-workspace.yaml');
64
+ expect(d.targets.map((t) => t.dir).sort()).toEqual(['apps/y', 'packages/x']);
65
+ });
66
+
67
+ it('falls back to a whole-repo scan when no manifest declares globs', () => {
68
+ const root = fixture({
69
+ 'package.json': JSON.stringify({ name: 'root' }),
70
+ 'nested/thing/package.json': JSON.stringify({ name: 'thing' }),
71
+ });
72
+ const d = discoverTargets(root);
73
+ expect(d.manifestSource).toBeNull();
74
+ expect(d.targets.map((t) => t.dir)).toEqual(['nested/thing']);
75
+ });
76
+
77
+ it('treats the root itself as the sole target when nothing nested exists either', () => {
78
+ const root = fixture({
79
+ 'package.json': JSON.stringify({ name: 'root' }),
80
+ });
81
+ const d = discoverTargets(root);
82
+ expect(d.targets.map((t) => t.dir)).toEqual(['.']);
83
+ });
84
+
85
+ it('records turbo.json/nx.json presence as informational monorepoTooling flags', () => {
86
+ const root = fixture({
87
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
88
+ 'turbo.json': '{}',
89
+ 'packages/a/package.json': JSON.stringify({ name: 'a' }),
90
+ });
91
+ const d = discoverTargets(root);
92
+ expect(d.monorepoTooling).toEqual({ turbo: true, nx: false });
93
+ });
94
+ });
95
+
96
+ describe('discoverTargets — scope classification', () => {
97
+ it('marks a direct @starklab/stk* dependent in scope', () => {
98
+ const root = fixture({
99
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
100
+ 'packages/consumer/package.json': JSON.stringify({
101
+ name: 'consumer',
102
+ dependencies: { '@starklab/stk-components': '^1.0.0' },
103
+ }),
104
+ });
105
+ const d = discoverTargets(root);
106
+ const t = target(d, 'packages/consumer');
107
+ expect(t.inScope).toBe(true);
108
+ expect(t.scopeReason).toMatch(/depends on @starklab\/stk\*/);
109
+ });
110
+
111
+ it('marks a target with no stk* dependency, direct or transitive, out of scope — not 0%', () => {
112
+ const root = fixture({
113
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
114
+ 'packages/unrelated/package.json': JSON.stringify({ name: 'unrelated', dependencies: { lodash: '^4.0.0' } }),
115
+ });
116
+ const d = discoverTargets(root);
117
+ const t = target(d, 'packages/unrelated');
118
+ expect(t.inScope).toBe(false);
119
+ expect(t.scopeReason).toBe('no @starklab/stk* dependency, direct or transitive');
120
+ expect(d.excluded.map((x) => x.dir)).toContain('packages/unrelated');
121
+ });
122
+
123
+ it('marks a transitive dependent (via a workspace sibling) in scope', () => {
124
+ const root = fixture({
125
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
126
+ 'packages/core/package.json': JSON.stringify({
127
+ name: 'core',
128
+ dependencies: { '@starklab/stk': '^1.0.0' },
129
+ }),
130
+ 'packages/app/package.json': JSON.stringify({ name: 'app', dependencies: { core: '1.0.0' } }),
131
+ });
132
+ const d = discoverTargets(root);
133
+ const t = target(d, 'packages/app');
134
+ expect(t.inScope).toBe(true);
135
+ expect(t.scopeReason).toMatch(/via a workspace sibling/);
136
+ });
137
+
138
+ it('flags an excluded target with a UI framework dependency as an opportunity', () => {
139
+ const root = fixture({
140
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
141
+ 'packages/ui-no-stk/package.json': JSON.stringify({ name: 'ui-no-stk', dependencies: { react: '^18.0.0' } }),
142
+ });
143
+ const d = discoverTargets(root);
144
+ const t = target(d, 'packages/ui-no-stk');
145
+ expect(t.inScope).toBe(false);
146
+ expect(t.rendersUi).toBe(true);
147
+ expect(d.opportunities.map((x) => x.dir)).toContain('packages/ui-no-stk');
148
+ });
149
+
150
+ it('does not list an excluded, non-UI target as an opportunity', () => {
151
+ const root = fixture({
152
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
153
+ 'packages/plain/package.json': JSON.stringify({ name: 'plain', dependencies: { lodash: '^4.0.0' } }),
154
+ });
155
+ const d = discoverTargets(root);
156
+ expect(d.opportunities.map((x) => x.dir)).not.toContain('packages/plain');
157
+ });
158
+ });
159
+
160
+ describe('discoverTargets — dominion.config.json overrides', () => {
161
+ it('force-includes a target with no stk* dependency, unrestricted', () => {
162
+ const root = fixture({
163
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
164
+ 'packages/early/package.json': JSON.stringify({ name: 'early', dependencies: { lodash: '^4.0.0' } }),
165
+ 'dominion.config.json': JSON.stringify({ targets: { forceInclude: ['packages/early'] } }),
166
+ });
167
+ const d = discoverTargets(root);
168
+ const t = target(d, 'packages/early');
169
+ expect(t.inScope).toBe(true);
170
+ expect(t.forceIncluded).toBe(true);
171
+ expect(t.scopeReason).toBe('force-included');
172
+ });
173
+
174
+ it('force-excludes a target that would otherwise be in scope, and requires a reason', () => {
175
+ const root = fixture({
176
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
177
+ 'packages/gaming/package.json': JSON.stringify({
178
+ name: 'gaming',
179
+ dependencies: { '@starklab/stk-components': '^1.0.0' },
180
+ }),
181
+ 'dominion.config.json': JSON.stringify({
182
+ targets: { forceExclude: { 'packages/gaming': 'prototype, not tracked yet' } },
183
+ }),
184
+ });
185
+ const d = discoverTargets(root);
186
+ const t = target(d, 'packages/gaming');
187
+ expect(t.inScope).toBe(false);
188
+ expect(t.forceExcluded).toBe(true);
189
+ expect(t.scopeReason).toBe('force-excluded: prototype, not tracked yet');
190
+ });
191
+
192
+ it('gives force-exclude priority over force-include when both name the same target', () => {
193
+ const root = fixture({
194
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
195
+ 'packages/contested/package.json': JSON.stringify({ name: 'contested' }),
196
+ 'dominion.config.json': JSON.stringify({
197
+ targets: {
198
+ forceInclude: ['packages/contested'],
199
+ forceExclude: { 'packages/contested': 'explicitly out' },
200
+ },
201
+ }),
202
+ });
203
+ const d = discoverTargets(root);
204
+ const t = target(d, 'packages/contested');
205
+ expect(t.inScope).toBe(false);
206
+ expect(t.scopeReason).toBe('force-excluded: explicitly out');
207
+ });
208
+ });
209
+
210
+ describe('workspacePackageMap', () => {
211
+ it('maps every target with a package.json#name to its absolute dir, in and out of scope alike', () => {
212
+ const root = fixture({
213
+ 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
214
+ 'packages/consumer/package.json': JSON.stringify({
215
+ name: 'consumer',
216
+ dependencies: { '@starklab/stk-components': '^1.0.0' },
217
+ }),
218
+ 'packages/unrelated/package.json': JSON.stringify({ name: 'unrelated' }),
219
+ });
220
+ const d = discoverTargets(root);
221
+ const map = workspacePackageMap(d);
222
+ expect(map.get('consumer')).toBe(path.join(root, 'packages/consumer'));
223
+ // Out-of-scope targets are still mapped — a cross-workspace wrapper
224
+ // chain can legitimately pass through one on its way to a DS component.
225
+ expect(map.get('unrelated')).toBe(path.join(root, 'packages/unrelated'));
226
+ });
227
+ });