loom-spec 0.3.0 → 0.5.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/dist/cli/exportHtml.d.ts +22 -0
- package/dist/cli/exportHtml.js +182 -0
- package/dist/cli/exportHtml.js.map +1 -0
- package/dist/cli/index.js +40 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +3 -207
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/app.js +2 -50
- package/dist/server/app.js.map +1 -1
- package/dist/server/exportConfig.d.ts +40 -0
- package/dist/server/exportConfig.js +78 -0
- package/dist/server/exportConfig.js.map +1 -0
- package/dist/server/exportFilter.d.ts +52 -0
- package/dist/server/exportFilter.js +138 -0
- package/dist/server/exportFilter.js.map +1 -0
- package/dist/server/fileOps.d.ts +0 -12
- package/dist/server/fileOps.js +0 -51
- package/dist/server/fileOps.js.map +1 -1
- package/dist/server/watch.d.ts +1 -1
- package/dist/server/watch.js +0 -5
- package/dist/server/watch.js.map +1 -1
- package/dist/validate.d.ts +1 -3
- package/dist/validate.js +0 -15
- package/dist/validate.js.map +1 -1
- package/dist/view/assets/index-D8qr-jiw.css +1 -0
- package/dist/view/assets/index-DI0VS0HQ.js +205 -0
- package/dist/view/index.html +2 -2
- package/dist/{view/assets/index-CvyHnPjR.css → view-export/assets/bundle.css} +1 -1
- package/dist/{view/assets/index-Du05xzao.js → view-export/assets/bundle.js} +44 -49
- package/dist/view-export/index.html +24 -0
- package/package.json +3 -2
- package/templates/.claude/skills/loom-spec/SKILL.md +91 -76
- package/templates/.loom/README.md +1 -1
- package/dist/cli/importTrace.d.ts +0 -15
- package/dist/cli/importTrace.js +0 -188
- package/dist/cli/importTrace.js.map +0 -1
- package/dist/server/otel.d.ts +0 -32
- package/dist/server/otel.js +0 -98
- package/dist/server/otel.js.map +0 -1
- package/dist/types/timeline.d.ts +0 -97
- package/dist/types/timeline.js +0 -7
- package/dist/types/timeline.js.map +0 -1
- package/dist/view/assets/TimelineView-DEfpV9mL.js +0 -16
- package/schema/timeline.schema.json +0 -135
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>loom-spec</title>
|
|
7
|
+
<script>
|
|
8
|
+
// Apply persisted theme before paint to avoid flash.
|
|
9
|
+
try {
|
|
10
|
+
const t = localStorage.getItem("loom-theme");
|
|
11
|
+
if (t === "light" || t === "dark") {
|
|
12
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
13
|
+
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
14
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
17
|
+
</script>
|
|
18
|
+
<script type="module" crossorigin src="/assets/bundle.js"></script>
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/assets/bundle.css">
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="root"></div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loom-spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Node-based architecture spec that lives in your repo. AI-readable, AI-writable, git-diffable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "René Jesser",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"./node-types-schema": "./schema/node-types.schema.json"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"build": "tsc -p tsconfig.json && vite build --config src/view/vite.config.ts",
|
|
50
|
+
"build": "tsc -p tsconfig.json && vite build --config src/view/vite.config.ts && vite build --config src/view/vite.config.export.ts",
|
|
51
|
+
"build:export": "vite build --config src/view/vite.config.export.ts",
|
|
51
52
|
"dev": "concurrently -n server,view -c blue,magenta \"pnpm dev:server\" \"pnpm dev:view\"",
|
|
52
53
|
"dev:server": "tsx src/cli/index.ts view --dev --root ../../examples/todo-app --port 7778",
|
|
53
54
|
"dev:view": "vite --config src/view/vite.config.ts",
|
|
@@ -59,6 +59,38 @@ For any task that touches structure:
|
|
|
59
59
|
`<name>.flow.json` instead of cramming it into an existing diagram.
|
|
60
60
|
- Link from the overview with a `drill_down` reference if appropriate.
|
|
61
61
|
|
|
62
|
+
### Tagging hygiene for exports
|
|
63
|
+
|
|
64
|
+
`loom-spec export-html` filters by node `tags` to produce scoped bundles
|
|
65
|
+
(public manual, ops runbook, internal overview). Tagging is the source
|
|
66
|
+
of truth for what ships where. Conventions:
|
|
67
|
+
|
|
68
|
+
- **`public`** — shows up in user-facing documentation. Default-off
|
|
69
|
+
(untagged nodes are *not* in public exports).
|
|
70
|
+
- **`internal`** — explicitly internal; can be used as `--exclude-tag`
|
|
71
|
+
for public exports, or as `--include-tag` for an internal-only bundle.
|
|
72
|
+
- **`ops`** — for operational runbooks (deploy paths, monitoring,
|
|
73
|
+
on-call docs).
|
|
74
|
+
- **`wip`** — work-in-progress; always exclude from any export.
|
|
75
|
+
|
|
76
|
+
When you set `tags: ["public"]` on a node, remember the cascade:
|
|
77
|
+
|
|
78
|
+
- Edges between two `public` nodes survive. Edges where one endpoint is
|
|
79
|
+
untagged are **dropped** in the public export. If the user-facing
|
|
80
|
+
flow depends on an "internal" node visually, either tag that node
|
|
81
|
+
`public` too or accept the dangling visualisation.
|
|
82
|
+
- A group with no `public` children disappears entirely.
|
|
83
|
+
- A `drill_down` chevron pointing at a diagram that has zero `public`
|
|
84
|
+
nodes is removed (the diagram doesn't ship).
|
|
85
|
+
|
|
86
|
+
**Security check before tagging `public`:** look at the node's
|
|
87
|
+
`code_refs[].path` and `description`. Tags filter nodes, not their
|
|
88
|
+
content. If a `code_ref` points at `src/server/admin/secrets.ts` or
|
|
89
|
+
the description names an internal system, that *text* ships in the
|
|
90
|
+
public export. Either remove the sensitive ref / rewrite the
|
|
91
|
+
description, or split the node into a public surface and an internal
|
|
92
|
+
implementation node and tag accordingly.
|
|
93
|
+
|
|
62
94
|
## Preferred tools (when the MCP server is wired up)
|
|
63
95
|
|
|
64
96
|
If a `loom-spec` MCP server is registered with the host (e.g. via
|
|
@@ -67,8 +99,6 @@ If a `loom-spec` MCP server is registered with the host (e.g. via
|
|
|
67
99
|
- `loom_list_diagrams`, `loom_read_diagram`, `loom_read_node_types`
|
|
68
100
|
- `loom_add_node`, `loom_update_node`, `loom_mark_stale`, `loom_delete_node`
|
|
69
101
|
- `loom_add_edge`, `loom_delete_edge`
|
|
70
|
-
- `loom_list_timelines`, `loom_read_timeline`
|
|
71
|
-
- `loom_add_event`, `loom_update_event`, `loom_delete_event`
|
|
72
102
|
- `loom_validate` (schema + code-ref drift across every diagram)
|
|
73
103
|
|
|
74
104
|
They validate against the schema before writing, so invalid edits fail fast
|
|
@@ -78,6 +108,16 @@ re-reading + re-writing JSON on every mutation.
|
|
|
78
108
|
If the MCP server is not available, edit the JSON files directly using the
|
|
79
109
|
rules above — the format is stable and tools-agnostic by design.
|
|
80
110
|
|
|
111
|
+
For exporting the spec to a self-contained interactive HTML (for manuals,
|
|
112
|
+
docs sites, GitHub Pages, embed-in-Notion, etc.), use the CLI:
|
|
113
|
+
|
|
114
|
+
- `loom-spec export-html` (full export)
|
|
115
|
+
- `loom-spec export-html <bundle-name>` (from `.loom/exports.json`)
|
|
116
|
+
- `loom-spec export-html --include-tag public --out manual.html`
|
|
117
|
+
(ad-hoc filter)
|
|
118
|
+
|
|
119
|
+
See Example 6 for the full workflow.
|
|
120
|
+
|
|
81
121
|
---
|
|
82
122
|
|
|
83
123
|
## Examples
|
|
@@ -252,91 +292,58 @@ loom_add_node({
|
|
|
252
292
|
})
|
|
253
293
|
```
|
|
254
294
|
|
|
255
|
-
### 6. User wants to
|
|
295
|
+
### 6. User wants to publish architecture docs to a manual
|
|
256
296
|
|
|
257
|
-
> User: "
|
|
258
|
-
>
|
|
259
|
-
|
|
260
|
-
A timeline is a horizontal time-axis overlay on a specific diagram. Each
|
|
261
|
-
event references a node in that diagram and a `[start_ms, duration_ms]`
|
|
262
|
-
interval. The view plays the events back like a DAW edit, lighting up the
|
|
263
|
-
referenced nodes in a mini graph beside the timeline.
|
|
297
|
+
> User: "We need to ship the checkout flow as an interactive diagram in
|
|
298
|
+
> our public user manual. Don't expose anything internal."
|
|
264
299
|
|
|
265
300
|
```
|
|
266
|
-
# Step 1:
|
|
267
|
-
loom_list_diagrams()
|
|
301
|
+
# Step 1: Identify which nodes belong in the public manual.
|
|
268
302
|
loom_read_diagram("overview")
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
#
|
|
303
|
+
# → review nodes; confirm with the user if scope is unclear
|
|
304
|
+
|
|
305
|
+
# Step 2: Tag the public-facing surface. Skip anything that exposes
|
|
306
|
+
# internal services, security-sensitive paths, or work-in-progress.
|
|
307
|
+
loom_update_node({ diagram: "overview", id: "checkout-page",
|
|
308
|
+
patch: { tags: ["public"] } })
|
|
309
|
+
loom_update_node({ diagram: "overview", id: "checkout-api",
|
|
310
|
+
patch: { tags: ["public"] } })
|
|
311
|
+
loom_update_node({ diagram: "overview", id: "payments-service",
|
|
312
|
+
patch: { tags: ["public"] } })
|
|
313
|
+
# … but NOT fraud-screening, admin-tools, internal-billing, etc.
|
|
314
|
+
|
|
315
|
+
# Step 3: Verify the tag set covers a connected slice. Edges between
|
|
316
|
+
# two public nodes survive; edges to untagged neighbours get dropped
|
|
317
|
+
# in the export. If the export would have orphans, either tag the
|
|
318
|
+
# missing neighbour or accept that the link disappears.
|
|
319
|
+
|
|
320
|
+
# Step 4: Write a named bundle to .loom/exports.json so the export is
|
|
321
|
+
# reproducible. (No MCP tool for this yet — write the file directly.)
|
|
272
322
|
#
|
|
273
|
-
# .loom/
|
|
323
|
+
# .loom/exports.json
|
|
274
324
|
{
|
|
275
|
-
"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{ "id": "server", "label": "API", "color": "#dcfce7" },
|
|
283
|
-
{ "id": "data", "label": "Postgres", "color": "#ede9fe" }
|
|
284
|
-
],
|
|
285
|
-
"events": []
|
|
325
|
+
"exports": {
|
|
326
|
+
"user-manual": {
|
|
327
|
+
"include-tags": ["public"],
|
|
328
|
+
"exclude-tags": ["wip"],
|
|
329
|
+
"out": "docs/architecture.html"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
286
332
|
}
|
|
287
333
|
|
|
288
|
-
# Step
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
timeline: "checkout",
|
|
292
|
-
node: "checkout-page",
|
|
293
|
-
track: "client",
|
|
294
|
-
start_ms: 0,
|
|
295
|
-
duration_ms: 8,
|
|
296
|
-
kind: "compute",
|
|
297
|
-
label: "click pay",
|
|
298
|
-
code_refs: [{ path: "src/views/Checkout.tsx", symbol: "handlePay" }],
|
|
299
|
-
tags: ["critical-path", "user-input"]
|
|
300
|
-
})
|
|
301
|
-
# → { ok: true, id: "ev1" }
|
|
302
|
-
|
|
303
|
-
loom_add_event({
|
|
304
|
-
timeline: "checkout",
|
|
305
|
-
node: "payments-api",
|
|
306
|
-
track: "server",
|
|
307
|
-
start_ms: 12,
|
|
308
|
-
duration_ms: 320,
|
|
309
|
-
kind: "io",
|
|
310
|
-
label: "POST /checkout (Stripe)",
|
|
311
|
-
triggered_by: "ev1", # explicit causation arrow
|
|
312
|
-
tags: ["critical-path", "external-io"]
|
|
313
|
-
})
|
|
314
|
-
# → { ok: true, id: "ev2" }
|
|
315
|
-
|
|
316
|
-
# Step 4: Adjust if you got something wrong.
|
|
317
|
-
loom_update_event({
|
|
318
|
-
timeline: "checkout",
|
|
319
|
-
id: "ev2",
|
|
320
|
-
patch: { duration_ms: 280 }
|
|
321
|
-
})
|
|
334
|
+
# Step 5: Generate the HTML.
|
|
335
|
+
# (Shell, not MCP — agents can invoke via Bash tool or similar.)
|
|
336
|
+
$ loom-spec export-html user-manual
|
|
322
337
|
|
|
323
|
-
# Step
|
|
324
|
-
|
|
338
|
+
# Step 6: Sanity-check the output. Open docs/architecture.html in a
|
|
339
|
+
# browser and confirm: no internal node names visible, no surprising
|
|
340
|
+
# code_refs paths leaked in the inspector, the flow makes sense as a
|
|
341
|
+
# standalone visualisation.
|
|
325
342
|
```
|
|
326
343
|
|
|
327
|
-
**
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
flow). The diagram alone shows topology, not sequencing.
|
|
331
|
-
- You want one node referenced multiple times because it does different
|
|
332
|
-
things at different moments (e.g. an auth-service that validates *then
|
|
333
|
-
later* re-issues a JWT). Use `code_refs` on each event for function-level
|
|
334
|
-
granularity inside the same node.
|
|
335
|
-
- The user describes a *trace*, a *user journey*, or a *failure case* that
|
|
336
|
-
has a clock.
|
|
337
|
-
|
|
338
|
-
**Don't create a timeline for static structure** — that's what diagrams are
|
|
339
|
-
for. A timeline of "the app boots, then runs forever" adds noise.
|
|
344
|
+
**Don't auto-publish** — the export is intentional. A `git add` of the
|
|
345
|
+
generated `.html` belongs in the change that updates the architecture,
|
|
346
|
+
not in an automated commit triggered by every `.loom/` edit.
|
|
340
347
|
|
|
341
348
|
---
|
|
342
349
|
|
|
@@ -364,3 +371,11 @@ for. A timeline of "the app boots, then runs forever" adds noise.
|
|
|
364
371
|
**flow**.
|
|
365
372
|
- Don't leave `drill_down` pointing at a non-existent diagram id.
|
|
366
373
|
- Don't `loom_delete_node` for code that was just removed — `mark_stale` it.
|
|
374
|
+
- Don't tag everything `public` "just in case" — the value of a tag is
|
|
375
|
+
that it means something. If `public` is on every node, scoped exports
|
|
376
|
+
stop being scoped.
|
|
377
|
+
- Don't manually edit the generated `.html` from `export-html` — re-run
|
|
378
|
+
the export instead. Edits to the generated file are lost on the next
|
|
379
|
+
run and obscure the source of truth.
|
|
380
|
+
- Don't add `loom-spec export-html` to `init` defaults or auto-run it
|
|
381
|
+
from a hook. Exports are intentional, not background.
|
|
@@ -6,7 +6,7 @@ This directory contains the node-based architecture spec for the project. It is
|
|
|
6
6
|
|
|
7
7
|
- `node-types.json` — defines the available node types for this project. Customize freely; add types specific to your domain.
|
|
8
8
|
- `diagrams/*.flow.json` — one file per subsystem. Each is a self-contained graph.
|
|
9
|
-
- `
|
|
9
|
+
- `exports.json` — (optional) named export bundles for `loom-spec export-html` (e.g. a `user-manual` bundle that filters to `tags: ["public"]`).
|
|
10
10
|
|
|
11
11
|
## Viewing and editing
|
|
12
12
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface ImportTraceArgs {
|
|
2
|
-
/** Path to the OTLP JSON trace file. */
|
|
3
|
-
trace: string;
|
|
4
|
-
/** Timeline id to create or append into. */
|
|
5
|
-
asId: string;
|
|
6
|
-
/** Diagram id the new timeline overlays. */
|
|
7
|
-
diagramId: string;
|
|
8
|
-
/** Optional path to a mapping file (see MappingFile shape below). */
|
|
9
|
-
map?: string;
|
|
10
|
-
/** Append to an existing timeline instead of overwriting. */
|
|
11
|
-
append: boolean;
|
|
12
|
-
/** Working directory root (walked up to find .loom/). */
|
|
13
|
-
root: string;
|
|
14
|
-
}
|
|
15
|
-
export declare function runImportTrace(args: ImportTraceArgs): Promise<void>;
|
package/dist/cli/importTrace.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { findLoomRoot } from "../server/findLoomRoot.js";
|
|
4
|
-
import { readDiagram, readTimeline, writeTimeline } from "../server/fileOps.js";
|
|
5
|
-
import { parseOtlpJson } from "../server/otel.js";
|
|
6
|
-
import { validateTimeline } from "../validate.js";
|
|
7
|
-
const KIND_TO_EVENT_KIND = {
|
|
8
|
-
internal: "compute",
|
|
9
|
-
server: "io",
|
|
10
|
-
client: "io",
|
|
11
|
-
producer: "io",
|
|
12
|
-
consumer: "io",
|
|
13
|
-
unknown: undefined,
|
|
14
|
-
};
|
|
15
|
-
async function loadMapping(path) {
|
|
16
|
-
const raw = await readFile(path, "utf8");
|
|
17
|
-
const parsed = JSON.parse(raw);
|
|
18
|
-
return {
|
|
19
|
-
serviceMap: new Map(Object.entries(parsed.services ?? {})),
|
|
20
|
-
spanMap: new Map(Object.entries(parsed.spans ?? {})),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Decide which node a span belongs to.
|
|
25
|
-
* Precedence: explicit span map > explicit service map > heuristic match.
|
|
26
|
-
*/
|
|
27
|
-
function resolveNode(span, mapping, nodes) {
|
|
28
|
-
if (mapping) {
|
|
29
|
-
const direct = mapping.spanMap.get(span.name);
|
|
30
|
-
if (direct)
|
|
31
|
-
return direct;
|
|
32
|
-
if (span.serviceName) {
|
|
33
|
-
const svc = mapping.serviceMap.get(span.serviceName);
|
|
34
|
-
if (svc)
|
|
35
|
-
return svc;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// Heuristic: try matching span.name first (it's most specific — for
|
|
39
|
-
// CLIENT/PRODUCER spans it names the *downstream* target, which is the
|
|
40
|
-
// node we want), then fall back to service.name.
|
|
41
|
-
const candidates = [];
|
|
42
|
-
candidates.push(span.name.toLowerCase());
|
|
43
|
-
if (span.serviceName)
|
|
44
|
-
candidates.push(span.serviceName.toLowerCase());
|
|
45
|
-
for (const c of candidates) {
|
|
46
|
-
// 1. exact id match
|
|
47
|
-
const idHit = nodes.find((n) => n.id.toLowerCase() === c);
|
|
48
|
-
if (idHit)
|
|
49
|
-
return idHit.id;
|
|
50
|
-
// 2. node id appears as a token inside the candidate
|
|
51
|
-
// (e.g. "todo-store update" → finds id "todo-store")
|
|
52
|
-
const idInCandidate = nodes.find((n) => c.includes(n.id.toLowerCase()));
|
|
53
|
-
if (idInCandidate)
|
|
54
|
-
return idInCandidate.id;
|
|
55
|
-
// 3. candidate appears in a node label
|
|
56
|
-
// (e.g. "todo-api" → finds label "Todo API")
|
|
57
|
-
const labelHit = nodes.find((n) => n.label.toLowerCase().includes(c));
|
|
58
|
-
if (labelHit)
|
|
59
|
-
return labelHit.id;
|
|
60
|
-
// 4. code-ref path includes the candidate
|
|
61
|
-
const refHit = nodes.find((n) => (n.code_refs ?? []).some((r) => r.path.toLowerCase().includes(c)));
|
|
62
|
-
if (refHit)
|
|
63
|
-
return refHit.id;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
function eventIdFor(spanId, existingIds) {
|
|
68
|
-
// Stable: take the first 8 hex chars of the span id, prefix with "ev".
|
|
69
|
-
// Fall back to a sequence if collision (shouldn't happen in practice).
|
|
70
|
-
const base = `ev-${spanId.slice(0, 8) || "span"}`.toLowerCase();
|
|
71
|
-
if (!existingIds.has(base))
|
|
72
|
-
return base;
|
|
73
|
-
let i = 2;
|
|
74
|
-
while (existingIds.has(`${base}-${i}`))
|
|
75
|
-
i++;
|
|
76
|
-
return `${base}-${i}`;
|
|
77
|
-
}
|
|
78
|
-
export async function runImportTrace(args) {
|
|
79
|
-
// 1. Locate the .loom/ root and load the diagram we're overlaying.
|
|
80
|
-
const loomRoot = await findLoomRoot(args.root);
|
|
81
|
-
const diagram = await readDiagram(loomRoot.loomPath, args.diagramId);
|
|
82
|
-
// 2. Load and parse the trace.
|
|
83
|
-
const traceRaw = await readFile(resolve(args.trace), "utf8");
|
|
84
|
-
const traceJson = JSON.parse(traceRaw);
|
|
85
|
-
const spans = parseOtlpJson(traceJson);
|
|
86
|
-
if (spans.length === 0) {
|
|
87
|
-
console.error("Trace contained 0 spans — nothing to import.");
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
// 3. Optional mapping file.
|
|
91
|
-
const mapping = args.map ? await loadMapping(resolve(args.map)) : null;
|
|
92
|
-
// 4. Compute t=0 (earliest span start) so the timeline is repo-portable.
|
|
93
|
-
const minStartNs = spans.reduce((m, s) => (s.startNs < m ? s.startNs : m), spans[0].startNs);
|
|
94
|
-
// 5. If appending, load existing timeline; otherwise start fresh.
|
|
95
|
-
let existing = null;
|
|
96
|
-
if (args.append) {
|
|
97
|
-
try {
|
|
98
|
-
existing = await readTimeline(loomRoot.loomPath, args.asId);
|
|
99
|
-
if (existing.diagram !== args.diagramId) {
|
|
100
|
-
console.error(`Refusing to append: existing timeline '${args.asId}' references diagram ` +
|
|
101
|
-
`'${existing.diagram}', not '${args.diagramId}'.`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
if (e.code !== "ENOENT")
|
|
107
|
-
throw e;
|
|
108
|
-
// Fall through: --append on a missing file behaves like create.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const existingIds = new Set(existing?.events.map((e) => e.id) ?? []);
|
|
112
|
-
const spanIdToEventId = new Map();
|
|
113
|
-
const events = [];
|
|
114
|
-
const skipped = [];
|
|
115
|
-
// 6. First pass: pick node + event id for each span.
|
|
116
|
-
for (const s of spans) {
|
|
117
|
-
const node = resolveNode(s, mapping, diagram.nodes);
|
|
118
|
-
if (!node) {
|
|
119
|
-
skipped.push({ span: s, reason: "no matching node" });
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const id = eventIdFor(s.spanId, existingIds);
|
|
123
|
-
existingIds.add(id);
|
|
124
|
-
spanIdToEventId.set(s.spanId, id);
|
|
125
|
-
const start_ms = Number((s.startNs - minStartNs) / 1000000n);
|
|
126
|
-
const duration_ms = Number((s.endNs - s.startNs) / 1000000n);
|
|
127
|
-
events.push({
|
|
128
|
-
id,
|
|
129
|
-
node,
|
|
130
|
-
start_ms: Math.max(0, start_ms),
|
|
131
|
-
duration_ms: Math.max(0, duration_ms),
|
|
132
|
-
label: s.name.length > 60 ? s.name.slice(0, 57) + "…" : s.name,
|
|
133
|
-
kind: KIND_TO_EVENT_KIND[s.kind] ?? "compute",
|
|
134
|
-
track: s.serviceName ?? undefined,
|
|
135
|
-
tags: [`otel-import`, `kind:${s.kind}`],
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
// 7. Second pass: wire triggered_by from span parent.
|
|
139
|
-
for (let i = 0; i < spans.length; i++) {
|
|
140
|
-
const s = spans[i];
|
|
141
|
-
const ev = events.find((e) => e.id === spanIdToEventId.get(s.spanId));
|
|
142
|
-
if (!ev)
|
|
143
|
-
continue;
|
|
144
|
-
if (s.parentSpanId) {
|
|
145
|
-
const parentEventId = spanIdToEventId.get(s.parentSpanId);
|
|
146
|
-
if (parentEventId)
|
|
147
|
-
ev.triggered_by = parentEventId;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// 8. Build the timeline. Auto-derive tracks from distinct services.
|
|
151
|
-
const trackIds = new Set((existing?.tracks?.map((t) => t.id) ?? []).concat(events.map((e) => e.track).filter((t) => !!t)));
|
|
152
|
-
const tracks = Array.from(trackIds).map((id) => ({ id, label: id }));
|
|
153
|
-
const timeline = {
|
|
154
|
-
version: "1",
|
|
155
|
-
id: args.asId,
|
|
156
|
-
title: existing?.title ?? `Imported: ${args.asId}`,
|
|
157
|
-
description: existing?.description ??
|
|
158
|
-
`Generated by 'loom-spec import-trace' from ${args.trace}.`,
|
|
159
|
-
diagram: args.diagramId,
|
|
160
|
-
events: existing ? [...existing.events, ...events] : events,
|
|
161
|
-
tracks,
|
|
162
|
-
};
|
|
163
|
-
// 9. Validate before writing.
|
|
164
|
-
const v = await validateTimeline(timeline);
|
|
165
|
-
if (!v.ok) {
|
|
166
|
-
console.error("Generated timeline failed schema validation:");
|
|
167
|
-
for (const e of v.errors)
|
|
168
|
-
console.error(` - ${e}`);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
await writeTimeline(loomRoot.loomPath, args.asId, timeline);
|
|
172
|
-
// 10. Report.
|
|
173
|
-
console.log(`Wrote ${events.length} event${events.length === 1 ? "" : "s"} to ` +
|
|
174
|
-
`${loomRoot.loomPath}/timelines/${args.asId}.timeline.json` +
|
|
175
|
-
(existing ? ` (appended; total ${timeline.events.length})` : ""));
|
|
176
|
-
if (skipped.length > 0) {
|
|
177
|
-
console.log(`Skipped ${skipped.length} span${skipped.length === 1 ? "" : "s"} ` +
|
|
178
|
-
`with no matching node. Pass --map mapping.json to override.`);
|
|
179
|
-
const sample = skipped.slice(0, 5);
|
|
180
|
-
for (const { span, reason } of sample) {
|
|
181
|
-
console.log(` • ${span.name} (service=${span.serviceName ?? "—"}) ${reason}`);
|
|
182
|
-
}
|
|
183
|
-
if (skipped.length > sample.length) {
|
|
184
|
-
console.log(` … and ${skipped.length - sample.length} more.`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
//# sourceMappingURL=importTrace.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"importTrace.js","sourceRoot":"","sources":["../../src/cli/importTrace.ts"],"names":[],"mappings":"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"}
|
package/dist/server/otel.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal OTLP JSON parser. Reads the standard OpenTelemetry resource/scope/
|
|
3
|
-
* spans shape (`resourceSpans[].scopeSpans[].spans[]`) and projects it down
|
|
4
|
-
* to a flat list of normalized spans suitable for translating into a
|
|
5
|
-
* loom-spec timeline.
|
|
6
|
-
*
|
|
7
|
-
* Scope: just enough of the OTLP JSON shape for the import-trace CLI.
|
|
8
|
-
* Does NOT validate exhaustively, does NOT handle protobuf binary, does NOT
|
|
9
|
-
* walk Jaeger or Zipkin formats — those can be follow-ons.
|
|
10
|
-
*/
|
|
11
|
-
export interface ParsedSpan {
|
|
12
|
-
spanId: string;
|
|
13
|
-
parentSpanId: string | null;
|
|
14
|
-
/** Service that emitted the span (from resource attribute). */
|
|
15
|
-
serviceName: string | null;
|
|
16
|
-
/** Span name (often the operation or route). */
|
|
17
|
-
name: string;
|
|
18
|
-
/** "server" | "client" | "internal" | "producer" | "consumer" | "unknown". */
|
|
19
|
-
kind: SpanKind;
|
|
20
|
-
/** Start time in nanoseconds since UNIX epoch. */
|
|
21
|
-
startNs: bigint;
|
|
22
|
-
/** End time in nanoseconds since UNIX epoch. */
|
|
23
|
-
endNs: bigint;
|
|
24
|
-
/** Flattened key→value of attributes (resource + span merged; span wins). */
|
|
25
|
-
attributes: Record<string, string | number | boolean>;
|
|
26
|
-
}
|
|
27
|
-
export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer" | "unknown";
|
|
28
|
-
/**
|
|
29
|
-
* Parse an OTLP JSON object (already JSON.parsed) into normalized spans.
|
|
30
|
-
* Throws if `resourceSpans` is missing — we don't try to recover or guess.
|
|
31
|
-
*/
|
|
32
|
-
export declare function parseOtlpJson(raw: unknown): ParsedSpan[];
|
package/dist/server/otel.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal OTLP JSON parser. Reads the standard OpenTelemetry resource/scope/
|
|
3
|
-
* spans shape (`resourceSpans[].scopeSpans[].spans[]`) and projects it down
|
|
4
|
-
* to a flat list of normalized spans suitable for translating into a
|
|
5
|
-
* loom-spec timeline.
|
|
6
|
-
*
|
|
7
|
-
* Scope: just enough of the OTLP JSON shape for the import-trace CLI.
|
|
8
|
-
* Does NOT validate exhaustively, does NOT handle protobuf binary, does NOT
|
|
9
|
-
* walk Jaeger or Zipkin formats — those can be follow-ons.
|
|
10
|
-
*/
|
|
11
|
-
const KIND_MAP = {
|
|
12
|
-
0: "unknown",
|
|
13
|
-
1: "internal",
|
|
14
|
-
2: "server",
|
|
15
|
-
3: "client",
|
|
16
|
-
4: "producer",
|
|
17
|
-
5: "consumer",
|
|
18
|
-
};
|
|
19
|
-
function attrValue(a) {
|
|
20
|
-
const v = a.value;
|
|
21
|
-
if (!v)
|
|
22
|
-
return undefined;
|
|
23
|
-
if (v.stringValue !== undefined)
|
|
24
|
-
return v.stringValue;
|
|
25
|
-
if (v.boolValue !== undefined)
|
|
26
|
-
return v.boolValue;
|
|
27
|
-
if (v.intValue !== undefined)
|
|
28
|
-
return Number(v.intValue);
|
|
29
|
-
if (v.doubleValue !== undefined)
|
|
30
|
-
return v.doubleValue;
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
function flattenAttrs(attrs) {
|
|
34
|
-
const out = {};
|
|
35
|
-
for (const a of attrs ?? []) {
|
|
36
|
-
const v = attrValue(a);
|
|
37
|
-
if (v !== undefined)
|
|
38
|
-
out[a.key] = v;
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
function toBigInt(t) {
|
|
43
|
-
if (t === undefined)
|
|
44
|
-
return 0n;
|
|
45
|
-
if (typeof t === "number")
|
|
46
|
-
return BigInt(t);
|
|
47
|
-
return BigInt(t);
|
|
48
|
-
}
|
|
49
|
-
function normalizeKind(k) {
|
|
50
|
-
if (typeof k === "number")
|
|
51
|
-
return KIND_MAP[k] ?? "unknown";
|
|
52
|
-
if (typeof k === "string") {
|
|
53
|
-
// OTLP exporters sometimes emit "SPAN_KIND_SERVER" etc.
|
|
54
|
-
const lower = k.replace(/^SPAN_KIND_/i, "").toLowerCase();
|
|
55
|
-
if (lower in { internal: 1, server: 1, client: 1, producer: 1, consumer: 1 }) {
|
|
56
|
-
return lower;
|
|
57
|
-
}
|
|
58
|
-
return "unknown";
|
|
59
|
-
}
|
|
60
|
-
return "unknown";
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Parse an OTLP JSON object (already JSON.parsed) into normalized spans.
|
|
64
|
-
* Throws if `resourceSpans` is missing — we don't try to recover or guess.
|
|
65
|
-
*/
|
|
66
|
-
export function parseOtlpJson(raw) {
|
|
67
|
-
if (!raw || typeof raw !== "object") {
|
|
68
|
-
throw new Error("Trace file is not a JSON object");
|
|
69
|
-
}
|
|
70
|
-
const root = raw;
|
|
71
|
-
if (!Array.isArray(root.resourceSpans)) {
|
|
72
|
-
throw new Error("Not an OTLP JSON trace — expected top-level 'resourceSpans' array. " +
|
|
73
|
-
"Jaeger/Zipkin formats are not yet supported.");
|
|
74
|
-
}
|
|
75
|
-
const out = [];
|
|
76
|
-
for (const rs of root.resourceSpans) {
|
|
77
|
-
const resourceAttrs = flattenAttrs(rs.resource?.attributes);
|
|
78
|
-
const serviceName = resourceAttrs["service.name"] ?? null;
|
|
79
|
-
for (const ss of rs.scopeSpans ?? []) {
|
|
80
|
-
for (const s of ss.spans ?? []) {
|
|
81
|
-
const spanAttrs = flattenAttrs(s.attributes);
|
|
82
|
-
const attributes = { ...resourceAttrs, ...spanAttrs };
|
|
83
|
-
out.push({
|
|
84
|
-
spanId: s.spanId ?? "",
|
|
85
|
-
parentSpanId: s.parentSpanId && s.parentSpanId.length > 0 ? s.parentSpanId : null,
|
|
86
|
-
serviceName,
|
|
87
|
-
name: s.name ?? "(unnamed)",
|
|
88
|
-
kind: normalizeKind(s.kind),
|
|
89
|
-
startNs: toBigInt(s.startTimeUnixNano),
|
|
90
|
-
endNs: toBigInt(s.endTimeUnixNano),
|
|
91
|
-
attributes,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
//# sourceMappingURL=otel.js.map
|
package/dist/server/otel.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../../src/server/otel.ts"],"names":[],"mappings":"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"}
|