trace-viz 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.
Files changed (110) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +385 -0
  4. package/dist/adapters/index.d.ts +32 -0
  5. package/dist/adapters/index.d.ts.map +1 -0
  6. package/dist/adapters/index.js +83 -0
  7. package/dist/adapters/index.js.map +1 -0
  8. package/dist/agent.d.ts +66 -0
  9. package/dist/agent.d.ts.map +1 -0
  10. package/dist/agent.js +329 -0
  11. package/dist/agent.js.map +1 -0
  12. package/dist/atlas/atlas.d.ts +203 -0
  13. package/dist/atlas/atlas.d.ts.map +1 -0
  14. package/dist/atlas/atlas.js +1466 -0
  15. package/dist/atlas/atlas.js.map +1 -0
  16. package/dist/atlas/index.d.ts +7 -0
  17. package/dist/atlas/index.d.ts.map +1 -0
  18. package/dist/atlas/index.js +4 -0
  19. package/dist/atlas/index.js.map +1 -0
  20. package/dist/atlas/layout.d.ts +44 -0
  21. package/dist/atlas/layout.d.ts.map +1 -0
  22. package/dist/atlas/layout.js +103 -0
  23. package/dist/atlas/layout.js.map +1 -0
  24. package/dist/atlas/model.d.ts +164 -0
  25. package/dist/atlas/model.d.ts.map +1 -0
  26. package/dist/atlas/model.js +489 -0
  27. package/dist/atlas/model.js.map +1 -0
  28. package/dist/channels.d.ts +105 -0
  29. package/dist/channels.d.ts.map +1 -0
  30. package/dist/channels.js +411 -0
  31. package/dist/channels.js.map +1 -0
  32. package/dist/components.d.ts +76 -0
  33. package/dist/components.d.ts.map +1 -0
  34. package/dist/components.js +432 -0
  35. package/dist/components.js.map +1 -0
  36. package/dist/estate.d.ts +25 -0
  37. package/dist/estate.d.ts.map +1 -0
  38. package/dist/estate.js +285 -0
  39. package/dist/estate.js.map +1 -0
  40. package/dist/failure.d.ts +36 -0
  41. package/dist/failure.d.ts.map +1 -0
  42. package/dist/failure.js +82 -0
  43. package/dist/failure.js.map +1 -0
  44. package/dist/geometry.d.ts +24 -0
  45. package/dist/geometry.d.ts.map +1 -0
  46. package/dist/geometry.js +92 -0
  47. package/dist/geometry.js.map +1 -0
  48. package/dist/index.d.ts +51 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +34 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/layout.d.ts +66 -0
  53. package/dist/layout.d.ts.map +1 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/layout.js.map +1 -0
  56. package/dist/layout3d.d.ts +98 -0
  57. package/dist/layout3d.d.ts.map +1 -0
  58. package/dist/layout3d.js +350 -0
  59. package/dist/layout3d.js.map +1 -0
  60. package/dist/mesh.d.ts +23 -0
  61. package/dist/mesh.d.ts.map +1 -0
  62. package/dist/mesh.js +263 -0
  63. package/dist/mesh.js.map +1 -0
  64. package/dist/model.d.ts +94 -0
  65. package/dist/model.d.ts.map +1 -0
  66. package/dist/model.js +84 -0
  67. package/dist/model.js.map +1 -0
  68. package/dist/particles.d.ts +80 -0
  69. package/dist/particles.d.ts.map +1 -0
  70. package/dist/particles.js +118 -0
  71. package/dist/particles.js.map +1 -0
  72. package/dist/project.d.ts +113 -0
  73. package/dist/project.d.ts.map +1 -0
  74. package/dist/project.js +288 -0
  75. package/dist/project.js.map +1 -0
  76. package/dist/react/index.d.ts +22 -0
  77. package/dist/react/index.d.ts.map +1 -0
  78. package/dist/react/index.js +69 -0
  79. package/dist/react/index.js.map +1 -0
  80. package/dist/renderer.d.ts +252 -0
  81. package/dist/renderer.d.ts.map +1 -0
  82. package/dist/renderer.js +1710 -0
  83. package/dist/renderer.js.map +1 -0
  84. package/dist/scales.d.ts +99 -0
  85. package/dist/scales.d.ts.map +1 -0
  86. package/dist/scales.js +126 -0
  87. package/dist/scales.js.map +1 -0
  88. package/dist/scene.d.ts +53 -0
  89. package/dist/scene.d.ts.map +1 -0
  90. package/dist/scene.js +44 -0
  91. package/dist/scene.js.map +1 -0
  92. package/dist/schema.d.ts +24 -0
  93. package/dist/schema.d.ts.map +1 -0
  94. package/dist/schema.js +131 -0
  95. package/dist/schema.js.map +1 -0
  96. package/dist/seed.d.ts +33 -0
  97. package/dist/seed.d.ts.map +1 -0
  98. package/dist/seed.js +173 -0
  99. package/dist/seed.js.map +1 -0
  100. package/dist/theme.d.ts +35 -0
  101. package/dist/theme.d.ts.map +1 -0
  102. package/dist/theme.js +83 -0
  103. package/dist/theme.js.map +1 -0
  104. package/dist/trace-viz.esm.js +15 -0
  105. package/dist/trace-viz.min.js +15 -0
  106. package/dist/types.d.ts +92 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +9 -0
  109. package/dist/types.js.map +1 -0
  110. package/package.json +105 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ ## 0.3.0
2
+
3
+ - **Atlas**: a map-style navigator for estates of hundreds of services (`examples/atlas.html`, `src/atlas/`, `docs/ATLAS.md`). Semantic zoom over an ownership hierarchy, swimlane layout (x = call depth, y = ownership), aggregation that conserves traffic at every level, focus with partial expansion ("N more" tiles), Kafka topics as rails with lag fill, lenses, trails, blast radius, breadcrumbs, ⌘K go-to, minimap heat strip, URL-encoded views.
4
+ - `generateEstate()`: 600 services / 80 topics / 12 domains / 61 teams seed with heavy-tailed traffic, hubs, cycles, a sick and a burning service and backed-up topics.
5
+
6
+ ## 0.2.1
7
+
8
+ - Error halo redesigned: bound to an error rate, the halo is now a red glow on an absolute scale (nothing under 0.4%, a rim at ~1%, a wide slow-breathing bloom past 6%) instead of a ring relative to the worst node. `NodeStyle.haloIntensity` exposes the 0..1 strength; `ERROR_FLOOR` / `ERROR_CEIL` are exported.
9
+ - Seed mesh now has a realistic error tail: one service having a bad day (2-5%) and one busy API on fire (12-30%), the same in both regions.
10
+
11
+ # Changelog
12
+
13
+ Published on npm as `trace-viz`.
14
+
15
+ ## 0.2.0
16
+
17
+ The picture became a projection of the data rather than a property of it.
18
+
19
+ - Dimensional flow model: a table of flows with arbitrary dimensions, per-thing
20
+ facts (instances, cpu, mem, engine), and `project(table, spec)` that chooses
21
+ what a box is at runtime.
22
+ - Twelve visual channels in a registry, each bindable to any compatible field.
23
+ Binding a dimension to an edge channel splits edges by it; a channel refuses a
24
+ binding it cannot show.
25
+ - `share` as a derived measure and the default for `distance`, so coupled
26
+ services sit together. `strength` dials it.
27
+ - Fail-closed as a first-party concept: rigid vs breakaway couplings and a
28
+ failure front that travels backwards up the call graph.
29
+ - World mode: three planes, a component library of software silhouettes,
30
+ instances as visible mass, gauges, an orbiting camera.
31
+ - Focus: expand one service into its APIs while the rest stays collapsed.
32
+ - Re-projection morphs; particle streams are carried across.
33
+ - A spec agent that composes views from a request and validates the result
34
+ field by field, with a deterministic fallback.
35
+ - The studio: the spec as an editable sentence, an inspector, an interactive
36
+ legend, one-click scenarios, a ⌘K palette, and a drawer with every knob.
37
+
38
+ ## 0.1.0
39
+
40
+ Flat topology renderer: density is rps, speed is 1/latency, rings are errors,
41
+ retry and challenge traffic is dashed. Seed generator and the auth-challenge
42
+ scenario pair.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yatharth Saluja and trace-viz contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,385 @@
1
+ # trace-viz
2
+
3
+ Live demo: https://salujayatharth.github.io/trace-viz/
4
+
5
+ A visualization layer for service topologies. You hand it a table of flows; it
6
+ draws them as a living picture — and *which* picture is a runtime decision, not
7
+ a property of the data.
8
+
9
+ It is only a visualization layer: no agent, no collector, no opinion about where
10
+ your metrics come from. MIT licensed, zero runtime dependencies, usable from a
11
+ `<script>` tag, an ESM import, or React.
12
+
13
+ ```
14
+ npm install trace-viz
15
+ ```
16
+
17
+ | Studio: one picture, every knob | Atlas: 600 services as a map |
18
+ |---|---|
19
+ | ![](./examples/studio-dark-map.png) | ![](./examples/atlas-dark-team.png) |
20
+ | One box per service, colour and coupling bound to dimensions | Semantic zoom over domain › team › service, Kafka topics as rails |
21
+
22
+ ## Atlas: the map for large estates
23
+
24
+ Service maps stop working at a few hundred nodes, and the usual answer -
25
+ filters - throws away the context you need. **Atlas** (`/atlas.html`,
26
+ [design notes](./docs/ATLAS.md)) treats the estate as a map instead:
27
+
28
+ - **Semantic zoom.** Domains are tiles; zoom into one and it becomes a band of
29
+ team tiles; zoom into a team and it becomes services. Everything else stays
30
+ on the map, collapsed, with all of its traffic aggregated onto it. Zoom out
31
+ when the map already fits and the group under the cursor folds back up.
32
+ - **Swimlanes, not force.** x is depth in the call chain, y is ownership. A
33
+ collapsed team's tile stretches across the depths its services occupy, and a
34
+ ribbon leaving a domain leaves at the depth the traffic actually leaves.
35
+ - **You are always somewhere.** Breadcrumbs, `⌘K` go-to, a URL that encodes
36
+ the view, an inspector whose callers and callees are links you walk, and a
37
+ minimap that is a heat strip of where the errors are.
38
+ - **Focus.** Click a service and its neighbourhood (k hops) opens at leaf
39
+ level; teams that only contribute one neighbour open *partially*, folding
40
+ their other 44 services into one "44 more" tile.
41
+ - **Kafka is not an edge.** A topic is a rail with partition ticks; producers
42
+ land on it, consumer groups leave it, and lag fills the pipe from the left,
43
+ amber turning red.
44
+ - **Lenses.** Traffic, Reliability, Latency, Ownership, Kafka, Blast radius -
45
+ one click each, one meaning each.
46
+
47
+ | Domains (13 tiles, 681 things) | Focus with partial expansion | Blast radius: a store dies |
48
+ |---|---|---|
49
+ | ![](./examples/atlas-dark-domains.png) | ![](./examples/atlas-dark-focus.png) | ![](./examples/atlas-dark-blast.png) |
50
+
51
+ ## The idea
52
+
53
+ Most service maps have one shape: a node is a box, an edge is a number. That
54
+ shape cannot express a real system, where a "service" is a dozen APIs, each API
55
+ runs in several environments and regions, every dependency has its own failure
56
+ contract, and every box is a fleet with a size and a CPU number.
57
+
58
+ Those are all *dimensions of the same traffic*. So trace-viz takes one flat
59
+ table and treats the picture as a **projection** of it:
60
+
61
+ - **What a box is** is a choice. `['service']` gives you twelve boxes;
62
+ `['service','api']` gives you forty; `['region','service']` gives you the same
63
+ services twice, once per region. Changing it re-projects and the mesh
64
+ **morphs** — boxes glide, merge and split from where they were, and the
65
+ particles stay in flight — because it is the same system seen differently, not
66
+ a different diagram.
67
+ - **Every dimension is a knob.** Bind any dimension or measure to any visual
68
+ channel. There is a registry of them, and adding one is how you extend this.
69
+ - **Failure semantics are first class**, not a colour. See below.
70
+
71
+ ## The channels
72
+
73
+ Each channel is a separate visual property with its own resolver, so any
74
+ compatible field can drive any of them at runtime.
75
+
76
+ | Channel | Takes | What it does |
77
+ |---|---|---|
78
+ | `density` | measure | particles per second along the edge (log-scaled) |
79
+ | `speed` | measure | how fast they travel; inverted, so high values crawl |
80
+ | `radius` | measure | particle size (sqrt-scaled) |
81
+ | `hue` | dimension | particle colour, one per category |
82
+ | `glyph` | dimension | particle *shape* — readable in greyscale, so it pairs with hue |
83
+ | `dash` | dimension | the line pattern under the particles |
84
+ | `lane` | dimension | splits one edge into parallel lanes, one per category |
85
+ | `coupling` | dimension | failure semantics: a rigid conduit, or a link designed to part |
86
+ | `distance` | either | **proximity** — by default pulls on `share`, the fraction of a caller's traffic on the edge, so *coupled* services sit together; bind `rps` for raw volume, or a dimension to cluster things that share a value |
87
+ | `nodeFill` | dimension | node colour |
88
+ | `halo` | measure | a ring around a node sized by a measure — traffic through it, or a numeric fact such as `instances` |
89
+ | `column` | dimension | columns by a dimension instead of inferred layers; `order: [...]` sets the left-to-right sequence, `layout.rowSort` orders the rows (`auto` for fewest crossings, `name`, `traffic`) |
90
+
91
+ `distance` takes a `strength` (0 off, 1 default, up to 3) so coupling can be
92
+ dialled up until the clusters are unmistakable or down until the layout is calm.
93
+
94
+ Two rules the library enforces rather than documents:
95
+
96
+ **Binding a dimension to an edge channel splits the edges by it.** Colour by
97
+ region and one edge becomes two lanes, one per region. Averaging a dimension you
98
+ are also encoding would draw a colour that describes nothing.
99
+
100
+ **A channel refuses a binding it cannot show.** Forty API names will not go on
101
+ `hue`; the binding is dropped and the reason is reported, rather than rendered as
102
+ forty indistinguishable colours.
103
+
104
+ **A small split is drawn overlaid, not separated.** Two regions or three
105
+ environments share one conduit: the streams scatter across its width and emit out
106
+ of phase, so blue and green dots *mix* and you can see at a glance that this
107
+ dependency carries both — and roughly in what ratio. Two tidy parallel lanes hide
108
+ exactly that. Past three values the lanes separate, because a mixed stream of six
109
+ colours is only noise. Reciprocal edges always separate: a response must never
110
+ hide under the request it answers.
111
+
112
+ Red and amber are absent from the front of the categorical palette on purpose.
113
+ They are semantic here — red is an error, amber is degraded — and a category that
114
+ borrows one makes a healthy region look like an incident.
115
+
116
+ `share` is derived at projection time — an edge's rps over everything its
117
+ caller sends — and is what coupling means here: two services that send most of
118
+ what they have to each other are coupled whatever their absolute volume. It is
119
+ what `distance` pulls on unless you bind something else, in both the flat map
120
+ (where it weights the ordering) and the world.
121
+
122
+ Measures aggregate by kind: throughput sums, everything else is a
123
+ throughput-weighted mean. A 10k rps hop at 2ms and a 1 rps hop at 900ms do not
124
+ average to 451ms.
125
+
126
+ ## Fail-closed is a first-party concept
127
+
128
+ `fail-open` and `fail-closed` are not decoration on a dependency — they are its
129
+ contract, so they get a model and a shape of their own.
130
+
131
+ - A **rigid** coupling (fail-closed) is drawn as a mechanical conduit: two rails
132
+ with cross-ties. It looks like something that transmits force, because it does.
133
+ - A **breakaway** coupling (fail-open) has a visible parting line — a gap with
134
+ two facing chevrons.
135
+
136
+ Then kill something. A failure front travels *backwards* up the call graph, one
137
+ hop at a time and slowly enough to watch: rigid couplings fracture and the caller
138
+ dies with the callee; breakaway couplings open, an amber shield marks where the
139
+ front stopped, and the caller is left degraded but serving. Dead nodes are
140
+ hatched, not merely greyed, so a screenshot survives being pasted into a chat.
141
+
142
+ In the bundled mesh, killing `fraud` costs you one service. Killing `identity`
143
+ takes the gateway with it. Killing `notify-queue` — the one nobody would have
144
+ guessed — takes down eight. That asymmetry is the entire argument for drawing it.
145
+
146
+ ```ts
147
+ tl.kill(['identity']);
148
+ tl.stats(); // { deadNodes, degradedNodes, deadRps, degradedRps, ... }
149
+ ```
150
+
151
+ ## World mode
152
+
153
+ `mode: 'world'` swaps the flat topology for a small three-dimensional place:
154
+ three planes — what talks to you, what runs your code, what holds your state —
155
+ and a camera you drag to look round a cluster.
156
+
157
+ Boxes become **software**, not rectangles, chosen from a component library:
158
+ a rack for compute, a cylinder for a relational store, separated discs for a
159
+ column store, a cube for key-value, a drum for a log, a chip for a cache, a
160
+ bucket for object storage. More instances is visibly more machine; CPU and memory
161
+ ride along as gauges on the body. You can read the tier, the kind and the size of
162
+ a thing at a glance, without reading a single label.
163
+
164
+ Two deliberate choices:
165
+
166
+ - **Orthographic, not perspective.** Far things are not drawn smaller, because
167
+ size already means the instance count and two encodings cannot share a channel.
168
+ - **Separation is computed in pixels, against the real camera.** Spacing things
169
+ out in world coordinates achieves nothing on its own — the camera just zooms
170
+ out to fit, leaving the overlap exactly where it was.
171
+
172
+ ### Focus
173
+
174
+ Looking at one service's APIs should not turn every service into five boxes:
175
+
176
+ ```ts
177
+ tl.setSpec({ ...spec, focus: { match: { service: 'orders' }, expandBy: ['api'] } });
178
+ ```
179
+
180
+ `orders` explodes into its APIs, everything else stays collapsed, and the mesh
181
+ reshapes around it.
182
+
183
+ ## Composing the view with a model
184
+
185
+ `trace-viz/agent` lets a model choose the projection from a plain request. It
186
+ never writes code and never draws: it selects from what is already registered —
187
+ node keys from the table's own dimensions, channels from the registry, components
188
+ from the component library — and emits a `ViewSpec`, which is then **validated
189
+ field by field** before it reaches the renderer.
190
+
191
+ ```ts
192
+ import { proposeSpec, describeTable } from 'trace-viz';
193
+
194
+ const { spec, issues, source } = await proposeSpec(
195
+ 'cluster tightly coupled services in 3D and show me what fails if identity dies',
196
+ { complete: myModelCall, schema: describeTable(table) },
197
+ );
198
+ tl.setSpec(spec);
199
+ ```
200
+
201
+ A hallucinated channel, a field that does not exist, a component that was never
202
+ registered, a forty-value dimension on a colour channel — each is dropped with a
203
+ reason in `issues`, not rendered. That containment is what makes it safe to run
204
+ on every question.
205
+
206
+ **On model size.** Picking a node key and two or three bindings is well within a
207
+ small, fast model, and that is the common case. Composing a whole *world* — tiers,
208
+ per-engine components, what belongs on a gauge, what should cluster near what —
209
+ is a design task, and small models are noticeably worse at it: they put
210
+ high-cardinality dimensions on colour, flatten everything onto one tier, and skip
211
+ the dimensions that would have been informative. So the model is a parameter, not
212
+ a dependency. Use the quick tier for re-bindings and a stronger one for world
213
+ composition, and `heuristicSpec()` covers both with no model at all — which is
214
+ also what makes the tests deterministic.
215
+
216
+ ## Data format
217
+
218
+ ```jsonc
219
+ {
220
+ "records": [
221
+ {
222
+ "from": { "service": "orders", "api": "POST /orders", "kind": "service" },
223
+ "to": { "service": "pricing", "api": "GET /quote", "kind": "service" },
224
+ "dims": { "env": "prod", "region": "eu-west", "failure": "fail-open", "protocol": "grpc" },
225
+ "metrics": { "rps": 840, "latencyMs": 18, "errorRate": 0.004, "bytes": 1400 }
226
+ }
227
+ ],
228
+ "nodes": [
229
+ {
230
+ "key": { "service": "orders" },
231
+ "dims": { "env": "prod", "region": "eu-west", "team": "fulfilment", "engine": "postgres" },
232
+ "attrs": { "instances": 16, "cpu": 0.62, "mem": 0.71 },
233
+ "component": "db-cylinder"
234
+ }
235
+ ]
236
+ }
237
+ ```
238
+
239
+ `from` and `to` are coordinates in dimension space, not ids — what counts as a
240
+ node is decided later. `dims` are dimensions of the flow itself. `nodes` are facts
241
+ about *things* rather than flows, and they attach at whatever resolution the
242
+ current projection happens to use: a fact keyed `{service: 'orders'}` lands on the
243
+ single `orders` box, and on every `orders / <api>` box when you expand it. Counts
244
+ sum; everything else is instance-weighted.
245
+
246
+ The v0.1 flat `{nodes, edges}` graph still works unchanged via `setGraph`.
247
+
248
+ ## Quick start
249
+
250
+ ```html
251
+ <div id="map" style="height: 720px"></div>
252
+ <script src="https://unpkg.com/trace-viz/dist/trace-viz.min.js"></script>
253
+ <script>
254
+ const { TraceLight, generateMesh } = tracelight; // the script-tag global keeps the library's internal name
255
+ const tl = new TraceLight(document.getElementById('map'), { theme: 'auto' });
256
+ tl.setTable(generateMesh(), {
257
+ nodeKey: ['service'],
258
+ where: { env: ['prod'], region: ['eu-west'] },
259
+ mode: 'world',
260
+ channels: { coupling: { field: 'failure' }, distance: { field: 'rps' } },
261
+ world: { instances: 'instances', gauges: ['cpu', 'mem'] },
262
+ });
263
+ </script>
264
+ ```
265
+
266
+ React:
267
+
268
+ ```tsx
269
+ import { TraceLightView } from 'trace-viz/react';
270
+ <TraceLightView graph={graph} theme="auto" style={{ height: 720 }} />;
271
+ ```
272
+
273
+ The renderer owns the canvas and the animation loop, so React re-renders never
274
+ restart the particle streams.
275
+
276
+ ## Numbers are off by default
277
+
278
+ Every edge can print its rps and latency, and by default none of them do. On a
279
+ real topology the numbers turn a legible picture into a wall of small type, and
280
+ they duplicate work the visuals already do better: throughput is particle density
281
+ *and* conduit thickness, latency is how fast the particles crawl. The exact
282
+ figures live one hover away, where you want them when you want them, and
283
+ `showEdgeLabels: true` puts them back for the tasks that are about reading
284
+ figures rather than seeing shape.
285
+
286
+ ## The particles
287
+
288
+ Each request is a soft-edged point of light with a short tail behind it along
289
+ its path. The tail's length is the particle's own speed, so a fast hop streaks
290
+ and a slow one crawls, and the eye reads motion rather than a dot that happens
291
+ to be somewhere else next frame. Overlapping lanes blend instead of occluding.
292
+ The defaults say what they encode: `density` is rps, `speed` is 1/latency,
293
+ `size` is bytes, `colour` is whatever dimension you bind. Particle *shape* is
294
+ parked until the particle system itself has settled.
295
+
296
+ In world mode the camera orbits on its own — a full turn in about two minutes —
297
+ so the depth of the mesh keeps revealing itself; it pauses under the pointer and
298
+ while you drag, and `orbit: false` turns it off.
299
+
300
+ ## On the pacing
301
+
302
+ The animation is deliberately slow. An earlier, faster set of defaults looked
303
+ impressive and told you nothing: at 260px/s the streams read as texture, and
304
+ texture cannot be counted. Slower and sparser means one request is a thing you
305
+ can watch leave one box and arrive at the next, which is the only reason to draw
306
+ them individually instead of writing a number on a line. `speed` scales it;
307
+ `prefers-reduced-motion` switches to a static rendering where throughput moves
308
+ into line width.
309
+
310
+ ## Where the data comes from
311
+
312
+ One interface, so a static file today and a live feed later are the same thing to
313
+ the renderer:
314
+
315
+ ```ts
316
+ interface GraphSource {
317
+ load(): Promise<Graph>;
318
+ subscribe?(onGraph: (g: Graph) => void, onError?: (e: Error) => void): () => void;
319
+ }
320
+ ```
321
+
322
+ Shipped: `fromObject`, `fromUrl(url, { pollMs })`, `fromEventSource`,
323
+ `pollSource`. Planned, in order: a **SQLite adapter** (a `flows` table read
324
+ Node-side and rolled up per time bucket, which also gives you a scrubber over
325
+ history), then **interpolation between snapshots** so live density eases into its
326
+ new value instead of stepping. Collecting the metrics is deliberately out of
327
+ scope.
328
+
329
+ ## Demos
330
+
331
+ ```bash
332
+ npm install && npm run build && npm run demo # → http://localhost:4173
333
+ ```
334
+
335
+ - `/studio.html` — the studio. The view spec is an editable sentence across the
336
+ top ("A box is one **service** in **prod** · colour **region** · density
337
+ **rps** · speed 1/**latencyMs** · size **bytes** · coupling **failure** ·
338
+ cluster **share** · columns **team**"); click any token to change it. The
339
+ opening view fills boxes by kind, halos them by instance count, arranges them
340
+ in team columns, and explodes the gateway's call into `orders` to API level. Click anything
341
+ on the canvas to inspect it, double-click a service to explode it into its
342
+ APIs, hover a legend swatch to isolate that value in context, ⌘K to ask for a
343
+ view in words. Five one-click scenarios along the top show what the framework
344
+ is for. The sentence is the fast path; `K` opens the **knobs** drawer, which is
345
+ the complete one — every dimension as a multi-value filter, all twelve
346
+ channels, the world's tier/shape/mass/gauge/camera settings, playback, a
347
+ multi-kill panel, and the raw spec as editable JSON with apply, copy, PNG
348
+ export and reset. Nothing the renderer takes is hidden from it.
349
+ - `/atlas.html` — the map: `generateEstate()` (600 services, 80 topics, 12
350
+ domains, 61 teams) with semantic zoom, lenses, focus, trails and blast radius.
351
+ - `/mesh.html` — the same data with plain controls, for reference
352
+ - `/index.html` — the small one: an API whose auth costs two round trips per call
353
+
354
+ | Studio, map | Studio, world under a kill |
355
+ |---|---|
356
+ | ![](./examples/studio-dark-inspect.png) | ![](./examples/studio-dark-blast.png) |
357
+
358
+ ## API
359
+
360
+ | Export | What it is |
361
+ |---|---|
362
+ | `TraceLight` | the renderer: `setGraph`, `setTable`, `setSpec`, `kill`, `health`, `select`, `setHighlight`, `setYaw`, `stats`, `schema`, `getLegend`, `toDataURL` |
363
+ | `TraceLightView` (`trace-viz/react`) | React wrapper |
364
+ | `project`, `describeTable` | projection and schema introspection |
365
+ | `CHANNELS`, `styleScene` | the channel registry and its resolver |
366
+ | `COMPONENTS`, `pickComponent` | the world component library |
367
+ | `layoutWorld`, `fitCamera`, `project3` | the 3D layout, usable on its own |
368
+ | `propagateFailure` | blast radius, without a canvas |
369
+ | `proposeSpec`, `validateSpec`, `heuristicSpec` | the spec agent and its validator |
370
+ | `generateMesh`, `generateSeedGraph`, `generateEstate`, `authChallengeScenario` | seed data |
371
+ | `Atlas`, `LENSES` | the map: `setTable`, `expand`, `collapse`, `up`, `focus`, `setHops`, `goTo`, `search`, `inspect`, `breadcrumb`, `setLens`, `kill`, `revive`, `setTrail`, `fit`, `drawMinimap`, `getState`/`setState` |
372
+ | `buildAtlas`, `visibleUnits`, `aggregateEdges`, `layoutAtlas`, `neighbourhood`, `trail`, `propagate` | the Atlas model, usable without a canvas |
373
+
374
+ ## Development
375
+
376
+ ```bash
377
+ npm run typecheck
378
+ npm test # 38 tests, no browser needed
379
+ npm run build
380
+ npm run screenshots
381
+ ```
382
+
383
+ ## License
384
+
385
+ MIT — see [LICENSE](./LICENSE). Use it for anything, commercial included.
@@ -0,0 +1,32 @@
1
+ import type { Graph } from '../types.js';
2
+ /**
3
+ * A source of graphs.
4
+ *
5
+ * `load()` is the only required method, which is what makes today's static
6
+ * JSON and tomorrow's live feed the same thing to the renderer. A source that
7
+ * can push updates implements `subscribe`; one that cannot is polled by
8
+ * {@link pollSource}. Nothing in the renderer knows the difference.
9
+ */
10
+ export interface GraphSource {
11
+ readonly name: string;
12
+ load(): Promise<Graph>;
13
+ /** Push updates. Return an unsubscribe function. */
14
+ subscribe?(onGraph: (graph: Graph) => void, onError?: (err: Error) => void): () => void;
15
+ close?(): void;
16
+ }
17
+ /** A graph you already have in memory. */
18
+ export declare function fromObject(graph: Graph, name?: string): GraphSource;
19
+ /** Fetch a JSON document. Add `pollMs` and it becomes a (crude) live source. */
20
+ export declare function fromUrl(url: string, { pollMs, fetchOptions }?: {
21
+ pollMs?: number;
22
+ fetchOptions?: RequestInit;
23
+ }): GraphSource;
24
+ /**
25
+ * Server-sent events carrying one JSON graph per message. This is the intended
26
+ * shape of the live mode: the server decides the aggregation window, the client
27
+ * just redraws.
28
+ */
29
+ export declare function fromEventSource(url: string): GraphSource;
30
+ /** Turn any `load()`-only source into a pushing one. */
31
+ export declare function pollSource(source: GraphSource, intervalMs: number, onGraph: (graph: Graph) => void, onError?: (err: Error) => void): () => void;
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,oDAAoD;IACpD,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxF,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,SAAW,GAAG,WAAW,CAErE;AAED,gFAAgF;AAChF,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,EAAE,MAAM,EAAE,YAAY,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,WAAW,CAAA;CAAO,GAC7E,WAAW,CAWb;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAkCxD;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAC/B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAC7B,MAAM,IAAI,CAiBZ"}
@@ -0,0 +1,83 @@
1
+ /** A graph you already have in memory. */
2
+ export function fromObject(graph, name = 'inline') {
3
+ return { name, load: async () => graph };
4
+ }
5
+ /** Fetch a JSON document. Add `pollMs` and it becomes a (crude) live source. */
6
+ export function fromUrl(url, { pollMs, fetchOptions } = {}) {
7
+ const load = async () => {
8
+ const res = await fetch(url, fetchOptions);
9
+ if (!res.ok)
10
+ throw new Error(`tracelight: ${url} responded ${res.status}`);
11
+ return (await res.json());
12
+ };
13
+ const source = { name: url, load };
14
+ if (pollMs && pollMs > 0) {
15
+ source.subscribe = (onGraph, onError) => pollSource(source, pollMs, onGraph, onError);
16
+ }
17
+ return source;
18
+ }
19
+ /**
20
+ * Server-sent events carrying one JSON graph per message. This is the intended
21
+ * shape of the live mode: the server decides the aggregation window, the client
22
+ * just redraws.
23
+ */
24
+ export function fromEventSource(url) {
25
+ let es;
26
+ return {
27
+ name: url,
28
+ load: () => new Promise((resolve, reject) => {
29
+ const once = new EventSource(url);
30
+ once.onmessage = (ev) => {
31
+ once.close();
32
+ try {
33
+ resolve(JSON.parse(ev.data));
34
+ }
35
+ catch (err) {
36
+ reject(err);
37
+ }
38
+ };
39
+ once.onerror = () => {
40
+ once.close();
41
+ reject(new Error(`tracelight: event source ${url} failed`));
42
+ };
43
+ }),
44
+ subscribe(onGraph, onError) {
45
+ es = new EventSource(url);
46
+ es.onmessage = (ev) => {
47
+ try {
48
+ onGraph(JSON.parse(ev.data));
49
+ }
50
+ catch (err) {
51
+ onError?.(err);
52
+ }
53
+ };
54
+ es.onerror = () => onError?.(new Error(`tracelight: event source ${url} failed`));
55
+ return () => es?.close();
56
+ },
57
+ close: () => es?.close(),
58
+ };
59
+ }
60
+ /** Turn any `load()`-only source into a pushing one. */
61
+ export function pollSource(source, intervalMs, onGraph, onError) {
62
+ let stopped = false;
63
+ let timer;
64
+ const tick = async () => {
65
+ try {
66
+ const g = await source.load();
67
+ if (!stopped)
68
+ onGraph(g);
69
+ }
70
+ catch (err) {
71
+ if (!stopped)
72
+ onError?.(err);
73
+ }
74
+ if (!stopped)
75
+ timer = setTimeout(tick, intervalMs);
76
+ };
77
+ void tick();
78
+ return () => {
79
+ stopped = true;
80
+ clearTimeout(timer);
81
+ };
82
+ }
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import type { TableSchema } from './model.js';
2
+ import { type ViewSpec } from './project.js';
3
+ /**
4
+ * Composing the view with a model.
5
+ *
6
+ * The model never draws anything and never writes code. It selects from what is
7
+ * already registered - node keys from the table's own dimensions, channels from
8
+ * the registry, components from the component library - and emits a `ViewSpec`,
9
+ * which is then validated field by field before it reaches the renderer. A
10
+ * hallucinated channel or a dimension that does not exist is dropped, not
11
+ * rendered. That containment is the whole reason this is safe to run every time
12
+ * the user asks a question.
13
+ *
14
+ * **On model size.** Picking a node key and two or three channel bindings is
15
+ * well within a small, fast model, and that is the common case. Composing a
16
+ * whole world - tiers, per-engine components, what belongs on a gauge, what
17
+ * clusters near what - is a design task, and small models are noticeably worse
18
+ * at it: they bind high-cardinality dimensions to colour, put everything on one
19
+ * tier, and ignore the ones that would have been informative. Pass a stronger
20
+ * model for `world` composition and keep the small one for quick re-bindings.
21
+ * `heuristicSpec` covers both with no model at all.
22
+ */
23
+ export type Completion = (input: {
24
+ system: string;
25
+ user: string;
26
+ }) => Promise<string>;
27
+ export interface ProposeOptions {
28
+ complete: Completion;
29
+ schema: TableSchema;
30
+ /** The spec being edited, so the model can make a small change rather than a new view. */
31
+ current?: ViewSpec;
32
+ }
33
+ export interface ProposalResult {
34
+ spec: ViewSpec;
35
+ /** Everything that was corrected or dropped during validation. */
36
+ issues: string[];
37
+ source: 'model' | 'fallback';
38
+ /** The raw model text, for debugging a bad proposal. */
39
+ raw?: string;
40
+ }
41
+ /** Ask a model to compose the view, then validate whatever comes back. */
42
+ export declare function proposeSpec(request: string, opts: ProposeOptions): Promise<ProposalResult>;
43
+ export declare function buildPrompt(request: string, schema: TableSchema, current?: ViewSpec): {
44
+ system: string;
45
+ user: string;
46
+ };
47
+ /**
48
+ * Validate a proposed spec against the table and the registry.
49
+ *
50
+ * Everything unrecognised is dropped with a reason rather than rejected whole:
51
+ * a proposal that got four things right and one wrong is still worth rendering,
52
+ * and the dropped item is reported so the user can see what happened.
53
+ */
54
+ export declare function validateSpec(raw: unknown, schema: TableSchema): {
55
+ spec: ViewSpec;
56
+ issues: string[];
57
+ };
58
+ /**
59
+ * A deterministic reading of the request, used when no model is available and
60
+ * as the floor under a bad proposal. Crude on purpose: it should never surprise
61
+ * anyone, and it makes the tests reproducible.
62
+ */
63
+ export declare function heuristicSpec(request: string, schema: TableSchema, current?: ViewSpec): ViewSpec;
64
+ /** Pull the first balanced JSON object out of a model response, fences and all. */
65
+ export declare function extractJson(text: string): unknown;
66
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAkC,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,UAAU,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAC;IACf,kEAAkE;IAClE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,GAAG,UAAU,CAAC;IAC7B,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,0EAA0E;AAC1E,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA6BhG;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,QAAQ,GACjB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmElC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAqHpG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAyChG;AAID,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA2BjD"}