modscape 2.0.4 → 2.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.
package/README.ja.md CHANGED
@@ -99,6 +99,7 @@ relationships – テーブル間のERカーディナリティ
99
99
  lineage – データの流れ / 変換パス
100
100
  annotations – キャンバス上のスティッキーノート・吹き出し
101
101
  layout – 全座標データ(tables/domains の中に x/y を書いてはいけない)
102
+ consumers – データの下流消費者(BIダッシュボード・MLモデル・アプリケーション等)
102
103
  ```
103
104
 
104
105
  ### Domains(ドメイン)
@@ -109,8 +110,7 @@ domains:
109
110
  name: "主要売上"
110
111
  description: "営業チームのトランザクションデータ。" # 任意
111
112
  color: "rgba(59, 130, 246, 0.1)" # 背景色
112
- tables: [orders, dim_customers] # 論理的な所属リスト
113
- isLocked: false # true にするとキャンバスでの誤ドラッグを防止
113
+ members: [orders, dim_customers] # 論理的な所属リスト
114
114
  ```
115
115
 
116
116
  ### Tables(テーブル)
@@ -197,6 +197,31 @@ relationships:
197
197
 
198
198
  > **ER関係** vs **リネージ**: 構造的な結合(外部キーなど)には `relationships` を、データの加工・変換の流れには `lineage` を使用してください。両方に同じ接続を記述しないでください。
199
199
 
200
+ ### Consumers(コンシューマー)
201
+
202
+ コンシューマーはデータモデルの下流消費者を表します。BIダッシュボード、MLモデル、アプリケーションなど、データを利用するあらゆるシステムを定義できます。キャンバス上に独自のノードとして表示され、リネージ矢印で接続されます。
203
+
204
+ ```yaml
205
+ consumers:
206
+ - id: revenue_dashboard # 一意のID — lineageやlayoutで使用
207
+ name: "Revenue Dashboard" # 表示名
208
+ description: "財務チーム向け月次KPIダッシュボード" # 任意
209
+ appearance:
210
+ icon: "📊" # 任意(デフォルト: 📊)
211
+ color: "#e0f2fe" # 任意のアクセントカラー
212
+ url: "https://bi.example.com/revenue" # 任意のリンク
213
+ ```
214
+
215
+ コンシューマーへのリネージは `lineage.to` にコンシューマーIDを指定します:
216
+
217
+ ```yaml
218
+ lineage:
219
+ - from: mart_monthly_revenue
220
+ to: revenue_dashboard # コンシューマーID
221
+ ```
222
+
223
+ テーブルと同様に、ドメインの `members` リストにも追加できます。
224
+
200
225
  ### Annotations(アノテーション)
201
226
 
202
227
  ```yaml
@@ -224,7 +249,6 @@ layout:
224
249
  y: 0
225
250
  width: 880
226
251
  height: 480
227
- isLocked: false # true でキャンバスのドラッグを防止
228
252
 
229
253
  # ドメイン内のテーブル – 座標はドメインの原点からの相対値
230
254
  orders:
@@ -265,6 +289,162 @@ modscape build ./models -o docs-site
265
289
  modscape export ./models -o docs/ARCHITECTURE.md
266
290
  ```
267
291
 
292
+ ---
293
+
294
+ ## dbt連携
295
+
296
+ 既存のdbtプロジェクトを `manifest.json` から直接インポートできます。
297
+
298
+ ### 事前準備
299
+
300
+ コマンドを実行する前に、dbtプロジェクトで `dbt parse`(または `target/manifest.json` を生成する任意のdbtコマンド)を実行してください。
301
+
302
+ ### dbtプロジェクトのインポート
303
+
304
+ ```bash
305
+ modscape dbt import [project-dir] [オプション]
306
+ ```
307
+
308
+ | オプション | 説明 |
309
+ |-----------|------|
310
+ | `-o, --output <dir>` | 出力ディレクトリ(デフォルト: `modscape-<プロジェクト名>`) |
311
+ | `--split-by <key>` | `schema`、`tag`、`folder` のいずれかでYAMLファイルを分割 |
312
+
313
+ **使用例:**
314
+
315
+ ```bash
316
+ # カレントディレクトリからインポート
317
+ modscape dbt import
318
+
319
+ # 特定のdbtプロジェクトパスを指定
320
+ modscape dbt import ./my_dbt_project
321
+
322
+ # スキーマ別にYAMLファイルを分割して出力
323
+ modscape dbt import --split-by schema
324
+
325
+ # dbtタグ別に分割し、出力先ディレクトリを指定
326
+ modscape dbt import --split-by tag -o ./modscape-models
327
+ ```
328
+
329
+ インポート後は以下でビジュアライザーを起動できます:
330
+ ```bash
331
+ modscape dev modscape-my_project
332
+ ```
333
+
334
+ > **インポートされる内容:** `manifest.json` 内の `model`、`seed`、`snapshot`、`source` ノード(カラム、説明文、`depends_on` によるリネージ含む)。
335
+ > **分割モード:** `--split-by` 指定時はグループごとに別YAMLファイルへ出力されます。自己完結率(self-contained rate)が80%未満のファイルは、クロスファイルのリネージ参照が単体では表示されないため注意してください。
336
+
337
+ ### dbt変更の同期
338
+
339
+ dbtプロジェクトを更新した後、既存のModscape YAMLファイルへ差分を反映できます。手動で追加したレイアウト・外観・アノテーション・リレーションシップは保持されます。
340
+
341
+ ```bash
342
+ modscape dbt sync [project-dir] [オプション]
343
+ ```
344
+
345
+ | オプション | 説明 |
346
+ |-----------|------|
347
+ | `-o, --output <dir>` | 同期対象のModscape YAMLが置かれたディレクトリ(デフォルト: `modscape-<プロジェクト名>`) |
348
+
349
+ ```bash
350
+ # カレントディレクトリのdbtプロジェクトを同期
351
+ modscape dbt sync
352
+
353
+ # パスを指定して同期
354
+ modscape dbt sync ./my_dbt_project -o ./modscape-models
355
+ ```
356
+
357
+ > **sync と import の違い:** `import` はYAMLをゼロから生成します。`sync` は既存ファイルを更新するため、手動で加えたテーブル種別・ビジネス定義・サンプルデータなどの情報が失われません。
358
+
359
+ ---
360
+
361
+ ## モデルファイル操作
362
+
363
+ ### YAMLファイルのマージ
364
+
365
+ 複数のYAMLモデルを1ファイルに統合します。テーブル/ドメインIDが重複した場合は先勝ちで処理されます。
366
+
367
+ ```bash
368
+ modscape merge model-a.yaml model-b.yaml -o merged.yaml
369
+
370
+ # ディレクトリ内のすべてのYAMLをマージ
371
+ modscape merge ./models -o merged.yaml
372
+ ```
373
+
374
+ ### テーブルの抽出
375
+
376
+ 特定のテーブル(関連するリレーションシップ・リネージも含む)を新しいYAMLファイルへ切り出します。
377
+
378
+ ```bash
379
+ modscape extract model.yaml --tables orders,dim_customers -o subset.yaml
380
+
381
+ # 複数ファイルから抽出
382
+ modscape extract ./models --tables fct_sales,dim_dates -o extracted.yaml
383
+ ```
384
+
385
+ ### 自動レイアウト
386
+
387
+ テーブルのリレーションシップをもとに、座標を自動計算してYAMLに書き込みます。
388
+
389
+ ```bash
390
+ modscape layout model.yaml
391
+
392
+ # 別ファイルに出力
393
+ modscape layout model.yaml -o model-with-layout.yaml
394
+ ```
395
+
396
+ ---
397
+
398
+ ## アトミックモデル操作コマンド
399
+
400
+ AIエージェントやスクリプトから、YAMLモデルファイルに対して精確な変更を加えるためのコマンドです。すべてのコマンドで `--json` オプションによる機械可読な出力が利用できます。
401
+
402
+ ### テーブルコマンド
403
+
404
+ ```bash
405
+ modscape table list <file> # テーブルID一覧を表示
406
+ modscape table get <file> --id <id> # 指定テーブルをJSONで取得
407
+ modscape table add <file> --data <json> # テーブルを追加
408
+ modscape table update <file> --id <id> --data <json> # テーブルを更新
409
+ modscape table remove <file> --id <id> # テーブルを削除
410
+ ```
411
+
412
+ ### カラムコマンド
413
+
414
+ ```bash
415
+ modscape column add <file> --table <id> --data <json>
416
+ modscape column update <file> --table <id> --id <col-id> --data <json>
417
+ modscape column remove <file> --table <id> --id <col-id>
418
+ ```
419
+
420
+ ### リレーションシップコマンド
421
+
422
+ ```bash
423
+ modscape relationship list <file>
424
+ modscape relationship add <file> --data <json>
425
+ modscape relationship remove <file> --index <n>
426
+ ```
427
+
428
+ ### リネージコマンド
429
+
430
+ ```bash
431
+ modscape lineage list <file>
432
+ modscape lineage add <file> --from <table-id> --to <table-id>
433
+ modscape lineage remove <file> --from <table-id> --to <table-id>
434
+ ```
435
+
436
+ ### ドメインコマンド
437
+
438
+ ```bash
439
+ modscape domain list <file>
440
+ modscape domain get <file> --id <id>
441
+ modscape domain add <file> --data <json>
442
+ modscape domain update <file> --id <id> --data <json>
443
+ modscape domain remove <file> --id <id>
444
+ modscape domain member add <file> --domain <id> --table <table-id>
445
+ modscape domain member remove <file> --domain <id> --table <table-id>
446
+ ```
447
+
268
448
  ## クレジット
269
449
 
270
450
  Modscape は以下の素晴らしいオープンソースプロジェクトによって支えられています:
package/README.md CHANGED
@@ -100,6 +100,7 @@ relationships – ER cardinality between tables
100
100
  lineage – data flow / transformation paths
101
101
  annotations – sticky notes / callouts on the canvas
102
102
  layout – ALL coordinate data (never put x/y inside tables or domains)
103
+ consumers – downstream consumers (BI dashboards, ML models, applications)
103
104
  ```
104
105
 
105
106
  ### Domains
@@ -110,8 +111,7 @@ domains:
110
111
  name: "Core Sales"
111
112
  description: "Transactional data for the sales team." # optional
112
113
  color: "rgba(59, 130, 246, 0.1)" # background fill
113
- tables: [orders, dim_customers] # logical membership
114
- isLocked: false # prevent accidental drag when true
114
+ members: [orders, dim_customers] # logical membership
115
115
  ```
116
116
 
117
117
  ### Tables
@@ -198,6 +198,31 @@ relationships:
198
198
 
199
199
  > **ER Relationships** vs **Lineage**: Use `relationships` for structural joins (FKs) and `lineage` for data flow (transformations). Do not duplicate them.
200
200
 
201
+ ### Consumers
202
+
203
+ Consumers represent the downstream users of your data model — BI dashboards, ML models, applications, or any other system that consumes the data. They appear as distinct nodes on the canvas and can receive lineage edges.
204
+
205
+ ```yaml
206
+ consumers:
207
+ - id: revenue_dashboard # unique ID — used in lineage and layout
208
+ name: "Revenue Dashboard" # display name
209
+ description: "Monthly KPI dashboard for the finance team." # optional
210
+ appearance:
211
+ icon: "📊" # optional (defaults to 📊)
212
+ color: "#e0f2fe" # optional accent color
213
+ url: "https://bi.example.com/revenue" # optional link
214
+ ```
215
+
216
+ Connect a consumer with lineage by using its `id` as the `to` field:
217
+
218
+ ```yaml
219
+ lineage:
220
+ - from: mart_monthly_revenue
221
+ to: revenue_dashboard # consumer ID
222
+ ```
223
+
224
+ Consumers can also be added to domain `members` lists just like tables.
225
+
201
226
  ### Annotations
202
227
 
203
228
  ```yaml
@@ -225,7 +250,6 @@ layout:
225
250
  y: 0
226
251
  width: 880
227
252
  height: 480
228
- isLocked: false # prevent drag in canvas
229
253
 
230
254
  # Table inside a domain – coordinates are relative to domain origin
231
255
  orders:
@@ -266,6 +290,162 @@ modscape build ./models -o docs-site
266
290
  modscape export ./models -o docs/ARCHITECTURE.md
267
291
  ```
268
292
 
293
+ ---
294
+
295
+ ## dbt Integration
296
+
297
+ Modscape can import your existing dbt project directly from its compiled `manifest.json`.
298
+
299
+ ### Prerequisites
300
+
301
+ Run `dbt parse` (or any dbt command that produces `target/manifest.json`) in your dbt project before using these commands.
302
+
303
+ ### Import dbt Project
304
+
305
+ ```bash
306
+ modscape dbt import [project-dir] [options]
307
+ ```
308
+
309
+ | Option | Description |
310
+ |--------|-------------|
311
+ | `-o, --output <dir>` | Output directory (default: `modscape-<project-name>`) |
312
+ | `--split-by <key>` | Split output files by `schema`, `tag`, or `folder` |
313
+
314
+ **Examples:**
315
+
316
+ ```bash
317
+ # Import from current directory, output to modscape-my_project/
318
+ modscape dbt import
319
+
320
+ # Import from a specific dbt project path
321
+ modscape dbt import ./my_dbt_project
322
+
323
+ # Split into separate YAML files by schema
324
+ modscape dbt import --split-by schema
325
+
326
+ # Split by dbt tag, with custom output directory
327
+ modscape dbt import --split-by tag -o ./modscape-models
328
+ ```
329
+
330
+ After import, visualize with:
331
+ ```bash
332
+ modscape dev modscape-my_project
333
+ ```
334
+
335
+ > **What gets imported:** All `model`, `seed`, `snapshot`, and `source` nodes from `manifest.json`, including columns, descriptions, and lineage (`depends_on`).
336
+ > **Split mode:** When `--split-by` is used, each group is written to a separate YAML file. A self-containment score is shown — files below 80% have cross-file lineage edges that won't render in isolation.
337
+
338
+ ### Sync dbt Changes
339
+
340
+ After modifying your dbt project, sync the changes into existing Modscape YAML files without losing any manual edits (layout, appearance, annotations, relationships):
341
+
342
+ ```bash
343
+ modscape dbt sync [project-dir] [options]
344
+ ```
345
+
346
+ | Option | Description |
347
+ |--------|-------------|
348
+ | `-o, --output <dir>` | Target directory containing existing Modscape YAML files (default: `modscape-<project-name>`) |
349
+
350
+ ```bash
351
+ # Sync changes from current dbt project
352
+ modscape dbt sync
353
+
354
+ # Sync from a specific path
355
+ modscape dbt sync ./my_dbt_project -o ./modscape-models
356
+ ```
357
+
358
+ > **sync vs import:** `import` creates YAML files from scratch; `sync` updates existing files, preserving your manual enrichments (table types, business definitions, sample data, etc.).
359
+
360
+ ---
361
+
362
+ ## Model File Operations
363
+
364
+ ### Merge YAML Files
365
+
366
+ Combine multiple YAML models into one. Duplicate table/domain IDs are resolved with first-wins semantics.
367
+
368
+ ```bash
369
+ modscape merge model-a.yaml model-b.yaml -o merged.yaml
370
+
371
+ # Merge all YAMLs in a directory
372
+ modscape merge ./models -o merged.yaml
373
+ ```
374
+
375
+ ### Extract Tables
376
+
377
+ Extract a subset of tables (and their relationships/lineage) into a new YAML file.
378
+
379
+ ```bash
380
+ modscape extract model.yaml --tables orders,dim_customers -o subset.yaml
381
+
382
+ # Extract from multiple files
383
+ modscape extract ./models --tables fct_sales,dim_dates -o extracted.yaml
384
+ ```
385
+
386
+ ### Auto-Layout
387
+
388
+ Automatically calculate and write layout coordinates based on table relationships.
389
+
390
+ ```bash
391
+ modscape layout model.yaml
392
+
393
+ # Write to a separate output file
394
+ modscape layout model.yaml -o model-with-layout.yaml
395
+ ```
396
+
397
+ ---
398
+
399
+ ## Atomic Model Mutation Commands
400
+
401
+ These commands let AI agents (or scripts) make precise, targeted changes to a YAML model file. All commands support `--json` for machine-readable output.
402
+
403
+ ### Table Commands
404
+
405
+ ```bash
406
+ modscape table list <file> # List all table IDs
407
+ modscape table get <file> --id <id> # Get a single table as JSON
408
+ modscape table add <file> --data <json> # Add a new table
409
+ modscape table update <file> --id <id> --data <json> # Update a table
410
+ modscape table remove <file> --id <id> # Remove a table
411
+ ```
412
+
413
+ ### Column Commands
414
+
415
+ ```bash
416
+ modscape column add <file> --table <id> --data <json>
417
+ modscape column update <file> --table <id> --id <col-id> --data <json>
418
+ modscape column remove <file> --table <id> --id <col-id>
419
+ ```
420
+
421
+ ### Relationship Commands
422
+
423
+ ```bash
424
+ modscape relationship list <file>
425
+ modscape relationship add <file> --data <json>
426
+ modscape relationship remove <file> --index <n>
427
+ ```
428
+
429
+ ### Lineage Commands
430
+
431
+ ```bash
432
+ modscape lineage list <file>
433
+ modscape lineage add <file> --from <table-id> --to <table-id>
434
+ modscape lineage remove <file> --from <table-id> --to <table-id>
435
+ ```
436
+
437
+ ### Domain Commands
438
+
439
+ ```bash
440
+ modscape domain list <file>
441
+ modscape domain get <file> --id <id>
442
+ modscape domain add <file> --data <json>
443
+ modscape domain update <file> --id <id> --data <json>
444
+ modscape domain remove <file> --id <id>
445
+ modscape domain member add <file> --domain <id> --table <table-id>
446
+ modscape domain member remove <file> --domain <id> --table <table-id>
447
+ ```
448
+
269
449
  ## Credits
270
450
 
271
451
  Modscape is made possible by these incredible open-source projects:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modscape",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Modscape: A YAML-driven data modeling visualizer CLI",
5
5
  "repository": {
6
6
  "type": "git",
package/src/layout.js CHANGED
@@ -28,7 +28,15 @@ export function applyLayout(inputPath, options = {}) {
28
28
  return;
29
29
  }
30
30
 
31
- console.log(` 🏗️ Calculating layout for ${schema.tables.length} tables...`);
31
+ const consumers = Array.isArray(schema.consumers) ? schema.consumers : [];
32
+ const totalNodes = schema.tables.length + consumers.length;
33
+ console.log(` 🏗️ Calculating layout for ${schema.tables.length} tables and ${consumers.length} consumers...`);
34
+
35
+ // Normalize domain members: support both `members` (new) and `tables` (legacy)
36
+ const domains = (schema.domains || []).map(d => ({
37
+ ...d,
38
+ members: Array.isArray(d.members) ? d.members : (Array.isArray(d.tables) ? d.tables : []),
39
+ }));
32
40
 
33
41
  // Initialize Dagre Graph
34
42
  const g = new dagre.graphlib.Graph({ compound: true });
@@ -43,11 +51,15 @@ export function applyLayout(inputPath, options = {}) {
43
51
 
44
52
  // 1. Add Tables
45
53
  schema.tables.forEach((table) => {
46
- // Standard table size in canvas units
47
54
  g.setNode(table.id, { width: 280, height: 160 });
48
55
  });
49
56
 
50
- // 2. Add Lineage Edges
57
+ // 2. Add Consumer nodes
58
+ consumers.forEach((uc) => {
59
+ g.setNode(uc.id, { width: 160, height: 60 });
60
+ });
61
+
62
+ // 3. Add Lineage Edges (tables and consumers)
51
63
  if (schema.lineage) {
52
64
  schema.lineage.forEach((edge) => {
53
65
  if (g.hasNode(edge.from) && g.hasNode(edge.to)) {
@@ -56,7 +68,7 @@ export function applyLayout(inputPath, options = {}) {
56
68
  });
57
69
  }
58
70
 
59
- // 3. Add ER Relationships
71
+ // 4. Add ER Relationships
60
72
  if (schema.relationships) {
61
73
  schema.relationships.forEach((rel) => {
62
74
  if (g.hasNode(rel.from.table) && g.hasNode(rel.to.table)) {
@@ -65,34 +77,32 @@ export function applyLayout(inputPath, options = {}) {
65
77
  });
66
78
  }
67
79
 
68
- // 4. Setup Domains
69
- const domainTableMap = new Map();
70
- if (schema.domains) {
71
- schema.domains.forEach((domain) => {
80
+ // 5. Setup Domains (members can be tables or consumers)
81
+ if (domains.length > 0) {
82
+ domains.forEach((domain) => {
72
83
  g.setNode(domain.id, { label: domain.name, cluster: true });
73
- domain.tables.forEach((tableId) => {
74
- if (g.hasNode(tableId)) {
75
- g.setParent(tableId, domain.id);
84
+ domain.members.forEach((memberId) => {
85
+ if (g.hasNode(memberId)) {
86
+ g.setParent(memberId, domain.id);
76
87
  }
77
88
  });
78
- domainTableMap.set(domain.id, domain.tables);
79
89
  });
80
90
  }
81
91
 
82
92
  // Execute Layout Calculation
83
93
  dagre.layout(g);
84
94
 
85
- // 5. Post-process: Convert Dagre results to Modscape Layout format
95
+ // 6. Post-process: Convert Dagre results to Modscape Layout format
86
96
  const newLayout = {};
87
97
  const PAD = 48;
88
98
  const TABLE_W = 280;
89
- const TABLE_H = 160; // Default height for layout estimation
99
+ const TABLE_H = 160;
90
100
  const GAP = 40;
91
101
 
92
102
  // Process Domains (Grid packing)
93
- if (schema.domains) {
94
- schema.domains.forEach(domain => {
95
- const members = domain.tables.filter(tid => g.hasNode(tid));
103
+ if (domains.length > 0) {
104
+ domains.forEach(domain => {
105
+ const members = domain.members.filter(mid => g.hasNode(mid));
96
106
  if (members.length === 0) return;
97
107
 
98
108
  // Sort by dagre rank (left -> right)
@@ -104,18 +114,17 @@ export function applyLayout(inputPath, options = {}) {
104
114
  const gridW = cols * (TABLE_W + GAP) - GAP;
105
115
  const gridH = rowCount * (TABLE_H + GAP) - GAP;
106
116
 
107
- // Anchor grid to dagre centroid
108
- const cx = members.reduce((s, tid) => s + g.node(tid).x, 0) / members.length;
109
- const cy = members.reduce((s, tid) => s + g.node(tid).y, 0) / members.length;
117
+ const cx = members.reduce((s, mid) => s + g.node(mid).x, 0) / members.length;
118
+ const cy = members.reduce((s, mid) => s + g.node(mid).y, 0) / members.length;
110
119
  const originX = cx - gridW / 2;
111
120
  const originY = cy - gridH / 2;
112
121
 
113
- members.forEach((tid, idx) => {
122
+ members.forEach((mid, idx) => {
114
123
  const col = idx % cols;
115
124
  const row = Math.floor(idx / cols);
116
125
  const xOff = col * (TABLE_W + GAP) + TABLE_W / 2;
117
126
  const yOff = row * (TABLE_H + GAP) + TABLE_H / 2;
118
- newLayout[tid] = {
127
+ newLayout[mid] = {
119
128
  x: Math.round(originX + xOff),
120
129
  y: Math.round(originY + yOff),
121
130
  parentId: domain.id
@@ -134,18 +143,30 @@ export function applyLayout(inputPath, options = {}) {
134
143
  }
135
144
 
136
145
  // Standalone Tables
137
- const domainTableIds = new Set(schema.domains?.flatMap(d => d.tables) ?? []);
146
+ const domainMemberIds = new Set(domains.flatMap(d => d.members));
138
147
  schema.tables.forEach(t => {
139
- if (!domainTableIds.has(t.id)) {
148
+ if (!domainMemberIds.has(t.id)) {
140
149
  const pos = g.node(t.id);
141
150
  newLayout[t.id] = { x: Math.round(pos.x), y: Math.round(pos.y) };
142
151
  }
143
152
  });
144
153
 
145
- // 6. Save back to YAML
154
+ // Standalone Usecases
155
+ consumers.forEach(uc => {
156
+ if (!domainMemberIds.has(uc.id)) {
157
+ const pos = g.node(uc.id);
158
+ if (pos) newLayout[uc.id] = { x: Math.round(pos.x), y: Math.round(pos.y) };
159
+ }
160
+ });
161
+
162
+ // 7. Save back to YAML (write members, not tables, for domains)
163
+ schema.domains = domains.map(d => {
164
+ const { members, ...rest } = d;
165
+ return { ...rest, members };
166
+ });
146
167
  schema.layout = newLayout;
147
168
  const outputPath = options.output ? path.resolve(process.cwd(), options.output) : absolutePath;
148
-
169
+
149
170
  fs.writeFileSync(outputPath, yaml.dump(schema, { indent: 2, lineWidth: -1, noRefs: true }), 'utf8');
150
171
  console.log(` ✅ Layout complete! Saved to: ${path.relative(process.cwd(), outputPath)}`);
151
172
  }