modscape 2.0.0 → 2.0.2
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 +14 -19
- package/README.md +16 -9
- package/package.json +2 -1
- package/src/index.js +10 -0
- package/src/layout.js +151 -0
- package/src/templates/claude/modeling.md +1 -1
- package/src/templates/codex/modscape-modeling/SKILL.md +2 -2
- package/src/templates/gemini/modscape-modeling/SKILL.md +2 -2
- package/src/templates/rules.md +38 -1
- package/visualizer/package.json +1 -1
- package/visualizer-dist/assets/{index-ChTOGXCm.js → index-BFdr345z.js} +51 -51
- package/visualizer-dist/index.html +1 -1
package/README.ja.md
CHANGED
|
@@ -92,6 +92,7 @@ YAMLのルートレベル構造は以下の通りです:
|
|
|
92
92
|
domains – 関連テーブルをまとめるビジュアルコンテナ
|
|
93
93
|
tables – 3階層メタデータを持つエンティティ定義
|
|
94
94
|
relationships – テーブル間のERカーディナリティ
|
|
95
|
+
lineage – データの流れ / 変換パス
|
|
95
96
|
annotations – キャンバス上のスティッキーノート・吹き出し
|
|
96
97
|
layout – 全座標データ(tables/domains の中に x/y を書いてはいけない)
|
|
97
98
|
```
|
|
@@ -104,7 +105,7 @@ domains:
|
|
|
104
105
|
name: "主要売上"
|
|
105
106
|
description: "営業チームのトランザクションデータ。" # 任意
|
|
106
107
|
color: "rgba(59, 130, 246, 0.1)" # 背景色
|
|
107
|
-
tables: [orders, dim_customers]
|
|
108
|
+
tables: [orders, dim_customers] # 論理的な所属リスト
|
|
108
109
|
isLocked: false # true にするとキャンバスでの誤ドラッグを防止
|
|
109
110
|
```
|
|
110
111
|
|
|
@@ -130,11 +131,6 @@ tables:
|
|
|
130
131
|
businessDefinitions:
|
|
131
132
|
revenue: "割引・返品後の純売上"
|
|
132
133
|
|
|
133
|
-
lineage: # mart/集計テーブルのみに定義
|
|
134
|
-
upstream:
|
|
135
|
-
- fct_sales
|
|
136
|
-
- dim_dates
|
|
137
|
-
|
|
138
134
|
implementation: # 任意 – AIコード生成へのヒント
|
|
139
135
|
materialization: incremental # table | view | incremental | ephemeral
|
|
140
136
|
incremental_strategy: merge # merge | append | delete+insert
|
|
@@ -165,23 +161,22 @@ tables:
|
|
|
165
161
|
type: "BIGINT"
|
|
166
162
|
constraints: [NOT NULL]
|
|
167
163
|
|
|
168
|
-
sampleData: # 2
|
|
169
|
-
- [order_id, amount, status]
|
|
164
|
+
sampleData: # 実数値の2次元配列
|
|
170
165
|
- [1001, 50.0, "COMPLETED"]
|
|
171
166
|
- [1002, 120.5, "PENDING"]
|
|
172
167
|
```
|
|
173
168
|
|
|
174
|
-
|
|
169
|
+
### Data Lineage(データリネージ)
|
|
170
|
+
|
|
171
|
+
ルートレベルの `lineage` セクションでテーブル間のデータの流れ(どのソースからどの集計テーブルが作られるか)を定義します。リネージモードではアニメーション付きの点線矢印として表示されます。
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
| `satellite` | Data Vault のハブに紐づく履歴属性 |
|
|
184
|
-
| `table` | 汎用 |
|
|
173
|
+
```yaml
|
|
174
|
+
lineage:
|
|
175
|
+
- from: fct_orders # ソーステーブル ID
|
|
176
|
+
to: mart_revenue # 派生テーブル ID
|
|
177
|
+
- from: dim_dates
|
|
178
|
+
to: mart_revenue
|
|
179
|
+
```
|
|
185
180
|
|
|
186
181
|
### Relationships(リレーションシップ)
|
|
187
182
|
|
|
@@ -196,7 +191,7 @@ relationships:
|
|
|
196
191
|
type: one-to-many # one-to-one | one-to-many | many-to-one | many-to-many
|
|
197
192
|
```
|
|
198
193
|
|
|
199
|
-
>
|
|
194
|
+
> **ER関係** vs **リネージ**: 構造的な結合(外部キーなど)には `relationships` を、データの加工・変換の流れには `lineage` を使用してください。両方に同じ接続を記述しないでください。
|
|
200
195
|
|
|
201
196
|
### Annotations(アノテーション)
|
|
202
197
|
|
package/README.md
CHANGED
|
@@ -95,6 +95,7 @@ Modscape uses a schema designed for data analysis contexts. The full YAML struct
|
|
|
95
95
|
domains – visual containers grouping related tables
|
|
96
96
|
tables – entity definitions with tri-layer metadata
|
|
97
97
|
relationships – ER cardinality between tables
|
|
98
|
+
lineage – data flow / transformation paths
|
|
98
99
|
annotations – sticky notes / callouts on the canvas
|
|
99
100
|
layout – ALL coordinate data (never put x/y inside tables or domains)
|
|
100
101
|
```
|
|
@@ -107,7 +108,7 @@ domains:
|
|
|
107
108
|
name: "Core Sales"
|
|
108
109
|
description: "Transactional data for the sales team." # optional
|
|
109
110
|
color: "rgba(59, 130, 246, 0.1)" # background fill
|
|
110
|
-
tables: [orders, dim_customers]
|
|
111
|
+
tables: [orders, dim_customers] # logical membership
|
|
111
112
|
isLocked: false # prevent accidental drag when true
|
|
112
113
|
```
|
|
113
114
|
|
|
@@ -133,11 +134,6 @@ tables:
|
|
|
133
134
|
businessDefinitions:
|
|
134
135
|
revenue: "Net revenue after discounts"
|
|
135
136
|
|
|
136
|
-
lineage: # for mart/aggregated tables only
|
|
137
|
-
upstream:
|
|
138
|
-
- fct_sales
|
|
139
|
-
- dim_dates
|
|
140
|
-
|
|
141
137
|
implementation: # optional – hints for AI code generation
|
|
142
138
|
materialization: incremental # table | view | incremental | ephemeral
|
|
143
139
|
incremental_strategy: merge # merge | append | delete+insert
|
|
@@ -168,12 +164,23 @@ tables:
|
|
|
168
164
|
type: "BIGINT"
|
|
169
165
|
constraints: [NOT NULL]
|
|
170
166
|
|
|
171
|
-
sampleData: # 2D array
|
|
172
|
-
- [order_id, amount, status]
|
|
167
|
+
sampleData: # 2D array of realistic values
|
|
173
168
|
- [1001, 50.0, "COMPLETED"]
|
|
174
169
|
- [1002, 120.5, "PENDING"]
|
|
175
170
|
```
|
|
176
171
|
|
|
172
|
+
### Data Lineage
|
|
173
|
+
|
|
174
|
+
Top-level `lineage` section declares data flow between tables (which source tables feed which derived tables). This is rendered as dashed arrows in **Lineage Mode**.
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
lineage:
|
|
178
|
+
- from: fct_orders # source table ID
|
|
179
|
+
to: mart_revenue # derived table ID
|
|
180
|
+
- from: dim_dates
|
|
181
|
+
to: mart_revenue
|
|
182
|
+
```
|
|
183
|
+
|
|
177
184
|
### Relationships
|
|
178
185
|
|
|
179
186
|
```yaml
|
|
@@ -187,7 +194,7 @@ relationships:
|
|
|
187
194
|
type: one-to-many # one-to-one | one-to-many | many-to-one | many-to-many
|
|
188
195
|
```
|
|
189
196
|
|
|
190
|
-
> **
|
|
197
|
+
> **ER Relationships** vs **Lineage**: Use `relationships` for structural joins (FKs) and `lineage` for data flow (transformations). Do not duplicate them.
|
|
191
198
|
|
|
192
199
|
### Annotations
|
|
193
200
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modscape",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Modscape: A YAML-driven data modeling visualizer CLI",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@inquirer/prompts": "^8.3.0",
|
|
30
30
|
"chokidar": "^5.0.0",
|
|
31
31
|
"commander": "^14.0.3",
|
|
32
|
+
"dagre": "^0.8.5",
|
|
32
33
|
"express": "^5.2.1",
|
|
33
34
|
"js-yaml": "^4.1.1",
|
|
34
35
|
"open": "^11.0.0",
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { exportModel } from './export.js';
|
|
|
10
10
|
import { createModel } from './create.js';
|
|
11
11
|
import { importDbt } from './import-dbt.js';
|
|
12
12
|
import { syncDbt } from './sync-dbt.js';
|
|
13
|
+
import { applyLayout } from './layout.js';
|
|
13
14
|
import { createRequire } from 'module';
|
|
14
15
|
import { mergeModels } from './merge.js';
|
|
15
16
|
|
|
@@ -104,4 +105,13 @@ program
|
|
|
104
105
|
mergeModels(paths, options);
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
program
|
|
109
|
+
.command('layout')
|
|
110
|
+
.description('Perform automatic layout calculation and update the YAML file')
|
|
111
|
+
.argument('<path>', 'path to the YAML model file')
|
|
112
|
+
.option('-o, --output <path>', 'output file path (defaults to overwriting input)')
|
|
113
|
+
.action((path, options) => {
|
|
114
|
+
applyLayout(path, options);
|
|
115
|
+
});
|
|
116
|
+
|
|
107
117
|
program.parse();
|
package/src/layout.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import dagre from 'dagre';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI-based layout engine for Modscape.
|
|
8
|
+
* Replicates the logic from the visualizer to ensure consistency.
|
|
9
|
+
*/
|
|
10
|
+
export function applyLayout(inputPath, options = {}) {
|
|
11
|
+
const absolutePath = path.resolve(process.cwd(), inputPath);
|
|
12
|
+
if (!fs.existsSync(absolutePath)) {
|
|
13
|
+
console.error(` ❌ File not found: ${inputPath}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const raw = fs.readFileSync(absolutePath, 'utf8');
|
|
18
|
+
let schema;
|
|
19
|
+
try {
|
|
20
|
+
schema = yaml.load(raw);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error(` ❌ Failed to parse YAML: ${e.message}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!schema || !schema.tables) {
|
|
27
|
+
console.error(' ❌ Invalid schema: No tables found.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(` 🏗️ Calculating layout for ${schema.tables.length} tables...`);
|
|
32
|
+
|
|
33
|
+
// Initialize Dagre Graph
|
|
34
|
+
const g = new dagre.graphlib.Graph({ compound: true });
|
|
35
|
+
g.setGraph({
|
|
36
|
+
rankdir: 'LR',
|
|
37
|
+
nodesep: 80,
|
|
38
|
+
ranksep: 200,
|
|
39
|
+
marginx: 80,
|
|
40
|
+
marginy: 80,
|
|
41
|
+
});
|
|
42
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
43
|
+
|
|
44
|
+
// 1. Add Tables
|
|
45
|
+
schema.tables.forEach((table) => {
|
|
46
|
+
// Standard table size in canvas units
|
|
47
|
+
g.setNode(table.id, { width: 280, height: 160 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 2. Add Lineage Edges
|
|
51
|
+
if (schema.lineage) {
|
|
52
|
+
schema.lineage.forEach((edge) => {
|
|
53
|
+
if (g.hasNode(edge.from) && g.hasNode(edge.to)) {
|
|
54
|
+
g.setEdge(edge.from, edge.to);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Add ER Relationships
|
|
60
|
+
if (schema.relationships) {
|
|
61
|
+
schema.relationships.forEach((rel) => {
|
|
62
|
+
if (g.hasNode(rel.from.table) && g.hasNode(rel.to.table)) {
|
|
63
|
+
g.setEdge(rel.from.table, rel.to.table);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Setup Domains
|
|
69
|
+
const domainTableMap = new Map();
|
|
70
|
+
if (schema.domains) {
|
|
71
|
+
schema.domains.forEach((domain) => {
|
|
72
|
+
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);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
domainTableMap.set(domain.id, domain.tables);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Execute Layout Calculation
|
|
83
|
+
dagre.layout(g);
|
|
84
|
+
|
|
85
|
+
// 5. Post-process: Convert Dagre results to Modscape Layout format
|
|
86
|
+
const newLayout = {};
|
|
87
|
+
const PAD = 48;
|
|
88
|
+
const TABLE_W = 280;
|
|
89
|
+
const TABLE_H = 160; // Default height for layout estimation
|
|
90
|
+
const GAP = 40;
|
|
91
|
+
|
|
92
|
+
// Process Domains (Grid packing)
|
|
93
|
+
if (schema.domains) {
|
|
94
|
+
schema.domains.forEach(domain => {
|
|
95
|
+
const members = domain.tables.filter(tid => g.hasNode(tid));
|
|
96
|
+
if (members.length === 0) return;
|
|
97
|
+
|
|
98
|
+
// Sort by dagre rank (left -> right)
|
|
99
|
+
members.sort((a, b) => g.node(a).x - g.node(b).x);
|
|
100
|
+
|
|
101
|
+
const cols = Math.min(3, Math.ceil(Math.sqrt(members.length)));
|
|
102
|
+
const rowCount = Math.ceil(members.length / cols);
|
|
103
|
+
|
|
104
|
+
const gridW = cols * (TABLE_W + GAP) - GAP;
|
|
105
|
+
const gridH = rowCount * (TABLE_H + GAP) - GAP;
|
|
106
|
+
|
|
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;
|
|
110
|
+
const originX = cx - gridW / 2;
|
|
111
|
+
const originY = cy - gridH / 2;
|
|
112
|
+
|
|
113
|
+
members.forEach((tid, idx) => {
|
|
114
|
+
const col = idx % cols;
|
|
115
|
+
const row = Math.floor(idx / cols);
|
|
116
|
+
const xOff = col * (TABLE_W + GAP) + TABLE_W / 2;
|
|
117
|
+
const yOff = row * (TABLE_H + GAP) + TABLE_H / 2;
|
|
118
|
+
newLayout[tid] = {
|
|
119
|
+
x: Math.round(originX + xOff),
|
|
120
|
+
y: Math.round(originY + yOff),
|
|
121
|
+
parentId: domain.id
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Domain bounding box
|
|
126
|
+
const HEADER = 28;
|
|
127
|
+
newLayout[domain.id] = {
|
|
128
|
+
x: Math.round(originX - PAD),
|
|
129
|
+
y: Math.round(originY - PAD - HEADER),
|
|
130
|
+
width: Math.round(gridW + PAD * 2),
|
|
131
|
+
height: Math.round(gridH + PAD * 2 + HEADER)
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Standalone Tables
|
|
137
|
+
const domainTableIds = new Set(schema.domains?.flatMap(d => d.tables) ?? []);
|
|
138
|
+
schema.tables.forEach(t => {
|
|
139
|
+
if (!domainTableIds.has(t.id)) {
|
|
140
|
+
const pos = g.node(t.id);
|
|
141
|
+
newLayout[t.id] = { x: Math.round(pos.x), y: Math.round(pos.y) };
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 6. Save back to YAML
|
|
146
|
+
schema.layout = newLayout;
|
|
147
|
+
const outputPath = options.output ? path.resolve(process.cwd(), options.output) : absolutePath;
|
|
148
|
+
|
|
149
|
+
fs.writeFileSync(outputPath, yaml.dump(schema, { indent: 2, lineWidth: -1, noRefs: true }), 'utf8');
|
|
150
|
+
console.log(` ✅ Layout complete! Saved to: ${path.relative(process.cwd(), outputPath)}`);
|
|
151
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Start an interactive data modeling session.
|
|
2
2
|
|
|
3
3
|
## Instructions
|
|
4
|
-
1. FIRST, read `.modscape/rules.md` to understand the project strategy, naming conventions, and YAML schema.
|
|
4
|
+
1. FIRST, read `.modscape/rules.md` to understand the project strategy, naming conventions, and YAML schema. If `.modscape/rules.custom.md` exists, read it too — custom rules take priority over the base rules.
|
|
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
|
|
|
@@ -9,7 +9,7 @@ You are a professional Data Modeler. Your primary directive is to manage `model.
|
|
|
9
9
|
|
|
10
10
|
## COMMAND: /modscape:modeling
|
|
11
11
|
When the user issues this command:
|
|
12
|
-
1. READ `.modscape/rules.md` to understand project strategy and conventions.
|
|
12
|
+
1. READ `.modscape/rules.md` to understand project strategy and conventions. If `.modscape/rules.custom.md` exists, read it too — custom rules take priority over the base rules.
|
|
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
|
|
|
@@ -17,7 +17,7 @@ When the user issues this command:
|
|
|
17
17
|
- **Appearance**: When creating new tables, include the `appearance` block with an appropriate `type`.
|
|
18
18
|
- **Layout**: For any new entity, assign logical `x` and `y` coordinates in the `layout` section to prevent overlapping and ensure a clean initial visualization.
|
|
19
19
|
|
|
20
|
-
ALWAYS follow the rules defined in `.modscape/rules.md` for any modeling tasks.
|
|
20
|
+
ALWAYS follow the rules defined in `.modscape/rules.md` (and `.modscape/rules.custom.md` if present) for any modeling tasks.
|
|
21
21
|
|
|
22
22
|
## COMMAND: /modscape:codegen
|
|
23
23
|
When the user issues this command:
|
|
@@ -7,7 +7,7 @@ description: Create the data model defined in `model.yaml` according to project
|
|
|
7
7
|
|
|
8
8
|
You are a professional Data Modeler. Your primary directive is to manage `model.yaml`.
|
|
9
9
|
|
|
10
|
-
BEFORE making any suggestions or changes, you MUST read and strictly follow the rules defined in `.modscape/rules.md`.
|
|
10
|
+
BEFORE making any suggestions or changes, you MUST read and strictly follow the rules defined in `.modscape/rules.md`. If `.modscape/rules.custom.md` exists, read it too — custom rules take priority over the base rules.
|
|
11
11
|
|
|
12
12
|
If a requested change violates these rules, warn the user.
|
|
13
13
|
|
|
@@ -24,4 +24,4 @@ If a requested change violates these rules, warn the user.
|
|
|
24
24
|
## Interactive Modeling
|
|
25
25
|
When the user wants to perform modeling tasks, ensure you are utilizing the strategy and conventions defined in the project rules.
|
|
26
26
|
|
|
27
|
-
ALWAYS follow the rules defined in `.modscape/rules.md` for any modeling tasks.
|
|
27
|
+
ALWAYS follow the rules defined in `.modscape/rules.md` (and `.modscape/rules.custom.md` if present) for any modeling tasks.
|
package/src/templates/rules.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
> **Purpose**: This file teaches AI agents how to write valid `model.yaml` files for Modscape.
|
|
4
4
|
> Read this file completely before generating or editing any YAML.
|
|
5
5
|
|
|
6
|
+
> **Extension**: If `.modscape/rules.custom.md` exists in this project, read it **in addition** to this file.
|
|
7
|
+
> Rules in `rules.custom.md` take **priority** over this file when they conflict.
|
|
8
|
+
|
|
6
9
|
---
|
|
7
10
|
|
|
8
11
|
## QUICK REFERENCE (read this first)
|
|
@@ -589,7 +592,41 @@ modscape merge ./sales ./marketing -o combined.yaml
|
|
|
589
592
|
|
|
590
593
|
---
|
|
591
594
|
|
|
592
|
-
## 13.
|
|
595
|
+
## 13. Project-Specific Rule Extensions
|
|
596
|
+
|
|
597
|
+
A project MAY place a `.modscape/rules.custom.md` file to define rules that extend or override this base file.
|
|
598
|
+
|
|
599
|
+
**How to use it:**
|
|
600
|
+
|
|
601
|
+
- Create `.modscape/rules.custom.md` in the project root (alongside `rules.md`)
|
|
602
|
+
- Write any project-specific rules, naming conventions, or overrides in Markdown
|
|
603
|
+
- AI agents reading this file will also check for `rules.custom.md` and apply it
|
|
604
|
+
|
|
605
|
+
**Priority:** Rules in `rules.custom.md` take priority over this file when they conflict.
|
|
606
|
+
|
|
607
|
+
**What to put in `rules.custom.md`** (examples):
|
|
608
|
+
|
|
609
|
+
```markdown
|
|
610
|
+
## Naming Conventions
|
|
611
|
+
- All fact table IDs must use the prefix `fct_` followed by the domain: e.g., `fct_sales_orders`
|
|
612
|
+
- All dimension table IDs must end in `_dim`: e.g., `customers_dim`
|
|
613
|
+
|
|
614
|
+
## Allowed Table Types
|
|
615
|
+
- This project only uses `fact`, `dimension`, and `mart`. Do NOT use `hub`, `link`, or `satellite`.
|
|
616
|
+
|
|
617
|
+
## Domain Topology
|
|
618
|
+
- This project has three domains: `sales`, `marketing`, `finance`
|
|
619
|
+
- Every new table MUST be assigned to one of these domains
|
|
620
|
+
|
|
621
|
+
## SCD Policy
|
|
622
|
+
- Dimension tables use SCD Type 1 only. Do NOT apply `scd: type2` or higher.
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
`rules.custom.md` is NOT generated by `modscape init`. Create it manually when your project needs it.
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## 14. Complete Example
|
|
593
630
|
|
|
594
631
|
```yaml
|
|
595
632
|
domains:
|