@flashdsm/genome 0.1.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 CHANGED
@@ -13,18 +13,30 @@ 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
+ When your app's dev server is running, `connect` also leaves a live preview in the background (localhost:4747): Flash's editor shows your real app and recolours it as you edit tokens, through its CSS variables. Nothing is written to your code. `npx @flashdsm/genome@latest preview` starts it again; `preview --stop` ends it.
38
+
39
+ The link works once and expires after 15 minutes. `init --code` does the same with a setup code instead of the browser.
28
40
 
29
41
  ## Quick start
30
42
 
@@ -50,6 +62,38 @@ With no flags it looks for `./genome.config.json`, then the older
50
62
  `./components.json`, and finally falls back to a small bundled fixture so the
51
63
  server always boots. It logs which file it resolved to stderr.
52
64
 
65
+ ## Project shell preview
66
+
67
+ After `scan_workspace` creates a design system from local tokens, capture a
68
+ running preview so Flash can show **Your project** in the editor:
69
+
70
+ | Tool | What it does |
71
+ | ----------------------- | ------------------------------------------------------------------------- |
72
+ | `capture_project_shell` | Headless capture of a preview URL → upload + attach as `source: project` |
73
+ | `scan_usage` | Where the repo's own components are used → the editor's "In your product" |
74
+
75
+ **Localhost vs public URL:** Flash’s API cannot open `localhost` on your
76
+ laptop, so Genome always captures on your machine (Playwright). Pass
77
+ `http://localhost:6006/` (or any public `https://…` preview you can reach
78
+ locally). The API only stores the sanitised HTML+CSS — it never fetches the
79
+ URL for you. Full teammate loop: [docs/workflows/README.md](../../docs/workflows/README.md)
80
+ § E.
81
+
82
+ Set an optional default URL in `genome.config.json`:
83
+
84
+ ```json
85
+ {
86
+ "version": 2,
87
+ "genomeId": 42,
88
+ "name": "Acme Product",
89
+ "previewUrl": "http://localhost:6006/"
90
+ }
91
+ ```
92
+
93
+ Then call `capture_project_shell` with no `url`, or pass `url` and
94
+ `designSystemId` explicitly. Sign in with `flash-genome login` or
95
+ `FLASH_GENOME_TOKEN` (same credential as other hosted tools — never commit it).
96
+
53
97
  ## Keeping a repo in sync
54
98
 
55
99
  Once linked:
@@ -70,19 +114,61 @@ diff behind.
70
114
  | Tool | What it does |
71
115
  | -------------- | ----------------------------------------------------------------------------------------------------- |
72
116
  | `detect_drift` | Read-only. Whether the repo's pulled files moved, whether Flash moved, and which token values changed |
73
- | `push_to_web` | Proposes the changed token values to Flash as one change set, reviewed on the Sync page |
117
+ | `push_to_web` | Proposes what the code changed to Flash as one change set, reviewed on the Sync page |
118
+
119
+ `push_to_web` reads the code two ways and sends both in one change set:
120
+
121
+ - **The app's own stylesheets.** A file layered on Flash's `tokens.css`
122
+ counts: a light block, `--danger: var(--danger-400)`, new `--text-*`
123
+ sizes. The files the pull wrote (listed in `genome/genome.lock`) are
124
+ never read as code.
125
+ - **The pulled `design-tokens.json`.** Value edits, aliases re-pointed
126
+ (`{color.danger-400}`), tokens added, and a light or dark value for a
127
+ colour Flash holds in the other theme only.
74
128
 
75
- `push_to_web` reads the `design-tokens.json` the pull wrote, compares it
76
- with the design system as it is now, and proposes value changes to tokens
77
- Flash already has. Nothing is applied until someone with edit rights says
78
- yes. A token added, deleted or re-aliased in the file is flagged rather than
79
- sent: create, remove and re-point tokens in Flash, then pull.
129
+ Flash merges them with one set of rules. A `var(--step)` re-points the
130
+ alias. A colour equal to a ramp step points at that step; a colour no step
131
+ holds gets a step of its own (`neutral-150`) in both themes, so nothing in
132
+ Semantic becomes a bare hex. New names are added, a value Flash has no
133
+ theme for is added and said so, and nothing is deleted. The plan says why
134
+ for every row that is more than a new value.
135
+
136
+ Nothing is applied until someone with edit rights says yes. A token deleted
137
+ from the file is flagged, never sent as a delete.
80
138
 
81
139
  It is a dry run without `confirm: true`, and it refuses to push while Flash
82
140
  is ahead of `genome/genome.lock`, because a value left over from the last
83
141
  pull would otherwise look like an edit and undo someone's change. Pull,
84
142
  re-apply your edits, then push.
85
143
 
144
+ ### Tokens that change with the viewport
145
+
146
+ A system with breakpoints writes each size that moves at its widest step,
147
+ and lists every step beside it:
148
+
149
+ ```json
150
+ "type-h1": {
151
+ "$value": "48px",
152
+ "$type": "font",
153
+ "$extensions": {
154
+ "com.flashdsm.breakpoints": [
155
+ { "name": "phone", "minWidth": 0, "value": "36px" },
156
+ { "name": "tablet", "minWidth": 768, "value": "42px" },
157
+ { "name": "desktop", "minWidth": 1280, "value": "48px" }
158
+ ]
159
+ }
160
+ }
161
+ ```
162
+
163
+ `tokens.css` carries the same steps as media queries, so `var(--type-h1)`
164
+ is already responsive.
165
+
166
+ A push carries the narrowest step only, because that is the one value Flash
167
+ stores for the token. Change `phone` in the list and it is proposed like any
168
+ other edit. Change `$value` or a wider step and it is flagged: set those
169
+ sizes in Flash, then pull. A repo that pulled before Genome wrote steps is
170
+ told to pull again.
171
+
86
172
  `AGENTS.md` is written once and then left alone however far it drifts: it is
87
173
  where a team puts its own instructions. The Cursor rule is Genome's to keep
88
174
  current, because a stale rule file is worse than none.
@@ -102,18 +188,20 @@ name. No linked repo needed.
102
188
 
103
189
  ## Tools
104
190
 
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) |
191
+ | Tool | Args | Description |
192
+ | --------------------------- | ------------------------------------------- | ---------------------------------------------------------------- |
193
+ | `get_tokens` | `category?` | Read tokens from the local token file. Read-only |
194
+ | `list_components` | `category?` | List components from `components.json` |
195
+ | `get_component` | `name` | One component spec |
196
+ | `get_brand_dna` | - | Brand DNA from a sibling file or `tokens['dsm-brand-dna']` |
197
+ | `suggest_token` | `context` | Nearest-token suggestions for hex/px in a snippet. Deterministic |
198
+ | `apply_token_patch` | `patch`, `confirm?` | Upsert/remove tokens. Dry-run unless `confirm: true` |
199
+ | `generate_component_prompt` | `name` | Handoff prompt for a component plus its tokens |
200
+ | `check_violations` | `file` | Lint a source file for hardcoded colors / arbitrary Tailwind |
201
+ | `capture_project_shell` | `url?`, `designSystemId?`, `viewportWidth?` | Local preview capture → upload + attach (localhost OK) |
202
+ | `scan_usage` | `designSystemId?`, `countsOnly?`, `dryRun?` | Count where components are used; `dryRun` sends nothing |
203
+ | `api_health` | - | `GET {api}/health` against a Flash DSM backend |
204
+ | `list_design_systems` | - | List your systems from a Flash DSM backend (needs a token) |
117
205
 
118
206
  Writes are guarded: `apply_token_patch` is a dry-run unless you pass
119
207
  `confirm: true`, so a curious agent cannot rewrite your tokens by accident.
@@ -140,6 +228,90 @@ flash-genome --tokens ./design-tokens.json --components ./components.json
140
228
  Against a backend on your own machine, pass
141
229
  `--api-url http://localhost:8008`.
142
230
 
231
+ ### Sync commands: `pull`, `push`, `drift`
232
+
233
+ Scripts and CI need no MCP client. These run the same code as the MCP tools
234
+ `pull_from_web`, `push_to_web` and `detect_drift`, in the repo that holds
235
+ `genome.config.json` (run `link` first).
236
+
237
+ ```bash
238
+ flash-genome pull # print what would be written; writes nothing
239
+ flash-genome pull --confirm # write the token files, the mirror and genome.lock
240
+ flash-genome push # print what the code changed, as Flash would propose it
241
+ flash-genome push --confirm # open the change set for review in Flash
242
+ flash-genome drift # has the repo moved, has Flash, which tokens
243
+ flash-genome drift --json # the same, as one JSON object (for watchers)
244
+ flash-genome pull --cwd ../web # run against another repo root
245
+ ```
246
+
247
+ | Flag | Applies to | Meaning |
248
+ | ---------------------------------- | ---------- | -------------------------------------------------------------------------- |
249
+ | `--confirm` | pull, push | Write / propose. Without it the command is a dry run |
250
+ | `--json` | all three | Print one JSON object on stdout instead of the plan (errors too) |
251
+ | `--cwd` | all three | Repo root (default: the current directory); `--config` resolves against it |
252
+ | `--config`, `--api-url`, `--token` | all three | As in the table above |
253
+
254
+ The human output is the plan: `+ create`, `~ update`, `! flag` lines (a flag
255
+ is something a person must look at), and a count of the unchanged items.
256
+
257
+ **Exit codes**
258
+
259
+ | Code | Meaning |
260
+ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
261
+ | `0` | Done. A dry run that printed its plan is done. `drift` exits 0 whether or not it found drift: read the output |
262
+ | `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 |
263
+ | `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 |
264
+
265
+ **`--json` output.** Always one object on stdout, always with `ok`, `command`,
266
+ `exitCode` (equal to the process exit code) and `schemaVersion` (currently
267
+ `1`; it moves only if a field is removed or changes meaning, fields are
268
+ otherwise only added).
269
+
270
+ - `pull` / `push`: `{ ok, command, exitCode, schemaVersion, result }`, where
271
+ `result` is the tool's own result (the change plan: `dryRun`, `entries`,
272
+ `counts`, plus `written` for pull, `status` / `changeSetId` for push).
273
+ - Any failure: `{ ok: false, command, exitCode, schemaVersion, error: { kind, message } }`
274
+ with `kind` one of `auth`, `config`, `refused`, `error`.
275
+ - `drift`:
276
+
277
+ ```json
278
+ {
279
+ "ok": true,
280
+ "command": "drift",
281
+ "exitCode": 0,
282
+ "schemaVersion": 1,
283
+ "genomeId": 42,
284
+ "state": "repo-dirty",
285
+ "neverPulled": false,
286
+ "flashAhead": false,
287
+ "dirty": true,
288
+ "pushable": 2,
289
+ "changedFiles": ["design-tokens.json"],
290
+ "missingFiles": [],
291
+ "changedTokens": [
292
+ {
293
+ "path": "color.brand-500 (dark)",
294
+ "before": "#2563eb",
295
+ "after": "#ff0000",
296
+ "code": "repo-edit"
297
+ }
298
+ ],
299
+ "pulledPublishedVersionId": 100,
300
+ "publishedVersionId": 100,
301
+ "pulledRevision": 7,
302
+ "revision": 7,
303
+ "summary": "The repo changed 2 token values. Run push_to_web to propose them for review in Flash."
304
+ }
305
+ ```
306
+
307
+ `state` is one of `in-step`, `flash-ahead` (Flash published since the pull),
308
+ `repo-dirty` (a pulled file changed or is missing), `both`, `never-pulled`.
309
+ `flashAhead` and `dirty` are the booleans behind it; `changedTokens` lists
310
+ the token values the repo changed (`code: "repo-edit"`, what `push` would
311
+ propose) and anything else a push does not carry (any other `code`).
312
+ `never-pulled` is the one `drift` result with exit code `1` and `ok: false`;
313
+ the object is still printed in full.
314
+
143
315
  ## Hosted alternative
144
316
 
145
317
  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,10 @@ export {
833
496
  TokenCategorySchema,
834
497
  TokenSchema,
835
498
  parseFlashAtomicDocument,
836
- makeChangePlan,
837
- tokenChangeToPlanEntry,
838
499
  GENOME_CONFIG_FILENAME,
839
500
  LEGACY_CONFIG_FILENAME,
501
+ GenomeOutputsSchema,
840
502
  GenomeConfigSchema,
841
- listModules,
842
- getModule,
843
- getRelations,
844
- getImpact,
845
503
  normalizeTokenDocument,
846
504
  configCandidates,
847
505
  resolveTokenFilePath,