flowfig 0.1.0 → 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/README.md +87 -24
- package/dist/check.js +29 -3
- package/dist/cli.js +112 -49
- package/dist/diff.d.ts +9 -0
- package/dist/diff.js +59 -0
- package/dist/draw.d.ts +12 -0
- package/dist/draw.js +178 -0
- package/dist/guide.d.ts +2 -2
- package/dist/guide.js +34 -12
- package/dist/index.js +74 -24
- package/dist/init.d.ts +15 -0
- package/dist/init.js +92 -6
- package/dist/load.d.ts +12 -0
- package/dist/load.js +59 -0
- package/dist/mcp.d.ts +139 -0
- package/dist/mcp.js +216 -0
- package/dist/model.d.ts +25 -3
- package/dist/model.js +14 -0
- package/dist/rail.d.ts +2 -1
- package/dist/rail.js +15 -1
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +18 -0
- package/dist/source.js +29 -0
- package/dist/svg.d.ts +2 -0
- package/dist/svg.js +113 -27
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +43 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +5 -1
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type Response = {
|
|
2
|
+
jsonrpc: '2.0';
|
|
3
|
+
id: unknown;
|
|
4
|
+
result?: unknown;
|
|
5
|
+
error?: {
|
|
6
|
+
code: number;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const TOOLS: ({
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties: {
|
|
16
|
+
paths?: undefined;
|
|
17
|
+
root?: undefined;
|
|
18
|
+
strict?: undefined;
|
|
19
|
+
old?: undefined;
|
|
20
|
+
new?: undefined;
|
|
21
|
+
};
|
|
22
|
+
required?: undefined;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: string;
|
|
29
|
+
properties: {
|
|
30
|
+
spec: {
|
|
31
|
+
description: string;
|
|
32
|
+
anyOf: {
|
|
33
|
+
type: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
strict: {
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
width: {
|
|
41
|
+
type: string;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
minText: {
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
required: string[];
|
|
50
|
+
};
|
|
51
|
+
} | {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: string;
|
|
56
|
+
properties: {
|
|
57
|
+
spec: {
|
|
58
|
+
description: string;
|
|
59
|
+
anyOf: {
|
|
60
|
+
type: string;
|
|
61
|
+
}[];
|
|
62
|
+
};
|
|
63
|
+
strict: {
|
|
64
|
+
type: string;
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
width: {
|
|
68
|
+
type: string;
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
minText: {
|
|
72
|
+
type: string;
|
|
73
|
+
description: string;
|
|
74
|
+
};
|
|
75
|
+
out: {
|
|
76
|
+
type: string;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
paths?: undefined;
|
|
80
|
+
root?: undefined;
|
|
81
|
+
old?: undefined;
|
|
82
|
+
new?: undefined;
|
|
83
|
+
};
|
|
84
|
+
required: string[];
|
|
85
|
+
};
|
|
86
|
+
} | {
|
|
87
|
+
name: string;
|
|
88
|
+
description: string;
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: string;
|
|
91
|
+
properties: {
|
|
92
|
+
paths: {
|
|
93
|
+
type: string;
|
|
94
|
+
items: {
|
|
95
|
+
type: string;
|
|
96
|
+
};
|
|
97
|
+
description: string;
|
|
98
|
+
};
|
|
99
|
+
root: {
|
|
100
|
+
type: string;
|
|
101
|
+
description: string;
|
|
102
|
+
};
|
|
103
|
+
strict: {
|
|
104
|
+
type: string;
|
|
105
|
+
};
|
|
106
|
+
old?: undefined;
|
|
107
|
+
new?: undefined;
|
|
108
|
+
};
|
|
109
|
+
required: string[];
|
|
110
|
+
};
|
|
111
|
+
} | {
|
|
112
|
+
name: string;
|
|
113
|
+
description: string;
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: string;
|
|
116
|
+
properties: {
|
|
117
|
+
paths?: undefined;
|
|
118
|
+
root?: undefined;
|
|
119
|
+
strict?: undefined;
|
|
120
|
+
old: {
|
|
121
|
+
description: string;
|
|
122
|
+
anyOf: {
|
|
123
|
+
type: string;
|
|
124
|
+
}[];
|
|
125
|
+
};
|
|
126
|
+
new: {
|
|
127
|
+
description: string;
|
|
128
|
+
anyOf: {
|
|
129
|
+
type: string;
|
|
130
|
+
}[];
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
required: string[];
|
|
134
|
+
};
|
|
135
|
+
})[];
|
|
136
|
+
/** One JSON-RPC message in, one response out; `undefined` for a notification. Pure, so the tests call it directly. */
|
|
137
|
+
export declare function handle(message: unknown): Response | undefined;
|
|
138
|
+
/** Read one message per line from `input`, write one response per line to `output`. Returns when `input` ends. */
|
|
139
|
+
export declare function serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<void>;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// The MCP server: the check, render, verify, diff and docs tools over stdio JSON-RPC, for an agent with no shell.
|
|
2
|
+
// No SDK: the subset the clients need is small, and the package keeps its zero dependencies.
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { diff, formatDiff } from './diff.js';
|
|
7
|
+
import { GUIDE } from './guide.js';
|
|
8
|
+
import { loadSpec, reportLines, sortFindings, svgWithSpec } from './load.js';
|
|
9
|
+
import { check } from './svg.js';
|
|
10
|
+
import { verify } from './verify.js';
|
|
11
|
+
import { VERSION } from './version.js';
|
|
12
|
+
const specSchema = {
|
|
13
|
+
description: 'The figure: the props object, { props }, or a path to a .json file or an SVG that flowfig wrote.',
|
|
14
|
+
anyOf: [{ type: 'object' }, { type: 'string' }],
|
|
15
|
+
};
|
|
16
|
+
const checkProps = {
|
|
17
|
+
spec: specSchema,
|
|
18
|
+
strict: { type: 'boolean', description: 'Warnings count as errors.' },
|
|
19
|
+
width: { type: 'number', description: 'The page width in px the reader sees. Default 830.' },
|
|
20
|
+
minText: { type: 'number', description: 'The smallest text in px at that width. Default 10.' },
|
|
21
|
+
};
|
|
22
|
+
export const TOOLS = [
|
|
23
|
+
{ name: 'docs', description: 'The flowfig guide. Read it before the first render.', inputSchema: { type: 'object', properties: {} } },
|
|
24
|
+
{
|
|
25
|
+
name: 'check',
|
|
26
|
+
description: 'List the faults of a figure: unknown ids, text overflow, crossings, label overlap, small text, low contrast. Fix every error before render.',
|
|
27
|
+
inputSchema: { type: 'object', properties: checkProps, required: ['spec'] },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'render',
|
|
31
|
+
description: 'Check a figure, then write it as one animated SVG file. Returns the check lines and the path. On an error, writes nothing.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: { ...checkProps, out: { type: 'string', description: 'The SVG path to write.' } },
|
|
35
|
+
required: ['spec', 'out'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'verify',
|
|
40
|
+
description: 'Check the code links (source) of one or more figures: the file and the symbol must exist under root.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Paths to .json files or SVGs.' },
|
|
45
|
+
root: { type: 'string', description: 'The folder the links resolve against. Default: the working directory.' },
|
|
46
|
+
strict: { type: 'boolean' },
|
|
47
|
+
},
|
|
48
|
+
required: ['paths'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'diff',
|
|
53
|
+
description: 'The spec changes between two figures, as a Markdown list.',
|
|
54
|
+
inputSchema: { type: 'object', properties: { old: specSchema, new: specSchema }, required: ['old', 'new'] },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
class Bad extends Error {
|
|
58
|
+
} // bad params: a JSON-RPC error, not a tool result
|
|
59
|
+
class Fault extends Error {
|
|
60
|
+
} // a path that cannot be read or written: a tool result with isError
|
|
61
|
+
const ok = (text, isError = false) => isError ? { content: [{ type: 'text', text }], isError: true } : { content: [{ type: 'text', text }] };
|
|
62
|
+
const spec = (v, name) => {
|
|
63
|
+
if (typeof v !== 'string' && (typeof v !== 'object' || v === null))
|
|
64
|
+
throw new Bad(`${name}: spec must be an object or a path`);
|
|
65
|
+
try {
|
|
66
|
+
return loadSpec(v);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// The message of a path failure already starts with the path.
|
|
70
|
+
throw typeof v === 'string' ? new Fault(e.message) : new Bad(`${name}: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const need = (args, tool, keys) => {
|
|
74
|
+
for (const k of keys)
|
|
75
|
+
if (args[k] == null)
|
|
76
|
+
throw new Bad(`${tool}: ${k} is required`);
|
|
77
|
+
};
|
|
78
|
+
/** A present argument must have its type; a number must be positive. */
|
|
79
|
+
const types = (args, tool, want) => {
|
|
80
|
+
for (const [k, t] of Object.entries(want)) {
|
|
81
|
+
const v = args[k];
|
|
82
|
+
if (v == null)
|
|
83
|
+
continue;
|
|
84
|
+
if (typeof v !== t || (t === 'number' && !(v > 0 && Number.isFinite(v)))) {
|
|
85
|
+
throw new Bad(`${tool}: ${k} must be ${t === 'number' ? 'a positive number' : `a ${t}`}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
function run(name, args) {
|
|
90
|
+
if (name === 'docs')
|
|
91
|
+
return ok(GUIDE);
|
|
92
|
+
if (name === 'check' || name === 'render') {
|
|
93
|
+
need(args, name, name === 'render' ? ['spec', 'out'] : ['spec']);
|
|
94
|
+
types(args, name, { width: 'number', minText: 'number', out: 'string', strict: 'boolean' });
|
|
95
|
+
const props = spec(args.spec, name);
|
|
96
|
+
const opts = { width: args.width, minText: args.minText };
|
|
97
|
+
const findings = sortFindings(check(props, opts), args.strict === true);
|
|
98
|
+
const errors = findings.some((f) => f.severity === 'error');
|
|
99
|
+
const lines = reportLines(props, findings);
|
|
100
|
+
if (name === 'check' || errors)
|
|
101
|
+
return ok(lines.join('\n'), errors);
|
|
102
|
+
const out = String(args.out);
|
|
103
|
+
const svg = svgWithSpec(props);
|
|
104
|
+
try {
|
|
105
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
106
|
+
writeFileSync(out, svg);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
throw new Fault(`${out}: ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
return ok([...lines, `${out} — ${(svg.length / 1024).toFixed(1)} kB`].join('\n'));
|
|
112
|
+
}
|
|
113
|
+
if (name === 'verify') {
|
|
114
|
+
need(args, name, ['paths']);
|
|
115
|
+
if (!Array.isArray(args.paths) || args.paths.some((p) => typeof p !== 'string'))
|
|
116
|
+
throw new Bad('verify: paths must be an array of strings');
|
|
117
|
+
types(args, name, { root: 'string', strict: 'boolean' });
|
|
118
|
+
const root = args.root == null ? process.cwd() : String(args.root);
|
|
119
|
+
const lines = [];
|
|
120
|
+
let errors = 0;
|
|
121
|
+
for (const p of args.paths) {
|
|
122
|
+
let props;
|
|
123
|
+
try {
|
|
124
|
+
props = spec(p, 'verify');
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
if (!(e instanceof Fault))
|
|
128
|
+
throw e;
|
|
129
|
+
errors++;
|
|
130
|
+
lines.push(`${'error'.padEnd(8)} ${'missing-file'.padEnd(18)} ${e.message}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const findings = sortFindings([...check(props), ...verify(props, { root })], args.strict === true);
|
|
134
|
+
errors += findings.filter((f) => f.severity === 'error').length;
|
|
135
|
+
lines.push(...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${p}: ${f.message}`));
|
|
136
|
+
}
|
|
137
|
+
const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`;
|
|
138
|
+
const total = lines.length;
|
|
139
|
+
lines.push(`${n(errors, 'error')}, ${n(total - errors, 'warning')}`);
|
|
140
|
+
return ok(lines.join('\n'), errors > 0);
|
|
141
|
+
}
|
|
142
|
+
if (name === 'diff') {
|
|
143
|
+
need(args, name, ['old', 'new']);
|
|
144
|
+
return ok(formatDiff(diff(spec(args.old, 'diff'), spec(args.new, 'diff')), 'md'));
|
|
145
|
+
}
|
|
146
|
+
throw new Bad(`unknown tool "${name}"`);
|
|
147
|
+
}
|
|
148
|
+
const reply = (id, result) => ({ jsonrpc: '2.0', id, result });
|
|
149
|
+
const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
150
|
+
/** One JSON-RPC message in, one response out; `undefined` for a notification. Pure, so the tests call it directly. */
|
|
151
|
+
export function handle(message) {
|
|
152
|
+
if (typeof message !== 'object' || message === null)
|
|
153
|
+
return fail(null, -32600, 'invalid request');
|
|
154
|
+
const { id, method, params } = message;
|
|
155
|
+
const notification = id === undefined;
|
|
156
|
+
if (typeof method !== 'string')
|
|
157
|
+
return notification ? undefined : fail(id, -32600, 'invalid request');
|
|
158
|
+
if (notification)
|
|
159
|
+
return undefined; // a notification gets no reply, and a call sent as one does not run
|
|
160
|
+
const info = { name: 'flowfig', version: VERSION };
|
|
161
|
+
try {
|
|
162
|
+
switch (method) {
|
|
163
|
+
case 'initialize':
|
|
164
|
+
return reply(id, {
|
|
165
|
+
protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2025-11-25',
|
|
166
|
+
capabilities: { tools: {} },
|
|
167
|
+
serverInfo: info,
|
|
168
|
+
});
|
|
169
|
+
case 'server/discover':
|
|
170
|
+
return reply(id, {
|
|
171
|
+
resultType: 'complete',
|
|
172
|
+
supportedVersions: ['2026-07-28'],
|
|
173
|
+
capabilities: { tools: {} },
|
|
174
|
+
_meta: { 'io.modelcontextprotocol/serverInfo': info },
|
|
175
|
+
});
|
|
176
|
+
case 'ping':
|
|
177
|
+
return reply(id, {});
|
|
178
|
+
case 'tools/list':
|
|
179
|
+
return reply(id, { tools: TOOLS });
|
|
180
|
+
case 'tools/call': {
|
|
181
|
+
const name = String(params?.name ?? '');
|
|
182
|
+
if (!TOOLS.some((t) => t.name === name))
|
|
183
|
+
throw new Bad(`unknown tool "${name}"`);
|
|
184
|
+
return reply(id, run(name, params?.arguments ?? {}));
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
return fail(id, -32601, `method not found: ${method}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
if (e instanceof Bad)
|
|
192
|
+
return fail(id, -32602, e.message);
|
|
193
|
+
if (e instanceof Fault)
|
|
194
|
+
return reply(id, ok(e.message, true));
|
|
195
|
+
return fail(id, -32603, e.message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Read one message per line from `input`, write one response per line to `output`. Returns when `input` ends. */
|
|
199
|
+
export async function serve(input, output) {
|
|
200
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
201
|
+
for await (const line of rl) {
|
|
202
|
+
if (!line.trim())
|
|
203
|
+
continue;
|
|
204
|
+
let message;
|
|
205
|
+
try {
|
|
206
|
+
message = JSON.parse(line);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
output.write(JSON.stringify(fail(null, -32700, 'parse error')) + '\n');
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const response = handle(message);
|
|
213
|
+
if (response)
|
|
214
|
+
output.write(JSON.stringify(response) + '\n');
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export type FigNode = {
|
|
|
13
13
|
lines?: number;
|
|
14
14
|
/** Width in px. Overrides the width the layout picks. */
|
|
15
15
|
width?: number;
|
|
16
|
+
/** A permanent state color for the box, such as a failing part. It gives a 1 px border and a light tint. An arrival with no hop tone uses it too. */
|
|
17
|
+
tone?: FigTone;
|
|
18
|
+
/** The code this draws, `path` or `path#symbol`, relative to the repo root. `flowfig verify` checks it. */
|
|
19
|
+
source?: string;
|
|
16
20
|
};
|
|
17
21
|
/** A frame that lays out its children in a row or a column. Groups can hold groups. */
|
|
18
22
|
export type FigGroup = {
|
|
@@ -43,6 +47,8 @@ export type FigEdge = {
|
|
|
43
47
|
around?: 'above' | 'below';
|
|
44
48
|
/** Draws the edge only while a step uses it. Use it for long edges that would cut across everything. */
|
|
45
49
|
quiet?: boolean;
|
|
50
|
+
/** The code this draws, `path` or `path#symbol`, relative to the repo root. `flowfig verify` checks it. */
|
|
51
|
+
source?: string;
|
|
46
52
|
};
|
|
47
53
|
/** One packet on one edge. A string is the edge `id`. */
|
|
48
54
|
export type FigHop = string | {
|
|
@@ -52,8 +58,12 @@ export type FigHop = string | {
|
|
|
52
58
|
back?: boolean;
|
|
53
59
|
/** A small card that rides along with the packet. */
|
|
54
60
|
data?: ReactNode;
|
|
61
|
+
/** Colors the packet, its data card, the edge and the box it arrives at, for this beat. The trail after the beat stays the accent. Default: the accent. */
|
|
62
|
+
tone?: FigTone;
|
|
55
63
|
/** Marks a message that does not wait for an answer. The rail draws it dashed, with an `async` tag. Default: `false`. */
|
|
56
64
|
async?: boolean;
|
|
65
|
+
/** The code this draws, `path` or `path#symbol`, relative to the repo root. `flowfig verify` checks it. */
|
|
66
|
+
source?: string;
|
|
57
67
|
};
|
|
58
68
|
/** One moment of a step. No edges means a pause. */
|
|
59
69
|
export type FigBeat = {
|
|
@@ -65,7 +75,7 @@ export type FigBeat = {
|
|
|
65
75
|
show?: Record<string, FigContent>;
|
|
66
76
|
/** Ids of boxes to highlight for this beat, without a packet or a content card. */
|
|
67
77
|
light?: string[];
|
|
68
|
-
/** How long the beat lasts in ms.
|
|
78
|
+
/** How long the beat lasts in ms. The packet crosses in `speed`; the rest is a hold. An explicit `ms` wins as written. Default: `speed` plus the time to read `say`. */
|
|
69
79
|
ms?: number;
|
|
70
80
|
};
|
|
71
81
|
/** One line of a content card. */
|
|
@@ -83,8 +93,8 @@ export type FigRow = {
|
|
|
83
93
|
/** Draws the text in a monospace font. Default: `false`. */
|
|
84
94
|
mono?: boolean;
|
|
85
95
|
};
|
|
86
|
-
/** The color of a row tag. Each tone is a fixed color. */
|
|
87
|
-
export type FigTone = 'blue' | 'purple' | 'green' | 'orange' | 'gray';
|
|
96
|
+
/** The color of a row tag, a hop or a box. Each tone is a fixed color. */
|
|
97
|
+
export type FigTone = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'gray';
|
|
88
98
|
/** What a content card shows: rows, or anything React can render. */
|
|
89
99
|
export type FigContent = FigRow[] | ReactNode;
|
|
90
100
|
/** One story the figure can tell. The player shows one tab for each step. */
|
|
@@ -131,7 +141,9 @@ export type Beat = Omit<FigBeat, 'edges'> & {
|
|
|
131
141
|
edge: string;
|
|
132
142
|
back: boolean;
|
|
133
143
|
data?: ReactNode;
|
|
144
|
+
tone?: FigTone;
|
|
134
145
|
async?: boolean;
|
|
146
|
+
source?: string;
|
|
135
147
|
}[];
|
|
136
148
|
};
|
|
137
149
|
export declare const toBeat: (b: FigHop | FigHop[] | FigBeat) => Beat;
|
|
@@ -148,6 +160,10 @@ export declare const BASE_RATE = 1.25;
|
|
|
148
160
|
export declare const CARD_LINE = 15, CARD_PAD = 6;
|
|
149
161
|
/** The fixed color of each row tone. */
|
|
150
162
|
export declare const TONES: Record<FigTone, string>;
|
|
163
|
+
/** A tone as a fill under white text: darker, so the text keeps its contrast. */
|
|
164
|
+
export declare const toneFill: (c: string) => string;
|
|
165
|
+
/** The light tint of a tone over a background: the fill of a toned box. */
|
|
166
|
+
export declare const toneTint: (c: string, bg: string, pct?: number) => string;
|
|
151
167
|
/** The width of an edge-label pill in px: the label text plus its padding. The rail uses it for a payload, so both look the same. */
|
|
152
168
|
export declare const labelPillW: (label: string) => number;
|
|
153
169
|
/** The width of the `async` tag in px. */
|
|
@@ -175,6 +191,12 @@ export declare const DARK: {
|
|
|
175
191
|
export declare const ON_ACCENT = "#ffffff";
|
|
176
192
|
/** Only text survives outside React: a React element has no string form. */
|
|
177
193
|
export declare const str: (n: unknown) => string;
|
|
194
|
+
/** The time a reader needs for a caption line: 400 ms plus 240 ms per word. The result is 0 for no text. */
|
|
195
|
+
export declare const readMs: (say: unknown) => number;
|
|
196
|
+
/** How long a beat lasts. An explicit `ms` wins as written. Else the beat lasts `speed` plus the time to read `say`. */
|
|
197
|
+
export declare const beatMs: (b: Beat, speed: number) => number;
|
|
198
|
+
/** The hold at the end of each step, before the next step starts. */
|
|
199
|
+
export declare const STEP_HOLD_MS = 2000;
|
|
178
200
|
export declare const nodes: (g: FigGroup) => FigNode[];
|
|
179
201
|
/** The part counts of a spec, for the line that `flowfig check` prints. An agent copies the line into its reply. */
|
|
180
202
|
export declare const counts: (p: FlowProps) => {
|
package/dist/model.js
CHANGED
|
@@ -25,8 +25,13 @@ export const TONES = {
|
|
|
25
25
|
purple: '#8b5cf6',
|
|
26
26
|
green: '#10b981',
|
|
27
27
|
orange: '#f59e0b',
|
|
28
|
+
red: '#ef4444',
|
|
28
29
|
gray: '#8b949e',
|
|
29
30
|
};
|
|
31
|
+
/** A tone as a fill under white text: darker, so the text keeps its contrast. */
|
|
32
|
+
export const toneFill = (c) => `color-mix(in srgb, ${c} 64%, #000)`;
|
|
33
|
+
/** The light tint of a tone over a background: the fill of a toned box. */
|
|
34
|
+
export const toneTint = (c, bg, pct = 8) => `color-mix(in srgb, ${c} ${pct}%, ${bg})`;
|
|
30
35
|
/** The width of an edge-label pill in px: the label text plus its padding. The rail uses it for a payload, so both look the same. */
|
|
31
36
|
export const labelPillW = (label) => textWidth(label, 11, true) + 14;
|
|
32
37
|
/** The width of the `async` tag in px. */
|
|
@@ -41,6 +46,15 @@ export const DARK = { accent: '#1f78c8', fg: '#e3e3e3', muted: '#9aa0a6', bg: '#
|
|
|
41
46
|
export const ON_ACCENT = '#ffffff';
|
|
42
47
|
/** Only text survives outside React: a React element has no string form. */
|
|
43
48
|
export const str = (n) => (typeof n === 'string' || typeof n === 'number' ? String(n) : '');
|
|
49
|
+
/** The time a reader needs for a caption line: 400 ms plus 240 ms per word. The result is 0 for no text. */
|
|
50
|
+
export const readMs = (say) => {
|
|
51
|
+
const w = str(say).split(/\s+/).filter(Boolean).length;
|
|
52
|
+
return w ? 400 + 240 * w : 0;
|
|
53
|
+
};
|
|
54
|
+
/** How long a beat lasts. An explicit `ms` wins as written. Else the beat lasts `speed` plus the time to read `say`. */
|
|
55
|
+
export const beatMs = (b, speed) => b.ms ?? speed + readMs(b.say);
|
|
56
|
+
/** The hold at the end of each step, before the next step starts. */
|
|
57
|
+
export const STEP_HOLD_MS = 2000;
|
|
44
58
|
export const nodes = (g) => g.children.flatMap((c) => (isGroup(c) ? nodes(c) : [c]));
|
|
45
59
|
const labeledGroups = (g) => (g.label ? 1 : 0) + g.children.reduce((n, c) => n + (isGroup(c) ? labeledGroups(c) : 0), 0);
|
|
46
60
|
/** The part counts of a spec, for the line that `flowfig check` prints. An agent copies the line into its reply. */
|
package/dist/rail.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Rect } from './geometry.ts';
|
|
2
|
-
import { type FlowProps } from './model.ts';
|
|
2
|
+
import { type FigTone, type FlowProps } from './model.ts';
|
|
3
3
|
export declare const RAIL: {
|
|
4
4
|
readonly gap: 24;
|
|
5
5
|
readonly band: 20;
|
|
@@ -45,6 +45,7 @@ export type RailRow = {
|
|
|
45
45
|
to: number;
|
|
46
46
|
text: string;
|
|
47
47
|
async: boolean;
|
|
48
|
+
tone?: FigTone;
|
|
48
49
|
pill?: {
|
|
49
50
|
x: number;
|
|
50
51
|
w: number;
|
package/dist/rail.js
CHANGED
|
@@ -43,6 +43,7 @@ export function layoutRail(fig, mapWidth) {
|
|
|
43
43
|
back: h.back,
|
|
44
44
|
text: str(h.data) || str(edges.get(h.edge).label),
|
|
45
45
|
async: h.async === true,
|
|
46
|
+
tone: h.tone,
|
|
46
47
|
}))));
|
|
47
48
|
const msgs = perStep.flat(2);
|
|
48
49
|
if (!msgs.length)
|
|
@@ -127,7 +128,20 @@ export function layoutRail(fig, mapWidth) {
|
|
|
127
128
|
const pill = m.text ? { x: mid - labelPillW(m.text) / 2, w: labelPillW(m.text) } : undefined;
|
|
128
129
|
if (group != null)
|
|
129
130
|
groups[group].rows.push(rows.length);
|
|
130
|
-
rows.push({
|
|
131
|
+
rows.push({
|
|
132
|
+
kind: 'message',
|
|
133
|
+
step: si,
|
|
134
|
+
beat: m.beat,
|
|
135
|
+
n: ++n,
|
|
136
|
+
edge: m.edge,
|
|
137
|
+
from,
|
|
138
|
+
to,
|
|
139
|
+
text: m.text,
|
|
140
|
+
async: m.async,
|
|
141
|
+
tone: m.tone,
|
|
142
|
+
pill,
|
|
143
|
+
group,
|
|
144
|
+
});
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
});
|
package/dist/scene.d.ts
CHANGED
|
@@ -12,12 +12,14 @@ export type SceneBox = {
|
|
|
12
12
|
rect: Rect;
|
|
13
13
|
texts: TextRun[];
|
|
14
14
|
};
|
|
15
|
+
/** `step` marks a rail row: rows of two steps never show at the same time, so their labels cannot overlap. */
|
|
15
16
|
export type SceneEdge = {
|
|
16
17
|
id: string;
|
|
17
18
|
from: string;
|
|
18
19
|
to: string;
|
|
19
20
|
curve: [Pt, Pt, Pt, Pt];
|
|
20
21
|
label?: Rect;
|
|
22
|
+
step?: number;
|
|
21
23
|
};
|
|
22
24
|
/** `minFont` is the smallest reading text in px, before any scale. Group frames are not boxes. */
|
|
23
25
|
export type Scene = {
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type FlowProps } from './model.ts';
|
|
2
|
+
/** `path` or `path#symbol`. No spaces, one `#` at most, both parts non-empty. `null` for a bad form. */
|
|
3
|
+
export declare function parseSource(s: string): {
|
|
4
|
+
path: string;
|
|
5
|
+
symbol?: string;
|
|
6
|
+
} | null;
|
|
7
|
+
/** The GitHub anchor of a heading: lower case, only letters, digits, spaces and hyphens, spaces to hyphens. */
|
|
8
|
+
export declare const headingSlug: (text: string) => string;
|
|
9
|
+
export type Link = {
|
|
10
|
+
owner: string;
|
|
11
|
+
source: string;
|
|
12
|
+
path: string;
|
|
13
|
+
symbol?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
16
|
+
export declare function owners(fig: FlowProps): [string, string | undefined][];
|
|
17
|
+
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
18
|
+
export declare function links(fig: FlowProps): Link[];
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Code links, pure: no Node import, so the React and SVG entries stay browser-safe. `verify` in verify.ts reads the disk.
|
|
2
|
+
import { edgeId, nodes, toBeat } from './model.js';
|
|
3
|
+
/** `path` or `path#symbol`. No spaces, one `#` at most, both parts non-empty. `null` for a bad form. */
|
|
4
|
+
export function parseSource(s) {
|
|
5
|
+
const m = /^([^\s#]+)(?:#([^\s#]+))?$/.exec(s);
|
|
6
|
+
if (!m)
|
|
7
|
+
return null;
|
|
8
|
+
return m[2] ? { path: m[1], symbol: m[2] } : { path: m[1] };
|
|
9
|
+
}
|
|
10
|
+
/** The GitHub anchor of a heading: lower case, only letters, digits, spaces and hyphens, spaces to hyphens. */
|
|
11
|
+
export const headingSlug = (text) => text
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^\p{L}\p{N} -]/gu, '')
|
|
14
|
+
.replace(/ /g, '-');
|
|
15
|
+
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
16
|
+
export function owners(fig) {
|
|
17
|
+
return [
|
|
18
|
+
...nodes(fig.layout).map((n) => [`box "${n.id}"`, n.source]),
|
|
19
|
+
...fig.edges.map((e) => [`edge "${edgeId(e)}"`, e.source]),
|
|
20
|
+
...(fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"`, h.source]))),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
24
|
+
export function links(fig) {
|
|
25
|
+
return owners(fig).flatMap(([owner, source]) => {
|
|
26
|
+
const p = source == null ? null : parseSource(source);
|
|
27
|
+
return p ? [{ owner, source: source, ...p }] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Finding, Scene } from './scene.ts';
|
|
|
3
3
|
export type { CheckOptions } from './check.ts';
|
|
4
4
|
export type { Finding, Scene } from './scene.ts';
|
|
5
5
|
export type * from './model.ts';
|
|
6
|
+
export { parseSource, links, type Link } from './source.ts';
|
|
7
|
+
export { diff, formatDiff, type Change } from './diff.ts';
|
|
6
8
|
import { type FigTheme, type FlowProps } from './model.ts';
|
|
7
9
|
export { LIGHT, DARK } from './model.ts';
|
|
8
10
|
/** Options for `toSvg`, `render` and `check`. */
|