modscape 0.1.2 → 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
@@ -1,4 +1,10 @@
1
- # <img src="./visualizer-dist/favicon.svg" width="32" height="32" align="center" /> Modscape
1
+ # <img src="./visualizer/public/favicon.svg" width="32" height="32" align="center" /> Modscape
2
+
3
+ [![npm version](https://img.shields.io/npm/v/modscape.svg?style=flat-square)](https://www.npmjs.com/package/modscape)
4
+ [![npm downloads](https://img.shields.io/npm/dm/modscape.svg?style=flat-square)](https://www.npmjs.com/package/modscape)
5
+ [![Deploy Demo](https://github.com/yujikawa/modscape/actions/workflows/deploy.yml/badge.svg)](https://github.com/yujikawa/modscape/actions/workflows/deploy.yml)
6
+ [![Publish to NPM](https://github.com/yujikawa/modscape/actions/workflows/publish.yml/badge.svg)](https://github.com/yujikawa/modscape/actions/workflows/publish.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
2
8
 
3
9
  Modscape is a YAML-driven data modeling visualizer. It helps data engineers and architects bridge the gap between conceptual, logical, and physical data models while maintaining sample data "stories".
4
10
 
@@ -11,6 +17,7 @@ Modscape is a YAML-driven data modeling visualizer. It helps data engineers and
11
17
  - **Sample Data "Stories"**: Attach sample data to entities to explain the data's purpose.
12
18
  - **Interactive Layout**: Arrange entities via drag-and-drop; positions are saved directly back to your YAML.
13
19
  - **Multi-file Support**: Manage multiple models in a single directory and switch between them seamlessly.
20
+ - **Documentation Export**: Generate Mermaid-compatible Markdown documentation including ER diagrams and domain catalogs.
14
21
  - **AI-Agent Ready**: Scaffolding for Gemini, Claude, and Codex to help you model via AI.
15
22
 
16
23
  ## Installation
@@ -21,29 +28,79 @@ Install Modscape globally via npm:
21
28
  npm install -g modscape
22
29
  ```
23
30
 
24
- ## Quick Start in 3 Steps
25
-
26
- ### 1. Initialize your project
27
- Create the necessary configuration and modeling rules for your project (and AI agents).
28
-
29
- ```bash
30
- modscape init
31
+ ---
32
+
33
+ ## Getting Started
34
+
35
+ Choose the path that fits your workflow.
36
+
37
+ ### Path A: AI-Driven Modeling (Recommended)
38
+ Best if you use AI coding assistants (Gemini CLI, Claude Code, Cursor/Codex).
39
+
40
+ 1. **Initialize**: Scaffold modeling rules and AI agent instructions.
41
+ ```bash
42
+ modscape init
43
+ ```
44
+ 2. **Start Dev**: Launch the visualizer on your model.
45
+ ```bash
46
+ modscape dev model.yaml
47
+ ```
48
+ 3. **Prompt Your AI**: Tell your agent to use the rules in `.modscape/rules.md` to add tables or columns to your `model.yaml`.
49
+
50
+ ### Path B: Manual Modeling
51
+ Best for direct YAML editing and architectural control.
52
+
53
+ 1. **Create YAML**: Create a file named `model.yaml` (see [Defining Your Model](#defining-your-model-yaml)).
54
+ 2. **Start Dev**: Launch the visualizer.
55
+ ```bash
56
+ modscape dev model.yaml
57
+ ```
58
+ 3. **Explore Samples**: Try it out with our built-in samples:
59
+ ```bash
60
+ # Clone the repo or download the samples directory
61
+ modscape dev samples/
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Defining Your Model (YAML)
67
+
68
+ Modscape uses a human-readable YAML schema. While you can write it manually, we **highly recommend using an AI coding assistant** (like Gemini, Claude, or Cursor) to handle the boilerplate.
69
+
70
+ ### Manual Definition Reference
71
+ Here is the basic structure for your `model.yaml`:
72
+
73
+ ```yaml
74
+ tables:
75
+ - id: users
76
+ name: USERS
77
+ appearance:
78
+ type: dimension # dimension | fact | hub | link | satellite
79
+ icon: 👤
80
+ columns:
81
+ - id: user_id
82
+ logical: { name: USER_ID, type: UUID, isPrimaryKey: true }
83
+ - id: email
84
+ logical: { name: EMAIL, type: String }
85
+
86
+ # Data Vault Example
87
+ - id: hub_customer
88
+ name: HUB_CUSTOMER
89
+ appearance: { type: hub, icon: 🔑 }
90
+ columns:
91
+ - id: hk_customer
92
+ logical: { name: HK_CUSTOMER, type: Binary, isPrimaryKey: true }
93
+ - id: customer_id
94
+ logical: { name: CUSTOMER_ID, type: String }
31
95
  ```
32
96
 
33
- ### 2. Explore Samples
34
- Try Modscape immediately using the built-in sample models.
97
+ - **Appearance Types**:
98
+ - `dimension` / `fact`: For Star Schema / Dimensional modeling.
99
+ - `hub` / `link` / `satellite`: For Data Vault 2.0 modeling.
100
+ - **IDs**: Use lowercase, snake_case for `id` fields (used for internal linking).
101
+ - **Layout**: Don't worry about the `layout:` section. Modscape will automatically add and update coordinates when you drag entities in the browser.
35
102
 
36
- ```bash
37
- # Clone the repo or download the samples directory to try this:
38
- modscape dev samples/
39
- ```
40
-
41
- ### 3. Start Modeling
42
- Create a `model.yaml` and launch the interactive visualizer.
43
-
44
- ```bash
45
- modscape dev model.yaml
46
- ```
103
+ ---
47
104
 
48
105
  ## Usage
49
106
 
@@ -68,6 +125,17 @@ Generate a standalone static website to share your documentation (perfect for Gi
68
125
  modscape build models/ -o dist-site
69
126
  ```
70
127
 
128
+ ### Export Mode (Static Documentation)
129
+ Generate a comprehensive Markdown document with embedded Mermaid diagrams.
130
+
131
+ ```bash
132
+ # Print to stdout
133
+ modscape export models/ecommerce.yaml
134
+
135
+ # Save to a file
136
+ modscape export models/ -o docs/
137
+ ```
138
+
71
139
  ## AI Agent Integration
72
140
 
73
141
  Modscape is designed to work alongside AI coding assistants. By running `modscape init`, you get:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modscape",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Modscape: A YAML-driven data modeling visualizer CLI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,7 @@
13
13
  "files": [
14
14
  "src",
15
15
  "visualizer-dist",
16
+ "visualizer/public/favicon.svg",
16
17
  "package.json",
17
18
  "README.md"
18
19
  ],
package/src/export.js ADDED
@@ -0,0 +1,193 @@
1
+ import fs from 'fs';
2
+ import yaml from 'js-yaml';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Normalizes the schema data to ensure it's in a consistent format.
7
+ * Similar to visualizer/src/lib/parser.ts but for the CLI side.
8
+ */
9
+ function normalizeSchema(data) {
10
+ if (!data || typeof data !== 'object') {
11
+ throw new Error('Invalid YAML: Root must be an object');
12
+ }
13
+
14
+ const schema = {
15
+ tables: Array.isArray(data.tables) ? data.tables : [],
16
+ relationships: Array.isArray(data.relationships) ? data.relationships : [],
17
+ domains: Array.isArray(data.domains) ? data.domains : [],
18
+ layout: data.layout || {}
19
+ };
20
+
21
+ schema.tables = schema.tables.map(table => ({
22
+ ...table,
23
+ id: table.id || 'unknown',
24
+ name: table.name || table.id || 'Unnamed Table',
25
+ columns: Array.isArray(table.columns) ? table.columns : []
26
+ }));
27
+
28
+ return schema;
29
+ }
30
+
31
+ /**
32
+ * Sanitizes a string for use as a Mermaid entity name.
33
+ * Spaces and hyphens are replaced with underscores.
34
+ */
35
+ function sanitize(str) {
36
+ return str.replace(/[^a-zA-Z0-9_]/g, '_');
37
+ }
38
+
39
+ /**
40
+ * Generates Mermaid ER diagram syntax.
41
+ */
42
+ function generateMermaidER(schema) {
43
+ let mermaid = 'erDiagram\n';
44
+
45
+ // Tables (Entities)
46
+ schema.tables.forEach(table => {
47
+ const tableName = sanitize(table.name);
48
+ mermaid += ` ${tableName} {\n`;
49
+ table.columns.forEach(col => {
50
+ const colName = sanitize(col.logical?.name || col.id);
51
+ const colType = sanitize(col.logical?.type || 'string');
52
+ let markers = '';
53
+ if (col.logical?.isPrimaryKey) markers += ' PK';
54
+ if (col.logical?.isForeignKey) markers += ' FK';
55
+ mermaid += ` ${colType} ${colName}${markers}\n`;
56
+ });
57
+ mermaid += ' }\n';
58
+ });
59
+
60
+ // Relationships
61
+ schema.relationships.forEach(rel => {
62
+ const fromTable = sanitize(schema.tables.find(t => t.id === rel.from.table)?.name || rel.from.table);
63
+ const toTable = sanitize(schema.tables.find(t => t.id === rel.to.table)?.name || rel.to.table);
64
+
65
+ // Default to one-to-many if type is missing
66
+ let connector = '||--o{';
67
+ if (rel.type === 'one-to-one') connector = '||--||';
68
+ if (rel.type === 'many-to-many') connector = '}|--|{';
69
+ if (rel.type === 'one-to-many') connector = '||--o{';
70
+
71
+ mermaid += ` ${fromTable} ${connector} ${toTable} : ""\n`;
72
+ });
73
+
74
+ return mermaid;
75
+ }
76
+
77
+ /**
78
+ * Generates the full Markdown document.
79
+ */
80
+ export function generateMarkdown(schema, modelName) {
81
+ let md = `# ${modelName} Data Model Definition\n\n`;
82
+
83
+ // Mermaid ER Diagram
84
+ md += '## ER Diagram\n\n';
85
+ md += '```mermaid\n';
86
+ md += generateMermaidER(schema);
87
+ md += '```\n\n';
88
+
89
+ // Domains
90
+ if (schema.domains && schema.domains.length > 0) {
91
+ md += '## Domains\n\n';
92
+ schema.domains.forEach(domain => {
93
+ md += `### ${domain.name}\n`;
94
+ if (domain.description) md += `${domain.description}\n\n`;
95
+ md += 'Associated Tables:\n';
96
+ domain.tables.forEach(tableId => {
97
+ const table = schema.tables.find(t => t.id === tableId);
98
+ md += `- ${table?.name || tableId}\n`;
99
+ });
100
+ md += '\n';
101
+ });
102
+ }
103
+
104
+ // Table Catalog
105
+ md += '## Table Catalog\n\n';
106
+ schema.tables.forEach(table => {
107
+ md += `### ${table.name} ${table.appearance?.icon || ''}\n`;
108
+ if (table.conceptual?.description) {
109
+ md += `**Description**: ${table.conceptual.description}\n\n`;
110
+ }
111
+ if (table.appearance?.type) {
112
+ md += `**Type**: ${table.appearance.type}\n\n`;
113
+ }
114
+
115
+ // Columns Table
116
+ if (table.columns && table.columns.length > 0) {
117
+ md += '#### Column Definitions\n\n';
118
+ md += '| ID | Logical Name | Type | Key | Description |\n';
119
+ md += '| --- | --- | --- | --- | --- |\n';
120
+ table.columns.forEach(col => {
121
+ const key = [
122
+ col.logical?.isPrimaryKey ? 'PK' : '',
123
+ col.logical?.isForeignKey ? 'FK' : ''
124
+ ].filter(Boolean).join(', ');
125
+ md += `| ${col.id} | ${col.logical?.name || '-'} | ${col.logical?.type || '-'} | ${key || '-'} | ${col.logical?.description || '-'} |\n`;
126
+ });
127
+ md += '\n';
128
+ }
129
+
130
+ // Sample Data
131
+ if (table.sampleData && table.sampleData.rows && table.sampleData.rows.length > 0) {
132
+ md += '#### Sample Data\n\n';
133
+ md += '| ' + table.sampleData.columns.join(' | ') + ' |\n';
134
+ md += '| ' + table.sampleData.columns.map(() => '---').join(' | ') + ' |\n';
135
+ table.sampleData.rows.slice(0, 5).forEach(row => {
136
+ md += '| ' + row.join(' | ') + ' |\n';
137
+ });
138
+ if (table.sampleData.rows.length > 5) {
139
+ md += `\n*(Showing 5 of ${table.sampleData.rows.length} rows)*\n`;
140
+ }
141
+ md += '\n';
142
+ }
143
+ });
144
+
145
+ return md;
146
+ }
147
+
148
+ /**
149
+ * Main export function called by the CLI
150
+ */
151
+ export async function exportModel(paths, options) {
152
+ for (const inputPath of paths) {
153
+ const absolutePath = path.resolve(process.cwd(), inputPath);
154
+ if (!fs.existsSync(absolutePath)) {
155
+ console.warn(` ⚠️ Warning: Path not found: ${inputPath}`);
156
+ continue;
157
+ }
158
+
159
+ const stats = fs.statSync(absolutePath);
160
+ const files = stats.isDirectory()
161
+ ? fs.readdirSync(absolutePath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).map(f => path.join(absolutePath, f))
162
+ : [absolutePath];
163
+
164
+ for (const file of files) {
165
+ try {
166
+ const content = fs.readFileSync(file, 'utf8');
167
+ const data = yaml.load(content);
168
+ const schema = normalizeSchema(data);
169
+ const modelName = path.parse(file).name.charAt(0).toUpperCase() + path.parse(file).name.slice(1).replace(/[-_]/g, ' ');
170
+ const markdown = generateMarkdown(schema, modelName);
171
+
172
+ if (options.output) {
173
+ // If output is specified, we either append or use it as a directory/base
174
+ // For now, let's just write to the specified output file if it's one file,
175
+ // or treat as a directory if multiple.
176
+ let outputPath = options.output;
177
+ if (files.length > 1 || stats.isDirectory()) {
178
+ // ensure directory exists
179
+ if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath, { recursive: true });
180
+ outputPath = path.join(outputPath, `${path.parse(file).name}.md`);
181
+ }
182
+
183
+ fs.writeFileSync(outputPath, markdown, 'utf8');
184
+ console.log(` ✅ Exported: ${path.relative(process.cwd(), file)} -> ${outputPath}`);
185
+ } else {
186
+ process.stdout.write(markdown + '\n');
187
+ }
188
+ } catch (e) {
189
+ console.error(` ❌ Error exporting ${file}: ${e.message}`);
190
+ }
191
+ }
192
+ }
193
+ }
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import { startDevServer } from './dev.js';
7
7
  import { build } from './build.js';
8
8
  import { initProject } from './init.js';
9
+ import { exportModel } from './export.js';
9
10
 
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const VISUALIZER_PATH = path.resolve(__dirname, '../visualizer');
@@ -14,7 +15,7 @@ const program = new Command();
14
15
  program
15
16
  .name('modscape')
16
17
  .description('A YAML-driven data modeling visualizer CLI')
17
- .version('0.1.2');
18
+ .version('0.3.0');
18
19
 
19
20
  program
20
21
  .command('init')
@@ -44,4 +45,13 @@ program
44
45
  build(paths, VISUALIZER_PATH, options.output);
45
46
  });
46
47
 
48
+ program
49
+ .command('export')
50
+ .description('Export YAML models to Mermaid-compatible Markdown documentation')
51
+ .argument('<paths...>', 'paths to YAML model files or directories')
52
+ .option('-o, --output <path>', 'output file or directory')
53
+ .action((paths, options) => {
54
+ exportModel(paths, options);
55
+ });
56
+
47
57
  program.parse();
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "visualizer",
3
+ "private": true,
4
+ "version": "0.3.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@radix-ui/react-scroll-area": "^1.2.10",
14
+ "@radix-ui/react-slot": "^1.2.4",
15
+ "@radix-ui/react-tabs": "^1.1.13",
16
+ "class-variance-authority": "^0.7.1",
17
+ "clsx": "^2.1.1",
18
+ "js-yaml": "^4.1.1",
19
+ "lucide-react": "^0.575.0",
20
+ "react": "^19.2.0",
21
+ "react-dom": "^19.2.0",
22
+ "reactflow": "^11.11.4",
23
+ "tailwind-merge": "^3.5.0",
24
+ "zustand": "^5.0.11"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.39.1",
28
+ "@types/js-yaml": "^4.0.9",
29
+ "@types/node": "^24.10.1",
30
+ "@types/react": "^19.2.7",
31
+ "@types/react-dom": "^19.2.3",
32
+ "@vitejs/plugin-react": "^5.1.1",
33
+ "autoprefixer": "^10.4.27",
34
+ "eslint": "^9.39.1",
35
+ "eslint-plugin-react-hooks": "^7.0.1",
36
+ "eslint-plugin-react-refresh": "^0.4.24",
37
+ "globals": "^16.5.0",
38
+ "postcss": "^8.5.6",
39
+ "tailwindcss": "^3.4.19",
40
+ "typescript": "~5.9.3",
41
+ "typescript-eslint": "^8.48.0",
42
+ "vite": "^7.3.1"
43
+ }
44
+ }
@@ -0,0 +1,24 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Background Round Rect -->
3
+ <rect width="512" height="512" rx="112" fill="#FFFFFF"/>
4
+
5
+ <!-- Relationship Lines (Connecting in M-shape) -->
6
+ <path d="M120 350 L120 160 L256 310 L392 160 L392 350"
7
+ stroke="#CBD5E1" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
8
+
9
+ <!-- Colorful Rounded Rect Nodes -->
10
+ <!-- Left Bottom (Satellite) -->
11
+ <rect x="80" y="310" width="80" height="80" rx="16" fill="#8b5cf6"/>
12
+
13
+ <!-- Left Top (Hub) -->
14
+ <rect x="80" y="120" width="80" height="80" rx="16" fill="#f59e0b"/>
15
+
16
+ <!-- Center "V" (Fact) -->
17
+ <rect x="216" y="270" width="80" height="80" rx="16" fill="#ef4444"/>
18
+
19
+ <!-- Right Top (Link) -->
20
+ <rect x="352" y="120" width="80" height="80" rx="16" fill="#10b981"/>
21
+
22
+ <!-- Right Bottom (Dimension) -->
23
+ <rect x="352" y="310" width="80" height="80" rx="16" fill="#3b82f6"/>
24
+ </svg>