@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,92 @@
|
|
|
1
|
+
import { isRecord, isTuple } from "./lib.mjs";
|
|
2
|
+
|
|
3
|
+
export function mapView(value, at = "v") {
|
|
4
|
+
if (isRecord(value))
|
|
5
|
+
return Object.fromEntries(Object.entries(value).map(([k, x]) => [k, mapView(x, `${at}.${k}`)]));
|
|
6
|
+
if (!Array.isArray(value)) return value;
|
|
7
|
+
if (isTuple(value))
|
|
8
|
+
return tupleView(
|
|
9
|
+
value.map((x, i) => mapView(x, `${at}[${i + 1}]`)),
|
|
10
|
+
at,
|
|
11
|
+
);
|
|
12
|
+
return arrayView(value.map((x, i) => mapView(x, `${at}[${i}]`)));
|
|
13
|
+
}
|
|
14
|
+
const arrayView = (items) =>
|
|
15
|
+
Object.defineProperties(items, {
|
|
16
|
+
flat: { value: (depth = 1) => flatten(items, depth) },
|
|
17
|
+
flatMap: { value: (f, self) => flatten(items.map(f, self), 1) },
|
|
18
|
+
});
|
|
19
|
+
const views = new WeakMap();
|
|
20
|
+
const itemAt = (k) => (typeof k === "string" && /^[1-9]\d*$/.test(k) ? k - 1 : -1);
|
|
21
|
+
const flatten = (xs, depth) =>
|
|
22
|
+
Array.prototype.flatMap.call(xs, (x) =>
|
|
23
|
+
depth >= 1 && Array.isArray(x) ? flatten(views.get(x)?.items ?? x, depth - 1) : [x],
|
|
24
|
+
);
|
|
25
|
+
function tupleView(items, at) {
|
|
26
|
+
const view = new Proxy(items, {
|
|
27
|
+
get(_, k) {
|
|
28
|
+
if (k === "0")
|
|
29
|
+
throw new Error(
|
|
30
|
+
`the mapping reads ${at}[0], but a tuple reads from 1, as in TLA+. Read ${at}[1] for its first item.`,
|
|
31
|
+
);
|
|
32
|
+
const i = itemAt(k);
|
|
33
|
+
if (i >= 0) return items[i];
|
|
34
|
+
if (k === "toJSON") return () => items;
|
|
35
|
+
if (k === "flat") return (depth = 1) => flatten(items, depth);
|
|
36
|
+
if (k === "flatMap") return (f, self) => flatten(items.map(f, self), 1);
|
|
37
|
+
const x = Reflect.get(items, k);
|
|
38
|
+
if (typeof x !== "function") return x;
|
|
39
|
+
return x.bind(x === Object.prototype[k] ? view : items);
|
|
40
|
+
},
|
|
41
|
+
set(_, k, x) {
|
|
42
|
+
if (k === "0")
|
|
43
|
+
throw new Error(
|
|
44
|
+
`the mapping writes ${at}[0], but a tuple reads from 1, as in TLA+. Write ${at}[1] for its first item.`,
|
|
45
|
+
);
|
|
46
|
+
const i = itemAt(k);
|
|
47
|
+
if (i < 0) return Reflect.set(items, k, x);
|
|
48
|
+
items[i] = x;
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
has(_, k) {
|
|
52
|
+
if (k === "0")
|
|
53
|
+
throw new Error(
|
|
54
|
+
`the mapping looks for index 0 in ${at}, with "in" or with an array method such as concat or flat, but a tuple reads from 1, as in TLA+. Use [...${at}] for an array that reads from 0.`,
|
|
55
|
+
);
|
|
56
|
+
const i = itemAt(k);
|
|
57
|
+
return i < 0 ? Reflect.has(items, k) : i in items;
|
|
58
|
+
},
|
|
59
|
+
ownKeys: () =>
|
|
60
|
+
Reflect.ownKeys(items).map((k) =>
|
|
61
|
+
typeof k === "string" && /^(0|[1-9]\d*)$/.test(k) ? String(+k + 1) : k,
|
|
62
|
+
),
|
|
63
|
+
getOwnPropertyDescriptor(_, k) {
|
|
64
|
+
if (k === "0") return undefined;
|
|
65
|
+
const i = itemAt(k);
|
|
66
|
+
return Reflect.getOwnPropertyDescriptor(items, i < 0 ? k : i);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
views.set(view, { items, at });
|
|
70
|
+
return view;
|
|
71
|
+
}
|
|
72
|
+
const nativeClone = globalThis.structuredClone;
|
|
73
|
+
function cloneView(x, seen) {
|
|
74
|
+
if (x === null || typeof x !== "object") return nativeClone(x);
|
|
75
|
+
if (seen.has(x)) return seen.get(x);
|
|
76
|
+
const view = views.get(x);
|
|
77
|
+
if (!view && !Array.isArray(x) && Object.getPrototypeOf(x) !== Object.prototype)
|
|
78
|
+
return nativeClone(x);
|
|
79
|
+
const out = view || Array.isArray(x) ? [] : {};
|
|
80
|
+
const copy = view ? tupleView(out, view.at) : Array.isArray(x) ? arrayView(out) : out;
|
|
81
|
+
seen.set(x, copy);
|
|
82
|
+
for (const [k, item] of Object.entries(view?.items ?? x)) out[k] = cloneView(item, seen);
|
|
83
|
+
return copy;
|
|
84
|
+
}
|
|
85
|
+
globalThis.structuredClone = (value, options) => {
|
|
86
|
+
try {
|
|
87
|
+
return nativeClone(value, options);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (e?.name !== "DataCloneError") throw e;
|
|
90
|
+
return cloneView(value, new Map());
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { joinAnd, parseDot, parseState, stateKey } from "./lib.mjs";
|
|
3
|
+
import { mapView } from "./map-view.mjs";
|
|
4
|
+
|
|
5
|
+
const TEMPORAL_BREAK = "Error: Temporal properties were violated.";
|
|
6
|
+
|
|
7
|
+
function lasso(tlcOutput) {
|
|
8
|
+
const start = tlcOutput.lastIndexOf(TEMPORAL_BREAK);
|
|
9
|
+
if (start < 0) return null;
|
|
10
|
+
const lines = tlcOutput.slice(start).split("\n");
|
|
11
|
+
const states = [];
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const back = /^Back to state (\d+): /.exec(lines[i]);
|
|
14
|
+
if (back) return { loop: states.slice(Number(back[1]) - 1), stutters: false };
|
|
15
|
+
const head = /^State \d+: (<|Stuttering)/.exec(lines[i]);
|
|
16
|
+
if (!head) continue;
|
|
17
|
+
if (head[1] === "Stuttering") return { loop: states.slice(-1), stutters: true };
|
|
18
|
+
const body = [];
|
|
19
|
+
while (i + 1 < lines.length && lines[i + 1].trim() !== "") body.push(lines[++i]);
|
|
20
|
+
states.push(parseState(body.join("\n")));
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const collapseRepeatsAroundLoop = (names) => {
|
|
26
|
+
const runs = names.filter((name, i) => name !== names[i - 1]);
|
|
27
|
+
return runs.length > 1 && runs.at(-1) === runs[0] ? runs.slice(0, -1) : runs;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function repeatsIn(tlcOutput, nameOf) {
|
|
31
|
+
const found = lasso(tlcOutput);
|
|
32
|
+
return (
|
|
33
|
+
found && { states: collapseRepeatsAroundLoop(found.loop.map(nameOf)), stutters: found.stutters }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function enabledByState({ nodes, edges, loops }) {
|
|
38
|
+
const byId = new Map([...nodes.keys()].map((id) => [id, new Set()]));
|
|
39
|
+
for (const e of [...edges, ...loops]) byId.get(e.from).add(e.action);
|
|
40
|
+
return new Map([...nodes.values()].map((n) => [stateKey(n.vars), byId.get(n.id)]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readerStateNamer(mapping, dotFile) {
|
|
44
|
+
const readerActions = new Set(mapping.readerActions ?? []);
|
|
45
|
+
let enabledAt;
|
|
46
|
+
return (vars) => {
|
|
47
|
+
enabledAt ??= enabledByState(parseDot(fs.readFileSync(dotFile, "utf8")));
|
|
48
|
+
const enabled = enabledAt.get(stateKey(vars)) ?? new Set();
|
|
49
|
+
const settled = [...enabled].every((a) => readerActions.has(a));
|
|
50
|
+
return String(mapping.state(mapView(vars), { enabled, settled }));
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function loopSentence(repeats) {
|
|
55
|
+
if (!repeats) return null;
|
|
56
|
+
const quoted = repeats.states.map((name) => `"${name}"`);
|
|
57
|
+
if (repeats.stutters) return `The run stops in ${quoted[0]} forever.`;
|
|
58
|
+
if (quoted.length === 1) return `The run stays in ${quoted[0]} forever.`;
|
|
59
|
+
return `The run loops through ${joinAnd(quoted)} forever.`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRecord } from "./lib.mjs";
|
|
2
|
+
import { SEP } from "./chart-parts.mjs";
|
|
3
|
+
|
|
4
|
+
const kindOf = (v) => (isRecord(v) ? "record" : Array.isArray(v) ? "list" : typeof v);
|
|
5
|
+
function shapeOf(graph, traces) {
|
|
6
|
+
const shape = new Map();
|
|
7
|
+
const walk = (o, path) => {
|
|
8
|
+
for (const [k, v] of Object.entries(o)) {
|
|
9
|
+
const p = [...path, k];
|
|
10
|
+
const id = p.join(SEP);
|
|
11
|
+
if (!shape.has(id)) shape.set(id, { path: p, kinds: new Set() });
|
|
12
|
+
shape.get(id).kinds.add(kindOf(v));
|
|
13
|
+
if (isRecord(v)) walk(v, p);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
for (const n of graph.nodes.values()) walk(n.vars, []);
|
|
17
|
+
for (const t of traces) for (const s of t.steps) walk(s.vars, []);
|
|
18
|
+
return shape;
|
|
19
|
+
}
|
|
20
|
+
const disjoint = (a, b) => ![...b.kinds].some((k) => a.kinds.has(k));
|
|
21
|
+
const foreign = (a, b) => [...b.kinds].some((k) => !a.kinds.has(k));
|
|
22
|
+
function lacking(a, b) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const [id, x] of a) {
|
|
25
|
+
const within = out.some((p) => id.startsWith(`${p.join(SEP)}${SEP}`));
|
|
26
|
+
if (!within && (!b.has(id) || disjoint(x, b.get(id)))) out.push(x.path);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
const unreadOf = (a, b) =>
|
|
31
|
+
[...a].flatMap(([id, x]) =>
|
|
32
|
+
!b.has(id) || foreign(x, b.get(id)) ? [{ ...x, somewhere: b.has(id) }] : [],
|
|
33
|
+
);
|
|
34
|
+
function guard(vars, unread, label, at = "v") {
|
|
35
|
+
if (!unread.length || !isRecord(vars)) return vars;
|
|
36
|
+
const here = new Map();
|
|
37
|
+
const deeper = new Map();
|
|
38
|
+
for (const { path, ...x } of unread) {
|
|
39
|
+
const [k, ...rest] = path;
|
|
40
|
+
if (!rest.length) here.set(k, x);
|
|
41
|
+
else deeper.set(k, [...(deeper.get(k) ?? []), { ...x, path: rest }]);
|
|
42
|
+
}
|
|
43
|
+
const check = (target, k, listing) => {
|
|
44
|
+
const x = here.get(k);
|
|
45
|
+
const absent = !Object.hasOwn(target, k);
|
|
46
|
+
if (!x || (absent ? x.somewhere : x.kinds.has(kindOf(target[k])))) return;
|
|
47
|
+
const name = `${at}.${k}`.slice(2);
|
|
48
|
+
const has = absent ? `no ${name}` : `${name} as a value of another type`;
|
|
49
|
+
const does = listing ? `lists the fields of ${at}` : `reads ${at}.${k}`;
|
|
50
|
+
const hint = listing
|
|
51
|
+
? ", and read the fields it needs by name"
|
|
52
|
+
: absent
|
|
53
|
+
? `, or test "${k}" in ${at} first`
|
|
54
|
+
: "";
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${label} has ${has}, and the mapping ${does} on its states. Label states and steps only from what both models have${hint}.`,
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
return new Proxy(vars, {
|
|
60
|
+
get(target, k) {
|
|
61
|
+
check(target, k, false);
|
|
62
|
+
return deeper.has(k) ? guard(target[k], deeper.get(k), label, `${at}.${k}`) : target[k];
|
|
63
|
+
},
|
|
64
|
+
ownKeys(target) {
|
|
65
|
+
for (const k of here.keys()) check(target, k, true);
|
|
66
|
+
return Reflect.ownKeys(target);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readCheck({ graph, rawTraces, secondGraph, rawSecondTraces, L }) {
|
|
72
|
+
const [a, b] = [shapeOf(graph, rawTraces), shapeOf(secondGraph, rawSecondTraces)];
|
|
73
|
+
const unread = unreadOf(a, b);
|
|
74
|
+
return {
|
|
75
|
+
lacks: lacking(a, b),
|
|
76
|
+
extra: lacking(b, a),
|
|
77
|
+
guard: (vars) => guard(vars, unread, L),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
NO_LOOPS,
|
|
4
|
+
compareCharts,
|
|
5
|
+
escapeMermaidQuotes,
|
|
6
|
+
parseMermaid,
|
|
7
|
+
productOf,
|
|
8
|
+
wrap,
|
|
9
|
+
} from "./lib.mjs";
|
|
10
|
+
import { SEP, addGuard, frontMatter, guarded } from "./chart-parts.mjs";
|
|
11
|
+
import { RANK, diverges, worst } from "./bug-states.mjs";
|
|
12
|
+
import { edgesAlong } from "./chart-edges.mjs";
|
|
13
|
+
|
|
14
|
+
export function regionsChart({
|
|
15
|
+
states,
|
|
16
|
+
starts,
|
|
17
|
+
edges,
|
|
18
|
+
loops,
|
|
19
|
+
classes,
|
|
20
|
+
proofPath,
|
|
21
|
+
allPath,
|
|
22
|
+
mapping,
|
|
23
|
+
status,
|
|
24
|
+
LEGEND,
|
|
25
|
+
groupOf,
|
|
26
|
+
drawnState,
|
|
27
|
+
groupCounts,
|
|
28
|
+
head,
|
|
29
|
+
}) {
|
|
30
|
+
const norm = (s) => s.replaceAll(";", ",").replace(/\s+/g, " ").trim();
|
|
31
|
+
const split = (k) => {
|
|
32
|
+
const at = k.indexOf(": ");
|
|
33
|
+
return at > 0 && at + 2 < k.length ? [norm(k.slice(0, at)), norm(k.slice(at + 2))] : null;
|
|
34
|
+
};
|
|
35
|
+
const nodes = [...states].sort();
|
|
36
|
+
if (!nodes.every(split)) return null;
|
|
37
|
+
const steps = [];
|
|
38
|
+
for (const e of edges.values()) {
|
|
39
|
+
if (e.labels.size === 0) return { why: "a step has no label" };
|
|
40
|
+
for (const l of e.labels)
|
|
41
|
+
steps.push({
|
|
42
|
+
from: split(e.from),
|
|
43
|
+
to: split(e.to),
|
|
44
|
+
label: norm(l),
|
|
45
|
+
words: e.guards?.get(l) ?? new Set([""]),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const bar = (p) => p.join(" | ");
|
|
49
|
+
const flat = {
|
|
50
|
+
Main: {
|
|
51
|
+
states: new Set(nodes.map((k) => bar(split(k)))),
|
|
52
|
+
initial: new Set(starts.map((k) => bar(split(k)))),
|
|
53
|
+
transitions: steps.map((s) => ({ from: bar(s.from), to: bar(s.to), label: s.label })),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const ids = new Map([...flat.Main.states].map((s, i) => [s, `p${i}`]));
|
|
57
|
+
const proof = [
|
|
58
|
+
...frontMatter(),
|
|
59
|
+
"stateDiagram-v2",
|
|
60
|
+
...(loops ? [] : [` ${NO_LOOPS}`]),
|
|
61
|
+
...[...ids].map(([s, id]) => ` state "${escapeMermaidQuotes(s)}" as ${id}`),
|
|
62
|
+
...[...flat.Main.initial].map((s) => ` [*] --> ${ids.get(s)}`),
|
|
63
|
+
...steps.map((s) => ` ${ids.get(bar(s.from))} --> ${ids.get(bar(s.to))} : ${s.label}`),
|
|
64
|
+
];
|
|
65
|
+
if (proofPath) fs.writeFileSync(proofPath, proof.join("\n") + "\n");
|
|
66
|
+
|
|
67
|
+
const beside = [new Map(), new Map()];
|
|
68
|
+
for (const k of nodes) {
|
|
69
|
+
const p = split(k);
|
|
70
|
+
for (const i of [0, 1]) {
|
|
71
|
+
if (!beside[i].has(p[i])) beside[i].set(p[i], new Set());
|
|
72
|
+
beside[i].get(p[i]).add(p[1 - i]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const size = beside[0].size + beside[1].size;
|
|
76
|
+
if (size >= nodes.length)
|
|
77
|
+
return { why: `${size} region states are not fewer than the ${nodes.length} flat states` };
|
|
78
|
+
|
|
79
|
+
const regionStatus = [new Map(), new Map()];
|
|
80
|
+
for (const k of nodes) {
|
|
81
|
+
const p = split(k);
|
|
82
|
+
for (const i of [0, 1]) {
|
|
83
|
+
const prev = regionStatus[i].get(p[i]);
|
|
84
|
+
if (!prev || RANK.indexOf(status(k)) < RANK.indexOf(prev))
|
|
85
|
+
regionStatus[i].set(p[i], status(k));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const k of nodes) {
|
|
89
|
+
const p = split(k);
|
|
90
|
+
const shown = worst(...[0, 1].map((i) => regionStatus[i].get(p[i])));
|
|
91
|
+
if (shown !== status(k))
|
|
92
|
+
return {
|
|
93
|
+
why: `"${k}" is "${LEGEND[status(k)][1]}", and its two region states would show "${LEGEND[shown][1]}"`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const moves = [new Map(), new Map()];
|
|
98
|
+
for (const s of steps)
|
|
99
|
+
for (const i of [0, 1]) {
|
|
100
|
+
const id = [s.from[i], s.label, s.to[i]].join(SEP);
|
|
101
|
+
if (!moves[i].has(id))
|
|
102
|
+
moves[i].set(id, {
|
|
103
|
+
from: s.from[i],
|
|
104
|
+
label: s.label,
|
|
105
|
+
to: s.to[i],
|
|
106
|
+
beside: new Set(),
|
|
107
|
+
guards: new Map(),
|
|
108
|
+
});
|
|
109
|
+
moves[i].get(id).beside.add(s.from[1 - i]);
|
|
110
|
+
for (const w of s.words) addGuard(moves[i].get(id).guards, s.label, norm(w));
|
|
111
|
+
}
|
|
112
|
+
for (const m of moves)
|
|
113
|
+
for (const [id, x] of m)
|
|
114
|
+
if (
|
|
115
|
+
x.from === x.to &&
|
|
116
|
+
![...m.values()].some((y) => y.from === x.from && y.label === x.label && y.to !== x.from)
|
|
117
|
+
)
|
|
118
|
+
m.delete(id);
|
|
119
|
+
const rid = beside.map((m, i) => new Map([...m.keys()].sort().map((s, j) => [s, `r${i}s${j}`])));
|
|
120
|
+
const regionGroup = [new Map(), new Map()];
|
|
121
|
+
for (const k of nodes) {
|
|
122
|
+
const p = split(k);
|
|
123
|
+
for (const i of [0, 1]) {
|
|
124
|
+
const prev = regionGroup[i].get(p[i]);
|
|
125
|
+
regionGroup[i].set(p[i], prev === undefined || prev === groupOf(k) ? groupOf(k) : "");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const drawn = rid.map((m, i) =>
|
|
129
|
+
[...m.keys()].map((s) => drawnState(regionGroup[i].get(s), regionStatus[i].get(s))),
|
|
130
|
+
);
|
|
131
|
+
const draw = (only) => {
|
|
132
|
+
const out = head(drawn.flat());
|
|
133
|
+
out.push(` state "At the same time" as regions {`);
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const i of [0, 1]) {
|
|
136
|
+
if (i) out.push(" --");
|
|
137
|
+
const name = mapping.regionNames?.[i];
|
|
138
|
+
const pad = name ? " " : " ";
|
|
139
|
+
if (name) out.push(` state "${escapeMermaidQuotes(wrap(name))}" as region${i} {`);
|
|
140
|
+
for (const s of new Set(starts.map((k) => split(k)[i])))
|
|
141
|
+
out.push(`${pad}[*] --> ${rid[i].get(s)}`);
|
|
142
|
+
for (const [s, id] of rid[i])
|
|
143
|
+
out.push(`${pad}state "${escapeMermaidQuotes(wrap(s))}" as ${id}`);
|
|
144
|
+
const shownMoves = [...moves[i]]
|
|
145
|
+
.filter(([id]) => !only || only[i].has(id))
|
|
146
|
+
.map(([, m]) => m)
|
|
147
|
+
.sort((a, b) => a.from.localeCompare(b.from) || a.label.localeCompare(b.label));
|
|
148
|
+
for (const m of shownMoves) {
|
|
149
|
+
const all = beside[i].get(m.from);
|
|
150
|
+
const not = [...all].filter((s) => !m.beside.has(s)).sort();
|
|
151
|
+
const yes = [...m.beside].sort();
|
|
152
|
+
const unless = not.length > 0 && not.length < yes.length;
|
|
153
|
+
const guard = not.length === 0 ? null : unless ? not : yes;
|
|
154
|
+
const worded = guarded(m.guards, m.label);
|
|
155
|
+
const text =
|
|
156
|
+
worded !== m.label || !guard
|
|
157
|
+
? worded
|
|
158
|
+
: `${m.label} [${unless ? "unless" : "when"} ${guard.join(" or ")}]`;
|
|
159
|
+
out.push(`${pad}${rid[i].get(m.from)} --> ${rid[i].get(m.to)} : ${wrap(text, 30)}`);
|
|
160
|
+
if (guard)
|
|
161
|
+
out.push(
|
|
162
|
+
`${pad}%% when ${unless ? "not " : ""}${guard.map((s) => rid[1 - i].get(s)).join(", ")}`,
|
|
163
|
+
);
|
|
164
|
+
count++;
|
|
165
|
+
}
|
|
166
|
+
if (name) out.push(" }");
|
|
167
|
+
}
|
|
168
|
+
out.push(" }");
|
|
169
|
+
for (const i of [0, 1])
|
|
170
|
+
[...rid[i].values()].forEach((id, j) => out.push(` class ${id} ${drawn[i][j].cls}`));
|
|
171
|
+
return { mermaid: out.join("\n") + "\n", count };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const all = draw(null);
|
|
175
|
+
const differences = compareCharts(parseMermaid(all.mermaid).regions, flat, { loops });
|
|
176
|
+
if (differences.length) {
|
|
177
|
+
const n = differences.reduce((sum, [, xs]) => sum + xs.length, 0);
|
|
178
|
+
return {
|
|
179
|
+
why: `the regions do not combine into the flat chart, with ${n} difference(s), such as ${differences[0][0]}: ${differences[0][1][0]}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (allPath) fs.writeFileSync(allPath, all.mermaid);
|
|
183
|
+
|
|
184
|
+
const parts = new Map(nodes.map((k) => [bar(split(k)), split(k)]));
|
|
185
|
+
const key = (from, label, to) => `${from} --> ${to} : ${label}`;
|
|
186
|
+
const real = new Set(steps.map((s) => key(bar(s.from), s.label, bar(s.to))));
|
|
187
|
+
const next = new Map();
|
|
188
|
+
for (const s of steps) {
|
|
189
|
+
if (!next.has(bar(s.from))) next.set(bar(s.from), []);
|
|
190
|
+
next.get(bar(s.from)).push(s);
|
|
191
|
+
}
|
|
192
|
+
const keep = [new Set(), new Set()];
|
|
193
|
+
const movesOf = (s) =>
|
|
194
|
+
[0, 1]
|
|
195
|
+
.filter((i) => s.from[i] !== s.to[i])
|
|
196
|
+
.map((i) => [i, [s.from[i], s.label, s.to[i]].join(SEP)]);
|
|
197
|
+
const price = (s) => movesOf(s).filter(([i, id]) => !keep[i].has(id)).length * nodes.length + 1;
|
|
198
|
+
const wanted = [];
|
|
199
|
+
const want = (s) => {
|
|
200
|
+
wanted.push(s);
|
|
201
|
+
for (const [i, id] of movesOf(s)) keep[i].add(id);
|
|
202
|
+
};
|
|
203
|
+
const stepsOf = (e, said) => {
|
|
204
|
+
const labels = [...e.labels].map(norm);
|
|
205
|
+
return (labels.includes(said) ? [said] : labels).map((label) => ({
|
|
206
|
+
from: split(e.from),
|
|
207
|
+
to: split(e.to),
|
|
208
|
+
label,
|
|
209
|
+
}));
|
|
210
|
+
};
|
|
211
|
+
for (const c of classes) {
|
|
212
|
+
for (const step of edgesAlong(c)) {
|
|
213
|
+
const e = edges.get(step.id);
|
|
214
|
+
if (e) stepsOf(e, norm(step.label)).forEach(want);
|
|
215
|
+
}
|
|
216
|
+
if (diverges(c)) stepsOf(edges.get(c.second.edge), "").forEach(want);
|
|
217
|
+
}
|
|
218
|
+
const cheapest = () => {
|
|
219
|
+
const cost = new Map(starts.map((k) => [bar(split(k)), 0]));
|
|
220
|
+
const parent = new Map();
|
|
221
|
+
const open = new Set(cost.keys());
|
|
222
|
+
while (open.size) {
|
|
223
|
+
const k = [...open].reduce((a, b) => (cost.get(b) < cost.get(a) ? b : a));
|
|
224
|
+
open.delete(k);
|
|
225
|
+
for (const s of next.get(k) ?? []) {
|
|
226
|
+
const c = cost.get(k) + price(s);
|
|
227
|
+
if (c < (cost.get(bar(s.to)) ?? Infinity)) {
|
|
228
|
+
cost.set(bar(s.to), c);
|
|
229
|
+
parent.set(bar(s.to), s);
|
|
230
|
+
open.add(bar(s.to));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { cost, parent };
|
|
235
|
+
};
|
|
236
|
+
const targets = [
|
|
237
|
+
...[0, 1].flatMap((i) =>
|
|
238
|
+
[...beside[i].keys()].sort().map((r) => ({ at: (k) => parts.get(k)[i] === r })),
|
|
239
|
+
),
|
|
240
|
+
...[...new Set(steps.map((s) => s.label))]
|
|
241
|
+
.filter((l) => !wanted.some((s) => s.label === l))
|
|
242
|
+
.sort()
|
|
243
|
+
.map((l) => ({ ends: (s) => s.label === l })),
|
|
244
|
+
];
|
|
245
|
+
while (targets.length) {
|
|
246
|
+
const { cost, parent } = cheapest();
|
|
247
|
+
let best = null;
|
|
248
|
+
targets.forEach((t, n) => {
|
|
249
|
+
const ways = t.at
|
|
250
|
+
? [...cost.keys()].filter(t.at).map((k) => ({ k, c: cost.get(k), last: null }))
|
|
251
|
+
: steps
|
|
252
|
+
.filter(t.ends)
|
|
253
|
+
.filter((s) => cost.has(bar(s.from)))
|
|
254
|
+
.map((s) => ({ k: bar(s.from), c: cost.get(bar(s.from)) + price(s), last: s }));
|
|
255
|
+
for (const w of ways) if (!best || w.c < best.c) best = { ...w, n };
|
|
256
|
+
});
|
|
257
|
+
if (!best) break;
|
|
258
|
+
targets.splice(best.n, 1);
|
|
259
|
+
const path = best.last ? [best.last] : [];
|
|
260
|
+
for (let s = parent.get(best.k); s; s = parent.get(bar(s.from))) path.push(s);
|
|
261
|
+
path.forEach(want);
|
|
262
|
+
}
|
|
263
|
+
const takes = (x, label) =>
|
|
264
|
+
[0, 1].flatMap((i) =>
|
|
265
|
+
[...moves[i]]
|
|
266
|
+
.filter(([, m]) => m.from === x[i] && m.label === label && m.beside.has(x[1 - i]))
|
|
267
|
+
.map(([id]) => [i, id]),
|
|
268
|
+
);
|
|
269
|
+
let scoped;
|
|
270
|
+
let made;
|
|
271
|
+
for (;;) {
|
|
272
|
+
scoped = draw(keep);
|
|
273
|
+
made = productOf(parseMermaid(scoped.mermaid).regions, { loops });
|
|
274
|
+
const madeKeys = new Set(made.transitions.map((t) => key(t.from, t.label, t.to)));
|
|
275
|
+
const redo = [
|
|
276
|
+
...made.transitions
|
|
277
|
+
.filter((t) => parts.has(t.from) && !real.has(key(t.from, t.label, t.to)))
|
|
278
|
+
.map((t) => [parts.get(t.from), t.label]),
|
|
279
|
+
...wanted
|
|
280
|
+
.filter((s) => made.states.has(bar(s.from)))
|
|
281
|
+
.filter((s) => !madeKeys.has(key(bar(s.from), s.label, bar(s.to))))
|
|
282
|
+
.map((s) => [s.from, s.label]),
|
|
283
|
+
];
|
|
284
|
+
const before = keep[0].size + keep[1].size;
|
|
285
|
+
for (const [x, label] of redo) for (const [i, id] of takes(x, label)) keep[i].add(id);
|
|
286
|
+
if (keep[0].size + keep[1].size === before) break;
|
|
287
|
+
}
|
|
288
|
+
const sound =
|
|
289
|
+
[...made.states].every((s) => parts.has(s)) &&
|
|
290
|
+
made.transitions.every((t) => real.has(key(t.from, t.label, t.to)));
|
|
291
|
+
if (!sound) scoped = all;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
mermaid: scoped.mermaid,
|
|
295
|
+
states: [beside[0].size, beside[1].size],
|
|
296
|
+
transitions: scoped.count,
|
|
297
|
+
all: all.count,
|
|
298
|
+
flat: { states: nodes.length, transitions: steps.length },
|
|
299
|
+
status: Object.fromEntries(
|
|
300
|
+
RANK.filter((s) => LEGEND[s]).map((s) => [
|
|
301
|
+
s,
|
|
302
|
+
regionStatus.reduce((n, m) => n + [...m.values()].filter((x) => x === s).length, 0),
|
|
303
|
+
]),
|
|
304
|
+
),
|
|
305
|
+
groups: groupCounts(drawn.flat()),
|
|
306
|
+
};
|
|
307
|
+
}
|