Add ESLint and Prettier, and close the node test gaps

Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 10:02:22 +02:00
1 parent f140c2fd77
commit d54a620ebd
113 files changed
+5049 -666

No files matched your search

+55 -10
View File
@@ -1,4 +1,11 @@
import { edgesInto, edgesOfNode, findNode, type GraphEdge, type GraphNode, portsCompatible } from '../graph/Graph';
import {
edgesInto,
edgesOfNode,
findNode,
type GraphEdge,
type GraphNode,
portsCompatible,
} from '../graph/Graph';
import { nodeDef } from '../graph/NodeRegistry';
import { newId } from '../ids';
import type { PropertyBag, PropertyValue } from '../model/Properties';
@@ -14,11 +21,23 @@ export class AddNodeCommand implements Command {
readonly label: string;
readonly node: GraphNode;
constructor(private readonly assetId: string, type: string, x: number, y: number, params: PropertyBag = {}) {
constructor(
private readonly assetId: string,
type: string,
x: number,
y: number,
params: PropertyBag = {},
) {
// An unknown type is rejected in `execute`, not here: constructors run outside the
// editor's error handling, so throwing here would crash instead of showing a notice.
const def = nodeDef(type);
this.node = { id: newId(), type, x: Math.round(x), y: Math.round(y), params: { ...defaultsOf(def?.params ?? []), ...params } };
this.node = {
id: newId(),
type,
x: Math.round(x),
y: Math.round(y),
params: { ...defaultsOf(def?.params ?? []), ...params },
};
this.label = def ? `Add ${def.label}` : `Add ${type}`;
}
@@ -38,7 +57,10 @@ export class RemoveNodeCommand implements Command {
readonly label = 'Remove node';
private removed?: { node: Removed<GraphNode>; edges: Removed<GraphEdge>[] };
constructor(private readonly assetId: string, private readonly nodeId: string) {}
constructor(
private readonly assetId: string,
private readonly nodeId: string,
) {}
execute(project: Project): void {
const graph = requireBehavior(project, this.assetId).graph;
@@ -65,7 +87,12 @@ export class MoveNodeCommand implements Command {
readonly label = 'Move node';
private previous = { x: 0, y: 0 };
constructor(private readonly assetId: string, private readonly nodeId: string, private readonly x: number, private readonly y: number) {}
constructor(
private readonly assetId: string,
private readonly nodeId: string,
private readonly x: number,
private readonly y: number,
) {}
execute(project: Project): void {
const node = requireNode(project, this.assetId, this.nodeId);
@@ -83,7 +110,12 @@ export class MoveNodeCommand implements Command {
/** Sets a node param or the inline value of an unconnected data input. */
export class SetNodeParamCommand extends SetBagValueCommand<GraphNode> {
constructor(private readonly assetId: string, private readonly nodeId: string, key: string, private readonly value: PropertyValue) {
constructor(
private readonly assetId: string,
private readonly nodeId: string,
key: string,
private readonly value: PropertyValue,
) {
super(key, `Set ${key}`);
}
@@ -111,7 +143,11 @@ export class ConnectCommand implements Command {
readonly edge: GraphEdge;
private replaced: Removed<GraphEdge>[] = [];
constructor(private readonly assetId: string, from: { node: string; port: string }, to: { node: string; port: string }) {
constructor(
private readonly assetId: string,
from: { node: string; port: string },
to: { node: string; port: string },
) {
this.edge = { id: newId(), from: { ...from }, to: { ...to } };
}
@@ -124,7 +160,8 @@ export class ConnectCommand implements Command {
const out = requireNodeDef(fromNode.type).outputs.find((p) => p.key === this.edge.from.port);
const inp = requireNodeDef(toNode.type).inputs.find((p) => p.key === this.edge.to.port);
if (!out || !inp) throw new CommandError('Port not found');
if (!portsCompatible(out.type, inp.type)) throw new CommandError(`Cannot connect ${out.type} to ${inp.type}`);
if (!portsCompatible(out.type, inp.type))
throw new CommandError(`Cannot connect ${out.type} to ${inp.type}`);
if (graph.edges.some((e) => sameEnds(e, this.edge))) throw new CommandError('Already connected');
const occupied = inp.type === 'exec' ? [] : edgesInto(graph, this.edge.to.node, this.edge.to.port);
this.replaced = removeWhere(graph.edges, (e) => occupied.includes(e));
@@ -143,7 +180,10 @@ export class DisconnectCommand implements Command {
readonly label = 'Disconnect';
private removed: Removed<GraphEdge>[] = [];
constructor(private readonly assetId: string, private readonly edgeId: string) {}
constructor(
private readonly assetId: string,
private readonly edgeId: string,
) {}
execute(project: Project): void {
const graph = requireBehavior(project, this.assetId).graph;
@@ -157,5 +197,10 @@ export class DisconnectCommand implements Command {
}
function sameEnds(a: GraphEdge, b: GraphEdge): boolean {
return a.from.node === b.from.node && a.from.port === b.from.port && a.to.node === b.to.node && a.to.port === b.to.port;
return (
a.from.node === b.from.node &&
a.from.port === b.from.port &&
a.to.node === b.to.node &&
a.to.port === b.to.port
);
}