retold 4.0.4 → 4.0.7

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.
Files changed (92) hide show
  1. package/.claude/launch.json +29 -0
  2. package/.claude/settings.local.json +60 -2
  3. package/CLAUDE.md +4 -2
  4. package/README.md +4 -2
  5. package/Retold-Modules-Manifest.json +576 -0
  6. package/docs/README.md +7 -1
  7. package/docs/{cover.md → _cover.md} +1 -1
  8. package/docs/_sidebar.md +30 -3
  9. package/docs/architecture/architecture.md +5 -2
  10. package/docs/architecture/dependencies/_generate-graph.js +186 -0
  11. package/docs/architecture/dependencies/_generate-svg.js +364 -0
  12. package/docs/architecture/dependencies/in-ecosystem-dependency-graph-generation.md +97 -0
  13. package/docs/architecture/dependencies/in-ecosystem-dependency-graph.json +3168 -0
  14. package/docs/architecture/dependencies/in-ecosystem-dependency-graph.md +221 -0
  15. package/docs/architecture/dependencies/in-ecosystem-dependency-graph.svg +664 -0
  16. package/docs/architecture/documentation-style-guide.md +65 -0
  17. package/docs/architecture/example-app-style-guide.md +154 -0
  18. package/docs/architecture/modules.md +17 -5
  19. package/docs/architecture/templating/data-access.md +196 -0
  20. package/docs/architecture/templating/data-formatting.md +350 -0
  21. package/docs/architecture/templating/data-generation.md +72 -0
  22. package/docs/architecture/templating/debugging.md +181 -0
  23. package/docs/architecture/templating/entity.md +99 -0
  24. package/docs/architecture/templating/iteration.md +170 -0
  25. package/docs/architecture/templating/jellyfish-deep-dive.md +271 -0
  26. package/docs/architecture/templating/jellyfish-templates.md +476 -0
  27. package/docs/architecture/templating/logic.md +185 -0
  28. package/docs/architecture/templating/ref-breakpoint.md +38 -0
  29. package/docs/architecture/templating/ref-data.md +51 -0
  30. package/docs/architecture/templating/ref-dateonlyformat.md +43 -0
  31. package/docs/architecture/templating/ref-dateonlyymd.md +39 -0
  32. package/docs/architecture/templating/ref-datetimeformat.md +59 -0
  33. package/docs/architecture/templating/ref-datetimeymd.md +44 -0
  34. package/docs/architecture/templating/ref-dejs.md +42 -0
  35. package/docs/architecture/templating/ref-digits.md +36 -0
  36. package/docs/architecture/templating/ref-dj.md +50 -0
  37. package/docs/architecture/templating/ref-dollars.md +36 -0
  38. package/docs/architecture/templating/ref-dt.md +38 -0
  39. package/docs/architecture/templating/ref-dvbk.md +46 -0
  40. package/docs/architecture/templating/ref-dwaf.md +45 -0
  41. package/docs/architecture/templating/ref-dwtf.md +45 -0
  42. package/docs/architecture/templating/ref-entity.md +47 -0
  43. package/docs/architecture/templating/ref-hce.md +29 -0
  44. package/docs/architecture/templating/ref-hcs.md +38 -0
  45. package/docs/architecture/templating/ref-join.md +45 -0
  46. package/docs/architecture/templating/ref-joinunique.md +34 -0
  47. package/docs/architecture/templating/ref-ls.md +37 -0
  48. package/docs/architecture/templating/ref-lv.md +38 -0
  49. package/docs/architecture/templating/ref-lvt.md +33 -0
  50. package/docs/architecture/templating/ref-ne.md +40 -0
  51. package/docs/architecture/templating/ref-pascalcaseidentifier.md +41 -0
  52. package/docs/architecture/templating/ref-pict.md +42 -0
  53. package/docs/architecture/templating/ref-pluckjoinunique.md +39 -0
  54. package/docs/architecture/templating/ref-rn.md +35 -0
  55. package/docs/architecture/templating/ref-rns.md +35 -0
  56. package/docs/architecture/templating/ref-sbr.md +36 -0
  57. package/docs/architecture/templating/ref-solve.md +46 -0
  58. package/docs/architecture/templating/ref-tbda.md +41 -0
  59. package/docs/architecture/templating/ref-tbr.md +43 -0
  60. package/docs/architecture/templating/ref-tbt.md +46 -0
  61. package/docs/architecture/templating/ref-template.md +40 -0
  62. package/docs/architecture/templating/ref-tfa.md +32 -0
  63. package/docs/architecture/templating/ref-tfm.md +43 -0
  64. package/docs/architecture/templating/ref-tif.md +45 -0
  65. package/docs/architecture/templating/ref-tifabs.md +41 -0
  66. package/docs/architecture/templating/ref-ts.md +41 -0
  67. package/docs/architecture/templating/ref-tsfm.md +42 -0
  68. package/docs/architecture/templating/ref-tswp.md +45 -0
  69. package/docs/architecture/templating/ref-tvs.md +48 -0
  70. package/docs/architecture/templating/ref-view.md +40 -0
  71. package/docs/architecture/templating/ref-vrs.md +39 -0
  72. package/docs/architecture/templating/solvers.md +153 -0
  73. package/docs/architecture/templating/template-composition.md +196 -0
  74. package/docs/architecture/templating/template-expressions.md +217 -0
  75. package/docs/architecture/templating/views.md +154 -0
  76. package/docs/examples/todolist/todo-list.md +1 -1
  77. package/docs/modules/apps.md +26 -0
  78. package/docs/modules/pict.md +18 -0
  79. package/docs/modules/utility.md +23 -1
  80. package/docs/retold-catalog.json +829 -102
  81. package/docs/retold-keyword-index.json +195212 -115957
  82. package/modules/CLAUDE.md +1 -0
  83. package/modules/Checkout.sh +1 -0
  84. package/modules/Diff.sh +86 -0
  85. package/modules/Include-Retold-Module-List.sh +4 -2
  86. package/modules/Status.sh +1 -0
  87. package/modules/Update.sh +1 -0
  88. package/modules/apps/Apps.md +1 -0
  89. package/modules/utility/Utility.md +1 -0
  90. package/package.json +9 -11
  91. package/docs/retold-building-documentation.md +0 -33
  92. package/modules/Retold-Modules.md +0 -24
package/docs/_sidebar.md CHANGED
@@ -1,14 +1,30 @@
1
1
  - Getting Started
2
2
 
3
3
  - [Getting Started](getting-started.md)
4
- - [Testing](testing.md)
5
- - [Contributing](contributing.md)
4
+
5
+ - Ecosystem Architecture
6
6
  - [Architecture](architecture/architecture.md)
7
7
  - [Ecosystem Architecture](architecture/module-architecture.md)
8
8
  - [Fluid Models](architecture/fluid-models.md)
9
9
  - [Comprehensions](architecture/comprehensions.md)
10
10
  - [All Modules](architecture/modules.md)
11
11
 
12
+ - Templating
13
+
14
+ - [Jellyfish Templates](architecture/templating/jellyfish-templates.md)
15
+ - [Jellyfish Deep Dive](architecture/templating/jellyfish-deep-dive.md)
16
+ - [Template Expressions](architecture/templating/template-expressions.md)
17
+
18
+ - Style Guides
19
+
20
+ - [Documentation Style Guide](architecture/documentation-style-guide.md)
21
+ - [Example App Style Guide](architecture/example-app-style-guide.md)
22
+
23
+ - Contributing
24
+
25
+ - [Contributing](contributing.md)
26
+ - [Testing](testing.md)
27
+
12
28
  - [Examples](examples/examples.md)
13
29
 
14
30
  - [Todo List Application](examples/todolist/todo-list.md)
@@ -68,14 +84,25 @@
68
84
  - [pict-section-recordset](/pict/pict-section-recordset/)
69
85
  - [pict-section-content](/pict/pict-section-content/)
70
86
  - [pict-section-form](/pict/pict-section-form/)
87
+ - [pict-section-objecteditor](/pict/pict-section-objecteditor/)
71
88
  - [pict-section-tuigrid](/pict/pict-section-tuigrid/)
89
+ - [pict-section-code](/pict/pict-section-code/)
90
+ - [pict-section-markdowneditor](/pict/pict-section-markdowneditor/)
91
+ - [pict-section-filebrowser](/pict/pict-section-filebrowser/)
72
92
  - [pict-router](/pict/pict-router/)
73
93
  - [pict-serviceproviderbase](/pict/pict-serviceproviderbase/)
74
94
  - [pict-terminalui](/pict/pict-terminalui/)
75
95
 
76
- - [Utility — Build Tools](modules/utility.md)
96
+ - [Utility — General Tools](modules/utility.md)
77
97
 
98
+ - [cachetrax](/utility/cachetrax/)
78
99
  - [indoctrinate](/utility/indoctrinate/)
79
100
  - [manyfest](/utility/manyfest/)
101
+ - [precedent](/utility/precedent/)
80
102
  - [quackage](/utility/quackage/)
81
103
  - [ultravisor](/utility/ultravisor/)
104
+
105
+ - [Apps — Applications](modules/apps.md)
106
+
107
+ - [retold-content-system](/apps/retold-content-system/)
108
+ - [retold-remote](/apps/retold-remote/)
@@ -143,7 +143,7 @@ graph LR
143
143
  endpoints --> r5["POST /Book → Create"]
144
144
  endpoints --> r6["PUT /Book → Update"]
145
145
  endpoints --> r7["DEL /Book/:id → Delete"]
146
- endpoints --> r8["DEL /Book/:id/Undelete"]
146
+ endpoints --> r8["DEL /Book/Undelete/:id"]
147
147
 
148
148
  entity --> hooks["+ Behavior injection hooks<br/>+ Dynamic filtering & pagination<br/>+ Bulk operations"]
149
149
 
@@ -205,8 +205,10 @@ graph TB
205
205
  subgraph Sections["Sections & Components"]
206
206
  forms["Forms<br/><i>pict-section-form</i>"]
207
207
  recordset["Recordset<br/><i>pict-section-recordset</i>"]
208
+ objecteditor["Object Editor<br/><i>pict-section-objecteditor</i>"]
208
209
  tuigrid["TUI Grid<br/><i>pict-section-tuigrid</i>"]
209
210
  content["Content<br/><i>pict-section-content</i>"]
211
+ mdeditor["Markdown Editor<br/><i>pict-section-markdowneditor</i>"]
210
212
  end
211
213
  Core --> Sections
212
214
  end
@@ -220,8 +222,10 @@ graph TB
220
222
  style appfw fill:#fff,stroke:#ce93d8,color:#333
221
223
  style forms fill:#fff,stroke:#ce93d8,color:#333
222
224
  style recordset fill:#fff,stroke:#ce93d8,color:#333
225
+ style objecteditor fill:#fff,stroke:#ce93d8,color:#333
223
226
  style tuigrid fill:#fff,stroke:#ce93d8,color:#333
224
227
  style content fill:#fff,stroke:#ce93d8,color:#333
228
+ style mdeditor fill:#fff,stroke:#ce93d8,color:#333
225
229
  ```
226
230
 
227
231
  Pict's core philosophy: UI is text. Views render templates into strings. Providers fetch data. The Application class coordinates lifecycle. Sections provide pre-built patterns for common UI needs (forms, record lists, grids).
@@ -291,7 +295,6 @@ Supporting the application stack are utility modules:
291
295
  | **[Quackage](/utility/quackage/)** | Standardized build tool for browser bundles, testing, and packaging |
292
296
  | **[Indoctrinate](/utility/indoctrinate/)** | Documentation scaffolding, catalog generation, and cross-module search |
293
297
  | **[Ultravisor](/utility/ultravisor/)** | Process supervision with scheduled tasks and LLM integration |
294
- | **[Choreographic](/utility/choreographic/)** | Scaffolding for single-run data processing scripts |
295
298
 
296
299
  ## Design Principles
297
300
 
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates the in-ecosystem dependency graph JSON for the Retold module suite.
4
+ * Run from the retold root directory.
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { execSync } = require('child_process');
9
+
10
+ const files = execSync('find . -maxdepth 4 -name package.json -not -path "*/node_modules/*" -not -path "*/.git/*"', { encoding: 'utf8' }).trim().split('\n');
11
+
12
+ // Collect all ecosystem package names and data
13
+ const ecosystemNames = new Set();
14
+ const moduleData = [];
15
+
16
+ for (const f of files)
17
+ {
18
+ try
19
+ {
20
+ const pkg = JSON.parse(fs.readFileSync(f, 'utf8'));
21
+ if (pkg.name)
22
+ {
23
+ ecosystemNames.add(pkg.name);
24
+ const dir = path.dirname(f);
25
+ let group = 'root';
26
+ const parts = dir.split('/');
27
+ const modIdx = parts.indexOf('modules');
28
+ if (modIdx >= 0 && parts.length > modIdx + 1)
29
+ {
30
+ group = parts[modIdx + 1];
31
+ }
32
+ // Determine category
33
+ let category = 'module';
34
+ if (dir.includes('/examples/'))
35
+ {
36
+ category = 'example';
37
+ }
38
+ else if (dir === '.')
39
+ {
40
+ category = 'root';
41
+ }
42
+ else if (dir.includes('/source/'))
43
+ {
44
+ category = 'internal';
45
+ }
46
+
47
+ moduleData.push({
48
+ name: pkg.name,
49
+ dir: dir,
50
+ group: group,
51
+ category: category,
52
+ version: pkg.version || '0.0.0',
53
+ description: pkg.description || '',
54
+ dependencies: pkg.dependencies || {},
55
+ devDependencies: pkg.devDependencies || {}
56
+ });
57
+ }
58
+ }
59
+ catch(e) {}
60
+ }
61
+
62
+ // Build the graph
63
+ const graph = {
64
+ metadata: {
65
+ generated: new Date().toISOString().split('T')[0],
66
+ description: 'In-ecosystem dependency graph for the Retold module suite',
67
+ totalModules: moduleData.length,
68
+ totalEcosystemPackages: ecosystemNames.size,
69
+ groups: ['fable', 'meadow', 'orator', 'pict', 'utility', 'apps', 'root']
70
+ },
71
+ nodes: [],
72
+ edges: [],
73
+ groups: {}
74
+ };
75
+
76
+ // Build nodes and edges
77
+ for (const mod of moduleData)
78
+ {
79
+ const prodEcoDeps = [];
80
+ const devEcoDeps = [];
81
+
82
+ for (const [dep, ver] of Object.entries(mod.dependencies))
83
+ {
84
+ if (ecosystemNames.has(dep))
85
+ {
86
+ prodEcoDeps.push(dep);
87
+ }
88
+ }
89
+ for (const [dep, ver] of Object.entries(mod.devDependencies))
90
+ {
91
+ if (ecosystemNames.has(dep))
92
+ {
93
+ devEcoDeps.push(dep);
94
+ }
95
+ }
96
+
97
+ const node = {
98
+ name: mod.name,
99
+ group: mod.group,
100
+ category: mod.category,
101
+ version: mod.version,
102
+ description: mod.description,
103
+ path: mod.dir,
104
+ ecosystemProductionDependencyCount: prodEcoDeps.length,
105
+ ecosystemDevDependencyCount: devEcoDeps.length,
106
+ totalExternalDependencyCount: Object.keys(mod.dependencies).length - prodEcoDeps.length,
107
+ totalExternalDevDependencyCount: Object.keys(mod.devDependencies).length - devEcoDeps.length
108
+ };
109
+ graph.nodes.push(node);
110
+
111
+ // Production edges
112
+ for (const [dep, ver] of Object.entries(mod.dependencies))
113
+ {
114
+ if (ecosystemNames.has(dep))
115
+ {
116
+ graph.edges.push({
117
+ from: mod.name,
118
+ to: dep,
119
+ version: ver.replace('file:', '').replace(/\.\.\//g, ''),
120
+ type: 'production'
121
+ });
122
+ }
123
+ }
124
+ // Development edges
125
+ for (const [dep, ver] of Object.entries(mod.devDependencies))
126
+ {
127
+ if (ecosystemNames.has(dep))
128
+ {
129
+ graph.edges.push({
130
+ from: mod.name,
131
+ to: dep,
132
+ version: ver.replace('file:', '').replace(/\.\.\//g, ''),
133
+ type: 'development'
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ // Group nodes
140
+ for (const node of graph.nodes)
141
+ {
142
+ if (!graph.groups[node.group])
143
+ {
144
+ graph.groups[node.group] = [];
145
+ }
146
+ graph.groups[node.group].push(node.name);
147
+ }
148
+
149
+ // Compute graph analytics
150
+ const inDegree = {};
151
+ const outDegree = {};
152
+ for (const edge of graph.edges)
153
+ {
154
+ outDegree[edge.from] = (outDegree[edge.from] || 0) + 1;
155
+ inDegree[edge.to] = (inDegree[edge.to] || 0) + 1;
156
+ }
157
+
158
+ // Leaves: no outgoing ecosystem deps
159
+ const leaves = graph.nodes.filter(n => !outDegree[n.name]).map(n => n.name);
160
+ // Roots: nothing depends on them
161
+ const roots = graph.nodes.filter(n => !inDegree[n.name]).map(n => n.name);
162
+ // Most depended upon
163
+ const mostDepended = Object.entries(inDegree).sort((a,b) => b[1] - a[1]).slice(0, 20);
164
+
165
+ graph.analytics = {
166
+ leafNodes: leaves,
167
+ rootNodes: roots,
168
+ mostDependedUpon: mostDepended.map(([name, count]) => ({ name, dependedUponBy: count })),
169
+ totalProductionEdges: graph.edges.filter(e => e.type === 'production').length,
170
+ totalDevelopmentEdges: graph.edges.filter(e => e.type === 'development').length
171
+ };
172
+
173
+ fs.writeFileSync('docs/architecture/dependencies/in-ecosystem-dependency-graph.json', JSON.stringify(graph, null, '\t'));
174
+ console.log('JSON written. Stats:');
175
+ console.log(' Nodes:', graph.nodes.length);
176
+ console.log(' Edges:', graph.edges.length);
177
+ console.log(' Production edges:', graph.analytics.totalProductionEdges);
178
+ console.log(' Development edges:', graph.analytics.totalDevelopmentEdges);
179
+ console.log(' Leaf nodes:', leaves.length);
180
+ console.log(' Root nodes:', roots.length);
181
+ console.log('');
182
+ console.log('Most depended upon:');
183
+ for (const m of mostDepended)
184
+ {
185
+ console.log(' ', m[0], '-', m[1], 'dependents');
186
+ }
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates an SVG dependency graph for the Retold module suite.
4
+ * Reads from in-ecosystem-dependency-graph.json and writes the SVG.
5
+ *
6
+ * Layout: 2-column grid of groups arranged to follow the architectural layering.
7
+ * Row 0: Fable (Core) | Utility (Build/Ops)
8
+ * Row 1: Meadow (Data) — full width since it has the most modules
9
+ * Row 2: Orator (API) | Apps (Full Stack)
10
+ * Row 3: Pict (MVC/UI) — full width since it has the most modules
11
+ * Row 4: Root
12
+ *
13
+ * Production deps = solid arrows, dev deps = dashed.
14
+ * Core nodes (>=10 dependents) are highlighted with filled backgrounds.
15
+ */
16
+ const fs = require('fs');
17
+ const graph = JSON.parse(fs.readFileSync('docs/architecture/dependencies/in-ecosystem-dependency-graph.json', 'utf8'));
18
+
19
+ // Filter to only module-category and root-category nodes (exclude examples, internal)
20
+ const includedCategories = new Set(['module', 'root']);
21
+ const includedNodes = graph.nodes.filter(n => includedCategories.has(n.category));
22
+ const includedNames = new Set(includedNodes.map(n => n.name));
23
+
24
+ // Filter edges to only included nodes
25
+ const edges = graph.edges.filter(e => includedNames.has(e.from) && includedNames.has(e.to));
26
+
27
+ // Precompute in-degree for each node
28
+ const inDegreeMap = {};
29
+ for (const e of edges)
30
+ {
31
+ inDegreeMap[e.to] = (inDegreeMap[e.to] || 0) + 1;
32
+ }
33
+
34
+ // Group configuration
35
+ const groupConfig = {
36
+ fable: { label: 'Fable (Core)', color: '#4A90D9', fill: '#EBF2FA' },
37
+ meadow: { label: 'Meadow (Data)', color: '#27AE60', fill: '#E8F8EF' },
38
+ orator: { label: 'Orator (API)', color: '#E67E22', fill: '#FDF2E9' },
39
+ pict: { label: 'Pict (MVC/UI)', color: '#8E44AD', fill: '#F4ECF7' },
40
+ utility: { label: 'Utility (Build/Ops)', color: '#2C3E50', fill: '#EBEDEF' },
41
+ apps: { label: 'Apps (Full Stack)', color: '#C0392B', fill: '#FDEDEC' },
42
+ root: { label: 'Root / Examples', color: '#7F8C8D', fill: '#F2F3F4' }
43
+ };
44
+
45
+ // Organize nodes by group
46
+ const nodesByGroup = {};
47
+ for (const node of includedNodes)
48
+ {
49
+ const g = node.group;
50
+ if (!nodesByGroup[g])
51
+ {
52
+ nodesByGroup[g] = [];
53
+ }
54
+ nodesByGroup[g].push(node);
55
+ }
56
+
57
+ // Sort nodes within groups alphabetically
58
+ for (const g of Object.keys(nodesByGroup))
59
+ {
60
+ nodesByGroup[g].sort((a, b) => a.name.localeCompare(b.name));
61
+ }
62
+
63
+ // Layout parameters
64
+ const NODE_W = 195;
65
+ const NODE_H = 26;
66
+ const NODE_PAD_X = 10;
67
+ const NODE_PAD_Y = 6;
68
+ const GROUP_PAD = 14;
69
+ const GROUP_HEADER = 32;
70
+ const GROUP_GAP_X = 24;
71
+ const GROUP_GAP_Y = 24;
72
+ const LEFT_MARGIN = 30;
73
+ const TOP_MARGIN = 72;
74
+
75
+ // Define the grid layout: rows of group placements
76
+ // Each placement: { group, col, colSpan }
77
+ // 2-column grid
78
+ const layout = [
79
+ [{ group: 'fable', col: 0, colSpan: 1 }, { group: 'utility', col: 1, colSpan: 1 }],
80
+ [{ group: 'meadow', col: 0, colSpan: 2 }],
81
+ [{ group: 'orator', col: 0, colSpan: 1 }, { group: 'apps', col: 1, colSpan: 1 }],
82
+ [{ group: 'pict', col: 0, colSpan: 2 }],
83
+ [{ group: 'root', col: 0, colSpan: 2 }]
84
+ ];
85
+
86
+ // Compute column widths
87
+ // Each group in a single column gets 3 node columns
88
+ // Full-span groups get 6 node columns
89
+ const SINGLE_COL_NODES = 3;
90
+ const FULL_COL_NODES = 6;
91
+ const singleColW = SINGLE_COL_NODES * (NODE_W + NODE_PAD_X) + GROUP_PAD * 2 - NODE_PAD_X;
92
+ const fullColW = FULL_COL_NODES * (NODE_W + NODE_PAD_X) + GROUP_PAD * 2 - NODE_PAD_X;
93
+
94
+ const SVG_W = LEFT_MARGIN * 2 + fullColW;
95
+
96
+ // Position nodes
97
+ const nodePositions = {};
98
+ const groupBounds = {};
99
+ let currentY = TOP_MARGIN;
100
+
101
+ for (const row of layout)
102
+ {
103
+ let maxRowH = 0;
104
+
105
+ for (const placement of row)
106
+ {
107
+ const grp = placement.group;
108
+ const nodes = nodesByGroup[grp] || [];
109
+ if (nodes.length === 0) continue;
110
+
111
+ const isFullSpan = placement.colSpan === 2;
112
+ const nodeCols = isFullSpan ? FULL_COL_NODES : SINGLE_COL_NODES;
113
+ const groupW = isFullSpan ? fullColW : singleColW;
114
+ const groupX = LEFT_MARGIN + placement.col * (singleColW + GROUP_GAP_X);
115
+
116
+ const nodeRows = Math.ceil(nodes.length / nodeCols);
117
+ const groupH = GROUP_HEADER + nodeRows * (NODE_H + NODE_PAD_Y) + GROUP_PAD;
118
+
119
+ groupBounds[grp] = { x: groupX, y: currentY, w: groupW, h: groupH };
120
+
121
+ for (let i = 0; i < nodes.length; i++)
122
+ {
123
+ const col = i % nodeCols;
124
+ const row = Math.floor(i / nodeCols);
125
+ const x = groupX + GROUP_PAD + col * (NODE_W + NODE_PAD_X);
126
+ const y = currentY + GROUP_HEADER + row * (NODE_H + NODE_PAD_Y);
127
+ nodePositions[nodes[i].name] = {
128
+ x: x,
129
+ y: y,
130
+ cx: x + NODE_W / 2,
131
+ cy: y + NODE_H / 2,
132
+ right: x + NODE_W,
133
+ bottom: y + NODE_H,
134
+ group: grp,
135
+ node: nodes[i]
136
+ };
137
+ }
138
+
139
+ if (groupH > maxRowH) maxRowH = groupH;
140
+ }
141
+
142
+ currentY += maxRowH + GROUP_GAP_Y;
143
+ }
144
+
145
+ const SVG_H = currentY + 30;
146
+
147
+ // Build SVG
148
+ let svg = '';
149
+
150
+ function esc(s)
151
+ {
152
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
153
+ }
154
+
155
+ function round(n)
156
+ {
157
+ return Math.round(n * 10) / 10;
158
+ }
159
+
160
+ // Header
161
+ svg += `<?xml version="1.0" encoding="UTF-8"?>\n`;
162
+ svg += `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SVG_W} ${SVG_H}" width="${SVG_W}" height="${SVG_H}" font-family="'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace">\n`;
163
+
164
+ // Style
165
+ svg += `<style>\n`;
166
+ svg += ` .node-box { transition: opacity 0.2s; }\n`;
167
+ svg += ` .edge-line { transition: opacity 0.2s; }\n`;
168
+ svg += `</style>\n`;
169
+
170
+ // Defs
171
+ svg += `<defs>\n`;
172
+ svg += ` <marker id="arrowProd" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto" markerUnits="strokeWidth">\n`;
173
+ svg += ` <path d="M0,0 L7,2.5 L0,5" fill="#666" />\n`;
174
+ svg += ` </marker>\n`;
175
+ svg += ` <marker id="arrowDev" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto" markerUnits="strokeWidth">\n`;
176
+ svg += ` <path d="M0,0 L7,2.5 L0,5" fill="#BBB" />\n`;
177
+ svg += ` </marker>\n`;
178
+ svg += `</defs>\n`;
179
+
180
+ // Background
181
+ svg += `<rect width="100%" height="100%" fill="#FAFBFC" />\n`;
182
+
183
+ // Title
184
+ svg += `<text x="${SVG_W / 2}" y="28" text-anchor="middle" font-size="16" font-weight="bold" fill="#2C3E50">Retold In-Ecosystem Dependency Graph</text>\n`;
185
+ const prodCount = edges.filter(e => e.type === 'production').length;
186
+ const devCount = edges.filter(e => e.type === 'development').length;
187
+ svg += `<text x="${SVG_W / 2}" y="46" text-anchor="middle" font-size="10" fill="#7F8C8D">${includedNodes.length} modules | ${prodCount} production deps | ${devCount} dev deps | Generated ${graph.metadata.generated}</text>\n`;
188
+
189
+ // Legend
190
+ svg += `<g transform="translate(${SVG_W - 310}, 14)">\n`;
191
+ svg += ` <line x1="0" y1="0" x2="24" y2="0" stroke="#555" stroke-width="1.5" marker-end="url(#arrowProd)" />\n`;
192
+ svg += ` <text x="30" y="4" font-size="9" fill="#555">Production dep</text>\n`;
193
+ svg += ` <line x1="120" y1="0" x2="144" y2="0" stroke="#BBB" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrowDev)" />\n`;
194
+ svg += ` <text x="150" y="4" font-size="9" fill="#555">Dev dep</text>\n`;
195
+ svg += ` <rect x="0" y="12" width="12" height="12" rx="2" fill="#8E44AD" stroke="#8E44AD" />\n`;
196
+ svg += ` <text x="18" y="22" font-size="9" fill="#555">Core module (10+ dependents)</text>\n`;
197
+ svg += `</g>\n`;
198
+
199
+ // Draw edges (behind nodes)
200
+ svg += `<g id="edges">\n`;
201
+
202
+ const prodEdges = edges.filter(e => e.type === 'production');
203
+ const devEdges = edges.filter(e => e.type === 'development');
204
+
205
+ function drawEdge(edge, isProd)
206
+ {
207
+ const from = nodePositions[edge.from];
208
+ const to = nodePositions[edge.to];
209
+ if (!from || !to) return '';
210
+
211
+ let x1, y1, x2, y2;
212
+
213
+ // Determine connection points based on relative positions
214
+ const fromGB = groupBounds[from.group];
215
+ const toGB = groupBounds[to.group];
216
+ const sameGroup = from.group === to.group;
217
+
218
+ if (sameGroup)
219
+ {
220
+ // Same group: connect right side to left side
221
+ x1 = from.right;
222
+ y1 = from.cy;
223
+ x2 = to.x;
224
+ y2 = to.cy;
225
+ // If target is to the left, connect from left to right
226
+ if (to.x <= from.x)
227
+ {
228
+ x1 = from.x;
229
+ x2 = to.right;
230
+ }
231
+ }
232
+ else
233
+ {
234
+ // Different groups
235
+ const fromCenterY = fromGB.y + fromGB.h / 2;
236
+ const toCenterY = toGB.y + toGB.h / 2;
237
+ const fromCenterX = fromGB.x + fromGB.w / 2;
238
+ const toCenterX = toGB.x + toGB.w / 2;
239
+
240
+ if (Math.abs(fromGB.y - toGB.y) < 10)
241
+ {
242
+ // Groups on same row: connect left/right
243
+ if (fromCenterX < toCenterX)
244
+ {
245
+ x1 = from.right;
246
+ y1 = from.cy;
247
+ x2 = to.x;
248
+ y2 = to.cy;
249
+ }
250
+ else
251
+ {
252
+ x1 = from.x;
253
+ y1 = from.cy;
254
+ x2 = to.right;
255
+ y2 = to.cy;
256
+ }
257
+ }
258
+ else if (fromCenterY < toCenterY)
259
+ {
260
+ // Source above target: connect bottom to top
261
+ x1 = from.cx;
262
+ y1 = from.bottom;
263
+ x2 = to.cx;
264
+ y2 = to.y;
265
+ }
266
+ else
267
+ {
268
+ // Target above source: connect top to bottom
269
+ x1 = from.cx;
270
+ y1 = from.y;
271
+ x2 = to.cx;
272
+ y2 = to.bottom;
273
+ }
274
+ }
275
+
276
+ const dx = x2 - x1;
277
+ const dy = y2 - y1;
278
+
279
+ // Use cubic bezier with gentle curves
280
+ let path;
281
+ if (Math.abs(dy) > Math.abs(dx))
282
+ {
283
+ // Vertical-dominant: curve via vertical control points
284
+ const cy1 = y1 + dy * 0.4;
285
+ const cy2 = y2 - dy * 0.4;
286
+ path = `M${round(x1)},${round(y1)} C${round(x1)},${round(cy1)} ${round(x2)},${round(cy2)} ${round(x2)},${round(y2)}`;
287
+ }
288
+ else
289
+ {
290
+ // Horizontal-dominant: curve via horizontal control points
291
+ const cx1 = x1 + dx * 0.4;
292
+ const cx2 = x2 - dx * 0.4;
293
+ path = `M${round(x1)},${round(y1)} C${round(cx1)},${round(y1)} ${round(cx2)},${round(y2)} ${round(x2)},${round(y2)}`;
294
+ }
295
+
296
+ const stroke = isProd ? '#555' : '#CCC';
297
+ const width = isProd ? '1' : '0.7';
298
+ const dash = isProd ? '' : ' stroke-dasharray="4,3"';
299
+ const marker = isProd ? 'url(#arrowProd)' : 'url(#arrowDev)';
300
+ const opacity = isProd ? '0.3' : '0.15';
301
+
302
+ return ` <path class="edge-line" d="${path}" fill="none" stroke="${stroke}" stroke-width="${width}"${dash} marker-end="${marker}" opacity="${opacity}" />\n`;
303
+ }
304
+
305
+ for (const e of devEdges)
306
+ {
307
+ svg += drawEdge(e, false);
308
+ }
309
+ for (const e of prodEdges)
310
+ {
311
+ svg += drawEdge(e, true);
312
+ }
313
+ svg += `</g>\n`;
314
+
315
+ // Draw group backgrounds
316
+ for (const grp of Object.keys(groupBounds))
317
+ {
318
+ const b = groupBounds[grp];
319
+ const cfg = groupConfig[grp] || { label: grp, color: '#999', fill: '#F5F5F5' };
320
+ const count = (nodesByGroup[grp] || []).length;
321
+
322
+ svg += `<g id="group-${grp}">\n`;
323
+ svg += ` <rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="8" fill="${cfg.fill}" stroke="${cfg.color}" stroke-width="1.5" opacity="0.85" />\n`;
324
+ svg += ` <text x="${b.x + 12}" y="${b.y + 20}" font-size="12" font-weight="bold" fill="${cfg.color}">${esc(cfg.label)}</text>\n`;
325
+ svg += ` <text x="${b.x + b.w - 12}" y="${b.y + 20}" text-anchor="end" font-size="9" fill="${cfg.color}" opacity="0.7">${count} modules</text>\n`;
326
+ svg += `</g>\n`;
327
+ }
328
+
329
+ // Draw nodes
330
+ svg += `<g id="nodes">\n`;
331
+ for (const [name, pos] of Object.entries(nodePositions))
332
+ {
333
+ const cfg = groupConfig[pos.group] || { color: '#999' };
334
+ const inDeg = inDegreeMap[name] || 0;
335
+
336
+ // Highlight core modules (>=10 dependents)
337
+ const isCore = inDeg >= 10;
338
+ const fillColor = isCore ? cfg.color : '#FFFFFF';
339
+ const textColor = isCore ? '#FFFFFF' : '#2C3E50';
340
+ const strokeW = isCore ? '2' : '1';
341
+
342
+ // Truncate long names for display
343
+ let displayName = name;
344
+ if (displayName.length > 26)
345
+ {
346
+ displayName = displayName.substring(0, 24) + '..';
347
+ }
348
+
349
+ svg += ` <g class="node-box">\n`;
350
+ svg += ` <rect x="${pos.x}" y="${pos.y}" width="${NODE_W}" height="${NODE_H}" rx="4" fill="${fillColor}" stroke="${cfg.color}" stroke-width="${strokeW}" />\n`;
351
+ svg += ` <text x="${pos.x + 6}" y="${pos.y + 17}" font-size="9" fill="${textColor}">${esc(displayName)}</text>\n`;
352
+ if (inDeg > 0)
353
+ {
354
+ svg += ` <text x="${pos.x + NODE_W - 6}" y="${pos.y + 17}" text-anchor="end" font-size="7" fill="${isCore ? 'rgba(255,255,255,0.8)' : '#AAA'}">${inDeg}</text>\n`;
355
+ }
356
+ svg += ` </g>\n`;
357
+ }
358
+ svg += `</g>\n`;
359
+
360
+ svg += `</svg>\n`;
361
+
362
+ fs.writeFileSync('docs/architecture/dependencies/in-ecosystem-dependency-graph.svg', svg);
363
+ console.log('SVG written:', Math.round(svg.length / 1024), 'KB');
364
+ console.log('Dimensions:', SVG_W, 'x', SVG_H);