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/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 store or an outside part may have none.
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\` or \`missing-symbol\`.
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. one line \`npx flowfig open <path>\` for each SVG, at the end of the reply. Do not run that command yourself.
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#symbol\`, relative to the repo root. \`flowfig verify\` checks it. |
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#symbol\`. \`flowfig verify\` checks it.
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 that the code each source names still exists
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;
@@ -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 { verify } from './verify.js';
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 findings = sortFindings([...check(props), ...verify(props, { root })], args.strict === true);
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 {};