@olierjs/fugue-cli 0.0.0-stage → 0.1.2
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/LICENSE +201 -0
- package/README.md +99 -2
- package/THIRD_PARTY_NOTICES +467 -0
- package/bin/fugue-cli.mjs +23 -0
- package/dist/darwin-arm64/fugue-cli +0 -0
- package/dist/darwin-x64/fugue-cli +0 -0
- package/dist/linux-arm64/fugue-cli +0 -0
- package/dist/linux-x64/fugue-cli +0 -0
- package/dist/win32-arm64/fugue-cli.exe +0 -0
- package/dist/win32-x64/fugue-cli.exe +0 -0
- package/package.json +27 -4
- package/scripts/postinstall.mjs +13 -0
- package/scripts/skills.mjs +104 -0
- package/skills/fugue-show-me/SKILL.md +16 -0
- package/skills/fugue-stack/SKILL.md +10 -0
- package/skills/plan-with-remote/SKILL.md +60 -0
- package/skills/plan-with-remote/references/configuration.md +43 -0
- package/skills/plan-with-remote/scripts/client.mjs +61 -0
- package/skills/plan-with-remote/scripts/config.mjs +53 -0
- package/skills/plan-with-remote/scripts/planning.mjs +279 -0
- package/skills/plan-with-remote/scripts/protocol.mjs +147 -0
- package/skills/plan-with-remote/scripts/service.mjs +146 -0
- package/skills/plan-with-remote/scripts/storage.mjs +74 -0
- package/skills/show-me/SKILL.md +128 -0
- package/skills/show-me/agents/openai.yaml +2 -0
- package/skills/statechart/SKILL.md +36 -0
- package/skills/statechart/scripts/bug-states.mjs +81 -0
- package/skills/statechart/scripts/chart-edges.mjs +68 -0
- package/skills/statechart/scripts/chart-parts.mjs +21 -0
- package/skills/statechart/scripts/chart-to-tla.mjs +205 -0
- package/skills/statechart/scripts/compare-charts.mjs +74 -0
- package/skills/statechart/scripts/lib.mjs +970 -0
- package/skills/statechart/scripts/map-view.mjs +92 -0
- package/skills/statechart/scripts/property-loop.mjs +60 -0
- package/skills/statechart/scripts/read-check.mjs +79 -0
- package/skills/statechart/scripts/regions-chart.mjs +307 -0
- package/skills/statechart/scripts/render.mjs +531 -0
- package/skills/statechart/scripts/rule-book.mjs +84 -0
- package/skills/statechart/scripts/second-replay.mjs +122 -0
- package/skills/statechart/scripts/snapshot.mjs +74 -0
- package/skills/statechart/scripts/spec-text.mjs +126 -0
- package/skills/statechart/scripts/statechart-help.mjs +350 -0
- package/skills/statechart/scripts/statechart.mjs +903 -0
- package/skills/statechart/scripts/test/band.mmd +15 -0
- package/skills/statechart/scripts/test/regions.mmd +62 -0
- package/skills/statechart/scripts/test/statechart.test.mjs +6854 -0
- package/skills/statechart/scripts/test/sync.mmd +19 -0
- package/skills/statechart/scripts/test/upload.mmd +16 -0
- package/skills/statechart/scripts/tlc-to-chart-args.mjs +52 -0
- package/skills/statechart/scripts/tlc-to-chart.mjs +802 -0
- package/skills/statechart/scripts/tlc-tools.mjs +69 -0
- package/skills/statechart/scripts/ways-in.mjs +69 -0
- package/upstream-skills.json +18 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { edgeId } from "./chart-parts.mjs";
|
|
2
|
+
import { diverges } from "./bug-states.mjs";
|
|
3
|
+
|
|
4
|
+
export const edgesAlong = (c) =>
|
|
5
|
+
c.path.slice(1).map((p, i) => ({
|
|
6
|
+
id: edgeId(c.path[i].key, p.key),
|
|
7
|
+
from: c.path[i].key,
|
|
8
|
+
to: p.key,
|
|
9
|
+
label: p.label,
|
|
10
|
+
guard: p.guard,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
export function chartEdges({ args, now, secondQ, modelEdges, classes, chosen, coverOff }) {
|
|
14
|
+
const edges = new Map(modelEdges);
|
|
15
|
+
for (const { second: s } of classes.filter(diverges)) {
|
|
16
|
+
if (modelEdges.has(s.edge)) continue;
|
|
17
|
+
if (!edges.has(s.edge))
|
|
18
|
+
edges.set(s.edge, {
|
|
19
|
+
from: s.from,
|
|
20
|
+
to: s.key,
|
|
21
|
+
labels: new Set(),
|
|
22
|
+
secondOnly: true,
|
|
23
|
+
apart: s.apart,
|
|
24
|
+
});
|
|
25
|
+
edges.get(s.edge).labels.add(s.overview);
|
|
26
|
+
}
|
|
27
|
+
const pathNumbers = new Map();
|
|
28
|
+
(chosen ? edgesAlong(chosen) : []).forEach((step, i) => {
|
|
29
|
+
if (!pathNumbers.has(step.id)) pathNumbers.set(step.id, []);
|
|
30
|
+
const shown = step.guard ? `${step.label} [${step.guard}]` : step.label;
|
|
31
|
+
pathNumbers.get(step.id).push({ n: i + 1, label: step.label, shown });
|
|
32
|
+
if (!edges.has(step.id))
|
|
33
|
+
edges.set(step.id, { from: step.from, to: step.to, labels: new Set([step.label]) });
|
|
34
|
+
});
|
|
35
|
+
const bugSteps = classes.flatMap(edgesAlong);
|
|
36
|
+
const pathEdges = new Map();
|
|
37
|
+
for (const step of bugSteps) {
|
|
38
|
+
if (edges.has(step.id)) continue;
|
|
39
|
+
if (!pathEdges.has(step.id))
|
|
40
|
+
pathEdges.set(step.id, { from: step.from, to: step.to, labels: new Set() });
|
|
41
|
+
pathEdges.get(step.id).labels.add(step.label);
|
|
42
|
+
}
|
|
43
|
+
for (const [id, e] of pathEdges) edges.set(id, e);
|
|
44
|
+
if (!args.cover || coverOff) return { edges, pathNumbers, coverEdges: null };
|
|
45
|
+
|
|
46
|
+
const coverEdges = new Set(bugSteps.map((step) => step.id));
|
|
47
|
+
const bfs = (q, targets) => {
|
|
48
|
+
const parent = new Map([...q.initial].map((k) => [k, null]));
|
|
49
|
+
const outEdges = new Map();
|
|
50
|
+
for (const [id, e] of q.edges) {
|
|
51
|
+
if (!outEdges.has(e.from)) outEdges.set(e.from, []);
|
|
52
|
+
outEdges.get(e.from).push([id, e]);
|
|
53
|
+
}
|
|
54
|
+
for (const k of parent.keys())
|
|
55
|
+
for (const [id, e] of outEdges.get(k) ?? []) if (!parent.has(e.to)) parent.set(e.to, id);
|
|
56
|
+
for (const t of targets) {
|
|
57
|
+
for (let id = parent.get(t); id; id = parent.get(q.edges.get(id).from)) coverEdges.add(id);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
bfs(now, now.settled);
|
|
61
|
+
if (secondQ)
|
|
62
|
+
bfs(
|
|
63
|
+
secondQ,
|
|
64
|
+
[...secondQ.settled].filter((k) => !now.nodes.has(k)),
|
|
65
|
+
);
|
|
66
|
+
for (const c of classes.filter(diverges)) coverEdges.add(c.second.edge);
|
|
67
|
+
return { edges, pathNumbers, coverEdges };
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SEP = "\u0000";
|
|
2
|
+
export const edgeId = (from, to) => `${from}${SEP}${SEP}${to}`;
|
|
3
|
+
|
|
4
|
+
export function guarded(guards, label) {
|
|
5
|
+
const words = guards?.get(label);
|
|
6
|
+
return words && !words.has("") ? `${label} [${[...words].sort().join(" or ")}]` : label;
|
|
7
|
+
}
|
|
8
|
+
export const addGuard = (guards, label, words) => {
|
|
9
|
+
if (!guards.has(label)) guards.set(label, new Set());
|
|
10
|
+
guards.get(label).add(words);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const frontMatter = (title) => [
|
|
14
|
+
"---",
|
|
15
|
+
...(title ? [`title: ${JSON.stringify(title)}`] : []),
|
|
16
|
+
"config:",
|
|
17
|
+
" markdownAutoWrap: false",
|
|
18
|
+
" state:",
|
|
19
|
+
" useMaxWidth: false",
|
|
20
|
+
"---",
|
|
21
|
+
];
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { parseMermaid } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
const USAGE = "Usage: node chart-to-tla.mjs CHART.mmd --module NAME [--dir OUT]";
|
|
8
|
+
|
|
9
|
+
const args = {};
|
|
10
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
11
|
+
const a = process.argv[i];
|
|
12
|
+
if (a === "--module") args.module = process.argv[++i];
|
|
13
|
+
else if (a === "--dir") args.dir = process.argv[++i];
|
|
14
|
+
else if (!a.startsWith("-") && !args.chart) args.chart = a;
|
|
15
|
+
else {
|
|
16
|
+
console.error(`unknown argument ${a}.`);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!args.chart || !/^[A-Za-z][A-Za-z0-9_]*$/.test(args.module ?? "")) {
|
|
21
|
+
console.error(USAGE);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const dir = args.dir ?? path.dirname(args.chart);
|
|
25
|
+
const { regions, nevers } = parseMermaid(fs.readFileSync(args.chart, "utf8"));
|
|
26
|
+
|
|
27
|
+
const RESERVED = new Set(["vars", "Init", "Next", "Spec", "TypeOK", "Settled"]);
|
|
28
|
+
const taken = new Set(RESERVED);
|
|
29
|
+
function ident(words, lower) {
|
|
30
|
+
const parts = words
|
|
31
|
+
.normalize("NFKD")
|
|
32
|
+
.replace(/[^A-Za-z0-9]+/g, " ")
|
|
33
|
+
.trim()
|
|
34
|
+
.split(" ")
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
let id =
|
|
37
|
+
parts
|
|
38
|
+
.map((p, i) => (lower && i === 0 ? p[0].toLowerCase() : p[0].toUpperCase()) + p.slice(1))
|
|
39
|
+
.join("") || "Step";
|
|
40
|
+
if (/^[0-9]/.test(id)) id = `${lower ? "r" : "S"}${id}`;
|
|
41
|
+
let unique = id;
|
|
42
|
+
for (let n = 2; taken.has(unique); n++) unique = `${id}${n}`;
|
|
43
|
+
taken.add(unique);
|
|
44
|
+
return unique;
|
|
45
|
+
}
|
|
46
|
+
const str = (s) => `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
47
|
+
const set = (xs) => `{${[...xs].map(str).join(", ")}}`;
|
|
48
|
+
|
|
49
|
+
const vars = Object.entries(regions).map(([name, r]) => {
|
|
50
|
+
if (r.states.size === 0) throw new Error(`region ${name} has no states`);
|
|
51
|
+
if (r.initial.size === 0) throw new Error(`region ${name} has no [*] --> start`);
|
|
52
|
+
return { name, v: ident(name, true), ...r };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function guardOf(g) {
|
|
56
|
+
if (!g) return null;
|
|
57
|
+
const parts = Object.entries(g.states).map(([name, states]) => {
|
|
58
|
+
const { v } = vars.find((x) => x.name === name);
|
|
59
|
+
return states.length === 1 ? `${v} = ${str(states[0])}` : `${v} \\in ${set(states)}`;
|
|
60
|
+
});
|
|
61
|
+
return g.not ? `~(${parts.join(" \\/ ")})` : parts.join(" /\\ ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const actions = [];
|
|
65
|
+
const byLabel = new Map();
|
|
66
|
+
for (const [ri, r] of vars.entries()) {
|
|
67
|
+
for (const t of r.transitions) {
|
|
68
|
+
let a = t.label ? byLabel.get(t.label) : null;
|
|
69
|
+
if (!a) {
|
|
70
|
+
a = { label: t.label, moves: new Map() };
|
|
71
|
+
actions.push(a);
|
|
72
|
+
if (t.label) byLabel.set(t.label, a);
|
|
73
|
+
}
|
|
74
|
+
if (!a.moves.has(ri)) a.moves.set(ri, []);
|
|
75
|
+
const moves = a.moves.get(ri);
|
|
76
|
+
const when = guardOf(t.guard);
|
|
77
|
+
if (!moves.some((m) => m.from === t.from && m.to === t.to && m.when === when))
|
|
78
|
+
moves.push({ from: t.from, to: t.to, when });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const a of actions) {
|
|
82
|
+
const only = a.label ? null : [...a.moves.values()][0][0];
|
|
83
|
+
a.name = ident(a.label || `${only.from} to ${only.to}`, false);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const out = [];
|
|
87
|
+
const conjuncts = (lines) => lines.map((l) => ` /\\ ${l}`);
|
|
88
|
+
out.push(`---- MODULE ${args.module} ----`);
|
|
89
|
+
out.push(`\\* Generated by chart-to-tla.mjs from ${path.basename(args.chart)}.`);
|
|
90
|
+
out.push("\\* Each region is one variable and each transition label is one action.");
|
|
91
|
+
out.push("\\* Once you add guards and variables from the real code, this file is the source.");
|
|
92
|
+
out.push(
|
|
93
|
+
`\\* statechart.mjs from-chart ${path.basename(args.chart)} keeps it, checks it, and compares it with the chart.`,
|
|
94
|
+
);
|
|
95
|
+
out.push("");
|
|
96
|
+
out.push(`VARIABLES ${vars.map((x) => x.v).join(", ")}`);
|
|
97
|
+
out.push(`vars == <<${vars.map((x) => x.v).join(", ")}>>`);
|
|
98
|
+
out.push("");
|
|
99
|
+
for (const x of vars)
|
|
100
|
+
out.push(`\\* Region ${x.name}`, `${x.v}States == ${set([...x.states].sort())}`);
|
|
101
|
+
out.push("");
|
|
102
|
+
out.push("TypeOK ==", ...conjuncts(vars.map((x) => `${x.v} \\in ${x.v}States`)));
|
|
103
|
+
out.push("");
|
|
104
|
+
out.push(
|
|
105
|
+
"Init ==",
|
|
106
|
+
...conjuncts(
|
|
107
|
+
vars.map((x) =>
|
|
108
|
+
x.initial.size === 1 ? `${x.v} = ${str([...x.initial][0])}` : `${x.v} \\in ${set(x.initial)}`,
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
const enabled = (v, m) => `${v} = ${str(m.from)}${m.when ? ` /\\ ${m.when}` : ""}`;
|
|
113
|
+
function whetherTakes(a, ri) {
|
|
114
|
+
const { v } = vars[ri];
|
|
115
|
+
const moves = a.moves.get(ri);
|
|
116
|
+
if (moves.every((m) => !m.when)) {
|
|
117
|
+
const from = set(new Set(moves.map((m) => m.from)));
|
|
118
|
+
return { can: `${v} \\in ${from}`, cannot: `${v} \\notin ${from}` };
|
|
119
|
+
}
|
|
120
|
+
const any = moves.map((m) => `(${enabled(v, m)})`).join(" \\/ ");
|
|
121
|
+
return { can: any, cannot: `~(${any})` };
|
|
122
|
+
}
|
|
123
|
+
for (const a of actions) {
|
|
124
|
+
out.push("");
|
|
125
|
+
if (a.label) out.push(`\\* ${a.label}`);
|
|
126
|
+
out.push(`${a.name} ==`);
|
|
127
|
+
const moving = [...a.moves.keys()];
|
|
128
|
+
const shared = moving.length > 1;
|
|
129
|
+
if (shared) out.push(` /\\ (${moving.map((ri) => whetherTakes(a, ri).can).join(" \\/ ")})`);
|
|
130
|
+
for (const ri of moving) {
|
|
131
|
+
const { v } = vars[ri];
|
|
132
|
+
const ds = a.moves.get(ri).map((m) => `(${enabled(v, m)} /\\ ${v}' = ${str(m.to)})`);
|
|
133
|
+
if (shared) ds.push(`(${whetherTakes(a, ri).cannot} /\\ UNCHANGED ${v})`);
|
|
134
|
+
out.push(` /\\ \\/ ${ds[0]}`);
|
|
135
|
+
for (const d of ds.slice(1)) out.push(` \\/ ${d}`);
|
|
136
|
+
}
|
|
137
|
+
const still = vars.filter((_, ri) => !a.moves.has(ri)).map((x) => x.v);
|
|
138
|
+
if (still.length) out.push(` /\\ UNCHANGED <<${still.join(", ")}>>`);
|
|
139
|
+
}
|
|
140
|
+
out.push("");
|
|
141
|
+
out.push(actions.length ? `Next == ${actions.map((a) => a.name).join(" \\/ ")}` : "Next == FALSE");
|
|
142
|
+
out.push("");
|
|
143
|
+
out.push("Spec == Init /\\ [][Next]_vars");
|
|
144
|
+
|
|
145
|
+
function resolve(item) {
|
|
146
|
+
const hits = [];
|
|
147
|
+
for (const [ri, x] of vars.entries()) {
|
|
148
|
+
const inside = x.states.has(item)
|
|
149
|
+
? [item]
|
|
150
|
+
: [...x.states].filter((s) => s.startsWith(`${item}.`));
|
|
151
|
+
if (inside.length) hits.push({ ri, inside });
|
|
152
|
+
}
|
|
153
|
+
if (hits.length === 0)
|
|
154
|
+
throw new Error(`never line names "${item}", which is not a state in the chart`);
|
|
155
|
+
if (hits.length > 1)
|
|
156
|
+
throw new Error(
|
|
157
|
+
`never line names "${item}", which is a state in regions ${hits.map((h) => vars[h.ri].name).join(" and ")}; rename one`,
|
|
158
|
+
);
|
|
159
|
+
return hits[0];
|
|
160
|
+
}
|
|
161
|
+
const invariants = [];
|
|
162
|
+
for (const n of nevers) {
|
|
163
|
+
if (taken.has(n.name)) throw new Error(`never name ${n.name} is already used in the spec`);
|
|
164
|
+
taken.add(n.name);
|
|
165
|
+
const parts = n.states.map(resolve);
|
|
166
|
+
const regionsUsed = new Set(parts.map((p) => p.ri));
|
|
167
|
+
if (regionsUsed.size !== parts.length)
|
|
168
|
+
throw new Error(
|
|
169
|
+
`never ${n.name} names two states of one region; one region is in one state at a time`,
|
|
170
|
+
);
|
|
171
|
+
const cond = parts.map((p) =>
|
|
172
|
+
p.inside.length === 1
|
|
173
|
+
? `${vars[p.ri].v} = ${str(p.inside[0])}`
|
|
174
|
+
: `${vars[p.ri].v} \\in ${set(p.inside)}`,
|
|
175
|
+
);
|
|
176
|
+
out.push("");
|
|
177
|
+
if (n.sentence) out.push(`\\* ${n.sentence}`);
|
|
178
|
+
out.push(`${n.name} == ~(${cond.join(" /\\ ")})`);
|
|
179
|
+
invariants.push(n.name);
|
|
180
|
+
}
|
|
181
|
+
out.push("", "====", "");
|
|
182
|
+
|
|
183
|
+
const rules = ["TypeOK", ...invariants];
|
|
184
|
+
const cfg = ["SPECIFICATION Spec", ...rules.map((r) => `INVARIANT ${r}`), ""];
|
|
185
|
+
|
|
186
|
+
const labels = Object.fromEntries(actions.map((a) => [a.name, a.label]));
|
|
187
|
+
const map = [
|
|
188
|
+
`// Generated by chart-to-tla.mjs from ${path.basename(args.chart)}. Reader states are the chart's own states.`,
|
|
189
|
+
`export const regions = ${JSON.stringify(vars.map((x) => x.v))};`,
|
|
190
|
+
`const LABELS = ${JSON.stringify(labels, null, "\t")};`,
|
|
191
|
+
"",
|
|
192
|
+
"// The actions the reader takes. facts.settled needs them. Add cause() and detail() as the spec grows.",
|
|
193
|
+
"export const readerActions = [];",
|
|
194
|
+
'export const state = (v) => regions.map((r) => v[r]).join(" | ");',
|
|
195
|
+
"export const step = (action) => LABELS[action] ?? action;",
|
|
196
|
+
"",
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
200
|
+
fs.writeFileSync(path.join(dir, `${args.module}.tla`), out.join("\n"));
|
|
201
|
+
fs.writeFileSync(path.join(dir, `${args.module}.cfg`), cfg.join("\n"));
|
|
202
|
+
fs.writeFileSync(path.join(dir, `${args.module}.map.mjs`), map.join("\n"));
|
|
203
|
+
console.error(
|
|
204
|
+
`${args.module}: ${vars.length} region variable(s), ${actions.length} action(s), rules ${rules.join(" ")} -> ${path.join(dir, args.module)}.{tla,cfg,map.mjs}`,
|
|
205
|
+
);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { FULL_LABELS, LEGEND_ID, NO_LOOPS, compareCharts, parseMermaid } from "./lib.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE = "Usage: node compare-charts.mjs EXPECTED.mmd ACTUAL.mmd";
|
|
7
|
+
|
|
8
|
+
const files = [];
|
|
9
|
+
for (const a of process.argv.slice(2)) {
|
|
10
|
+
if (a.startsWith("-") || files.length === 2) {
|
|
11
|
+
console.error(`unknown argument ${a}.`);
|
|
12
|
+
process.exit(2);
|
|
13
|
+
} else files.push(a);
|
|
14
|
+
}
|
|
15
|
+
if (files.length < 2) {
|
|
16
|
+
console.error(USAGE);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
const [expectedFile, actualFile] = files;
|
|
20
|
+
|
|
21
|
+
const orExit = (file, f) => {
|
|
22
|
+
try {
|
|
23
|
+
return f();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(`${file}: ${e.message}`);
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const [expectedText, actualText] = files.map((f) => orExit(f, () => fs.readFileSync(f, "utf8")));
|
|
30
|
+
const has = (re) => re.test(actualText);
|
|
31
|
+
const textRefusals = [
|
|
32
|
+
[
|
|
33
|
+
has(/^\s*%% second model/m),
|
|
34
|
+
"it has a second model; draw ACTUAL without --fixed-dot or --against-dot",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
has(new RegExp(`^\\s*state\\s+"[^"]*"\\s+as\\s+(?!${LEGEND_ID}\\b)\\w+\\s*\\{`, "m")),
|
|
38
|
+
"it has boxes; draw ACTUAL with --flat",
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
has(new RegExp(`^\\s*state\\s+"[^"]*"\\s+as\\s+${LEGEND_ID}\\s*\\{`, "m")) &&
|
|
42
|
+
!actualText.includes(FULL_LABELS),
|
|
43
|
+
"edge labels write shared words once; draw ACTUAL with --full-labels",
|
|
44
|
+
],
|
|
45
|
+
];
|
|
46
|
+
for (const [bad, why] of textRefusals) {
|
|
47
|
+
if (bad) {
|
|
48
|
+
console.error(`${actualFile}: ${why}`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const expectedRegions = orExit(expectedFile, () => parseMermaid(expectedText).regions);
|
|
53
|
+
const actualRegions = orExit(actualFile, () => parseMermaid(actualText).regions);
|
|
54
|
+
const numbered = Object.values(actualRegions)
|
|
55
|
+
.flatMap((r) => r.transitions)
|
|
56
|
+
.some((t) => /^\d+(, \d+)*\. /.test(t.label));
|
|
57
|
+
if (numbered) {
|
|
58
|
+
console.error(
|
|
59
|
+
`${actualFile}: an edge label is a numbered path step; draw ACTUAL with --highlight 0`,
|
|
60
|
+
);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rows = compareCharts(expectedRegions, actualRegions, {
|
|
65
|
+
loops: !actualText.includes(NO_LOOPS),
|
|
66
|
+
});
|
|
67
|
+
let differences = 0;
|
|
68
|
+
for (const [title, xs] of rows) {
|
|
69
|
+
console.log(`${title}:`);
|
|
70
|
+
for (const x of xs) console.log(` ${x}`);
|
|
71
|
+
differences += xs.length;
|
|
72
|
+
}
|
|
73
|
+
console.log(differences === 0 ? "same" : `${differences} difference(s)`);
|
|
74
|
+
process.exit(differences === 0 ? 0 : 1);
|