@olierjs/fugue-cli 0.1.0 → 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.
Files changed (40) hide show
  1. package/README.md +52 -2
  2. package/THIRD_PARTY_NOTICES +52 -1
  3. package/dist/darwin-arm64/fugue-cli +0 -0
  4. package/dist/darwin-x64/fugue-cli +0 -0
  5. package/dist/linux-arm64/fugue-cli +0 -0
  6. package/dist/linux-x64/fugue-cli +0 -0
  7. package/dist/win32-arm64/fugue-cli.exe +0 -0
  8. package/dist/win32-x64/fugue-cli.exe +0 -0
  9. package/package.json +2 -1
  10. package/skills/fugue-show-me/SKILL.md +16 -0
  11. package/skills/show-me/SKILL.md +128 -0
  12. package/skills/show-me/agents/openai.yaml +2 -0
  13. package/skills/statechart/SKILL.md +36 -0
  14. package/skills/statechart/scripts/bug-states.mjs +81 -0
  15. package/skills/statechart/scripts/chart-edges.mjs +68 -0
  16. package/skills/statechart/scripts/chart-parts.mjs +21 -0
  17. package/skills/statechart/scripts/chart-to-tla.mjs +205 -0
  18. package/skills/statechart/scripts/compare-charts.mjs +74 -0
  19. package/skills/statechart/scripts/lib.mjs +970 -0
  20. package/skills/statechart/scripts/map-view.mjs +92 -0
  21. package/skills/statechart/scripts/property-loop.mjs +60 -0
  22. package/skills/statechart/scripts/read-check.mjs +79 -0
  23. package/skills/statechart/scripts/regions-chart.mjs +307 -0
  24. package/skills/statechart/scripts/render.mjs +531 -0
  25. package/skills/statechart/scripts/rule-book.mjs +84 -0
  26. package/skills/statechart/scripts/second-replay.mjs +122 -0
  27. package/skills/statechart/scripts/snapshot.mjs +74 -0
  28. package/skills/statechart/scripts/spec-text.mjs +126 -0
  29. package/skills/statechart/scripts/statechart-help.mjs +350 -0
  30. package/skills/statechart/scripts/statechart.mjs +903 -0
  31. package/skills/statechart/scripts/test/band.mmd +15 -0
  32. package/skills/statechart/scripts/test/regions.mmd +62 -0
  33. package/skills/statechart/scripts/test/statechart.test.mjs +6854 -0
  34. package/skills/statechart/scripts/test/sync.mmd +19 -0
  35. package/skills/statechart/scripts/test/upload.mmd +16 -0
  36. package/skills/statechart/scripts/tlc-to-chart-args.mjs +52 -0
  37. package/skills/statechart/scripts/tlc-to-chart.mjs +802 -0
  38. package/skills/statechart/scripts/tlc-tools.mjs +69 -0
  39. package/skills/statechart/scripts/ways-in.mjs +69 -0
  40. package/upstream-skills.json +18 -0
@@ -0,0 +1,122 @@
1
+ import { isRecord, stateKey } from "./lib.mjs";
2
+ import { SEP, edgeId } from "./chart-parts.mjs";
3
+ import { badOf } from "./bug-states.mjs";
4
+
5
+ const holder = (o, p) => p.slice(0, -1).reduce((x, k) => (isRecord(x?.[k]) ? x[k] : null), o);
6
+ function omit(vars, paths) {
7
+ if (!paths.length) return vars;
8
+ const out = structuredClone(vars);
9
+ for (const p of paths) {
10
+ const o = holder(out, p);
11
+ if (o) delete o[p.at(-1)];
12
+ }
13
+ return out;
14
+ }
15
+
16
+ export function replaySecond(
17
+ trace,
18
+ { main, lacks, extra, second, hiddenStep, readerPath, mainActions, secondRules, rawSecondTraces },
19
+ ) {
20
+ if (!second) return null;
21
+ const shared = (vars) => stateKey(omit(vars, lacks));
22
+ const sharedOf = (n) => stateKey(omit(n.vars, extra));
23
+ const out = new Map();
24
+ for (const e of [...second.graph.edges, ...second.graph.loops]) {
25
+ if (!out.has(e.from)) out.set(e.from, []);
26
+ out.get(e.from).push(e);
27
+ }
28
+ const actions = new Set([...second.graph.edges, ...second.graph.loops].map((e) => e.action));
29
+ const nodeOf = (id) => second.graph.nodes.get(id);
30
+ const screen = new Map(
31
+ [...second.graph.nodes.values()].map((n) => [n.id, second.customer(n.vars)]),
32
+ );
33
+ const words = (e) =>
34
+ second.step(e.action, nodeOf(e.from).vars, nodeOf(e.to).vars, "customer") || e.action;
35
+ const settle = (ids) => {
36
+ const all = new Set(ids);
37
+ for (const id of all)
38
+ for (const e of out.get(id) ?? []) {
39
+ const sameLabel = screen.get(e.to) === screen.get(id);
40
+ if (
41
+ !mainActions.has(e.action) &&
42
+ hiddenStep(trace.invariant, sameLabel, e.action, nodeOf(id).vars, nodeOf(e.to).vars)
43
+ )
44
+ all.add(e.to);
45
+ }
46
+ return all;
47
+ };
48
+ const seen = readerPath(trace, main, main.customer, "customer");
49
+ const shownAt = new Map(seen.map((p, j) => [p.modelStep, j]));
50
+ let here = settle(
51
+ [...second.graph.nodes.values()]
52
+ .filter((n) => n.init && screen.get(n.id) === seen[0].key)
53
+ .map((n) => n.id),
54
+ );
55
+ if (here.size === 0) return { reason: "starts in another state" };
56
+ const passed = new Set(here);
57
+ let j = 0;
58
+ for (let i = 1; i < trace.steps.length; i++) {
59
+ const { action } = trace.steps[i];
60
+ const hidden = !shownAt.has(i);
61
+ if (!hidden) j = shownAt.get(i);
62
+ const want = seen[j];
63
+ const [before, after] = [trace.steps[i - 1].vars, trace.steps[i].vars];
64
+ const known = actions.has(action);
65
+ if (!known && hidden) continue;
66
+ const moved = shared(before) !== shared(after);
67
+ const steps = (keep) => [...here].flatMap((id) => (out.get(id) ?? []).filter(keep));
68
+ const tries = steps((e) =>
69
+ known ? e.action === action : moved && sharedOf(nodeOf(e.to)) === shared(after),
70
+ );
71
+ if (hidden && tries.length === 0) continue;
72
+ const looksSame = (e) => screen.get(e.to) === want.key && (hidden || words(e) === want.label);
73
+ const matched = known ? tries.filter(looksSame) : tries;
74
+ const same = matched.length || hidden ? matched : steps(looksSame);
75
+ if (same.length) {
76
+ here = settle(same.map((e) => e.to));
77
+ for (const id of here) passed.add(id);
78
+ continue;
79
+ }
80
+ const outcomes = new Map();
81
+ for (const e of tries) {
82
+ const id = `${screen.get(e.to)}${SEP}${words(e)}`;
83
+ outcomes.set(id, [...(outcomes.get(id) ?? []), e]);
84
+ }
85
+ const taken = [...outcomes.values()].sort((a, b) => b.length - a.length)[0]?.[0];
86
+ const from = main.key(before);
87
+ const said = hidden ? main.step(action, before, after, "customer") || action : want.label;
88
+ const at = { modelStep: i, said, from, hidden };
89
+ if (!taken)
90
+ return {
91
+ ...at,
92
+ key: from,
93
+ label: main.step(action, before, after) || action,
94
+ disabled: true,
95
+ };
96
+ const [b, a] = [nodeOf(taken.from).vars, nodeOf(taken.to).vars];
97
+ return {
98
+ ...at,
99
+ key: second.key(a),
100
+ label: second.step(taken.action, b, a) || taken.action,
101
+ own: words(taken),
102
+ };
103
+ }
104
+ const bad = badOf(rawSecondTraces, trace.invariant);
105
+ const checked = secondRules.has(trace.invariant);
106
+ const ruleHolds = checked && ![...passed].some((id) => bad.has(stateKey(nodeOf(id).vars)));
107
+ return { sameEnd: true, checked, ruleHolds };
108
+ }
109
+
110
+ export function divergence(s, path, n, { inSecond, modelEdges }) {
111
+ if (s?.key === undefined) return s;
112
+ const step = path.findLastIndex((p) => p.modelStep < s.modelStep) + (s.hidden ? 0 : 1);
113
+ const plain = edgeId(s.from, s.key);
114
+ if (s.disabled) {
115
+ const text = inSecond(`${s.label} cannot happen at step ${step}`);
116
+ const edge = `${plain}${SEP}cannot${SEP}second`;
117
+ return { ...s, step, text, overview: `${text} of bug ${n}`, edge, apart: true };
118
+ }
119
+ const text = inSecond(s.label);
120
+ const drawn = [s.label, text].some((l) => modelEdges.get(plain)?.labels.has(l));
121
+ return { ...s, step, text, overview: text, edge: drawn ? plain : `${plain}${SEP}second` };
122
+ }
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ const imports = (text, dir) =>
7
+ [...text.matchAll(/\b(?:from|import)\s*\(?\s*["'](\.{1,2}\/[^"']+)["']/g)].map((m) =>
8
+ path.resolve(dir, m[1]),
9
+ );
10
+
11
+ const DEPENDENCIES = {
12
+ ".tla": (text, dir) =>
13
+ [
14
+ ...text
15
+ .replace(/\(\*[^]*?\*\)/g, " ")
16
+ .replace(/\\\*.*/g, " ")
17
+ .matchAll(/\b(?:EXTENDS|INSTANCE)\s+(\w+(?:\s*,\s*\w+)*)/g),
18
+ ].flatMap((m) => m[1].split(/\s*,\s*/).map((name) => path.join(dir, `${name}.tla`))),
19
+ ".mjs": imports,
20
+ ".js": imports,
21
+ };
22
+
23
+ export const copyPath = (root, file, paths = path) => {
24
+ const absolute = paths.resolve(file);
25
+ const pathRoot = paths.parse(absolute).root;
26
+ return paths.join(root, pathRoot.replace(/\W+/g, ""), paths.relative(pathRoot, absolute));
27
+ };
28
+
29
+ const contentOf = (file) => {
30
+ try {
31
+ return fs.readFileSync(file, "base64");
32
+ } catch {
33
+ return null;
34
+ }
35
+ };
36
+
37
+ export class Inputs {
38
+ #root = fs.mkdtempSync(path.join(os.tmpdir(), "statechart-inputs-"));
39
+ #files = new Set();
40
+
41
+ constructor() {
42
+ process.on("exit", () => fs.rmSync(this.#root, { recursive: true, force: true }));
43
+ }
44
+
45
+ add(files) {
46
+ for (const file of files) if (file) this.#take(path.resolve(file), true);
47
+ return this;
48
+ }
49
+
50
+ copyOf(file) {
51
+ return copyPath(this.#root, file);
52
+ }
53
+
54
+ changed() {
55
+ return [...this.#files].find((file) => contentOf(file) !== contentOf(this.copyOf(file)));
56
+ }
57
+
58
+ #take(file, listed) {
59
+ if (this.#files.has(file)) return;
60
+ let content;
61
+ try {
62
+ content = fs.readFileSync(file);
63
+ } catch {
64
+ if (listed) this.#files.add(file);
65
+ return;
66
+ }
67
+ this.#files.add(file);
68
+ const copy = this.copyOf(file);
69
+ fs.mkdirSync(path.dirname(copy), { recursive: true });
70
+ fs.writeFileSync(copy, content);
71
+ const found = DEPENDENCIES[path.extname(file)]?.(content.toString("utf8"), path.dirname(file));
72
+ for (const dependency of found ?? []) this.#take(dependency, false);
73
+ }
74
+ }
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { BROKEN_MARK, PRIMES_OR_UNCHANGED, tlaVariables } from "./lib.mjs";
4
+
5
+ export const withoutComments = (spec) =>
6
+ fs
7
+ .readFileSync(spec, "utf8")
8
+ .replace(/\(\*[^]*?\*\)/g, " ")
9
+ .replace(/\\\*.*/g, " ");
10
+
11
+ function specVariables(spec, seen) {
12
+ const text = withoutComments(spec);
13
+ const extended = /\bEXTENDS\s+(\w+(?:\s*,\s*\w+)*)/.exec(text)?.[1].split(/\s*,\s*/) ?? [];
14
+ return [
15
+ ...extended.flatMap((m) => {
16
+ const file = path.join(path.dirname(spec), `${m}.tla`);
17
+ if (seen.has(m) || !fs.existsSync(file)) return [];
18
+ seen.add(m);
19
+ return specVariables(file, seen);
20
+ }),
21
+ ...tlaVariables(text),
22
+ ];
23
+ }
24
+
25
+ export const RULES_INVARIANT = "StatechartRules";
26
+
27
+ export function writeRulesModule(spec, rules, dir) {
28
+ const name = path.basename(spec, ".tla");
29
+ const file = path.join(dir, `${name}_rules.tla`);
30
+ const fields = specVariables(spec, new Set([name])).map((v) => `${v} |-> ${v}`);
31
+ const broken = rules.map((r) => `(IF ${r} THEN {} ELSE {"${r}"})`).join(" \\cup ");
32
+ fs.writeFileSync(
33
+ file,
34
+ `---- MODULE ${name}_rules ----
35
+ EXTENDS ${name}
36
+ StatechartTLC == INSTANCE TLC
37
+ StatechartBroken == ${broken}
38
+ ${RULES_INVARIANT} == StatechartBroken = {} \\/ StatechartTLC!PrintT(<<"${BROKEN_MARK}", [${fields.join(", ")}], StatechartBroken>>)
39
+ ====
40
+ `,
41
+ );
42
+ return file;
43
+ }
44
+
45
+ export const SCAFFOLD = (name, spec) => {
46
+ const text = withoutComments(spec);
47
+ const vars = tlaVariables(text);
48
+ const primedDefinitions = [
49
+ ...new Set(
50
+ text
51
+ .split(/\n(?=\S)/)
52
+ .map((d) => /^(\w+)\s*(?:\([^)]*\))?\s*==([^]*)/.exec(d))
53
+ .filter((d) => d && PRIMES_OR_UNCHANGED.test(d[2]))
54
+ .map((d) => d[1]),
55
+ ),
56
+ ].sort();
57
+ return `// Chart labels for ${name}, for statechart.mjs. Write labels in the words of the person who reads
58
+ // the chart, not in variable or action names.
59
+ // v, before, and after map each variable to its value. Strings, numbers, and booleans keep their kind.
60
+ // Sequences and tuples, including functions over 1..n, read from 1 as in TLA+, so v.seat[1] is the
61
+ // first item. Reading index 0 stops the run. length, array methods, and spread still work. A
62
+ // method's index, such as indexOf or the i in map((x, i) => ...), still counts from 0.
63
+ // Sets become arrays. Records and other functions become objects.
64
+ // Variables: ${vars.join(", ")}.
65
+
66
+ // The actions that the reader takes, by action name, when the chart separates what the reader does
67
+ // from what happens around the reader. facts.settled is true when only these are enabled. With
68
+ // --cover, the overview keeps only the shortest path to each settled state, plus every bug path.
69
+ // Leave it empty when every action counts the same.
70
+ export const readerActions = [];
71
+
72
+ // The label for one model state. facts.enabled is the set of actions the state enables,
73
+ // including steps that change no variable. A label "context: progress", such as
74
+ // "Primary down: write queued", puts the states of one context in one box, and can draw the
75
+ // overview as two parallel regions. Use that form only when the context and the progress change
76
+ // independently. A label without ": " draws each state on its own.
77
+ // Do not claim a result that a pending step can still change, such as "write lost" while a
78
+ // retry is still queued.
79
+ export function state(v, facts) {
80
+ return JSON.stringify(v);
81
+ }
82
+
83
+ // The names of the two parallel regions, when the overview draws them: contexts, then progress.
84
+ // export const regionNames = ["Primary", "Write"];
85
+
86
+ // The group of one model state, in the reader's words, such as "Order shipped". The chart fills
87
+ // the states of one group with one color, and the legend names the groups. TLC still decides
88
+ // which states break a rule, and draws them with a thick red border on top of the group color.
89
+ // groupColors gives a group a light color. Groups without one take a palette color.
90
+ // export function group(v, facts) {}
91
+ // export const groupColors = { "Order shipped": "#d1f2eb" };
92
+
93
+ // The label for one step. A step that leaves the reader in the same state is not drawn,
94
+ // unless the reader takes it on a bug path. "" draws no label, and a numbered bug step shows the action.
95
+ // action is the action's name without its parameters, so compare before and after to tell which
96
+ // process or item the step changed.
97
+ // STEPS lists every definition that changes a variable. TLC names no step after one that another
98
+ // action uses in its /\\ list, such as Log in Save == Log /\\ saved' = TRUE, so delete its entry.
99
+ const STEPS = {
100
+ ${primedDefinitions.map((a) => `\t${a}: ${JSON.stringify(a)},`).join("\n")}
101
+ };
102
+ export function step(action, before, after) {
103
+ return STEPS[action] ?? action;
104
+ }
105
+
106
+ // When the step can happen, in plain words, such as "after the lease expires". The chart shows
107
+ // "step [words]". "" adds nothing.
108
+ // export function guard(action, before, after) {}
109
+
110
+ // For a timer step, how many seconds the code waits, from the constant in the code. The chart
111
+ // shows "after N s, step". Return nothing for other steps.
112
+ // export function timer(action, before, after) {}
113
+
114
+ // Why a bad state is bad, in the reader's words, such as "a worker crashed after it took the job,
115
+ // and no other worker can take it". No variable, function, or event names.
116
+ // Counterexamples that break one rule with one cause are one bug, so two bugs that end in the
117
+ // same state stay apart only if their causes differ. Return null to group by the end state.
118
+ // export function cause(v, facts, invariant) {}
119
+
120
+ // The developer's words for one model state. With it, each bug is also drawn through these states.
121
+ // export function detail(v, facts) {}
122
+
123
+ // The legend's words for each status, in place of the defaults. Group entries keep their names.
124
+ // export const legend = { works: "Works on main", wrong: "Wrong on main", fixed: "Fixed in the open PR" };
125
+ `;
126
+ };