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.
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/adapters/index.d.ts +32 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +329 -0
- package/dist/agent.js.map +1 -0
- package/dist/atlas/atlas.d.ts +203 -0
- package/dist/atlas/atlas.d.ts.map +1 -0
- package/dist/atlas/atlas.js +1466 -0
- package/dist/atlas/atlas.js.map +1 -0
- package/dist/atlas/index.d.ts +7 -0
- package/dist/atlas/index.d.ts.map +1 -0
- package/dist/atlas/index.js +4 -0
- package/dist/atlas/index.js.map +1 -0
- package/dist/atlas/layout.d.ts +44 -0
- package/dist/atlas/layout.d.ts.map +1 -0
- package/dist/atlas/layout.js +103 -0
- package/dist/atlas/layout.js.map +1 -0
- package/dist/atlas/model.d.ts +164 -0
- package/dist/atlas/model.d.ts.map +1 -0
- package/dist/atlas/model.js +489 -0
- package/dist/atlas/model.js.map +1 -0
- package/dist/channels.d.ts +105 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +411 -0
- package/dist/channels.js.map +1 -0
- package/dist/components.d.ts +76 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +432 -0
- package/dist/components.js.map +1 -0
- package/dist/estate.d.ts +25 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +285 -0
- package/dist/estate.js.map +1 -0
- package/dist/failure.d.ts +36 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +82 -0
- package/dist/failure.js.map +1 -0
- package/dist/geometry.d.ts +24 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +92 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +66 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +152 -0
- package/dist/layout.js.map +1 -0
- package/dist/layout3d.d.ts +98 -0
- package/dist/layout3d.d.ts.map +1 -0
- package/dist/layout3d.js +350 -0
- package/dist/layout3d.js.map +1 -0
- package/dist/mesh.d.ts +23 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +263 -0
- package/dist/mesh.js.map +1 -0
- package/dist/model.d.ts +94 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +84 -0
- package/dist/model.js.map +1 -0
- package/dist/particles.d.ts +80 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +118 -0
- package/dist/particles.js.map +1 -0
- package/dist/project.d.ts +113 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +288 -0
- package/dist/project.js.map +1 -0
- package/dist/react/index.d.ts +22 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +69 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer.d.ts +252 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +1710 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scales.d.ts +99 -0
- package/dist/scales.d.ts.map +1 -0
- package/dist/scales.js +126 -0
- package/dist/scales.js.map +1 -0
- package/dist/scene.d.ts +53 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +44 -0
- package/dist/scene.js.map +1 -0
- package/dist/schema.d.ts +24 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +131 -0
- package/dist/schema.js.map +1 -0
- package/dist/seed.d.ts +33 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +173 -0
- package/dist/seed.js.map +1 -0
- package/dist/theme.d.ts +35 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +83 -0
- package/dist/theme.js.map +1 -0
- package/dist/trace-viz.esm.js +15 -0
- package/dist/trace-viz.min.js +15 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- 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
|
+
|  |  |
|
|
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
|
+
|  |  |  |
|
|
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
|
+
|  |  |
|
|
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":"AAkBA,0CAA0C;AAC1C,MAAM,UAAU,UAAU,CAAC,KAAY,EAAE,IAAI,GAAG,QAAQ;IACtD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;AAC3C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,OAAO,CACrB,GAAW,EACX,EAAE,MAAM,EAAE,YAAY,KAAsD,EAAE;IAE9E,MAAM,IAAI,GAAG,KAAK,IAAoB,EAAE;QACtC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,cAAc,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAC3E,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAU,CAAC;IACrC,CAAC,CAAC;IACF,MAAM,MAAM,GAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAChD,IAAI,MAAM,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,SAAS,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,EAA2B,CAAC;IAChC,OAAO;QACL,IAAI,EAAE,GAAG;QACT,IAAI,EAAE,GAAG,EAAE,CACT,IAAI,OAAO,CAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE;gBACtB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACb,IAAI,CAAC;oBACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAU,CAAC,CAAC;gBACxC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,GAAY,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC,CAAC;YACF,IAAI,CAAC,OAAO,GAAG,GAAG,EAAE;gBAClB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAC,CAAC;YAC9D,CAAC,CAAC;QACJ,CAAC,CAAC;QACJ,SAAS,CAAC,OAAO,EAAE,OAAO;YACxB,EAAE,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC;YAC1B,EAAE,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE;gBACpB,IAAI,CAAC;oBACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAU,CAAC,CAAC;gBACxC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,OAAO,EAAE,CAAC,GAAY,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC;YACF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAC,CAAC;YAClF,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;QAC3B,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE;KACzB,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,UAAU,CACxB,MAAmB,EACnB,UAAkB,EAClB,OAA+B,EAC/B,OAA8B;IAE9B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,KAAoC,CAAC;IACzC,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;QACrC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,IAAI,CAAC,OAAO;gBAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAC,GAAY,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,OAAO;YAAE,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACrD,CAAC,CAAC;IACF,KAAK,IAAI,EAAE,CAAC;IACZ,OAAO,GAAG,EAAE;QACV,OAAO,GAAG,IAAI,CAAC;QACf,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/agent.d.ts
ADDED
|
@@ -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"}
|