@flowsilver/algoviz 0.0.0-stage → 0.4.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.
@@ -0,0 +1,65 @@
1
+ /** @import { JsonObject, JsonValue, StateChange } from "./types.js" */
2
+
3
+ /**
4
+ * @param {JsonValue | undefined} value
5
+ * @returns {value is JsonObject}
6
+ */
7
+ function isObject(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ /** @param {string} key */
12
+ function escapeToken(key) {
13
+ return key.replaceAll("~", "~0").replaceAll("/", "~1");
14
+ }
15
+
16
+ /**
17
+ * @param {JsonValue | undefined} before
18
+ * @param {JsonValue | undefined} after
19
+ * @param {string} path
20
+ * @param {StateChange[]} changes
21
+ */
22
+ function compare(before, after, path, changes) {
23
+ if (before === after) return;
24
+
25
+ if (Array.isArray(before) && Array.isArray(after)) {
26
+ const length = Math.max(before.length, after.length);
27
+ for (let index = 0; index < length; index++) {
28
+ compare(before[index], after[index], `${path}/${index}`, changes);
29
+ }
30
+ return;
31
+ }
32
+
33
+ if (isObject(before) && isObject(after)) {
34
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
35
+ for (const key of keys) {
36
+ compare(
37
+ Object.hasOwn(before, key) ? before[key] : undefined,
38
+ Object.hasOwn(after, key) ? after[key] : undefined,
39
+ `${path}/${escapeToken(key)}`,
40
+ changes,
41
+ );
42
+ }
43
+ return;
44
+ }
45
+
46
+ if (before === undefined) {
47
+ if (after !== undefined) changes.push({ kind: "added", path, after });
48
+ } else if (after === undefined) {
49
+ changes.push({ kind: "removed", path, before });
50
+ } else {
51
+ changes.push({ kind: "changed", path, before, after });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @param {JsonObject} before
57
+ * @param {JsonObject} after
58
+ * @returns {StateChange[]}
59
+ */
60
+ export function diffStates(before, after) {
61
+ /** @type {StateChange[]} */
62
+ const changes = [];
63
+ compare(before, after, "", changes);
64
+ return changes;
65
+ }
@@ -0,0 +1,29 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
2
+ export type JsonObject = { [key: string]: JsonValue };
3
+
4
+ export type StateUpdate =
5
+ | { op: "add" | "replace"; path: string; value: JsonValue }
6
+ | { op: "remove"; path: string; value?: never };
7
+
8
+ export type SemanticEvent = { type: string; data?: JsonObject };
9
+ export type InitialRecord = { type: "initial"; version: 1; state: JsonObject };
10
+ export type StepRecord = { type: "step"; updates: StateUpdate[]; events?: SemanticEvent[] };
11
+ export type TraceRecords = [InitialRecord, ...StepRecord[]];
12
+
13
+ export type ItemBinding = { label: string; value: string; nullLabel?: string };
14
+ type View = { id: string; source: string };
15
+ export type ViewSpec = (View & { type: "graph" }) | (View & { type: "array"; item?: ItemBinding })
16
+ | (View & { type: "custom"; renderer: string });
17
+ export type VisualizationSpec = { version: 1; layout: "row" | "column"; views: ViewSpec[] };
18
+ export type GraphViewData = {
19
+ nodes: { id: string; label?: string; status?: string }[];
20
+ edges: { id: string; source: string; target: string; label?: JsonValue }[];
21
+ };
22
+
23
+ export type ReplayFrame = { step: number; state: JsonObject; events: SemanticEvent[] };
24
+ export type ReplayOptions = { traceSource?: string; specText?: string; specSource?: string; checkpointInterval?: number };
25
+ export type ReplayLocation = { source: string; line?: number; operationIndex?: number; path?: string; viewId?: string };
26
+ export type StateChange =
27
+ | { kind: "added"; path: string; after: JsonValue }
28
+ | { kind: "removed"; path: string; before: JsonValue }
29
+ | { kind: "changed"; path: string; before: JsonValue; after: JsonValue };
@@ -0,0 +1,140 @@
1
+ import { resolveArrayItem } from "../core/json-pointer.js";
2
+
3
+ const svgNamespace = "http://www.w3.org/2000/svg";
4
+ const nodeRadius = 21;
5
+
6
+ function svgElement(name, attributes = {}) {
7
+ const element = document.createElementNS(svgNamespace, name);
8
+ for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value);
9
+ return element;
10
+ }
11
+
12
+ function shorten(label, limit) {
13
+ const characters = Array.from(label);
14
+ return characters.length > limit ? `${characters.slice(0, limit - 1).join("")}…` : label;
15
+ }
16
+
17
+ function edgePath(source, target) {
18
+ if (source === target) {
19
+ const { x, y } = source;
20
+ return `M ${x + 16} ${y - 15} C ${x + 52} ${y - 49}, ${x + 2} ${y - 64}, ${x - 4} ${y - 24}`;
21
+ }
22
+ const dx = target.x - source.x;
23
+ const dy = target.y - source.y;
24
+ const length = Math.hypot(dx, dy);
25
+ const start = nodeRadius / length;
26
+ const end = (nodeRadius + 4) / length;
27
+ return `M ${source.x + dx * start} ${source.y + dy * start} L ${target.x - dx * end} ${target.y - dy * end}`;
28
+ }
29
+
30
+ function edgeLabelPosition(source, target, lane) {
31
+ if (source === target) return { x: source.x + 34 + lane * 15, y: source.y - 42 };
32
+ const dx = target.x - source.x;
33
+ const dy = target.y - source.y;
34
+ const offset = (10 + lane * 14) / Math.hypot(dx, dy);
35
+ return {
36
+ x: (source.x + target.x) / 2 + dy * offset,
37
+ y: (source.y + target.y) / 2 - dx * offset,
38
+ };
39
+ }
40
+
41
+ export function renderGraph({ nodes, edges }, title, viewIndex) {
42
+ if (nodes.length === 0) return empty("No nodes");
43
+
44
+ const ordered = [...nodes].sort((a, b) => a.id < b.id ? -1 : Number(a.id > b.id));
45
+ const radius = Math.max(97, ordered.length * 13);
46
+ const width = 2 * (radius + 92);
47
+ const height = 2 * (radius + 55);
48
+ const positions = new Map(ordered.map((node, index) => {
49
+ const angle = index * 2 * Math.PI / ordered.length - Math.PI / 2;
50
+ return [node.id, ordered.length === 1
51
+ ? { x: width / 2, y: height / 2 }
52
+ : { x: width / 2 + radius * Math.cos(angle), y: height / 2 + radius * Math.sin(angle) }];
53
+ }));
54
+
55
+ const graph = svgElement("svg", { class: "graph", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": title });
56
+ const arrowId = `edge-arrow-${viewIndex}`;
57
+ const defs = svgElement("defs");
58
+ const marker = svgElement("marker", { id: arrowId, viewBox: "0 0 8 8", refX: 7, refY: 4, markerWidth: 6, markerHeight: 6, orient: "auto" });
59
+ marker.append(svgElement("path", { d: "M 0 0 L 8 4 L 0 8 Z", class: "graph-arrow" }));
60
+ defs.append(marker);
61
+ graph.append(defs);
62
+
63
+ const edgeLabels = svgElement("g");
64
+ const labelLanes = new Map();
65
+ for (const edge of edges) {
66
+ const source = positions.get(edge.source);
67
+ const target = positions.get(edge.target);
68
+ const path = svgElement("path", { class: "graph-edge", "data-edge-id": edge.id, "marker-end": `url(#${arrowId})`, d: edgePath(source, target) });
69
+ const label = typeof edge.label === "string" || typeof edge.label === "number" ? String(edge.label) : "";
70
+ const description = svgElement("title");
71
+ description.textContent = `${edge.source} → ${edge.target}${label ? ` (${label})` : ""}`;
72
+ path.append(description);
73
+ graph.append(path);
74
+
75
+ if (label) {
76
+ const key = JSON.stringify([edge.source, edge.target]);
77
+ const lane = labelLanes.get(key) ?? 0;
78
+ labelLanes.set(key, lane + 1);
79
+ const position = edgeLabelPosition(source, target, lane);
80
+ const text = svgElement("text", { ...position, class: "graph-edge-label", "data-edge-label-for": edge.id });
81
+ text.textContent = shorten(label, 18);
82
+ edgeLabels.append(text);
83
+ }
84
+ }
85
+ if (edgeLabels.childElementCount) graph.append(edgeLabels);
86
+
87
+ for (const node of ordered) {
88
+ const { x, y } = positions.get(node.id);
89
+ const name = node.label ?? node.id;
90
+ const status = node.status ?? "";
91
+ const group = svgElement("g", { class: "graph-node", transform: `translate(${x} ${y})`, "data-node-id": node.id, "data-status": status });
92
+ const description = svgElement("title");
93
+ description.textContent = status ? `${name} (${status})` : name;
94
+ const label = svgElement("text", { y: 4 });
95
+ label.textContent = shorten(name, 6);
96
+ group.append(description, svgElement("circle", { r: nodeRadius }), label);
97
+ if (status) {
98
+ const state = svgElement("text", { class: "graph-status", y: 35 });
99
+ state.textContent = shorten(status, 18);
100
+ group.append(state);
101
+ }
102
+ graph.append(group);
103
+ }
104
+ return graph;
105
+ }
106
+
107
+ function empty(message) {
108
+ const paragraph = document.createElement("p");
109
+ paragraph.className = "empty";
110
+ paragraph.textContent = message;
111
+ return paragraph;
112
+ }
113
+
114
+ export function renderArray(values, binding) {
115
+ if (values.length === 0) return empty("Empty");
116
+ const list = document.createElement("ol");
117
+ list.className = binding ? "array array--labeled" : "array";
118
+ for (const entry of values) {
119
+ const item = document.createElement("li");
120
+ let value = entry;
121
+ if (binding) {
122
+ const resolved = resolveArrayItem(entry, binding);
123
+ value = resolved.value;
124
+ const label = document.createElement("span");
125
+ label.className = "array-label";
126
+ label.textContent = resolved.label;
127
+ item.dataset.label = resolved.label;
128
+ item.append(label);
129
+ }
130
+
131
+ const display = document.createElement("code");
132
+ const json = JSON.stringify(value);
133
+ item.dataset.value = json;
134
+ display.textContent = typeof value === "string" && value !== "" ? value : json;
135
+ if (binding && value === null) display.textContent = binding.nullLabel ?? "—";
136
+ item.append(display);
137
+ list.append(item);
138
+ }
139
+ return list;
140
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "AlgoViz Graph View Data v1",
4
+ "type": "object",
5
+ "required": ["nodes", "edges"],
6
+ "properties": {
7
+ "nodes": {
8
+ "type": "array",
9
+ "items": {
10
+ "type": "object",
11
+ "required": ["id"],
12
+ "properties": {
13
+ "id": { "type": "string", "minLength": 1 },
14
+ "label": { "type": "string" },
15
+ "status": { "type": "string" }
16
+ }
17
+ }
18
+ },
19
+ "edges": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "object",
23
+ "required": ["id", "source", "target"],
24
+ "properties": {
25
+ "id": { "type": "string", "minLength": 1 },
26
+ "source": { "type": "string", "minLength": 1 },
27
+ "target": { "type": "string", "minLength": 1 },
28
+ "label": {
29
+ "description": "Optional edge label. The inspector displays text and numbers, and preserves other values as trace metadata.",
30
+ "examples": ["shortcut", 5]
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "AlgoViz Execution Trace Record v1",
4
+ "oneOf": [
5
+ { "$ref": "#/$defs/initial" },
6
+ { "$ref": "#/$defs/step" }
7
+ ],
8
+ "$defs": {
9
+ "initial": {
10
+ "type": "object",
11
+ "required": ["type", "version", "state"],
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "type": { "const": "initial" },
15
+ "version": { "const": 1 },
16
+ "state": { "type": "object" }
17
+ }
18
+ },
19
+ "step": {
20
+ "type": "object",
21
+ "required": ["type", "updates"],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "type": { "const": "step" },
25
+ "updates": {
26
+ "type": "array",
27
+ "items": { "$ref": "#/$defs/update" }
28
+ },
29
+ "events": {
30
+ "type": "array",
31
+ "items": { "$ref": "#/$defs/event" }
32
+ }
33
+ }
34
+ },
35
+ "update": {
36
+ "type": "object",
37
+ "required": ["op", "path"],
38
+ "additionalProperties": false,
39
+ "properties": {
40
+ "op": { "enum": ["add", "remove", "replace"] },
41
+ "path": { "type": "string", "pattern": "^/(?:[^~]|~[01])*$" },
42
+ "value": true
43
+ },
44
+ "if": { "properties": { "op": { "const": "remove" } } },
45
+ "then": { "not": { "required": ["value"] } },
46
+ "else": { "required": ["value"] }
47
+ },
48
+ "event": {
49
+ "type": "object",
50
+ "required": ["type"],
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "type": { "type": "string", "minLength": 1 },
54
+ "data": { "type": "object" }
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "AlgoViz Visualization Specification v1",
4
+ "type": "object",
5
+ "required": ["version", "layout", "views"],
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "version": { "const": 1 },
9
+ "layout": { "enum": ["row", "column"] },
10
+ "views": {
11
+ "type": "array",
12
+ "minItems": 1,
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["id", "type", "source"],
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "id": { "type": "string", "minLength": 1 },
19
+ "type": { "enum": ["graph", "array", "custom"] },
20
+ "renderer": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
21
+ "source": { "$ref": "#/$defs/pointer" },
22
+ "item": {
23
+ "type": "object",
24
+ "required": ["label", "value"],
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "label": { "$ref": "#/$defs/pointer" },
28
+ "value": { "$ref": "#/$defs/pointer" },
29
+ "nullLabel": { "type": "string", "minLength": 1 }
30
+ }
31
+ }
32
+ },
33
+ "allOf": [
34
+ {
35
+ "if": { "required": ["item"] },
36
+ "then": { "properties": { "type": { "const": "array" } } }
37
+ },
38
+ {
39
+ "if": { "properties": { "type": { "const": "custom" } } },
40
+ "then": { "required": ["renderer"] },
41
+ "else": { "not": { "required": ["renderer"] } }
42
+ }
43
+ ]
44
+ }
45
+ }
46
+ },
47
+ "$defs": {
48
+ "pointer": {
49
+ "type": "string",
50
+ "anyOf": [
51
+ { "const": "" },
52
+ { "pattern": "^/(?:[^~]|~[01])*$" }
53
+ ]
54
+ }
55
+ }
56
+ }