flowfig 0.7.0 → 0.8.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 +28 -8
- package/dist/check.js +4 -1
- package/dist/cli.js +11 -3
- package/dist/code.d.ts +26 -0
- package/dist/code.js +568 -0
- package/dist/edges.d.ts +7 -0
- package/dist/edges.js +510 -0
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +10 -7
- package/dist/imports.d.ts +11 -0
- package/dist/imports.js +235 -0
- package/dist/mcp.js +6 -3
- package/dist/model.d.ts +5 -0
- package/dist/source.d.ts +1 -1
- package/dist/source.js +3 -3
- package/dist/type-index.d.ts +55 -0
- package/dist/type-index.js +383 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +733 -0
- package/dist/verify.d.ts +25 -3
- package/dist/verify.js +67 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -66,7 +66,8 @@ If the code does not show a fact, leave the fact out. Do not guess a part, a nam
|
|
|
66
66
|
|
|
67
67
|
Each fact becomes a box, an edge, a beat, a \`data\` card or a \`show\` row. Use the real names from the code as labels, and
|
|
68
68
|
real example data from the code or its tests. Keep each fact that you leave out, with the reason, for the reply.
|
|
69
|
-
Give each box and edge that draws code a \`source\`, from the fact list: \`"src/auth/login.ts#verifyPassword"\`. A
|
|
69
|
+
Give each box and edge that draws code a \`source\`, from the fact list: \`"src/auth/login.ts#verifyPassword"\`. A method uses \`Owner.name\`: \`"src/auth/session.ts#Session.refresh"\`. An edge \`source\` names the function that makes the call. An edge needs no \`source\` when the code of its \`from\` box makes the call. An edge across a process (HTTP, a queue, a topic, a table) gets \`via\`: the route, queue, topic or table name that both sides use in the code, for example \`"via": "order-paid"\`. A store box may have no \`source\`, but an edge to it still needs \`via\`.
|
|
70
|
+
For an HTTP edge, \`via\` is the path that both sides use: \`"via": "/internal/users"\`. An edge to a store box (a table, a file, a folder, a cache key) uses \`via\` with the name that the caller code uses: \`"via": "sales_facts"\`. A route handler with no name keeps a file \`source\`, and an edge into it uses \`via\` with the route path. An edge out of it in the same process uses no \`via\`.
|
|
70
71
|
A box that draws a step from a document links to that document: \`"docs/sop/refunds.md#step-3-approve-the-refund"\`, the heading as a GitHub anchor. \`verify\` checks it the same way.
|
|
71
72
|
|
|
72
73
|
Mermaid mapping:
|
|
@@ -112,7 +113,7 @@ command writes no SVG and lists the faults. Fix each fault and render again.
|
|
|
112
113
|
Run \`npx flowfig --spec out.svg\`. Compare the spec with the fact list. Check that each edge goes from the caller to the callee
|
|
113
114
|
that its fact names. Add each fact that is missing, fix each wrong edge, and render again.
|
|
114
115
|
|
|
115
|
-
Run \`npx flowfig verify out.svg\` from the repo root. Fix each \`missing-file\`
|
|
116
|
+
Run \`npx flowfig verify out.svg\` from the repo root. Fix each \`missing-file\` and \`missing-symbol\`. For each \`edge-not-found\`, the code does not make that call: remove the edge, or point its \`source\` at the function that makes the call. An \`unsure\` edge is allowed, but if a function with a typed receiver makes the call, point the edge \`source\` at it. A \`not checked\` edge has no code to check: give it a \`source\` or a \`via\` if one exists. Do not invent an edge to make verify pass.
|
|
116
117
|
|
|
117
118
|
### 6. Look at the SVG
|
|
118
119
|
|
|
@@ -132,7 +133,8 @@ The reply has these parts, in this order:
|
|
|
132
133
|
4. what the figure leaves out, and why;
|
|
133
134
|
5. the two check lines that the render printed, \`0 errors, 0 warnings\` and \`figure: ...\`, copied as printed, and whether
|
|
134
135
|
you looked at the SVG;
|
|
135
|
-
6.
|
|
136
|
+
6. the \`verify\` edge counts from its count line, for example "edges: 5 found, 0 not found, 1 unsure, 0 not checked";
|
|
137
|
+
7. one line \`npx flowfig open <path>\` for each SVG, at the end of the reply. Do not run that command yourself.
|
|
136
138
|
|
|
137
139
|
To change an SVG later, print its spec with \`--spec\`, change the spec, and render again.
|
|
138
140
|
|
|
@@ -174,7 +176,7 @@ Every other item is a box.
|
|
|
174
176
|
| \`from\` | In a \`timeline\` figure: the start of the item, or the date of a milestone, as YYYY-MM-DD. |
|
|
175
177
|
| \`to\` | In a \`timeline\` figure: the last day of the item, as YYYY-MM-DD. Without it the box is a milestone. |
|
|
176
178
|
| \`width\` | Width in px. This overrides the width that the layout picks. |
|
|
177
|
-
| \`source\` | The code this draws: \`path\` or \`path#
|
|
179
|
+
| \`source\` | The code this draws: \`path\` or \`path#Owner.name\`, relative to the repo root. \`flowfig verify\` checks it. |
|
|
178
180
|
| \`lines\` | The least number of text lines that a content card keeps. The card still grows to fit its content. |
|
|
179
181
|
| \`tone\` | The state color of the box: a 1 px border and a light tint. Use \`blue\`, \`purple\`, \`green\`, \`orange\`, \`red\` or \`gray\`. |
|
|
180
182
|
| \`mark\` | \`"start"\` or \`"end"\` | A lifecycle mark: a filled dot before a start state, a ringed dot after an end state. |
|
|
@@ -188,7 +190,8 @@ An edge has these fields.
|
|
|
188
190
|
- \`label\`: text on the edge, drawn as a small pill.
|
|
189
191
|
- \`around\`: \`"above"\` or \`"below"\`. This routes the edge over or under the boxes in between (loops, skip-ahead edges).
|
|
190
192
|
- \`quiet\`: if \`true\`, the SVG draws the edge only while a step uses it.
|
|
191
|
-
- \`source\`: the code this edge draws, as \`path\` or \`path#
|
|
193
|
+
- \`source\`: the code this edge draws, as \`path\` or \`path#Owner.name\`. \`flowfig verify\` checks it.
|
|
194
|
+
- \`via\`: the route, queue, topic or table name that both sides of a cross-process edge use in the code.
|
|
192
195
|
|
|
193
196
|
### steps
|
|
194
197
|
|
|
@@ -203,7 +206,7 @@ boxes to highlight for the whole step).
|
|
|
203
206
|
|
|
204
207
|
The beat fields:
|
|
205
208
|
|
|
206
|
-
- \`edges\`: an edge id, a hop object, or an array of these. A hop object is \`{ edge, back?, data?, tone?, async?, source? }\`.
|
|
209
|
+
- \`edges\`: an edge id, a hop object, or an array of these. A hop object is \`{ edge, back?, data?, tone?, async?, source?, via? }\`.
|
|
207
210
|
- \`back: true\` runs the packet from the \`to\` box to the \`from\` box.
|
|
208
211
|
- \`data\` is a small card that moves with the packet.
|
|
209
212
|
- \`async: true\` marks a message that does not wait for an answer. The rail draws it dashed, with an \`async\` tag.
|
|
@@ -275,7 +278,7 @@ npx flowfig - out.svg < spec.json # render a spec from stdin
|
|
|
275
278
|
npx flowfig spec.json out.svg # render a spec file
|
|
276
279
|
npx flowfig figure.ts out.svg # render a module whose default export is a spec
|
|
277
280
|
npx flowfig check <input> [--json] # list the faults. The input can also be an SVG that flowfig wrote.
|
|
278
|
-
npx flowfig verify <input>... # check
|
|
281
|
+
npx flowfig verify <input>... # check each source and each edge against the code
|
|
279
282
|
npx flowfig diff <old> <new> # list what changed in the spec between two figures
|
|
280
283
|
npx flowfig --spec out.svg # print the spec that an SVG carries
|
|
281
284
|
npx flowfig open out.svg # show the figure in the default browser
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CodeFile, type Read } from './code.ts';
|
|
2
|
+
export type Import = {
|
|
3
|
+
name: string;
|
|
4
|
+
local: string;
|
|
5
|
+
path: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare const OUTSIDE = "#outside";
|
|
8
|
+
export declare function resolveTs(root: string, from: string, spec: string, read: Read): string | null;
|
|
9
|
+
export declare function resolvePy(root: string, from: string, mod: string, read: Read, top?: string): string | null;
|
|
10
|
+
export declare function importsOf(root: string, file: CodeFile, read: Read): Import[];
|
|
11
|
+
export declare function leadsTo(root: string, fromPath: string, toPath: string, name: string, read?: Read, cache?: Map<string, CodeFile | null>, depth?: number): string | null;
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { builtinModules } from 'node:module';
|
|
2
|
+
import { posix, resolve } from 'node:path';
|
|
3
|
+
import { codeFile, esc, langOf, outside, readFile } from './code.js';
|
|
4
|
+
export const OUTSIDE = '#outside';
|
|
5
|
+
const TS_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js'];
|
|
6
|
+
const readIn = (root, path, read) => (outside(root, path) ? null : read(resolve(root, path)));
|
|
7
|
+
const has = (root, path, read) => readIn(root, path, read) != null;
|
|
8
|
+
function tsConfig(root, read) {
|
|
9
|
+
const paths = [];
|
|
10
|
+
let baseUrl;
|
|
11
|
+
let cfg = 'tsconfig.json';
|
|
12
|
+
for (let k = 0; k < 4 && cfg; k++) {
|
|
13
|
+
const raw = readIn(root, cfg, read);
|
|
14
|
+
if (raw == null)
|
|
15
|
+
break;
|
|
16
|
+
const dir = posix.dirname(cfg);
|
|
17
|
+
const own = /"baseUrl"\s*:\s*"([^"]*)"/.exec(raw)?.[1];
|
|
18
|
+
if (own != null)
|
|
19
|
+
baseUrl ??= posix.join(dir, own);
|
|
20
|
+
const block = /"paths"\s*:\s*\{([^}]*)\}/.exec(raw)?.[1] ?? '';
|
|
21
|
+
for (const m of block.matchAll(/"([^"]+)"\s*:\s*\[\s*"([^"]+)"/g))
|
|
22
|
+
paths.push({ base: own != null ? posix.join(dir, own) : dir, key: m[1], target: m[2] });
|
|
23
|
+
const ext = /"extends"\s*:\s*"(\.[^"]+)"/.exec(raw)?.[1];
|
|
24
|
+
cfg = ext ? posix.join(dir, ext.endsWith('.json') ? ext : `${ext}.json`) : '';
|
|
25
|
+
}
|
|
26
|
+
return { paths, baseUrl };
|
|
27
|
+
}
|
|
28
|
+
export function resolveTs(root, from, spec, read) {
|
|
29
|
+
const bases = [];
|
|
30
|
+
if (spec.startsWith('.'))
|
|
31
|
+
bases.push(posix.join(posix.dirname(from), spec));
|
|
32
|
+
else {
|
|
33
|
+
const cfg = tsConfig(root, read);
|
|
34
|
+
for (const { base, key, target } of cfg.paths) {
|
|
35
|
+
const star = key.indexOf('*');
|
|
36
|
+
const pre = star < 0 ? key : key.slice(0, star);
|
|
37
|
+
const post = star < 0 ? '' : key.slice(star + 1);
|
|
38
|
+
if (star < 0 ? spec !== key : !(spec.startsWith(pre) && spec.endsWith(post) && spec.length >= key.length - 1))
|
|
39
|
+
continue;
|
|
40
|
+
bases.push(posix.join(base, star < 0 ? target : target.replace('*', spec.slice(pre.length, spec.length - post.length))));
|
|
41
|
+
}
|
|
42
|
+
if (cfg.baseUrl != null)
|
|
43
|
+
bases.push(posix.join(cfg.baseUrl, spec));
|
|
44
|
+
}
|
|
45
|
+
for (const b of bases) {
|
|
46
|
+
const base = b.replace(/\.(js|mjs|cjs|jsx)$/, '');
|
|
47
|
+
const hit = TS_EXT.map((e) => base + e).find((p) => has(root, p, read));
|
|
48
|
+
if (hit)
|
|
49
|
+
return hit;
|
|
50
|
+
}
|
|
51
|
+
return spec.startsWith('node:') || builtinModules.includes(spec.split('/')[0]) ? OUTSIDE : null;
|
|
52
|
+
}
|
|
53
|
+
function pyTop(root, from, read) {
|
|
54
|
+
let d = posix.dirname(from);
|
|
55
|
+
while (d !== '.' && has(root, `${d}/__init__.py`, read))
|
|
56
|
+
d = posix.dirname(d);
|
|
57
|
+
return d;
|
|
58
|
+
}
|
|
59
|
+
export function resolvePy(root, from, mod, read, top) {
|
|
60
|
+
const hit = (b) => [`${b}.py`, `${b}/__init__.py`].find((p) => has(root, p, read)) ?? null;
|
|
61
|
+
if (mod.startsWith('.')) {
|
|
62
|
+
const dots = /^\.+/.exec(mod)[0].length;
|
|
63
|
+
let d = posix.dirname(from);
|
|
64
|
+
for (let k = 1; k < dots; k++) {
|
|
65
|
+
if (d === '.')
|
|
66
|
+
return null;
|
|
67
|
+
d = posix.dirname(d);
|
|
68
|
+
}
|
|
69
|
+
return hit(posix.join(d, mod.slice(dots).replace(/\./g, '/')));
|
|
70
|
+
}
|
|
71
|
+
const rel = mod.replace(/\./g, '/');
|
|
72
|
+
const d = top ?? pyTop(root, from, read);
|
|
73
|
+
return hit(rel) ?? (d === '.' ? null : hit(posix.join(d, rel)));
|
|
74
|
+
}
|
|
75
|
+
function goModule(root, from, read) {
|
|
76
|
+
let dir = posix.dirname(from);
|
|
77
|
+
for (;;) {
|
|
78
|
+
const text = readIn(root, posix.join(dir, 'go.mod'), read);
|
|
79
|
+
if (text != null)
|
|
80
|
+
return { mod: /module\s+(\S+)/.exec(text)?.[1], dir, text };
|
|
81
|
+
if (dir === '.')
|
|
82
|
+
return { dir, text: '' };
|
|
83
|
+
dir = posix.dirname(dir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function goWork(root, from, read) {
|
|
87
|
+
for (let d = posix.dirname(from);; d = posix.dirname(d)) {
|
|
88
|
+
const text = readIn(root, posix.join(d, 'go.work'), read);
|
|
89
|
+
if (text != null)
|
|
90
|
+
return [...text.matchAll(/^\s*(?:use\s+)?"?(\.[^\s")]*)"?\s*$/gm)].flatMap((m) => {
|
|
91
|
+
const dir = posix.join(d, m[1]);
|
|
92
|
+
const mod = /module\s+(\S+)/.exec(readIn(root, posix.join(dir, 'go.mod'), read) ?? '')?.[1];
|
|
93
|
+
return mod ? [{ mod, dir }] : [];
|
|
94
|
+
});
|
|
95
|
+
if (d === '.')
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const goRequired = (text, p) => [...text.matchAll(/^\s*(?:require\s+)?([\w.~-]+\/[\w.~/-]+)\s+v\S+/gm)].some((m) => (p === m[1] || p.startsWith(`${m[1]}/`)) && !new RegExp(`^\\s*(?:replace\\s+)?${esc(m[1])}\\b[^\\n]*=>`, 'm').test(text));
|
|
100
|
+
export function importsOf(root, file, read) {
|
|
101
|
+
const { keep, lang, path } = file;
|
|
102
|
+
const out = [];
|
|
103
|
+
const add = (name, local, to) => out.push({ name, local, path: to });
|
|
104
|
+
if (lang === 'ts') {
|
|
105
|
+
for (const m of keep.matchAll(/\bimport\s+(type\s+)?([\s\S]*?)\s+from\s+(["'])([^"']+)\3/g)) {
|
|
106
|
+
const to = resolveTs(root, path, m[4], read);
|
|
107
|
+
const clause = m[2];
|
|
108
|
+
const def = /^([A-Za-z_$][\w$]*)\s*(,|$)/.exec(clause);
|
|
109
|
+
if (def)
|
|
110
|
+
add('default', def[1], to);
|
|
111
|
+
const ns = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
|
|
112
|
+
if (ns)
|
|
113
|
+
add('*', ns[1], to);
|
|
114
|
+
const named = /\{([\s\S]*)\}/.exec(clause);
|
|
115
|
+
if (named)
|
|
116
|
+
for (const part of named[1].split(',')) {
|
|
117
|
+
const p = part.trim().replace(/^type\s+/, '');
|
|
118
|
+
if (!p)
|
|
119
|
+
continue;
|
|
120
|
+
const [a, b] = p.split(/\s+as\s+/);
|
|
121
|
+
add(a.trim(), (b ?? a).trim(), to);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const m of keep.matchAll(/\b(?:const|let|var)\s+(\{[^}]*\}|[A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:require|import)\((["'])([^"']+)\2\)/g)) {
|
|
125
|
+
const to = resolveTs(root, path, m[3], read);
|
|
126
|
+
if (m[1].startsWith('{'))
|
|
127
|
+
for (const p of m[1].slice(1, -1).split(',')) {
|
|
128
|
+
const [a, b] = p.split(':').map((x) => x.trim());
|
|
129
|
+
if (a)
|
|
130
|
+
add(a, b ?? a, to);
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
add('*', m[1], to);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (lang === 'py') {
|
|
137
|
+
const top = pyTop(root, path, read);
|
|
138
|
+
for (const m of keep.matchAll(/^[ \t]*from\s+([\w.]+)\s+import\s+(\([^)]*\)|[^\n]+)/gm)) {
|
|
139
|
+
const to = resolvePy(root, path, m[1], read, top);
|
|
140
|
+
for (const part of m[2].replace(/[()]/g, '').split(',')) {
|
|
141
|
+
const p = part.trim();
|
|
142
|
+
if (!p)
|
|
143
|
+
continue;
|
|
144
|
+
const [a, b] = p.split(/\s+as\s+/).map((x) => x.trim());
|
|
145
|
+
const sub = resolvePy(root, path, (m[1].endsWith('.') ? m[1] : `${m[1]}.`) + a, read, top);
|
|
146
|
+
if (sub)
|
|
147
|
+
add('*', b ?? a, sub);
|
|
148
|
+
else
|
|
149
|
+
add(a, b ?? a, to);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const m of keep.matchAll(/^[ \t]*import\s+([\w.]+)(\s+as\s+(\w+))?/gm))
|
|
153
|
+
add('*', m[3] ?? m[1].split('.')[0], resolvePy(root, path, m[1], read, top));
|
|
154
|
+
}
|
|
155
|
+
else if (lang === 'go') {
|
|
156
|
+
const { mod, dir, text } = goModule(root, path, read);
|
|
157
|
+
let work;
|
|
158
|
+
for (const m of keep.matchAll(/^\s*(?:import\s+)?(\w+|\.|_)?\s*"([^"]+)"/gm)) {
|
|
159
|
+
const p = m[2];
|
|
160
|
+
const inside = (w) => w.mod && (p === w.mod || p.startsWith(`${w.mod}/`));
|
|
161
|
+
const own = [
|
|
162
|
+
{ mod, dir },
|
|
163
|
+
...(p.split('/')[0].includes('.') && !inside({ mod, dir }) ? (work ??= goWork(root, path, read)) : []),
|
|
164
|
+
].find(inside);
|
|
165
|
+
const to = own
|
|
166
|
+
? posix.join(own.dir, p.slice(own.mod.length + 1))
|
|
167
|
+
: p.split('/')[0].includes('.') && !goRequired(text, p)
|
|
168
|
+
? null
|
|
169
|
+
: OUTSIDE;
|
|
170
|
+
add('*', m[1] ?? p.split('/').at(-1), to);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function reexports(root, file, name, read) {
|
|
176
|
+
const N = esc(name);
|
|
177
|
+
const out = [];
|
|
178
|
+
if (name === '*')
|
|
179
|
+
return out;
|
|
180
|
+
if (file.lang === 'py') {
|
|
181
|
+
if (new RegExp(`^(?:(?:async\\s+)?def|class)\\s+${N}\\b|^${N}\\s*[:=]`, 'm').test(file.code))
|
|
182
|
+
return out;
|
|
183
|
+
for (const i of importsOf(root, file, read))
|
|
184
|
+
if (i.local === name || i.local === '*')
|
|
185
|
+
out.push([i.path, i.local === '*' ? '?' : i.name]);
|
|
186
|
+
return out.length > 1 ? [[null, '?']] : out;
|
|
187
|
+
}
|
|
188
|
+
if (file.lang !== 'ts')
|
|
189
|
+
return out;
|
|
190
|
+
if (new RegExp(`^export\\s+(?:default\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var|enum)\\s*${N}\\b`, 'm').test(file.code))
|
|
191
|
+
return out;
|
|
192
|
+
for (const m of file.keep.matchAll(/\bexport\s+(?:type\s+)?(\*(?:\s+as\s+([\w$]+))?|\{([^}]*)\})\s+from\s+(["'])([^"']+)\4/g)) {
|
|
193
|
+
const p = resolveTs(root, file.path, m[5], read);
|
|
194
|
+
if (m[3] != null) {
|
|
195
|
+
for (const part of m[3].split(',')) {
|
|
196
|
+
const [a, b] = part
|
|
197
|
+
.trim()
|
|
198
|
+
.replace(/^type\s+/, '')
|
|
199
|
+
.split(/\s+as\s+/);
|
|
200
|
+
if (a && (b ?? a).trim() === name)
|
|
201
|
+
out.push([p, a.trim()]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (m[2] == null ? name !== 'default' : m[2] === name)
|
|
205
|
+
out.push([p, m[2] == null ? name : '*']);
|
|
206
|
+
}
|
|
207
|
+
for (const m of file.keep.matchAll(/\bexport\s*\{([^}]*)\}(?!\s*from)/g))
|
|
208
|
+
for (const part of m[1].split(',')) {
|
|
209
|
+
const [a, b] = part.trim().split(/\s+as\s+/);
|
|
210
|
+
if (!a || (b ?? a).trim() !== name)
|
|
211
|
+
continue;
|
|
212
|
+
const i = importsOf(root, file, read).find((x) => x.local === a.trim());
|
|
213
|
+
if (i)
|
|
214
|
+
out.push([i.path, i.name]);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
export function leadsTo(root, fromPath, toPath, name, read = readFile, cache = new Map(), depth = 0) {
|
|
219
|
+
if (fromPath === toPath)
|
|
220
|
+
return name;
|
|
221
|
+
if (langOf(fromPath) == null)
|
|
222
|
+
return posix.dirname(toPath) === fromPath ? name : null;
|
|
223
|
+
const file = depth < 5 ? codeFile(root, fromPath, read, cache) : null;
|
|
224
|
+
if (!file)
|
|
225
|
+
return null;
|
|
226
|
+
let star = false;
|
|
227
|
+
for (const [path, inner] of reexports(root, file, name, read)) {
|
|
228
|
+
const r = path == null || inner === '?' ? null : leadsTo(root, path, toPath, inner, read, cache, depth + 1);
|
|
229
|
+
if (r === '?' || inner === '?')
|
|
230
|
+
star = true;
|
|
231
|
+
else if (r != null)
|
|
232
|
+
return r;
|
|
233
|
+
}
|
|
234
|
+
return star ? '?' : null;
|
|
235
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -6,7 +6,7 @@ import { GUIDE } from './guide.js';
|
|
|
6
6
|
import { loadSpec, reportLines, sortFindings, svgWithSpec } from './load.js';
|
|
7
7
|
import { check, render } from './svg.js';
|
|
8
8
|
import { checkRendered } from './check.js';
|
|
9
|
-
import {
|
|
9
|
+
import { coverageLine, unsureLines, verifyReport } from './verify.js';
|
|
10
10
|
import { VERSION } from './version.js';
|
|
11
11
|
const specSchema = {
|
|
12
12
|
description: 'The figure: the props object, { props }, or a path to a .json file or an SVG that flowfig wrote.',
|
|
@@ -115,6 +115,7 @@ function run(name, args) {
|
|
|
115
115
|
types(args, name, { root: 'string', strict: 'boolean' });
|
|
116
116
|
const root = args.root == null ? process.cwd() : String(args.root);
|
|
117
117
|
const lines = [];
|
|
118
|
+
const counts = [];
|
|
118
119
|
let errors = 0;
|
|
119
120
|
for (const p of args.paths) {
|
|
120
121
|
let props;
|
|
@@ -128,13 +129,15 @@ function run(name, args) {
|
|
|
128
129
|
lines.push(`${'error'.padEnd(8)} ${'missing-file'.padEnd(18)} ${e.message}`);
|
|
129
130
|
continue;
|
|
130
131
|
}
|
|
131
|
-
const
|
|
132
|
+
const report = verifyReport(props, { root });
|
|
133
|
+
counts.push(coverageLine(p, report.coverage), ...unsureLines(report.coverage));
|
|
134
|
+
const findings = sortFindings([...check(props), ...report.findings], args.strict === true);
|
|
132
135
|
errors += findings.filter((f) => f.severity === 'error').length;
|
|
133
136
|
lines.push(...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${p}: ${f.message}`));
|
|
134
137
|
}
|
|
135
138
|
const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`;
|
|
136
139
|
const total = lines.length;
|
|
137
|
-
lines.push(`${n(errors, 'error')}, ${n(total - errors, 'warning')}
|
|
140
|
+
lines.push(`${n(errors, 'error')}, ${n(total - errors, 'warning')}`, ...counts);
|
|
138
141
|
return ok(lines.join('\n'), errors > 0);
|
|
139
142
|
}
|
|
140
143
|
if (name === 'diff') {
|
package/dist/model.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export type FigEdge = {
|
|
|
58
58
|
quiet?: boolean;
|
|
59
59
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
60
60
|
source?: string;
|
|
61
|
+
/** The route, queue, topic or table that carries this edge. Checked by `flowfig verify`. */
|
|
62
|
+
via?: string;
|
|
61
63
|
};
|
|
62
64
|
/** One packet on one edge. A string is the edge `id`. */
|
|
63
65
|
export type FigHop = string | {
|
|
@@ -73,6 +75,8 @@ export type FigHop = string | {
|
|
|
73
75
|
async?: boolean;
|
|
74
76
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
75
77
|
source?: string;
|
|
78
|
+
/** The route, queue, topic or table that carries this edge. Checked by `flowfig verify`. */
|
|
79
|
+
via?: string;
|
|
76
80
|
};
|
|
77
81
|
/** One moment of a step. No edges means a pause. */
|
|
78
82
|
export type FigBeat = {
|
|
@@ -161,6 +165,7 @@ export type Beat = Omit<FigBeat, 'edges'> & {
|
|
|
161
165
|
tone?: FigTone;
|
|
162
166
|
async?: boolean;
|
|
163
167
|
source?: string;
|
|
168
|
+
via?: string;
|
|
164
169
|
}[];
|
|
165
170
|
};
|
|
166
171
|
export declare const toBeat: (b: FigHop | FigHop[] | FigBeat) => Beat;
|
package/dist/source.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export type Link = {
|
|
|
12
12
|
symbol?: string;
|
|
13
13
|
};
|
|
14
14
|
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
15
|
-
export declare function owners(fig: FlowProps): [string, string | undefined][];
|
|
15
|
+
export declare function owners(fig: FlowProps): [string, string | undefined, string | undefined][];
|
|
16
16
|
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
17
17
|
export declare function links(fig: FlowProps): Link[];
|
package/dist/source.js
CHANGED
|
@@ -15,9 +15,9 @@ export const headingSlug = (text) => text
|
|
|
15
15
|
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
16
16
|
export function owners(fig) {
|
|
17
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]))),
|
|
18
|
+
...nodes(fig.layout).map((n) => [`box "${n.id}"`, n.source, undefined]),
|
|
19
|
+
...fig.edges.map((e) => [`edge "${edgeId(e)}"`, e.source, e.via]),
|
|
20
|
+
...(fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"`, h.source, h.via]))),
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
23
|
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type CodeFile, type Lang, type Read } from './code.ts';
|
|
2
|
+
export type Decl = {
|
|
3
|
+
info: CodeFile;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
name: string;
|
|
7
|
+
kind: 'class' | 'interface';
|
|
8
|
+
dir?: string;
|
|
9
|
+
};
|
|
10
|
+
export type Bind = {
|
|
11
|
+
mod: string | null;
|
|
12
|
+
imported: string;
|
|
13
|
+
dir: boolean;
|
|
14
|
+
};
|
|
15
|
+
type Globals = {
|
|
16
|
+
types: Map<string, string[]>;
|
|
17
|
+
impls: Map<string, Set<string>>;
|
|
18
|
+
aliases: Map<string, {
|
|
19
|
+
to: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}>;
|
|
22
|
+
usings: Set<string>;
|
|
23
|
+
projects: string[];
|
|
24
|
+
};
|
|
25
|
+
type Index = {
|
|
26
|
+
types: Map<string, Decl | null>;
|
|
27
|
+
binds: Map<string, Map<string, Bind>>;
|
|
28
|
+
repo: string[] | null;
|
|
29
|
+
globals: Map<Lang, Globals>;
|
|
30
|
+
};
|
|
31
|
+
type Cache = Map<string, CodeFile | null>;
|
|
32
|
+
export type Ctx = {
|
|
33
|
+
root: string;
|
|
34
|
+
read: Read;
|
|
35
|
+
cache: Cache;
|
|
36
|
+
ix: Index;
|
|
37
|
+
};
|
|
38
|
+
export declare function makeCtx(root: string, read?: Read, cache?: Cache): Ctx;
|
|
39
|
+
export declare const lastSeg: (s: string) => string;
|
|
40
|
+
export declare const file: (cx: Ctx, path: string) => CodeFile | null;
|
|
41
|
+
export declare function bindings(cx: Ctx, f: CodeFile): Map<string, Bind>;
|
|
42
|
+
export declare function goDirFiles(cx: Ctx, dir: string): CodeFile[];
|
|
43
|
+
export declare function global(cx: Ctx, lang: 'java' | 'cs' | 'rs'): Globals;
|
|
44
|
+
export declare function declIn(info: CodeFile | null, name: string): Decl | null;
|
|
45
|
+
export declare function findType(cx: Ctx, ctx: CodeFile, name: string | undefined, qual?: string | null): Decl | null;
|
|
46
|
+
export declare function ancestors(cx: Ctx, d: Decl): {
|
|
47
|
+
name: string;
|
|
48
|
+
d: Decl | null;
|
|
49
|
+
}[];
|
|
50
|
+
export declare function goMethods(cx: Ctx, d: Decl): Set<string>;
|
|
51
|
+
export declare const goIfaceMethods: (d: Decl) => string[];
|
|
52
|
+
export declare function defines(cx: Ctx, d: Decl, n: string): boolean;
|
|
53
|
+
export declare function rustUses(fi: CodeFile, n: string, tp: string): 'yes' | 'no' | 'outside';
|
|
54
|
+
export declare const rustMod: (tp: string) => string;
|
|
55
|
+
export {};
|