modscape 2.0.2 → 2.0.4

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/src/table.js ADDED
@@ -0,0 +1,118 @@
1
+ import { Command } from 'commander';
2
+ import { readYaml, writeYaml, findTableById, outputError, outputWarn, outputOk } from './model-utils.js';
3
+
4
+ export function tableCommand() {
5
+ const cmd = new Command('table').description('Manage tables in a YAML model');
6
+
7
+ // list
8
+ cmd
9
+ .command('list <file>')
10
+ .description('List all tables in the model')
11
+ .option('--json', 'output as JSON')
12
+ .action((file, opts) => {
13
+ const data = readYaml(file);
14
+ const tables = (data.tables || []).map(t => ({ id: t.id, name: t.name }));
15
+ if (opts.json) {
16
+ console.log(JSON.stringify(tables));
17
+ } else {
18
+ if (tables.length === 0) {
19
+ console.log(' (no tables)');
20
+ } else {
21
+ tables.forEach(t => console.log(` ${t.id} ${t.name || ''}`));
22
+ }
23
+ }
24
+ });
25
+
26
+ // get
27
+ cmd
28
+ .command('get <file>')
29
+ .description('Get a table definition by ID')
30
+ .requiredOption('--id <id>', 'table ID')
31
+ .option('--json', 'output as JSON')
32
+ .action((file, opts) => {
33
+ const data = readYaml(file);
34
+ const table = findTableById(data, opts.id);
35
+ if (!table) return outputError(opts.json, `Table "${opts.id}" not found`);
36
+ if (opts.json) {
37
+ console.log(JSON.stringify(table));
38
+ } else {
39
+ console.log(JSON.stringify(table, null, 2));
40
+ }
41
+ });
42
+
43
+ // add
44
+ cmd
45
+ .command('add <file>')
46
+ .description('Add a new table to the model')
47
+ .requiredOption('--id <id>', 'table ID (snake_case)')
48
+ .requiredOption('--name <name>', 'conceptual name')
49
+ .option('--type <type>', 'appearance type (fact|dimension|mart|hub|link|satellite|table)')
50
+ .option('--logical-name <name>', 'logical (business) name')
51
+ .option('--physical-name <name>', 'physical (database) table name')
52
+ .option('--description <text>', 'conceptual description')
53
+ .option('--json', 'output as JSON')
54
+ .action((file, opts) => {
55
+ const data = readYaml(file);
56
+ if (findTableById(data, opts.id)) {
57
+ return outputError(opts.json, `Table "${opts.id}" already exists`, 'Use `table update` instead');
58
+ }
59
+ const table = { id: opts.id, name: opts.name };
60
+ if (opts.logicalName) table.logical_name = opts.logicalName;
61
+ if (opts.physicalName) table.physical_name = opts.physicalName;
62
+ if (opts.type) table.appearance = { type: opts.type };
63
+ if (opts.description) table.conceptual = { description: opts.description };
64
+ if (!data.tables) data.tables = [];
65
+ data.tables.push(table);
66
+ writeYaml(file, data);
67
+ outputOk(opts.json, 'add', 'table', opts.id);
68
+ });
69
+
70
+ // update
71
+ cmd
72
+ .command('update <file>')
73
+ .description('Update fields of an existing table')
74
+ .requiredOption('--id <id>', 'table ID to update')
75
+ .option('--name <name>', 'conceptual name')
76
+ .option('--type <type>', 'appearance type')
77
+ .option('--logical-name <name>', 'logical (business) name')
78
+ .option('--physical-name <name>', 'physical (database) table name')
79
+ .option('--description <text>', 'conceptual description')
80
+ .option('--json', 'output as JSON')
81
+ .action((file, opts) => {
82
+ const data = readYaml(file);
83
+ const table = findTableById(data, opts.id);
84
+ if (!table) return outputError(opts.json, `Table "${opts.id}" not found`, 'Use `table add` instead');
85
+ if (opts.name) table.name = opts.name;
86
+ if (opts.logicalName) table.logical_name = opts.logicalName;
87
+ if (opts.physicalName) table.physical_name = opts.physicalName;
88
+ if (opts.type) {
89
+ table.appearance = table.appearance || {};
90
+ table.appearance.type = opts.type;
91
+ }
92
+ if (opts.description) {
93
+ table.conceptual = table.conceptual || {};
94
+ table.conceptual.description = opts.description;
95
+ }
96
+ writeYaml(file, data);
97
+ outputOk(opts.json, 'update', 'table', opts.id);
98
+ });
99
+
100
+ // remove
101
+ cmd
102
+ .command('remove <file>')
103
+ .description('Remove a table from the model')
104
+ .requiredOption('--id <id>', 'table ID to remove')
105
+ .option('--json', 'output as JSON')
106
+ .action((file, opts) => {
107
+ const data = readYaml(file);
108
+ const before = (data.tables || []).length;
109
+ data.tables = (data.tables || []).filter(t => t.id !== opts.id);
110
+ if (data.tables.length === before) {
111
+ return outputWarn(opts.json, `Table "${opts.id}" not found, nothing removed`);
112
+ }
113
+ writeYaml(file, data);
114
+ outputOk(opts.json, 'remove', 'table', opts.id);
115
+ });
116
+
117
+ return cmd;
118
+ }
@@ -5,6 +5,19 @@ Start an interactive data modeling session.
5
5
  2. SECOND, analyze the existing `model.yaml` if it exists.
6
6
  3. Listen to the user's requirements and propose/apply changes to `model.yaml` strictly following the rules.
7
7
 
8
+ ## Mutation CLI — Use Before Editing YAML Directly
9
+
10
+ For targeted changes to tables, columns, relationships, lineage, or domains, **PREFER the mutation CLI commands** over editing YAML directly. CLI commands validate input and write atomically.
11
+
12
+ Recommended flow:
13
+ 1. Check existence: `modscape table get model.yaml --id <id> --json`
14
+ 2. Add or update: `modscape table add` / `modscape table update`
15
+ 3. After adding tables: `modscape layout model.yaml` to assign coordinates
16
+
17
+ See Section 13 of `.modscape/rules.md` for the full command reference.
18
+
19
+ Only edit YAML directly for complex nested fields not covered by CLI flags (e.g., `implementation`, `sampleData`, full `columns` definition).
20
+
8
21
  ## Appearance & Layout
9
22
  - **Appearance**: For new tables, include the `appearance: { type: "..." }` block.
10
23
  - **Layout**: When creating new entities, always assign initial `x` and `y` coordinates in the `layout` section. Position them logically near their related entities to avoid stacking.
@@ -13,6 +13,19 @@ When the user issues this command:
13
13
  2. ANALYZE `model.yaml` (if present).
14
14
  3. INTERACT with the user to gather requirements and update the model strictly following the rules.
15
15
 
16
+ ## Mutation CLI — Use Before Editing YAML Directly
17
+
18
+ For targeted changes to tables, columns, relationships, lineage, or domains, **PREFER the mutation CLI commands** over editing YAML directly. CLI commands validate input and write atomically.
19
+
20
+ Recommended flow:
21
+ 1. Check existence: `modscape table get model.yaml --id <id> --json`
22
+ 2. Add or update: `modscape table add` / `modscape table update`
23
+ 3. After adding tables: `modscape layout model.yaml` to assign coordinates
24
+
25
+ See Section 13 of `.modscape/rules.md` for the full command reference.
26
+
27
+ Only edit YAML directly for complex nested fields not covered by CLI flags (e.g., `implementation`, `sampleData`, full `columns` definition).
28
+
16
29
  ## Appearance & Layout
17
30
  - **Appearance**: When creating new tables, include the `appearance` block with an appropriate `type`.
18
31
  - **Layout**: For any new entity, assign logical `x` and `y` coordinates in the `layout` section to prevent overlapping and ensure a clean initial visualization.
@@ -11,6 +11,19 @@ BEFORE making any suggestions or changes, you MUST read and strictly follow the
11
11
 
12
12
  If a requested change violates these rules, warn the user.
13
13
 
14
+ ## Mutation CLI — Use Before Editing YAML Directly
15
+
16
+ For targeted changes to tables, columns, relationships, lineage, or domains, **PREFER the mutation CLI commands** over editing YAML directly. CLI commands validate input and write atomically.
17
+
18
+ Recommended flow:
19
+ 1. Check existence: `modscape table get model.yaml --id <id> --json`
20
+ 2. Add or update: `modscape table add` / `modscape table update`
21
+ 3. After adding tables: `modscape layout model.yaml` to assign coordinates
22
+
23
+ See Section 13 of `.modscape/rules.md` for the full command reference.
24
+
25
+ Only edit YAML directly for complex nested fields not covered by CLI flags (e.g., `implementation`, `sampleData`, full `columns` definition).
26
+
14
27
  ## 📁 Multi-file Awareness
15
28
  `modscape dev` supports pointing to a directory (e.g., `modscape dev samples/`).
16
29
  - **Switching Models**: Identify which YAML file you are editing from the directory.
@@ -16,7 +16,7 @@ COORDINATES ONLY in `layout`. NEVER inside tables or domains.
16
16
  LINEAGE Use top-level `lineage` section (not relationships, not table.lineage.upstream).
17
17
  parentId Declare a table's domain membership inside layout, not inside domains.
18
18
  IDs Every object (table, domain, annotation) needs a unique `id`.
19
- sampleData First row = column IDs. At least 3 realistic data rows.
19
+ sampleData At least 3 realistic data rows. No header row.
20
20
  Grid All x/y values must be multiples of 40.
21
21
  ```
22
22
 
@@ -45,13 +45,12 @@ layout: # (object) ALL coordinates — REQUIRED if any objects exist
45
45
 
46
46
  | Field | Required | Description |
47
47
  |-------|----------|-------------|
48
- | `id` | **REQUIRED** | Unique identifier used as a key in `layout`, `domains.tables`, `lineage.upstream`, etc. Use snake_case. |
48
+ | `id` | **REQUIRED** | Unique identifier used as a key in `layout`, `domains.tables`, `lineage`, etc. Use snake_case. |
49
49
  | `name` | **REQUIRED** | Conceptual (business) name shown large on the canvas. |
50
50
  | `logical_name` | optional | Formal business name shown medium. Omit if same as `name`. |
51
51
  | `physical_name` | optional | Actual database table name shown small. |
52
52
  | `appearance` | optional | Visual type, icon, color. |
53
53
  | `conceptual` | optional | AI-friendly business context metadata. |
54
- | `lineage` | optional | Upstream table IDs. Only for `mart` / aggregated tables. |
55
54
  | `columns` | optional | Column definitions. |
56
55
  | `sampleData` | optional | 2D array of sample rows. Strongly recommended. |
57
56
 
@@ -72,7 +71,7 @@ appearance:
72
71
  |------|-------------|
73
72
  | `fact` | Events, transactions, measurements. Has measures (numbers) and FK columns. |
74
73
  | `dimension` | Entities, master data, reference lists. Descriptive attributes. |
75
- | `mart` | Aggregated or consumer-facing output. **Always add `lineage.upstream`.** |
74
+ | `mart` | Aggregated or consumer-facing output. **Always add a top-level `lineage` entry.** |
76
75
  | `hub` | Data Vault: stores a single unique business key. |
77
76
  | `link` | Data Vault: joins two or more hubs (transaction or relationship). |
78
77
  | `satellite` | Data Vault: descriptive attributes of a hub, tracked over time. |
@@ -96,7 +95,7 @@ Each column has an `id` plus optional `logical` and `physical` blocks.
96
95
 
97
96
  ```yaml
98
97
  columns:
99
- - id: order_id # REQUIRED. Unique within the table. Used in sampleData header.
98
+ - id: order_id # REQUIRED. Unique within the table.
100
99
  logical:
101
100
  name: "Order ID" # Display name
102
101
  type: Int # Int | String | Decimal | Date | Timestamp | Boolean | ...
@@ -138,7 +137,7 @@ relationships:
138
137
  | `many-to-one` | Fact → Dimension *(inverse notation of above)* |
139
138
  | `many-to-many` | Via a bridge / link table |
140
139
 
141
- **MUST NOT** use `relationships` to express data lineage (use `lineage.upstream` instead).
140
+ **MUST NOT** use `relationships` to express data lineage (use the top-level `lineage` section instead).
142
141
 
143
142
  ---
144
143
 
@@ -497,7 +496,7 @@ The command reads `target/manifest.json` and produces YAML with:
497
496
  | `physical_name` | `node.alias` | Falls back to `node.name` |
498
497
  | `conceptual.description` | `node.description` | From dbt docs |
499
498
  | `columns[].logical.name/type/description` | `node.columns` | From dbt schema.yml |
500
- | `lineage.upstream` | `node.depends_on.nodes` | Auto-populated |
499
+ | `lineage` (top-level) | `node.depends_on.nodes` | Auto-populated as `{from, to}` entries |
501
500
  | `appearance.type` | — | **Always `table`. Must be reclassified.** |
502
501
  | `sampleData` | — | **Not generated. Must be added.** |
503
502
  | `layout` | — | **Not generated. Must be added.** |
@@ -517,7 +516,7 @@ After running `modscape dbt import`, the generated YAML needs enrichment. AI age
517
516
 
518
517
  3. **Add `sampleData`** — The import does not generate sample data. Add at least 3 realistic rows per table.
519
518
 
520
- 4. **Do NOT re-generate `lineage.upstream`**It is already correctly populated from `depends_on.nodes`.
519
+ 4. **Do NOT re-generate `lineage` entries** Top-level `lineage` is already correctly populated from `depends_on.nodes`.
521
520
 
522
521
  ### 11-4. `dbt sync` — Incremental updates
523
522
 
@@ -527,7 +526,7 @@ Use `modscape dbt sync` when the dbt project has changed (new models, updated co
527
526
  - `name`, `logical_name`, `physical_name`
528
527
  - `conceptual.description`
529
528
  - `columns` (all)
530
- - `lineage.upstream`
529
+ - `lineage` (top-level)
531
530
 
532
531
  **What `sync` preserves (safe to edit manually):**
533
532
  - `appearance` (type, icon, color, scd)
@@ -549,14 +548,15 @@ id: "model.my_project.fct_orders"
549
548
  id: "source.my_project.raw.orders"
550
549
  id: "seed.my_project.product_categories"
551
550
 
552
- # lineage.upstream also uses unique_id format
551
+ # top-level lineage also uses unique_id format
553
552
  lineage:
554
- upstream:
555
- - "model.my_project.stg_orders"
556
- - "source.my_project.raw.customers"
553
+ - from: "model.my_project.stg_orders"
554
+ to: "model.my_project.fct_orders"
555
+ - from: "source.my_project.raw.customers"
556
+ to: "model.my_project.fct_orders"
557
557
  ```
558
558
 
559
- **MUST NOT** shorten these IDs. They are the join keys between `tables`, `domains.tables`, `lineage.upstream`, and `layout`.
559
+ **MUST NOT** shorten these IDs. They are the join keys between `tables`, `domains.tables`, `lineage`, and `layout`.
560
560
 
561
561
  ---
562
562
 
@@ -592,7 +592,116 @@ modscape merge ./sales ./marketing -o combined.yaml
592
592
 
593
593
  ---
594
594
 
595
- ## 13. Project-Specific Rule Extensions
595
+ ## 13. Model Mutation CLI
596
+
597
+ Use the built-in mutation commands to **add, update, or remove individual entities** in a YAML model. These commands validate input and write atomically — safer than editing YAML directly.
598
+
599
+ **MUST** use these commands when making targeted changes. Only edit YAML directly for complex nested fields not covered by CLI flags (e.g., `implementation`, `sampleData`, `columns` full definition).
600
+
601
+ ### 13-1. Available Operations
602
+
603
+ | Resource | Operations |
604
+ |----------|-----------|
605
+ | `table` | `list` `get` `add` `update` `remove` |
606
+ | `column` | `add` `update` `remove` |
607
+ | `relationship` | `list` `add` `remove` |
608
+ | `lineage` | `list` `add` `remove` |
609
+ | `domain` | `list` `get` `add` `update` `remove` |
610
+ | `domain member` | `add` `remove` |
611
+
612
+ ### 13-2. Recommended AI Agent Flow
613
+
614
+ When inspecting a model's current state, **prefer using the list/get commands** over reading the YAML file directly.
615
+ They return validated, structured JSON output that is easier to process.
616
+
617
+ ```bash
618
+ # Inspect current state before making changes
619
+ modscape table list model.yaml --json
620
+ modscape domain list model.yaml --json
621
+ modscape relationship list model.yaml --json
622
+ modscape lineage list model.yaml --json
623
+ ```
624
+
625
+ Before `add` or `update`, check existence with `get` or `list`:
626
+
627
+ ```bash
628
+ # 1. Check if table exists
629
+ modscape table get model.yaml --id fct_orders --json
630
+ # → found: use update / not found: use add
631
+
632
+ # 2a. Add new table
633
+ modscape table add model.yaml --id fct_orders --name "Orders" --type fact
634
+
635
+ # 2b. Update existing table
636
+ modscape table update model.yaml --id fct_orders --physical-name fct_sales_orders
637
+ ```
638
+
639
+ ### 13-3. CLI Flag Reference
640
+
641
+ **table add / update**
642
+ ```bash
643
+ modscape table add model.yaml \
644
+ --id <id> --name <name> \
645
+ [--type fact|dimension|mart|hub|link|satellite|table] \
646
+ [--logical-name <name>] [--physical-name <name>] \
647
+ [--description <text>] [--json]
648
+ ```
649
+
650
+ **column add / update**
651
+ ```bash
652
+ modscape column add model.yaml \
653
+ --table <tableId> --id <id> --name <name> \
654
+ [--type Int|String|Decimal|Date|Timestamp|Boolean] \
655
+ [--primary-key] [--foreign-key] \
656
+ [--physical-name <name>] [--physical-type <type>] [--json]
657
+ ```
658
+
659
+ **relationship add**
660
+ ```bash
661
+ modscape relationship add model.yaml \
662
+ --from <table.column> --to <table.column> \
663
+ --type one-to-one|one-to-many|many-to-one|many-to-many [--json]
664
+ ```
665
+
666
+ **lineage add**
667
+ ```bash
668
+ modscape lineage add model.yaml --from <tableId> --to <tableId> [--json]
669
+ ```
670
+
671
+ **domain add / update**
672
+ ```bash
673
+ modscape domain add model.yaml \
674
+ --id <id> --name <name> [--description <text>] [--color <color>] [--json]
675
+ ```
676
+
677
+ **domain member add / remove**
678
+ ```bash
679
+ modscape domain member add model.yaml --domain <domainId> --table <tableId> [--json]
680
+ modscape domain member remove model.yaml --domain <domainId> --table <tableId> [--json]
681
+ ```
682
+
683
+ ### 13-4. After Adding Tables
684
+
685
+ `table add` does **not** create layout coordinates. After adding tables, run:
686
+
687
+ ```bash
688
+ modscape layout model.yaml
689
+ ```
690
+
691
+ This assigns coordinates to all layout-less entries automatically.
692
+
693
+ ### 13-5. JSON Output for AI Pipelines
694
+
695
+ All commands support `--json` for machine-readable output:
696
+
697
+ ```json
698
+ { "ok": true, "action": "add", "resource": "table", "id": "fct_orders" }
699
+ { "ok": false, "error": "Table \"fct_orders\" already exists", "hint": "Use `table update` instead" }
700
+ ```
701
+
702
+ ---
703
+
704
+ ## 15. Project-Specific Rule Extensions
596
705
 
597
706
  A project MAY place a `.modscape/rules.custom.md` file to define rules that extend or override this base file.
598
707
 
@@ -626,7 +735,7 @@ A project MAY place a `.modscape/rules.custom.md` file to define rules that exte
626
735
 
627
736
  ---
628
737
 
629
- ## 14. Complete Example
738
+ ## 16. Complete Example
630
739
 
631
740
  ```yaml
632
741
  domains:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "visualizer",
3
3
  "private": true,
4
- "version": "2.0.2",
4
+ "version": "2.0.4",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",