turbometro 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/turbometro.js +2 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +107 -0
  6. package/dist/find-root.d.ts +2 -0
  7. package/dist/find-root.js +14 -0
  8. package/dist/graph.d.ts +14 -0
  9. package/dist/graph.js +74 -0
  10. package/dist/layout.d.ts +19 -0
  11. package/dist/layout.js +15 -0
  12. package/dist/parse-turbo.d.ts +5 -0
  13. package/dist/parse-turbo.js +29 -0
  14. package/dist/parse-workspace.d.ts +4 -0
  15. package/dist/parse-workspace.js +100 -0
  16. package/dist/render-html.d.ts +8 -0
  17. package/dist/render-html.js +699 -0
  18. package/dist/render-map.d.ts +29 -0
  19. package/dist/render-map.js +287 -0
  20. package/dist/replay.d.ts +9 -0
  21. package/dist/replay.js +74 -0
  22. package/dist/scale.d.ts +7 -0
  23. package/dist/scale.js +19 -0
  24. package/dist/scene-data.d.ts +40 -0
  25. package/dist/scene-data.js +146 -0
  26. package/dist/theme.d.ts +15 -0
  27. package/dist/theme.js +15 -0
  28. package/dist/types.d.ts +30 -0
  29. package/dist/types.js +1 -0
  30. package/fixtures/mini-mono/apps/web/package.json +11 -0
  31. package/fixtures/mini-mono/package.json +5 -0
  32. package/fixtures/mini-mono/packages/ui/package.json +11 -0
  33. package/fixtures/mini-mono/packages/utils/package.json +8 -0
  34. package/fixtures/mini-mono/pnpm-workspace.yaml +3 -0
  35. package/fixtures/mini-mono/turbo.json +10 -0
  36. package/package.json +44 -0
  37. package/vendor/dag-map/LICENSE +201 -0
  38. package/vendor/dag-map/NOTICE +6 -0
  39. package/vendor/dag-map/src/color-scales.js +61 -0
  40. package/vendor/dag-map/src/dag-map.css +63 -0
  41. package/vendor/dag-map/src/events.js +108 -0
  42. package/vendor/dag-map/src/graph-utils.js +185 -0
  43. package/vendor/dag-map/src/hasse.css +61 -0
  44. package/vendor/dag-map/src/index.js +55 -0
  45. package/vendor/dag-map/src/layout-flow.js +1066 -0
  46. package/vendor/dag-map/src/layout-hasse.js +485 -0
  47. package/vendor/dag-map/src/layout-metro.js +542 -0
  48. package/vendor/dag-map/src/occupancy.js +138 -0
  49. package/vendor/dag-map/src/render-flow-station.js +132 -0
  50. package/vendor/dag-map/src/render.js +360 -0
  51. package/vendor/dag-map/src/route-angular.js +137 -0
  52. package/vendor/dag-map/src/route-bezier.js +51 -0
  53. package/vendor/dag-map/src/route-metro.js +122 -0
  54. package/vendor/dag-map/src/themes.js +49 -0
@@ -0,0 +1,185 @@
1
+ // ================================================================
2
+ // graph-utils.js — Shared graph primitives for dag-map layout engines
3
+ // ================================================================
4
+ // Adjacency map construction and Kahn's algorithm topological sort
5
+ // with longest-path rank assignment. Used by all three layout engines.
6
+
7
+ /**
8
+ * Build adjacency maps from nodes and edges.
9
+ * @param {Array<{id: string}>} nodes
10
+ * @param {Array<[string, string]>} edges
11
+ * @returns {{ nodeMap: Map, childrenOf: Map, parentsOf: Map }}
12
+ */
13
+ export function buildGraph(nodes, edges) {
14
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
15
+ const childrenOf = new Map();
16
+ const parentsOf = new Map();
17
+ nodes.forEach(n => { childrenOf.set(n.id, []); parentsOf.set(n.id, []); });
18
+ edges.forEach(([f, t], edgeIdx) => {
19
+ const srcChildren = childrenOf.get(f);
20
+ const dstParents = parentsOf.get(t);
21
+ if (!srcChildren || !dstParents) {
22
+ const parts = [];
23
+ if (!srcChildren) parts.push(`source "${f}"`);
24
+ if (!dstParents) parts.push(`target "${t}"`);
25
+ throw new Error(`buildGraph: edge[${edgeIdx}] references unknown ${parts.join(' and ')}`);
26
+ }
27
+ srcChildren.push(t);
28
+ dstParents.push(f);
29
+ });
30
+ return { nodeMap, childrenOf, parentsOf };
31
+ }
32
+
33
+ /**
34
+ * Topological sort via Kahn's algorithm with longest-path rank assignment.
35
+ * @param {Array<{id: string}>} nodes
36
+ * @param {Map} childrenOf
37
+ * @param {Map} parentsOf
38
+ * @returns {{ topo: string[], rank: Map<string, number>, maxRank: number }}
39
+ */
40
+ export function topoSortAndRank(nodes, childrenOf, parentsOf) {
41
+ const rank = new Map();
42
+ const inDeg = new Map();
43
+ nodes.forEach(nd => inDeg.set(nd.id, parentsOf.get(nd.id).length));
44
+
45
+ const queue = nodes.filter(nd => inDeg.get(nd.id) === 0).map(nd => nd.id);
46
+ queue.forEach(id => rank.set(id, 0));
47
+
48
+ const topo = [];
49
+ while (queue.length) {
50
+ const u = queue.shift();
51
+ topo.push(u);
52
+ for (const v of childrenOf.get(u)) {
53
+ rank.set(v, Math.max(rank.get(v) || 0, rank.get(u) + 1));
54
+ inDeg.set(v, inDeg.get(v) - 1);
55
+ if (inDeg.get(v) === 0) queue.push(v);
56
+ }
57
+ }
58
+
59
+ const maxRank = topo.length > 0 ? Math.max(...topo.map(id => rank.get(id))) : 0;
60
+ return { topo, rank, maxRank };
61
+ }
62
+
63
+ /**
64
+ * Validate a DAG definition and return warnings for common issues.
65
+ * Non-throwing — returns an array of human-readable warning strings.
66
+ * @param {Array<{id: string}>} nodes
67
+ * @param {Array<[string, string]>} edges
68
+ * @returns {string[]} warnings (empty if valid)
69
+ */
70
+ export function validateDag(nodes, edges) {
71
+ const warnings = [];
72
+ const ids = new Set();
73
+
74
+ // Duplicate node IDs
75
+ for (const n of nodes) {
76
+ if (ids.has(n.id)) warnings.push(`Duplicate node ID: "${n.id}"`);
77
+ ids.add(n.id);
78
+ }
79
+
80
+ // Edges referencing unknown nodes
81
+ for (const [f, t] of edges) {
82
+ if (!ids.has(f)) warnings.push(`Edge source "${f}" is not a known node`);
83
+ if (!ids.has(t)) warnings.push(`Edge target "${t}" is not a known node`);
84
+ }
85
+
86
+ // Cycle detection via topo sort
87
+ if (warnings.length === 0 && nodes.length > 0) {
88
+ const { childrenOf, parentsOf } = buildGraph(nodes, edges);
89
+ const { topo } = topoSortAndRank(nodes, childrenOf, parentsOf);
90
+ if (topo.length < nodes.length) {
91
+ const missing = nodes.filter(n => !topo.includes(n.id)).map(n => n.id);
92
+ warnings.push(`Cycle detected — ${missing.length} node(s) unreachable: ${missing.join(', ')}`);
93
+ }
94
+ }
95
+
96
+ return warnings;
97
+ }
98
+
99
+ /**
100
+ * Validate a DAG definition and throw with a useful message if invalid.
101
+ * @param {Array<{id: string}>} nodes
102
+ * @param {Array<[string, string]>} edges
103
+ * @param {string} [context='DAG']
104
+ */
105
+ export function assertValidDag(nodes, edges, context = 'DAG') {
106
+ const warnings = validateDag(nodes, edges);
107
+ if (warnings.length > 0) {
108
+ throw new Error(`${context}: invalid DAG input. ${warnings.join('; ')}`);
109
+ }
110
+ }
111
+
112
+ // ================================================================
113
+ // SVG path coordinate swap (X↔Y) for orientation transforms
114
+ // ================================================================
115
+
116
+ // How many coordinate values each SVG command consumes per repetition.
117
+ // Commands that take (x,y) pairs: values are swapped pairwise.
118
+ // H↔V are single-axis and swap command letter instead.
119
+ // A (arc) is not supported — its 7-param layout doesn't pair-swap cleanly.
120
+ const CMD_PARAMS = {
121
+ M: 2, L: 2, T: 2, // 1 pair
122
+ Q: 4, S: 4, // 2 pairs
123
+ C: 6, // 3 pairs
124
+ H: 1, V: 1, // single axis — letter swaps
125
+ Z: 0, // no params
126
+ };
127
+
128
+ /**
129
+ * Swap X↔Y coordinates in an SVG path string.
130
+ * Handles M, L, C, Q, S, T, H, V, Z (both absolute and relative).
131
+ * Throws on A/a (arc) — arc parameter layout requires special handling.
132
+ *
133
+ * @param {string} d - SVG path data string
134
+ * @returns {string} path with all X and Y coordinates swapped
135
+ */
136
+ export function swapPathXY(d) {
137
+ if (!d) return '';
138
+
139
+ // Tokenize: split into command + numbers sequences.
140
+ // Regex captures a command letter followed by its numeric arguments.
141
+ const tokens = [];
142
+ const re = /([MLCSQTHVZAmlcsqthvza])\s*([^MLCSQTHVZAmlcsqthvza]*)/g;
143
+ let m;
144
+ while ((m = re.exec(d)) !== null) {
145
+ const cmd = m[1];
146
+ const argStr = m[2].trim();
147
+ const nums = argStr.length > 0 ? argStr.split(/[\s,]+/).map(Number) : [];
148
+ tokens.push({ cmd, nums });
149
+ }
150
+
151
+ const parts = [];
152
+ for (const { cmd, nums } of tokens) {
153
+ const upper = cmd.toUpperCase();
154
+
155
+ if (upper === 'A') {
156
+ throw new Error('swapPathXY: arc commands (A/a) are not supported');
157
+ }
158
+
159
+ if (upper === 'Z') {
160
+ parts.push(cmd);
161
+ continue;
162
+ }
163
+
164
+ if (upper === 'H') {
165
+ // H x → V x (swap command letter, keep value)
166
+ parts.push((cmd === 'H' ? 'V' : 'v') + ' ' + nums.join(' '));
167
+ continue;
168
+ }
169
+
170
+ if (upper === 'V') {
171
+ // V y → H y
172
+ parts.push((cmd === 'V' ? 'H' : 'h') + ' ' + nums.join(' '));
173
+ continue;
174
+ }
175
+
176
+ // Pair-swapping commands: swap every (x, y) → (y, x)
177
+ const swapped = [];
178
+ for (let i = 0; i < nums.length; i += 2) {
179
+ swapped.push(nums[i + 1], nums[i]);
180
+ }
181
+ parts.push(cmd + ' ' + swapped.join(' '));
182
+ }
183
+
184
+ return parts.join(' ');
185
+ }
@@ -0,0 +1,61 @@
1
+ /* =================================================================
2
+ * hasse.css — Library stylesheet for dag-map Hasse diagrams
3
+ * =================================================================
4
+ * The minimal CSS needed to use layoutHasse in your application.
5
+ * Provides CSS custom properties for theming and base container styles.
6
+ *
7
+ * Usage:
8
+ * <link rel="stylesheet" href="hasse.css">
9
+ * <div class="dm-container" id="my-diagram"></div>
10
+ *
11
+ * Override theme via CSS custom properties:
12
+ * :root { --dm-paper: #1E1E2E; --dm-ink: #CDD6F4; }
13
+ *
14
+ * Or via the JS theme system (takes precedence):
15
+ * layoutHasse(dag, { theme: 'dark' });
16
+ */
17
+
18
+ /* ── CSS Custom Properties (defaults = cream theme) ── */
19
+ :root {
20
+ --dm-paper: #F5F0E8;
21
+ --dm-ink: #2C2C2C;
22
+ --dm-muted: #8C8680;
23
+ --dm-border: #D4CFC7;
24
+ --dm-font: 'IBM Plex Mono', 'Courier New', monospace;
25
+ --dm-radius: 4px;
26
+
27
+ /* Node class colors (used when cssVars: true) */
28
+ --dm-cls-top: #C45B4A; /* ⊤ — maximum element */
29
+ --dm-cls-type: #2B8A8E; /* regular elements */
30
+ --dm-cls-bottom: #5B7FA8; /* ⊥ — minimum element */
31
+ }
32
+
33
+ /* ── Container ── */
34
+ .dm-container {
35
+ background: var(--dm-paper);
36
+ font-family: var(--dm-font);
37
+ color: var(--dm-ink);
38
+ transition: background 0.2s;
39
+ }
40
+
41
+ .dm-container svg {
42
+ display: block;
43
+ }
44
+
45
+ /* ── Code snippet (optional — for showing generated options) ── */
46
+ .dm-code {
47
+ margin: 12px 24px;
48
+ padding: 12px 16px;
49
+ background: rgba(0, 0, 0, 0.03);
50
+ border: 1px solid var(--dm-border);
51
+ border-radius: var(--dm-radius);
52
+ font-family: var(--dm-font);
53
+ font-size: 11px;
54
+ line-height: 1.5;
55
+ color: var(--dm-ink);
56
+ white-space: pre;
57
+ overflow-x: auto;
58
+ cursor: text;
59
+ user-select: all;
60
+ -webkit-user-select: all;
61
+ }
@@ -0,0 +1,55 @@
1
+ // ================================================================
2
+ // dag-map — DAG visualization as metro maps
3
+ // ================================================================
4
+ // Public API
5
+
6
+ import { layoutMetro, dominantClass } from './layout-metro.js';
7
+ import { layoutHasse } from './layout-hasse.js';
8
+ import { layoutFlow } from './layout-flow.js';
9
+ import { renderSVG } from './render.js';
10
+ import { bezierPath } from './route-bezier.js';
11
+ import { angularPath, progressiveCurve } from './route-angular.js';
12
+ import { metroPath } from './route-metro.js';
13
+ import { THEMES, resolveTheme } from './themes.js';
14
+ import { createStationRenderer, createEdgeRenderer } from './render-flow-station.js';
15
+ import { validateDag, swapPathXY } from './graph-utils.js';
16
+ import { colorScales } from './color-scales.js';
17
+ import { bindEvents } from './events.js';
18
+
19
+ export { layoutMetro, dominantClass };
20
+ export { layoutHasse };
21
+ export { layoutFlow };
22
+ export { renderSVG };
23
+ export { bindEvents };
24
+ export { bezierPath };
25
+ export { angularPath, progressiveCurve };
26
+ export { metroPath };
27
+ export { THEMES, resolveTheme };
28
+ export { createStationRenderer, createEdgeRenderer };
29
+ export { validateDag, swapPathXY };
30
+ export { colorScales };
31
+
32
+ /**
33
+ * Convenience function: compute layout and render SVG in one call.
34
+ *
35
+ * @param {object} dag - { nodes: [{id, label, cls}], edges: [[from, to]] }
36
+ * @param {object} [options] - combined layout + render options
37
+ * @param {'bezier'|'angular'|'metro'} [options.routing='angular']
38
+ * @param {string|object} [options.theme='cream'] - theme name or custom theme object
39
+ * @param {string} [options.title]
40
+ * @param {boolean} [options.diagonalLabels=false]
41
+ * @param {boolean} [options.showLegend=true]
42
+ * @param {number} [options.trunkY=160]
43
+ * @param {number} [options.mainSpacing=34]
44
+ * @param {number} [options.subSpacing=16]
45
+ * @param {number} [options.layerSpacing=38]
46
+ * @param {number} [options.progressivePower=2.2]
47
+ * @param {number} [options.scale=1.5]
48
+ * @param {number} [options.maxLanes] - max number of lanes to search
49
+ * @returns {{ layout: object, svg: string }}
50
+ */
51
+ export function dagMap(dag, options = {}) {
52
+ const layout = layoutMetro(dag, options);
53
+ const svg = renderSVG(dag, layout, options);
54
+ return { layout, svg };
55
+ }