@chatpanel/events 0.10.0 → 0.12.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/flowchart.js +380 -0
- package/index.js +1 -0
- package/observability.js +6 -3
- package/package.json +4 -2
package/flowchart.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// flowchart.js — Mermaid `flowchart` text → a self-contained SVG string.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: models answer "draw me a diagram" with a ```mermaid block, and showing
|
|
4
|
+
// the source instead of the picture is a dead end for the reader. Mermaid itself is a ~3 MB
|
|
5
|
+
// dependency that needs a live DOM to measure text, which rules it out for a side panel that
|
|
6
|
+
// treats first-paint weight as a release gate — and for a renderer we want to unit-test.
|
|
7
|
+
//
|
|
8
|
+
// So this is a focused renderer for the shape models actually emit: `flowchart TB/LR` with
|
|
9
|
+
// labelled nodes, edges and classDef styling. It is PURE (string in, string out), so it is
|
|
10
|
+
// testable without a browser and runs identically in the extension, a desktop app or a
|
|
11
|
+
// mobile client — which is why it lives in the shared package rather than in one client.
|
|
12
|
+
//
|
|
13
|
+
// SAFETY: the output is only ever shown through an <img src="data:image/svg+xml,…">, which
|
|
14
|
+
// loads SVG in restricted mode (no scripts, no external fetches). Every piece of model text
|
|
15
|
+
// is XML-escaped here as well, so a label can't break out of its element either way.
|
|
16
|
+
//
|
|
17
|
+
// Anything it can't parse returns null, and the caller keeps showing the code block.
|
|
18
|
+
|
|
19
|
+
const NODE_SHAPES = {
|
|
20
|
+
'[': ']', // rect
|
|
21
|
+
'(': ')', // rounded
|
|
22
|
+
'{': '}', // diamond → drawn as a rounded rect with a tint; shape fidelity is not the point
|
|
23
|
+
'>': ']', // asymmetric
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PALETTE = {
|
|
27
|
+
fill: '#f8fafc', stroke: '#cbd5e1', color: '#1e293b',
|
|
28
|
+
};
|
|
29
|
+
// Ranks get progressively lighter accents when the diagram declares no classes of its own,
|
|
30
|
+
// so an unstyled chart still reads as designed rather than as a grey wireframe.
|
|
31
|
+
const RANK_TINTS = [
|
|
32
|
+
{ fill: '#1e293b', stroke: '#1e293b', color: '#ffffff' },
|
|
33
|
+
{ fill: '#e0e7ff', stroke: '#a5b4fc', color: '#312e81' },
|
|
34
|
+
{ fill: '#f1f5f9', stroke: '#cbd5e1', color: '#0f172a' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function esc(s) {
|
|
38
|
+
return String(s == null ? '' : s)
|
|
39
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Split a label into display lines: explicit <br/> first, then greedy wrap on words.
|
|
44
|
+
function wrapLabel(raw, maxChars = 22) {
|
|
45
|
+
const parts = String(raw || '').split(/<br\s*\/?>/i);
|
|
46
|
+
const lines = [];
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
const words = part.trim().split(/\s+/).filter(Boolean);
|
|
49
|
+
if (!words.length) continue;
|
|
50
|
+
let line = '';
|
|
51
|
+
for (const w of words) {
|
|
52
|
+
if (!line) line = w;
|
|
53
|
+
else if ((line + ' ' + w).length <= maxChars) line += ' ' + w;
|
|
54
|
+
else { lines.push(line); line = w; }
|
|
55
|
+
}
|
|
56
|
+
if (line) lines.push(line);
|
|
57
|
+
}
|
|
58
|
+
return lines.length ? lines : [''];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strip mermaid's quoting around a label.
|
|
62
|
+
function cleanLabel(s) {
|
|
63
|
+
let t = String(s || '').trim();
|
|
64
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) t = t.slice(1, -1);
|
|
65
|
+
return t.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Split one line into node tokens + the edge labels between them, honouring brackets and
|
|
69
|
+
// quotes so an arrow inside a label can't be read as a connector. Returns null when the line
|
|
70
|
+
// holds no top-level edge operator. Handles chains: A --> B -->|yes| C.
|
|
71
|
+
const EDGE_OPS = ['-.->', '==>', '===>', '-->', '--->', '---', '-.-'];
|
|
72
|
+
export function splitEdgeChain(line) {
|
|
73
|
+
const s = String(line || '');
|
|
74
|
+
const parts = [];
|
|
75
|
+
const labels = [];
|
|
76
|
+
let buf = '';
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let quote = '';
|
|
79
|
+
for (let i = 0; i < s.length; i++) {
|
|
80
|
+
const ch = s[i];
|
|
81
|
+
if (quote) { buf += ch; if (ch === quote) quote = ''; continue; }
|
|
82
|
+
if (ch === '"' || ch === "'") { quote = ch; buf += ch; continue; }
|
|
83
|
+
if (ch === '[' || ch === '(' || ch === '{') { depth++; buf += ch; continue; }
|
|
84
|
+
if (ch === ']' || ch === ')' || ch === '}') { depth = Math.max(0, depth - 1); buf += ch; continue; }
|
|
85
|
+
if (depth === 0 && (ch === '-' || ch === '=')) {
|
|
86
|
+
const op = EDGE_OPS.find((o) => s.startsWith(o, i));
|
|
87
|
+
if (op) {
|
|
88
|
+
i += op.length - 1;
|
|
89
|
+
// An optional |label| directly after the arrow.
|
|
90
|
+
let label = '';
|
|
91
|
+
const rest = s.slice(i + 1);
|
|
92
|
+
const lm = rest.match(/^\s*\|([^|]*)\|/);
|
|
93
|
+
if (lm) { label = lm[1]; i += lm[0].length; }
|
|
94
|
+
parts.push(buf); labels.push(label); buf = '';
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
buf += ch;
|
|
99
|
+
}
|
|
100
|
+
parts.push(buf);
|
|
101
|
+
if (parts.length < 2) return null;
|
|
102
|
+
return { parts: parts.map((p) => p.trim().replace(/;$/, '')).filter(Boolean), labels };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse a mermaid flowchart into { dir, nodes: Map, edges: [] , classes }.
|
|
107
|
+
* Returns null when the text isn't a flowchart we handle.
|
|
108
|
+
*/
|
|
109
|
+
export function parseFlowchart(text) {
|
|
110
|
+
const src = String(text || '');
|
|
111
|
+
const header = src.match(/^\s*(?:flowchart|graph)\s+(TB|TD|BT|LR|RL)\b/im);
|
|
112
|
+
if (!header) return null;
|
|
113
|
+
const dir = header[1].toUpperCase();
|
|
114
|
+
|
|
115
|
+
const nodes = new Map(); // id -> { id, label }
|
|
116
|
+
const edges = []; // { from, to, label }
|
|
117
|
+
const classDefs = new Map();
|
|
118
|
+
const nodeClass = new Map();
|
|
119
|
+
|
|
120
|
+
const ensure = (id, label) => {
|
|
121
|
+
const key = String(id).trim();
|
|
122
|
+
if (!key) return null;
|
|
123
|
+
if (!nodes.has(key)) nodes.set(key, { id: key, label: label != null ? label : key });
|
|
124
|
+
else if (label != null) nodes.get(key).label = label;
|
|
125
|
+
return nodes.get(key);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// `ID["label"]` / `ID(label)` / `ID{label}` → id + label, else a bare id.
|
|
129
|
+
function readNodeToken(token) {
|
|
130
|
+
const t = token.trim();
|
|
131
|
+
if (!t) return null;
|
|
132
|
+
const m = t.match(/^([A-Za-z0-9_.-]+)\s*([[({>])([\s\S]*)$/);
|
|
133
|
+
if (!m) return ensure(t.replace(/^[[(]|[\])]$/g, ''), null);
|
|
134
|
+
const [, id, open, rest] = m;
|
|
135
|
+
const close = NODE_SHAPES[open] || ']';
|
|
136
|
+
// Take everything up to the LAST closing bracket, so labels may contain brackets.
|
|
137
|
+
const end = rest.lastIndexOf(close);
|
|
138
|
+
const label = end >= 0 ? rest.slice(0, end) : rest;
|
|
139
|
+
return ensure(id, cleanLabel(label.replace(/^[([{>]+/, '')));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (let raw of src.split('\n')) {
|
|
143
|
+
const line = raw.trim();
|
|
144
|
+
if (!line || /^%%/.test(line)) continue; // comment
|
|
145
|
+
if (/^(?:flowchart|graph)\b/i.test(line)) continue; // header
|
|
146
|
+
if (/^(?:subgraph|end)\b/i.test(line)) continue; // subgraphs: flattened, not drawn
|
|
147
|
+
|
|
148
|
+
// classDef name fill:#fff,color:#000,stroke:#ccc
|
|
149
|
+
const cd = line.match(/^classDef\s+([A-Za-z0-9_-]+)\s+(.+?);?$/i);
|
|
150
|
+
if (cd) {
|
|
151
|
+
const style = {};
|
|
152
|
+
for (const pair of cd[2].split(',')) {
|
|
153
|
+
const [k, v] = pair.split(':').map((x) => (x || '').trim());
|
|
154
|
+
if (k && v) style[k.toLowerCase()] = v;
|
|
155
|
+
}
|
|
156
|
+
classDefs.set(cd[1], style);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// class A,B,C name
|
|
160
|
+
const cl = line.match(/^class\s+([A-Za-z0-9_,.\s-]+?)\s+([A-Za-z0-9_-]+)\s*;?$/i);
|
|
161
|
+
if (cl) {
|
|
162
|
+
for (const id of cl[1].split(',').map((x) => x.trim()).filter(Boolean)) nodeClass.set(id, cl[2]);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (/^(?:style|linkStyle|click)\b/i.test(line)) continue; // not modelled
|
|
166
|
+
|
|
167
|
+
// Edges, including CHAINS: `A --> B --> C` is two edges, and mermaid emits them often.
|
|
168
|
+
// Split on edge operators that are at bracket/quote depth 0, so an arrow inside a label
|
|
169
|
+
// ("a --> b") is never mistaken for a connector.
|
|
170
|
+
const seg = splitEdgeChain(line);
|
|
171
|
+
if (seg && seg.parts.length > 1) {
|
|
172
|
+
let prev = readNodeToken(seg.parts[0]);
|
|
173
|
+
for (let k = 1; k < seg.parts.length; k++) {
|
|
174
|
+
const next = readNodeToken(seg.parts[k]);
|
|
175
|
+
if (prev && next) edges.push({ from: prev.id, to: next.id, label: cleanLabel(seg.labels[k - 1] || '') });
|
|
176
|
+
prev = next;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// A standalone node definition.
|
|
181
|
+
if (/^[A-Za-z0-9_.-]+\s*[[({>]/.test(line)) { readNodeToken(line); continue; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!nodes.size) return null;
|
|
185
|
+
return { dir, nodes, edges, classDefs, nodeClass };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Longest-path ranking: a node sits one level below its deepest parent. Cycles are broken by
|
|
189
|
+
// the visited guard, so a malformed graph still lays out instead of hanging.
|
|
190
|
+
function rankNodes(nodes, edges) {
|
|
191
|
+
const parents = new Map();
|
|
192
|
+
const children = new Map();
|
|
193
|
+
for (const id of nodes.keys()) { parents.set(id, []); children.set(id, []); }
|
|
194
|
+
for (const e of edges) {
|
|
195
|
+
if (!nodes.has(e.from) || !nodes.has(e.to)) continue;
|
|
196
|
+
parents.get(e.to).push(e.from);
|
|
197
|
+
children.get(e.from).push(e.to);
|
|
198
|
+
}
|
|
199
|
+
const rank = new Map();
|
|
200
|
+
const roots = [...nodes.keys()].filter((id) => parents.get(id).length === 0);
|
|
201
|
+
const queue = roots.length ? [...roots] : [nodes.keys().next().value];
|
|
202
|
+
for (const r of queue) rank.set(r, 0);
|
|
203
|
+
let guard = nodes.size * 4;
|
|
204
|
+
while (queue.length && guard-- > 0) {
|
|
205
|
+
const id = queue.shift();
|
|
206
|
+
const r = rank.get(id) || 0;
|
|
207
|
+
for (const c of children.get(id) || []) {
|
|
208
|
+
const want = r + 1;
|
|
209
|
+
if ((rank.get(c) ?? -1) < want) { rank.set(c, want); queue.push(c); }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const id of nodes.keys()) if (!rank.has(id)) rank.set(id, 0);
|
|
213
|
+
return { rank, parents, children };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const CHAR_W = 7.1; // ~13px system-ui average advance; good enough for box sizing
|
|
217
|
+
const LINE_H = 18;
|
|
218
|
+
const PAD_X = 14;
|
|
219
|
+
const PAD_Y = 12;
|
|
220
|
+
|
|
221
|
+
function measure(labelLines) {
|
|
222
|
+
const w = Math.max(...labelLines.map((l) => l.length)) * CHAR_W + PAD_X * 2;
|
|
223
|
+
const h = labelLines.length * LINE_H + PAD_Y * 2;
|
|
224
|
+
return { w: Math.max(72, Math.round(w)), h: Math.round(h) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Lay the graph out on a grid: rank → row (TB) or column (LR). Pure geometry. */
|
|
228
|
+
export function layoutFlowchart(graph, { dir = graph.dir, gapMain = 56, gapCross = 22 } = {}) {
|
|
229
|
+
const { nodes, edges } = graph;
|
|
230
|
+
const { rank, parents, children } = rankNodes(nodes, edges);
|
|
231
|
+
|
|
232
|
+
const byRank = new Map();
|
|
233
|
+
for (const [id, r] of rank) {
|
|
234
|
+
if (!byRank.has(r)) byRank.set(r, []);
|
|
235
|
+
byRank.get(r).push(id);
|
|
236
|
+
}
|
|
237
|
+
// Order each rank by the average position of its parents (one barycenter sweep) so edges
|
|
238
|
+
// cross as little as possible without a full layout engine.
|
|
239
|
+
const order = new Map();
|
|
240
|
+
const ranks = [...byRank.keys()].sort((a, b) => a - b);
|
|
241
|
+
for (const r of ranks) {
|
|
242
|
+
const ids = byRank.get(r);
|
|
243
|
+
if (r === ranks[0]) { ids.forEach((id, i) => order.set(id, i)); continue; }
|
|
244
|
+
ids.sort((a, b) => {
|
|
245
|
+
const pa = parents.get(a).map((p) => order.get(p) ?? 0);
|
|
246
|
+
const pb = parents.get(b).map((p) => order.get(p) ?? 0);
|
|
247
|
+
const ma = pa.length ? pa.reduce((x, y) => x + y, 0) / pa.length : 0;
|
|
248
|
+
const mb = pb.length ? pb.reduce((x, y) => x + y, 0) / pb.length : 0;
|
|
249
|
+
return ma - mb;
|
|
250
|
+
});
|
|
251
|
+
ids.forEach((id, i) => order.set(id, i));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const horizontal = dir === 'LR' || dir === 'RL';
|
|
255
|
+
const box = new Map();
|
|
256
|
+
for (const [id, n] of nodes) {
|
|
257
|
+
const lines = wrapLabel(n.label);
|
|
258
|
+
const { w, h } = measure(lines);
|
|
259
|
+
box.set(id, { id, lines, w, h, rank: rank.get(id) || 0 });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Cross-axis extent of each rank, then centre every rank in the widest one.
|
|
263
|
+
const rankExtent = new Map();
|
|
264
|
+
for (const r of ranks) {
|
|
265
|
+
const ids = byRank.get(r);
|
|
266
|
+
const total = ids.reduce((sum, id) => sum + (horizontal ? box.get(id).h : box.get(id).w), 0)
|
|
267
|
+
+ gapCross * Math.max(0, ids.length - 1);
|
|
268
|
+
rankExtent.set(r, total);
|
|
269
|
+
}
|
|
270
|
+
const maxExtent = Math.max(...rankExtent.values(), 1);
|
|
271
|
+
|
|
272
|
+
// Main-axis offset per rank = the tallest/widest box in each preceding rank.
|
|
273
|
+
const mainOffset = new Map();
|
|
274
|
+
let main = 0;
|
|
275
|
+
for (const r of ranks) {
|
|
276
|
+
mainOffset.set(r, main);
|
|
277
|
+
const size = Math.max(...byRank.get(r).map((id) => (horizontal ? box.get(id).w : box.get(id).h)));
|
|
278
|
+
main += size + gapMain;
|
|
279
|
+
}
|
|
280
|
+
const mainTotal = Math.max(0, main - gapMain);
|
|
281
|
+
|
|
282
|
+
for (const r of ranks) {
|
|
283
|
+
const ids = byRank.get(r);
|
|
284
|
+
let cross = (maxExtent - rankExtent.get(r)) / 2;
|
|
285
|
+
for (const id of ids) {
|
|
286
|
+
const b = box.get(id);
|
|
287
|
+
if (horizontal) { b.x = mainOffset.get(r); b.y = cross; cross += b.h + gapCross; }
|
|
288
|
+
else { b.x = cross; b.y = mainOffset.get(r); cross += b.w + gapCross; }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const width = horizontal ? mainTotal : maxExtent;
|
|
293
|
+
const height = horizontal ? maxExtent : mainTotal;
|
|
294
|
+
return { boxes: box, edges, dir, width, height, horizontal, children };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function styleFor(id, graph, rankIdx) {
|
|
298
|
+
const cls = graph.nodeClass.get(id);
|
|
299
|
+
const def = cls ? graph.classDefs.get(cls) : null;
|
|
300
|
+
if (def) {
|
|
301
|
+
return {
|
|
302
|
+
fill: def.fill || DEFAULT_PALETTE.fill,
|
|
303
|
+
stroke: def.stroke || def['stroke-width'] ? (def.stroke || DEFAULT_PALETTE.stroke) : DEFAULT_PALETTE.stroke,
|
|
304
|
+
color: def.color || DEFAULT_PALETTE.color,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (graph.classDefs.size) return DEFAULT_PALETTE; // the author styled some nodes; stay neutral
|
|
308
|
+
return RANK_TINTS[Math.min(rankIdx, RANK_TINTS.length - 1)];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Mermaid flowchart text → a complete SVG document string, or null if it isn't a flowchart
|
|
313
|
+
* this renderer handles (the caller then keeps the code block).
|
|
314
|
+
*/
|
|
315
|
+
export function renderFlowchartSvg(text, { padding = 18, maxWidth = 1400 } = {}) {
|
|
316
|
+
const graph = parseFlowchart(text);
|
|
317
|
+
if (!graph) return null;
|
|
318
|
+
const L = layoutFlowchart(graph);
|
|
319
|
+
if (!L.boxes.size) return null;
|
|
320
|
+
|
|
321
|
+
const W = Math.min(maxWidth, Math.ceil(L.width + padding * 2));
|
|
322
|
+
const H = Math.ceil(L.height + padding * 2);
|
|
323
|
+
const out = [];
|
|
324
|
+
out.push(
|
|
325
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${Math.ceil(L.width + padding * 2)} ${H}" width="${W}" height="${H}" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">`,
|
|
326
|
+
);
|
|
327
|
+
out.push(
|
|
328
|
+
'<defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
|
|
329
|
+
+ '<path d="M0,0 L10,5 L0,10 z" fill="#94a3b8"/></marker></defs>',
|
|
330
|
+
);
|
|
331
|
+
out.push(`<rect width="100%" height="100%" fill="#ffffff" rx="10"/>`);
|
|
332
|
+
|
|
333
|
+
const px = (v) => Math.round(v * 10) / 10;
|
|
334
|
+
|
|
335
|
+
// Edges first, so boxes paint over the joins.
|
|
336
|
+
for (const e of L.edges) {
|
|
337
|
+
const a = L.boxes.get(e.from);
|
|
338
|
+
const b = L.boxes.get(e.to);
|
|
339
|
+
if (!a || !b) continue;
|
|
340
|
+
let x1, y1, x2, y2, d;
|
|
341
|
+
if (L.horizontal) {
|
|
342
|
+
x1 = a.x + a.w + padding; y1 = a.y + a.h / 2 + padding;
|
|
343
|
+
x2 = b.x + padding; y2 = b.y + b.h / 2 + padding;
|
|
344
|
+
const mx = (x1 + x2) / 2;
|
|
345
|
+
d = `M${px(x1)},${px(y1)} C${px(mx)},${px(y1)} ${px(mx)},${px(y2)} ${px(x2)},${px(y2)}`;
|
|
346
|
+
} else {
|
|
347
|
+
x1 = a.x + a.w / 2 + padding; y1 = a.y + a.h + padding;
|
|
348
|
+
x2 = b.x + b.w / 2 + padding; y2 = b.y + padding;
|
|
349
|
+
const my = (y1 + y2) / 2;
|
|
350
|
+
d = `M${px(x1)},${px(y1)} C${px(x1)},${px(my)} ${px(x2)},${px(my)} ${px(x2)},${px(y2)}`;
|
|
351
|
+
}
|
|
352
|
+
out.push(`<path d="${d}" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#a)"/>`);
|
|
353
|
+
if (e.label) {
|
|
354
|
+
const lx = (x1 + x2) / 2;
|
|
355
|
+
const ly = (y1 + y2) / 2;
|
|
356
|
+
out.push(
|
|
357
|
+
`<rect x="${px(lx - e.label.length * 3.4 - 5)}" y="${px(ly - 9)}" width="${px(e.label.length * 6.8 + 10)}" height="18" rx="5" fill="#ffffff" stroke="#e2e8f0"/>`
|
|
358
|
+
+ `<text x="${px(lx)}" y="${px(ly + 4)}" font-size="11" fill="#475569" text-anchor="middle">${esc(e.label)}</text>`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const [id, b] of L.boxes) {
|
|
364
|
+
const s = styleFor(id, graph, b.rank);
|
|
365
|
+
out.push(
|
|
366
|
+
`<rect x="${px(b.x + padding)}" y="${px(b.y + padding)}" width="${px(b.w)}" height="${px(b.h)}" rx="9" `
|
|
367
|
+
+ `fill="${esc(s.fill)}" stroke="${esc(s.stroke)}" stroke-width="1.5"/>`,
|
|
368
|
+
);
|
|
369
|
+
const startY = b.y + padding + PAD_Y + LINE_H - 5;
|
|
370
|
+
b.lines.forEach((line, i) => {
|
|
371
|
+
out.push(
|
|
372
|
+
`<text x="${px(b.x + b.w / 2 + padding)}" y="${px(startY + i * LINE_H)}" font-size="13" `
|
|
373
|
+
+ `fill="${esc(s.color)}" text-anchor="middle">${esc(line)}</text>`,
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
out.push('</svg>');
|
|
379
|
+
return out.join('');
|
|
380
|
+
}
|
package/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { createRegistry, REGISTRY_STATES } from './registry.js';
|
|
|
28
28
|
export { defineSearchEngine, reconcileEngines, attemptOrder, ENGINE_KINDS, SearchEngineError } from './search-engines.js';
|
|
29
29
|
export { defineToolGroup, createToolGroupRegistry, ToolGroupError } from './tool-groups.js';
|
|
30
30
|
export { toolNeedFor } from './tool-need.js';
|
|
31
|
+
export { parseFlowchart, layoutFlowchart, renderFlowchartSvg } from './flowchart.js';
|
|
31
32
|
export {
|
|
32
33
|
ACCESS_LOG_VERSION, ACCESS_LOG_MAX, redactAccessArgs, makeAccessEvent,
|
|
33
34
|
createAccessLog, makeStorageTier, formatBytes,
|
package/observability.js
CHANGED
|
@@ -25,10 +25,13 @@ export const ACCESS_LOG_MAX = 500;
|
|
|
25
25
|
// listed here is dropped. Content-bearing fields (a search `query`) are deliberately ABSENT —
|
|
26
26
|
// the tool name already says "a search happened"; the words searched are not logged.
|
|
27
27
|
const SAFE_ARGS = {
|
|
28
|
-
|
|
28
|
+
// Metadata filters are safe to keep (they are not content) and useful to see in the log:
|
|
29
|
+
// "type=meeting since=7d". The search QUERY is deliberately absent — never recorded.
|
|
30
|
+
search_history: ['type', 'since', 'before', 'limit', 'offset'],
|
|
29
31
|
list_history: ['limit', 'offset'],
|
|
30
|
-
get_record: ['id'],
|
|
31
|
-
|
|
32
|
+
get_record: ['id', 'maxChars', 'offset'], // opaque record id + paging, not content
|
|
33
|
+
find_related: ['id', 'limit'], // graph navigation from an opaque id
|
|
34
|
+
open_skill: ['skill'], // skill names are catalog identifiers, not PII
|
|
32
35
|
read_skill_file: ['skill', 'path'],
|
|
33
36
|
list_skills: ['limit'],
|
|
34
37
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"./tool-need.js": "./tool-need.js",
|
|
39
39
|
"./trajectory.js": "./trajectory.js",
|
|
40
40
|
"./upcast.js": "./upcast.js",
|
|
41
|
-
"./observability.js": "./observability.js"
|
|
41
|
+
"./observability.js": "./observability.js",
|
|
42
|
+
"./flowchart.js": "./flowchart.js"
|
|
42
43
|
},
|
|
43
44
|
"files": [
|
|
44
45
|
"LICENSE",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"capability.js",
|
|
48
49
|
"citations.js",
|
|
49
50
|
"event.js",
|
|
51
|
+
"flowchart.js",
|
|
50
52
|
"harness.js",
|
|
51
53
|
"index.js",
|
|
52
54
|
"invariants.js",
|