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/README.ja.md +6 -2
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/column.js +100 -0
- package/src/domain.js +149 -0
- package/src/extract.js +72 -0
- package/src/index.js +24 -2
- package/src/lineage.js +69 -0
- package/src/model-utils.js +53 -0
- package/src/relationship.js +93 -0
- package/src/table.js +118 -0
- package/src/templates/claude/modeling.md +13 -0
- package/src/templates/codex/modscape-modeling/SKILL.md +13 -0
- package/src/templates/gemini/modscape-modeling/SKILL.md +13 -0
- package/src/templates/rules.md +125 -16
- package/visualizer/package.json +1 -1
- package/visualizer-dist/assets/{index-BFdr345z.js → index-FOHZfUSs.js} +17 -17
- package/visualizer-dist/index.html +1 -1
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.
|
package/src/templates/rules.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
551
|
+
# top-level lineage also uses unique_id format
|
|
553
552
|
lineage:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
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.
|
|
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
|
-
##
|
|
738
|
+
## 16. Complete Example
|
|
630
739
|
|
|
631
740
|
```yaml
|
|
632
741
|
domains:
|