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.
- package/.claude/launch.json +29 -0
- package/.claude/settings.local.json +60 -2
- package/CLAUDE.md +4 -2
- package/README.md +4 -2
- package/Retold-Modules-Manifest.json +576 -0
- package/docs/README.md +7 -1
- package/docs/{cover.md → _cover.md} +1 -1
- package/docs/_sidebar.md +30 -3
- package/docs/architecture/architecture.md +5 -2
- package/docs/architecture/dependencies/_generate-graph.js +186 -0
- package/docs/architecture/dependencies/_generate-svg.js +364 -0
- package/docs/architecture/dependencies/in-ecosystem-dependency-graph-generation.md +97 -0
- package/docs/architecture/dependencies/in-ecosystem-dependency-graph.json +3168 -0
- package/docs/architecture/dependencies/in-ecosystem-dependency-graph.md +221 -0
- package/docs/architecture/dependencies/in-ecosystem-dependency-graph.svg +664 -0
- package/docs/architecture/documentation-style-guide.md +65 -0
- package/docs/architecture/example-app-style-guide.md +154 -0
- package/docs/architecture/modules.md +17 -5
- package/docs/architecture/templating/data-access.md +196 -0
- package/docs/architecture/templating/data-formatting.md +350 -0
- package/docs/architecture/templating/data-generation.md +72 -0
- package/docs/architecture/templating/debugging.md +181 -0
- package/docs/architecture/templating/entity.md +99 -0
- package/docs/architecture/templating/iteration.md +170 -0
- package/docs/architecture/templating/jellyfish-deep-dive.md +271 -0
- package/docs/architecture/templating/jellyfish-templates.md +476 -0
- package/docs/architecture/templating/logic.md +185 -0
- package/docs/architecture/templating/ref-breakpoint.md +38 -0
- package/docs/architecture/templating/ref-data.md +51 -0
- package/docs/architecture/templating/ref-dateonlyformat.md +43 -0
- package/docs/architecture/templating/ref-dateonlyymd.md +39 -0
- package/docs/architecture/templating/ref-datetimeformat.md +59 -0
- package/docs/architecture/templating/ref-datetimeymd.md +44 -0
- package/docs/architecture/templating/ref-dejs.md +42 -0
- package/docs/architecture/templating/ref-digits.md +36 -0
- package/docs/architecture/templating/ref-dj.md +50 -0
- package/docs/architecture/templating/ref-dollars.md +36 -0
- package/docs/architecture/templating/ref-dt.md +38 -0
- package/docs/architecture/templating/ref-dvbk.md +46 -0
- package/docs/architecture/templating/ref-dwaf.md +45 -0
- package/docs/architecture/templating/ref-dwtf.md +45 -0
- package/docs/architecture/templating/ref-entity.md +47 -0
- package/docs/architecture/templating/ref-hce.md +29 -0
- package/docs/architecture/templating/ref-hcs.md +38 -0
- package/docs/architecture/templating/ref-join.md +45 -0
- package/docs/architecture/templating/ref-joinunique.md +34 -0
- package/docs/architecture/templating/ref-ls.md +37 -0
- package/docs/architecture/templating/ref-lv.md +38 -0
- package/docs/architecture/templating/ref-lvt.md +33 -0
- package/docs/architecture/templating/ref-ne.md +40 -0
- package/docs/architecture/templating/ref-pascalcaseidentifier.md +41 -0
- package/docs/architecture/templating/ref-pict.md +42 -0
- package/docs/architecture/templating/ref-pluckjoinunique.md +39 -0
- package/docs/architecture/templating/ref-rn.md +35 -0
- package/docs/architecture/templating/ref-rns.md +35 -0
- package/docs/architecture/templating/ref-sbr.md +36 -0
- package/docs/architecture/templating/ref-solve.md +46 -0
- package/docs/architecture/templating/ref-tbda.md +41 -0
- package/docs/architecture/templating/ref-tbr.md +43 -0
- package/docs/architecture/templating/ref-tbt.md +46 -0
- package/docs/architecture/templating/ref-template.md +40 -0
- package/docs/architecture/templating/ref-tfa.md +32 -0
- package/docs/architecture/templating/ref-tfm.md +43 -0
- package/docs/architecture/templating/ref-tif.md +45 -0
- package/docs/architecture/templating/ref-tifabs.md +41 -0
- package/docs/architecture/templating/ref-ts.md +41 -0
- package/docs/architecture/templating/ref-tsfm.md +42 -0
- package/docs/architecture/templating/ref-tswp.md +45 -0
- package/docs/architecture/templating/ref-tvs.md +48 -0
- package/docs/architecture/templating/ref-view.md +40 -0
- package/docs/architecture/templating/ref-vrs.md +39 -0
- package/docs/architecture/templating/solvers.md +153 -0
- package/docs/architecture/templating/template-composition.md +196 -0
- package/docs/architecture/templating/template-expressions.md +217 -0
- package/docs/architecture/templating/views.md +154 -0
- package/docs/examples/todolist/todo-list.md +1 -1
- package/docs/modules/apps.md +26 -0
- package/docs/modules/pict.md +18 -0
- package/docs/modules/utility.md +23 -1
- package/docs/retold-catalog.json +829 -102
- package/docs/retold-keyword-index.json +195212 -115957
- package/modules/CLAUDE.md +1 -0
- package/modules/Checkout.sh +1 -0
- package/modules/Diff.sh +86 -0
- package/modules/Include-Retold-Module-List.sh +4 -2
- package/modules/Status.sh +1 -0
- package/modules/Update.sh +1 -0
- package/modules/apps/Apps.md +1 -0
- package/modules/utility/Utility.md +1 -0
- package/package.json +9 -11
- package/docs/retold-building-documentation.md +0 -33
- 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
|
-
|
|
5
|
-
|
|
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 —
|
|
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
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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);
|