@flashdsm/genome 0.1.0 → 0.2.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 CHANGED
@@ -13,18 +13,28 @@ your project is enough.
13
13
 
14
14
  ## Connect it to a Flash design system
15
15
 
16
- Mint an MCP token in Flash under Connect, then in your repository:
16
+ Sign in once. Genome opens Flash in the browser, the same OAuth flow Cursor uses, and stores the session under `~/.config/flash-dsm/`. Nothing is printed and nothing goes in the repo.
17
17
 
18
18
  ```bash
19
- export FLASH_GENOME_TOKEN=flash_mcp_…
19
+ npx @flashdsm/genome login
20
20
  npx @flashdsm/genome link
21
21
  ```
22
22
 
23
- `link` asks the server which system the token belongs to and writes
24
- `genome.config.json` next to your code: the system's id, its revision, and
25
- where token files and agent context live. The token is never written to that
26
- file - it is committed, and a token in it would be a token in the
27
- repository's history.
23
+ `link` asks the server which system you signed in to and writes `genome.config.json` next to your code.
24
+
25
+ A script that cannot open a browser can still set `FLASH_GENOME_TOKEN` to a `flash_mcp_…` value. That is the same kind of token OAuth issues.
26
+
27
+ ## No design system yet? Start from your code
28
+
29
+ In your app's folder, in a terminal or in your AI editor's chat:
30
+
31
+ ```bash
32
+ npx @flashdsm/genome@latest connect
33
+ ```
34
+
35
+ `connect` scans the folder (stylesheets, CSS inside JS/TS, Tailwind config, theme modules, tokens files; nothing else is read) and opens Flash in your browser, showing what it found and a short code that matches the one in your terminal. Click **Connect** there: Flash creates the system in your account, and the command links the repo (`genome.config.json`), writes the system into it (`pull --confirm`) and stores a token bound to that system under `~/.config/flash-dsm/`. Inside Claude Code it also adds Genome as the project's MCP server; elsewhere it prints the line to add. `--name` overrides the name, which otherwise comes from `package.json`; `--no-open` prints the link instead of opening it.
36
+
37
+ The link works once and expires after 15 minutes. `init --code` does the same with a setup code instead of the browser.
28
38
 
29
39
  ## Quick start
30
40
 
@@ -50,6 +60,38 @@ With no flags it looks for `./genome.config.json`, then the older
50
60
  `./components.json`, and finally falls back to a small bundled fixture so the
51
61
  server always boots. It logs which file it resolved to stderr.
52
62
 
63
+ ## Project shell preview
64
+
65
+ After `scan_workspace` creates a design system from local tokens, capture a
66
+ running preview so Flash can show **Your project** in the editor:
67
+
68
+ | Tool | What it does |
69
+ | ----------------------- | ------------------------------------------------------------------------- |
70
+ | `capture_project_shell` | Headless capture of a preview URL → upload + attach as `source: project` |
71
+ | `scan_usage` | Where the repo's own components are used → the editor's "In your product" |
72
+
73
+ **Localhost vs public URL:** Flash’s API cannot open `localhost` on your
74
+ laptop, so Genome always captures on your machine (Playwright). Pass
75
+ `http://localhost:6006/` (or any public `https://…` preview you can reach
76
+ locally). The API only stores the sanitised HTML+CSS — it never fetches the
77
+ URL for you. Full teammate loop: [docs/workflows/README.md](../../docs/workflows/README.md)
78
+ § E.
79
+
80
+ Set an optional default URL in `genome.config.json`:
81
+
82
+ ```json
83
+ {
84
+ "version": 2,
85
+ "genomeId": 42,
86
+ "name": "Acme Product",
87
+ "previewUrl": "http://localhost:6006/"
88
+ }
89
+ ```
90
+
91
+ Then call `capture_project_shell` with no `url`, or pass `url` and
92
+ `designSystemId` explicitly. Sign in with `flash-genome login` or
93
+ `FLASH_GENOME_TOKEN` (same credential as other hosted tools — never commit it).
94
+
53
95
  ## Keeping a repo in sync
54
96
 
55
97
  Once linked:
@@ -83,6 +125,34 @@ is ahead of `genome/genome.lock`, because a value left over from the last
83
125
  pull would otherwise look like an edit and undo someone's change. Pull,
84
126
  re-apply your edits, then push.
85
127
 
128
+ ### Tokens that change with the viewport
129
+
130
+ A system with breakpoints writes each size that moves at its widest step,
131
+ and lists every step beside it:
132
+
133
+ ```json
134
+ "type-h1": {
135
+ "$value": "48px",
136
+ "$type": "font",
137
+ "$extensions": {
138
+ "com.flashdsm.breakpoints": [
139
+ { "name": "phone", "minWidth": 0, "value": "36px" },
140
+ { "name": "tablet", "minWidth": 768, "value": "42px" },
141
+ { "name": "desktop", "minWidth": 1280, "value": "48px" }
142
+ ]
143
+ }
144
+ }
145
+ ```
146
+
147
+ `tokens.css` carries the same steps as media queries, so `var(--type-h1)`
148
+ is already responsive.
149
+
150
+ A push carries the narrowest step only, because that is the one value Flash
151
+ stores for the token. Change `phone` in the list and it is proposed like any
152
+ other edit. Change `$value` or a wider step and it is flagged: set those
153
+ sizes in Flash, then pull. A repo that pulled before Genome wrote steps is
154
+ told to pull again.
155
+
86
156
  `AGENTS.md` is written once and then left alone however far it drifts: it is
87
157
  where a team puts its own instructions. The Cursor rule is Genome's to keep
88
158
  current, because a stale rule file is worse than none.
@@ -102,18 +172,20 @@ name. No linked repo needed.
102
172
 
103
173
  ## Tools
104
174
 
105
- | Tool | Args | Description |
106
- | --------------------------- | ------------------- | ---------------------------------------------------------------- |
107
- | `get_tokens` | `category?` | Read tokens from the local token file. Read-only |
108
- | `list_components` | `category?` | List components from `components.json` |
109
- | `get_component` | `name` | One component spec |
110
- | `get_brand_dna` | - | Brand DNA from a sibling file or `tokens['dsm-brand-dna']` |
111
- | `suggest_token` | `context` | Nearest-token suggestions for hex/px in a snippet. Deterministic |
112
- | `apply_token_patch` | `patch`, `confirm?` | Upsert/remove tokens. Dry-run unless `confirm: true` |
113
- | `generate_component_prompt` | `name` | Handoff prompt for a component plus its tokens |
114
- | `check_violations` | `file` | Lint a source file for hardcoded colors / arbitrary Tailwind |
115
- | `api_health` | - | `GET {api}/health` against a Flash DSM backend |
116
- | `list_design_systems` | - | List your systems from a Flash DSM backend (needs a token) |
175
+ | Tool | Args | Description |
176
+ | --------------------------- | ------------------------------------------- | ---------------------------------------------------------------- |
177
+ | `get_tokens` | `category?` | Read tokens from the local token file. Read-only |
178
+ | `list_components` | `category?` | List components from `components.json` |
179
+ | `get_component` | `name` | One component spec |
180
+ | `get_brand_dna` | - | Brand DNA from a sibling file or `tokens['dsm-brand-dna']` |
181
+ | `suggest_token` | `context` | Nearest-token suggestions for hex/px in a snippet. Deterministic |
182
+ | `apply_token_patch` | `patch`, `confirm?` | Upsert/remove tokens. Dry-run unless `confirm: true` |
183
+ | `generate_component_prompt` | `name` | Handoff prompt for a component plus its tokens |
184
+ | `check_violations` | `file` | Lint a source file for hardcoded colors / arbitrary Tailwind |
185
+ | `capture_project_shell` | `url?`, `designSystemId?`, `viewportWidth?` | Local preview capture → upload + attach (localhost OK) |
186
+ | `scan_usage` | `designSystemId?`, `countsOnly?`, `dryRun?` | Count where components are used; `dryRun` sends nothing |
187
+ | `api_health` | - | `GET {api}/health` against a Flash DSM backend |
188
+ | `list_design_systems` | - | List your systems from a Flash DSM backend (needs a token) |
117
189
 
118
190
  Writes are guarded: `apply_token_patch` is a dry-run unless you pass
119
191
  `confirm: true`, so a curious agent cannot rewrite your tokens by accident.
@@ -140,6 +212,90 @@ flash-genome --tokens ./design-tokens.json --components ./components.json
140
212
  Against a backend on your own machine, pass
141
213
  `--api-url http://localhost:8008`.
142
214
 
215
+ ### Sync commands: `pull`, `push`, `drift`
216
+
217
+ Scripts and CI need no MCP client. These run the same code as the MCP tools
218
+ `pull_from_web`, `push_to_web` and `detect_drift`, in the repo that holds
219
+ `genome.config.json` (run `link` first).
220
+
221
+ ```bash
222
+ flash-genome pull # print what would be written; writes nothing
223
+ flash-genome pull --confirm # write the token files, the mirror and genome.lock
224
+ flash-genome push # print the token edits that would be proposed
225
+ flash-genome push --confirm # open the change set for review in Flash
226
+ flash-genome drift # has the repo moved, has Flash, which tokens
227
+ flash-genome drift --json # the same, as one JSON object (for watchers)
228
+ flash-genome pull --cwd ../web # run against another repo root
229
+ ```
230
+
231
+ | Flag | Applies to | Meaning |
232
+ | ---------------------------------- | ---------- | -------------------------------------------------------------------------- |
233
+ | `--confirm` | pull, push | Write / propose. Without it the command is a dry run |
234
+ | `--json` | all three | Print one JSON object on stdout instead of the plan (errors too) |
235
+ | `--cwd` | all three | Repo root (default: the current directory); `--config` resolves against it |
236
+ | `--config`, `--api-url`, `--token` | all three | As in the table above |
237
+
238
+ The human output is the plan: `+ create`, `~ update`, `! flag` lines (a flag
239
+ is something a person must look at), and a count of the unchanged items.
240
+
241
+ **Exit codes**
242
+
243
+ | Code | Meaning |
244
+ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `0` | Done. A dry run that printed its plan is done. `drift` exits 0 whether or not it found drift: read the output |
246
+ | `1` | Refused or failed: `push` while Flash is ahead of the last pull, `push` or `drift` in a repo that never pulled, a `pull` where a file was flagged (refused or failed to write), or a network/server error |
247
+ | `2` | Auth or config: no token, a token Flash rejects (401/403), no `genome.config.json`, or a token for a different design system than the config names |
248
+
249
+ **`--json` output.** Always one object on stdout, always with `ok`, `command`,
250
+ `exitCode` (equal to the process exit code) and `schemaVersion` (currently
251
+ `1`; it moves only if a field is removed or changes meaning, fields are
252
+ otherwise only added).
253
+
254
+ - `pull` / `push`: `{ ok, command, exitCode, schemaVersion, result }`, where
255
+ `result` is the tool's own result (the change plan: `dryRun`, `entries`,
256
+ `counts`, plus `written` for pull, `status` / `changeSetId` for push).
257
+ - Any failure: `{ ok: false, command, exitCode, schemaVersion, error: { kind, message } }`
258
+ with `kind` one of `auth`, `config`, `refused`, `error`.
259
+ - `drift`:
260
+
261
+ ```json
262
+ {
263
+ "ok": true,
264
+ "command": "drift",
265
+ "exitCode": 0,
266
+ "schemaVersion": 1,
267
+ "genomeId": 42,
268
+ "state": "repo-dirty",
269
+ "neverPulled": false,
270
+ "flashAhead": false,
271
+ "dirty": true,
272
+ "pushable": 2,
273
+ "changedFiles": ["design-tokens.json"],
274
+ "missingFiles": [],
275
+ "changedTokens": [
276
+ {
277
+ "path": "color.brand-500 (dark)",
278
+ "before": "#2563eb",
279
+ "after": "#ff0000",
280
+ "code": "repo-edit"
281
+ }
282
+ ],
283
+ "pulledPublishedVersionId": 100,
284
+ "publishedVersionId": 100,
285
+ "pulledRevision": 7,
286
+ "revision": 7,
287
+ "summary": "The repo changed 2 token values. Run push_to_web to propose them for review in Flash."
288
+ }
289
+ ```
290
+
291
+ `state` is one of `in-step`, `flash-ahead` (Flash published since the pull),
292
+ `repo-dirty` (a pulled file changed or is missing), `both`, `never-pulled`.
293
+ `flashAhead` and `dirty` are the booleans behind it; `changedTokens` lists
294
+ the token values the repo changed (`code: "repo-edit"`, what `push` would
295
+ propose) and anything else a push does not carry (any other `code`).
296
+ `never-pulled` is the one `drift` result with exit code `1` and `ok: false`;
297
+ the object is still printed in full.
298
+
143
299
  ## Hosted alternative
144
300
 
145
301
  If your design system lives in Flash DSM, you may not need this package at all:
@@ -29,6 +29,7 @@ var TokenCategorySchema = z.enum([
29
29
  "icons"
30
30
  ]);
31
31
  var TokenStatusSchema = z.enum(["active", "deprecated", "legacy"]);
32
+ var ColorGroupTypeSchema = z.enum(["solid", "gradient"]);
32
33
  var TokenSchema = z.object({
33
34
  name: z.string(),
34
35
  value: z.string(),
@@ -43,6 +44,7 @@ var TokenSchema = z.object({
43
44
  foundationDetachedRowIds: z.array(z.string()).optional(),
44
45
  foundationServingRowIds: z.array(z.string()).optional(),
45
46
  metadata: z.record(z.any()).optional(),
47
+ responsive: z.record(z.string()).optional(),
46
48
  documentation: z.string().optional(),
47
49
  lastModified: z.number().optional(),
48
50
  lastSynced: z.number().optional(),
@@ -65,6 +67,7 @@ var TokenSchema = z.object({
65
67
  "focus"
66
68
  ]).optional(),
67
69
  referencedTokenId: z.string().optional(),
70
+ inheritsLight: z.boolean().optional(),
68
71
  tokenId: z.number().int().optional()
69
72
  });
70
73
  var DesignTokenSchema = z.object({
@@ -201,7 +204,10 @@ var AtomicBindingSchema = z2.object({
201
204
  variantId: z2.string().optional(),
202
205
  stateId: ComponentPreviewStateSchema.optional(),
203
206
  slot: BindingSlotSchema.or(z2.string().min(1)),
204
- tokenId: z2.string().min(1)
207
+ tokenId: z2.string().min(1),
208
+ /** The Dark side, when it differs from Light (`tokenId`); missing, Dark
209
+ * reads `tokenId`. */
210
+ darkTokenId: z2.string().min(1).optional()
205
211
  });
206
212
  var AtomicComponentProvenanceSchema = z2.object({
207
213
  figmaNodeId: z2.string().optional(),
@@ -339,357 +345,14 @@ var GenomeConfigSchema = z3.object({
339
345
  outputs: GenomeOutputsSchema.default({}),
340
346
  framework: z3.string().optional(),
341
347
  /** Absent only in configs written before codebase identity existed. */
342
- codebase: GenomeCodebaseSchema.optional()
343
- });
344
-
345
- // ../core/dist/change-plan.js
346
- import { z as z4 } from "zod";
347
- var ChangeActionSchema = z4.enum(["create", "update", "skip", "flag"]);
348
- var ChangeTargetKindSchema = z4.enum([
349
- "file",
350
- "token",
351
- "variable",
352
- "style",
353
- "component"
354
- ]);
355
- var ChangePlanEntrySchema = z4.object({
356
- action: ChangeActionSchema,
357
- targetKind: ChangeTargetKindSchema,
358
- /** Repo path, token name, variable path, style name, component id. */
359
- path: z4.string().min(1),
360
- /** The value on the target before; null when it did not exist. */
361
- before: z4.string().nullable().optional(),
362
- /** The value after; null when the entry removes the target. */
363
- after: z4.string().nullable().optional(),
364
- /** Why an entry is flagged or skipped, in a sentence a person can act on. */
365
- reason: z4.string().optional(),
366
- /** Machine code for the reason: `alias-missing`, `path-collision`, … */
367
- code: z4.string().optional()
368
- });
369
- var ChangePlanCountsSchema = z4.object({
370
- create: z4.number().int().nonnegative(),
371
- update: z4.number().int().nonnegative(),
372
- skip: z4.number().int().nonnegative(),
373
- flag: z4.number().int().nonnegative()
374
- });
375
- var ChangePlanSchema = z4.object({
376
- /** True when nothing was written; the entries say what would be. */
377
- dryRun: z4.boolean(),
378
- entries: z4.array(ChangePlanEntrySchema),
379
- counts: ChangePlanCountsSchema
348
+ codebase: GenomeCodebaseSchema.optional(),
349
+ /**
350
+ * Local or public preview URL for `capture_project_shell` (Storybook,
351
+ * Vite preview, etc.). Capture runs on the developer machine, so
352
+ * localhost is fine — Flash’s servers never fetch this URL.
353
+ */
354
+ previewUrl: z3.string().url().optional()
380
355
  });
381
- function countChangePlan(entries) {
382
- const counts = { create: 0, update: 0, skip: 0, flag: 0 };
383
- for (const entry of entries)
384
- counts[entry.action] += 1;
385
- return counts;
386
- }
387
- function makeChangePlan(entries, dryRun) {
388
- return { dryRun, entries: [...entries], counts: countChangePlan(entries) };
389
- }
390
- function tokenChangeToPlanEntry(change) {
391
- const base = {
392
- targetKind: "token",
393
- path: change.name,
394
- before: change.before ?? null,
395
- after: change.after ?? null
396
- };
397
- switch (change.kind) {
398
- case "added":
399
- return { ...base, action: "create" };
400
- case "changed":
401
- return { ...base, action: "update" };
402
- case "removed":
403
- return {
404
- ...base,
405
- action: "flag",
406
- code: "removed",
407
- reason: "Not in the incoming set; applying removes it from the system."
408
- };
409
- }
410
- }
411
-
412
- // ../core/dist/graph.js
413
- var FOUNDATION_ORDER = [
414
- "colors",
415
- "typography",
416
- "spacing",
417
- "radius",
418
- "shadows",
419
- "border",
420
- "motion",
421
- "effects",
422
- "icons"
423
- ];
424
- var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
425
- var str = (v) => typeof v === "string" && v ? v : void 0;
426
- var list = (v) => Array.isArray(v) ? v : [];
427
- var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
428
- function plural(n, one, many) {
429
- return `${n} ${n === 1 ? one : many}`;
430
- }
431
- function tokensOf(doc) {
432
- const out = /* @__PURE__ */ new Map();
433
- const foundations = isObject(doc.foundations) ? doc.foundations : {};
434
- const lists = [
435
- ...FOUNDATION_ORDER.map((key) => list(foundations[key])),
436
- list(doc.semantics)
437
- ];
438
- for (const tokens of lists) {
439
- for (const token of tokens) {
440
- if (!isObject(token))
441
- continue;
442
- const key = str(token.id) ?? str(token.name);
443
- if (key && !out.has(key))
444
- out.set(key, token);
445
- }
446
- }
447
- return out;
448
- }
449
- function resolver(tokens) {
450
- const byName = /* @__PURE__ */ new Map();
451
- const byLower = /* @__PURE__ */ new Map();
452
- for (const [key, token] of tokens) {
453
- const name = str(token.name);
454
- if (name && !byName.has(name))
455
- byName.set(name, key);
456
- for (const candidate of [key, name]) {
457
- if (candidate && !byLower.has(candidate.toLowerCase())) {
458
- byLower.set(candidate.toLowerCase(), key);
459
- }
460
- }
461
- }
462
- return (ref) => {
463
- const wanted = str(ref);
464
- if (!wanted)
465
- return void 0;
466
- if (tokens.has(wanted))
467
- return wanted;
468
- return byName.get(wanted) ?? byLower.get(wanted.toLowerCase());
469
- };
470
- }
471
- function edgesOf(doc, tokens, resolve2) {
472
- const seen = /* @__PURE__ */ new Set();
473
- const edges = [];
474
- const add = (source, target, type = "alias_of") => {
475
- if (!source || !target || source === target)
476
- return;
477
- const key = `${source}\0${target}`;
478
- if (seen.has(key))
479
- return;
480
- seen.add(key);
481
- edges.push({ source, target, type });
482
- };
483
- for (const relation of list(doc.relations?.tokens)) {
484
- if (!isObject(relation))
485
- continue;
486
- add(resolve2(relation.source), resolve2(relation.target), str(relation.type) ?? "alias_of");
487
- }
488
- for (const [key, token] of tokens) {
489
- add(key, resolve2(token.referencedTokenId));
490
- }
491
- return edges.sort((a, b) => a.source !== b.source ? a.source < b.source ? -1 : 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
492
- }
493
- function bindingsOf(owner) {
494
- const out = [];
495
- for (const binding of list(owner.bindings)) {
496
- if (!isObject(binding))
497
- continue;
498
- const slot = str(binding.slot);
499
- const tokenId = str(binding.tokenId);
500
- if (!slot || !tokenId)
501
- continue;
502
- const entry = { slot, tokenId };
503
- const variantId = str(binding.variantId);
504
- const stateId = str(binding.stateId);
505
- if (variantId)
506
- entry.variantId = variantId;
507
- if (stateId)
508
- entry.stateId = stateId;
509
- out.push(entry);
510
- }
511
- return out;
512
- }
513
- function ownersOf(items) {
514
- return items.filter((item) => isObject(item) && !!str(item.id) && !!str(item.name));
515
- }
516
- function tokenSummary(key, token) {
517
- const out = { id: key, name: str(token.name) ?? key };
518
- const layer = str(token.layer);
519
- const category = str(token.category);
520
- if (layer)
521
- out.layer = layer;
522
- if (category)
523
- out.category = category;
524
- return out;
525
- }
526
- function listModules(doc) {
527
- const components = new Map(ownersOf(list(doc.components)).map((c) => [str(c.id), str(c.name)]));
528
- return ownersOf(list(doc.modules)).map((module) => {
529
- const used = [];
530
- const seen = /* @__PURE__ */ new Set();
531
- const parts = list(module.parts).filter(isObject);
532
- for (const part of parts) {
533
- const id = str(part.componentId);
534
- if (!id || seen.has(id))
535
- continue;
536
- seen.add(id);
537
- used.push({ id, name: components.get(id) ?? null });
538
- }
539
- return {
540
- id: str(module.id),
541
- name: str(module.name),
542
- parts: parts.length,
543
- components: used,
544
- bindings: bindingsOf(module).length
545
- };
546
- });
547
- }
548
- function getModule(doc, ref) {
549
- const modules = ownersOf(list(doc.modules));
550
- const wanted = ref.trim().toLowerCase();
551
- const module = modules.find((m) => str(m.id) === ref) ?? modules.find((m) => str(m.name) === ref) ?? modules.find((m) => str(m.id).toLowerCase() === wanted || str(m.name).toLowerCase() === wanted);
552
- if (!module) {
553
- return {
554
- found: false,
555
- available: modules.map((m) => str(m.name))
556
- };
557
- }
558
- const components = new Map(ownersOf(list(doc.components)).map((c) => [str(c.id), str(c.name)]));
559
- const parts = list(module.parts).filter(isObject).flatMap((part) => {
560
- const slotId = str(part.slotId);
561
- const componentId = str(part.componentId);
562
- if (!slotId || !componentId)
563
- return [];
564
- const entry = {
565
- slotId,
566
- componentId,
567
- componentName: components.get(componentId) ?? null
568
- };
569
- const variantId = str(part.variantId);
570
- if (variantId)
571
- entry.variantId = variantId;
572
- return [entry];
573
- });
574
- return {
575
- found: true,
576
- module: {
577
- id: str(module.id),
578
- name: str(module.name),
579
- parts,
580
- bindings: bindingsOf(module)
581
- }
582
- };
583
- }
584
- function getRelations(doc, ref) {
585
- const tokens = tokensOf(doc);
586
- const resolve2 = resolver(tokens);
587
- const edges = edgesOf(doc, tokens, resolve2);
588
- const components = ownersOf(list(doc.components));
589
- const modules = ownersOf(list(doc.modules));
590
- if (ref === void 0 || ref === "") {
591
- const bindings = [];
592
- for (const [kind, owners2] of [
593
- ["component", components],
594
- ["module", modules]
595
- ]) {
596
- for (const owner of owners2) {
597
- for (const binding of bindingsOf(owner)) {
598
- bindings.push({ owner: kind, ownerId: str(owner.id), ...binding });
599
- }
600
- }
601
- }
602
- const parts = [];
603
- for (const module of modules) {
604
- for (const part of list(module.parts).filter(isObject)) {
605
- const slotId = str(part.slotId);
606
- const componentId = str(part.componentId);
607
- if (!slotId || !componentId)
608
- continue;
609
- parts.push({ moduleId: str(module.id), slotId, componentId });
610
- }
611
- }
612
- return { tokens: edges, bindings, parts };
613
- }
614
- const key = resolve2(ref);
615
- if (!key)
616
- return { found: false, token: ref };
617
- const boundHere = (owner) => bindingsOf(owner).filter((b) => resolve2(b.tokenId) === key);
618
- const owners = (items) => items.map((owner) => ({
619
- id: str(owner.id),
620
- name: str(owner.name),
621
- bindings: boundHere(owner)
622
- })).filter((o) => o.bindings.length).sort(byId);
623
- return {
624
- found: true,
625
- token: tokenSummary(key, tokens.get(key)),
626
- aliasOf: edges.filter((e) => e.source === key).map((e) => ({ id: e.target, type: e.type })),
627
- aliasedBy: edges.filter((e) => e.target === key).map((e) => ({ id: e.source, type: e.type })),
628
- components: owners(components),
629
- modules: owners(modules)
630
- };
631
- }
632
- function getImpact(doc, ref) {
633
- const tokens = tokensOf(doc);
634
- const resolve2 = resolver(tokens);
635
- const root = resolve2(ref);
636
- if (!root)
637
- return { found: false, token: ref };
638
- const edges = edgesOf(doc, tokens, resolve2);
639
- const pointedAtBy = /* @__PURE__ */ new Map();
640
- for (const edge of edges) {
641
- const sources = pointedAtBy.get(edge.target) ?? [];
642
- sources.push(edge.source);
643
- pointedAtBy.set(edge.target, sources);
644
- }
645
- const reached = /* @__PURE__ */ new Map();
646
- let frontier = [root];
647
- let depth = 0;
648
- while (frontier.length) {
649
- depth += 1;
650
- const next = [];
651
- for (const target of frontier) {
652
- for (const source of (pointedAtBy.get(target) ?? []).slice().sort()) {
653
- if (source === root || reached.has(source))
654
- continue;
655
- reached.set(source, { depth, via: target });
656
- next.push(source);
657
- }
658
- }
659
- frontier = next.sort();
660
- }
661
- const affected = /* @__PURE__ */ new Set([root, ...reached.keys()]);
662
- const hits = (owner) => bindingsOf(owner).filter((b) => {
663
- const key = resolve2(b.tokenId);
664
- return key !== void 0 && affected.has(key);
665
- });
666
- const components = ownersOf(list(doc.components)).map((c) => ({ id: str(c.id), name: str(c.name), bindings: hits(c) })).filter((c) => c.bindings.length).sort(byId);
667
- const affectedComponents = new Set(components.map((c) => c.id));
668
- const modules = ownersOf(list(doc.modules)).map((m) => {
669
- const reasons = hits(m).map((b) => ({ kind: "binding", ...b }));
670
- for (const part of list(m.parts).filter(isObject)) {
671
- const componentId = str(part.componentId);
672
- const slotId = str(part.slotId);
673
- if (componentId && slotId && affectedComponents.has(componentId)) {
674
- reasons.push({ kind: "part", slotId, componentId });
675
- }
676
- }
677
- return { id: str(m.id), name: str(m.name), reasons };
678
- }).filter((m) => m.reasons.length).sort(byId);
679
- const reachedTokens = [...reached.entries()].map(([id, r]) => ({
680
- ...tokenSummary(id, tokens.get(id)),
681
- depth: r.depth,
682
- via: r.via
683
- })).sort((a, b) => a.depth !== b.depth ? a.depth - b.depth : byId(a, b));
684
- return {
685
- found: true,
686
- token: tokenSummary(root, tokens.get(root)),
687
- tokens: reachedTokens,
688
- components,
689
- modules,
690
- summary: `Changing ${root} reaches ${plural(reachedTokens.length, "token", "tokens")}, ${plural(components.length, "component", "components")} and ${plural(modules.length, "module", "modules")}.`
691
- };
692
- }
693
356
 
694
357
  // src/load-tokens.ts
695
358
  import { readFile } from "fs/promises";
@@ -833,15 +496,9 @@ export {
833
496
  TokenCategorySchema,
834
497
  TokenSchema,
835
498
  parseFlashAtomicDocument,
836
- makeChangePlan,
837
- tokenChangeToPlanEntry,
838
499
  GENOME_CONFIG_FILENAME,
839
500
  LEGACY_CONFIG_FILENAME,
840
501
  GenomeConfigSchema,
841
- listModules,
842
- getModule,
843
- getRelations,
844
- getImpact,
845
502
  normalizeTokenDocument,
846
503
  configCandidates,
847
504
  resolveTokenFilePath,