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/verify.d.ts CHANGED
@@ -1,7 +1,29 @@
1
- import type { FlowProps } from './model.ts';
1
+ import { type FlowProps } from './model.ts';
2
2
  import type { Finding } from './scene.ts';
3
+ import { type Read } from './code.ts';
3
4
  export { parseSource, headingSlug, owners, links, type Link } from './source.ts';
5
+ export type Coverage = {
6
+ boxes: number;
7
+ boxesDefined: number;
8
+ found: number;
9
+ notFound: number;
10
+ unsure: number;
11
+ notChecked: number;
12
+ unsureEdges: {
13
+ id: string;
14
+ reason: string;
15
+ }[];
16
+ };
17
+ export declare const coverageLine: (figure: string, c: Coverage) => string;
18
+ export declare const unsureLines: (c: Coverage) => string[];
4
19
  /** The links against the files under `root`. A path outside `root` counts as a missing file. */
5
- export declare function verify(fig: FlowProps, { root }?: {
20
+ export declare function verifyReport(fig: FlowProps, { root, read }?: {
6
21
  root?: string;
7
- }): Finding[];
22
+ read?: Read;
23
+ }): {
24
+ findings: Finding[];
25
+ coverage: Coverage;
26
+ };
27
+ export declare const verify: (fig: FlowProps, opts?: {
28
+ root?: string;
29
+ }) => Finding[];
package/dist/verify.js CHANGED
@@ -1,40 +1,85 @@
1
- import { readFileSync, statSync } from 'node:fs';
2
- import { isAbsolute, relative, resolve, sep } from 'node:path';
1
+ import { resolve } from 'node:path';
2
+ import { edgeId, nodes, toBeat } from './model.js';
3
3
  import { headingSlug, links } from './source.js';
4
+ import { codeFile, isDefined, outside, readFile } from './code.js';
5
+ import { edgeResult } from './edges.js';
4
6
  export { parseSource, headingSlug, owners, links } from './source.js';
5
7
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
- function read(full) {
7
- try {
8
- return statSync(full).isFile() ? readFileSync(full, 'utf8') : null;
9
- }
10
- catch {
11
- return null;
12
- }
13
- }
14
8
  const hasHeading = (text, slug) => text.split('\n').some((line) => {
15
9
  const m = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
16
10
  return m != null && headingSlug(m[1]) === slug;
17
11
  });
12
+ const emptyCoverage = () => ({ boxes: 0, boxesDefined: 0, found: 0, notFound: 0, unsure: 0, notChecked: 0, unsureEdges: [] });
13
+ export const coverageLine = (figure, c) => `${figure}: ${c.boxesDefined} of ${c.boxes} boxes defined` +
14
+ (c.found + c.notFound + c.unsure + c.notChecked
15
+ ? `; edges: ${c.found} found, ${c.notFound} not found, ${c.unsure} unsure, ${c.notChecked} not checked`
16
+ : '');
17
+ export const unsureLines = (c) => c.unsureEdges.map((e) => `unsure edge "${e.id}": ${e.reason}`);
18
18
  /** The links against the files under `root`. A path outside `root` counts as a missing file. */
19
- export function verify(fig, { root = process.cwd() } = {}) {
20
- const out = [];
19
+ export function verifyReport(fig, { root = process.cwd(), read = readFile } = {}) {
20
+ const findings = [];
21
+ const coverage = emptyCoverage();
21
22
  const all = links(fig);
22
23
  if (!all.length)
23
- return [{ rule: 'no-source', severity: 'warning', ids: [], message: 'no box, edge or hop has a source' }];
24
+ return {
25
+ findings: [{ rule: 'no-source', severity: 'warning', ids: [], message: 'no box, edge or hop has a source' }],
26
+ coverage,
27
+ };
24
28
  const files = new Map();
29
+ const parsed = new Map();
25
30
  for (const l of all) {
26
31
  const full = resolve(root, l.path);
27
- const rel = relative(root, full);
28
- const outside = isAbsolute(l.path) || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
29
32
  if (!files.has(full))
30
- files.set(full, outside ? null : read(full));
33
+ files.set(full, outside(root, l.path) ? null : read(full));
31
34
  const text = files.get(full);
35
+ const before = findings.length;
36
+ const fail = (rule, what) => findings.push({ rule, severity: 'error', ids: [], message: `${l.owner} -> ${l.source}: ${what}` });
32
37
  if (text == null)
33
- out.push({ rule: 'missing-file', severity: 'error', ids: [], message: `${l.owner} -> ${l.source}: file not found` });
34
- else if (l.symbol &&
35
- !new RegExp(`(^|[^\\w$])${escape(l.symbol)}(?![\\w$])`).test(text) &&
36
- !(/\.(md|markdown)$/i.test(l.path) && hasHeading(text, l.symbol)))
37
- out.push({ rule: 'missing-symbol', severity: 'error', ids: [], message: `${l.owner} -> ${l.source}: symbol not found` });
38
+ fail('missing-file', 'file not found');
39
+ else if (l.symbol) {
40
+ const code = codeFile(root, l.path, read, parsed);
41
+ if (code) {
42
+ if (!isDefined(code, l.symbol))
43
+ fail('missing-symbol', 'symbol not defined');
44
+ }
45
+ else if (!new RegExp(`(^|[^\\w$])${escape(l.symbol)}(?![\\w$])`).test(text) &&
46
+ !(/\.(md|markdown)$/i.test(l.path) && hasHeading(text, l.symbol)))
47
+ fail('missing-symbol', 'symbol not found');
48
+ }
49
+ if (l.owner.startsWith('box "')) {
50
+ coverage.boxes++;
51
+ if (findings.length === before)
52
+ coverage.boxesDefined++;
53
+ }
38
54
  }
39
- return out;
55
+ const source = new Map(nodes(fig.layout).map((n) => [n.id, n.source]));
56
+ const check = (who, id, caller, callee, via) => {
57
+ const { result, reason } = caller ? edgeResult(root, caller, callee, via, read, parsed) : { result: 'not-checked', reason: '' };
58
+ if (result === 'found')
59
+ coverage.found++;
60
+ else if (result === 'not-found') {
61
+ coverage.notFound++;
62
+ findings.push({ rule: 'edge-not-found', severity: 'warning', ids: [id], message: `${who}: ${reason}` });
63
+ }
64
+ else if (result === 'unsure') {
65
+ coverage.unsure++;
66
+ coverage.unsureEdges.push({ id, reason });
67
+ }
68
+ else
69
+ coverage.notChecked++;
70
+ };
71
+ for (const e of fig.edges)
72
+ check(`edge "${edgeId(e)}"`, edgeId(e), e.source ?? source.get(e.from), source.get(e.to), e.via);
73
+ const byId = new Map(fig.edges.map((e) => [edgeId(e), e]));
74
+ for (const step of fig.steps ?? [])
75
+ for (const beat of step.flow)
76
+ for (const h of toBeat(beat).hops) {
77
+ const e = byId.get(h.edge);
78
+ if (!e || (h.source == null && h.via == null))
79
+ continue;
80
+ const [a, b] = h.back ? [e.to, e.from] : [e.from, e.to];
81
+ check(`hop on "${h.edge}"`, h.edge, h.source ?? (h.back ? undefined : e.source) ?? source.get(a), source.get(b), h.via);
82
+ }
83
+ return { findings, coverage };
40
84
  }
85
+ export const verify = (fig, opts = {}) => verifyReport(fig, opts).findings;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.0";
1
+ export declare const VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The release list bumps this with package.json.
2
- export const VERSION = '0.7.0';
2
+ export const VERSION = '0.8.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowfig",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Your coding agent draws animated diagrams from your code, and flowfig checks them. One animated SVG for a GitHub README, PR or issue, and a React player. Set up Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, Windsurf and Kiro with npx flowfig init.",
5
5
  "license": "MIT",
6
6
  "repository": {