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

+15 -4
View File
@@ -194,7 +194,10 @@ export class EditorState {
select(selection: Selection): void {
// Test hooks pass the short form `{ kind: 'object', sceneId, objectId }` without `objectIds`.
if (selection?.kind === 'object' && !selection.objectIds) selection = objectSelection(selection.sceneId, [selection.objectId]);
// The type says `objectIds` is always there – untyped callers are exactly why this guard exists.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (selection?.kind === 'object' && !selection.objectIds)
selection = objectSelection(selection.sceneId, [selection.objectId]);
if (sameSelection(this.selection, selection)) return;
this.selection = selection;
this.emit('selection');
@@ -204,7 +207,9 @@ export class EditorState {
toggleObject(sceneId: string, objectId: string): void {
const s = this.selection;
const current = s?.kind === 'object' && s.sceneId === sceneId ? s.objectIds : [];
const next = current.includes(objectId) ? current.filter((id) => id !== objectId) : [...current, objectId];
const next = current.includes(objectId)
? current.filter((id) => id !== objectId)
: [...current, objectId];
this.select(objectSelection(sceneId, next, current.includes(objectId) ? undefined : objectId));
}
@@ -227,7 +232,8 @@ export class EditorState {
const asset = findAsset(this.project, s.assetId);
// Fall back to the behavior asset so the graph stays open when its node vanished (undo).
if (!asset || asset.type !== 'behavior') this.select(null);
else if (!asset.graph.nodes.some((n) => n.id === s.nodeId)) this.select({ kind: 'asset', assetId: s.assetId });
else if (!asset.graph.nodes.some((n) => n.id === s.nodeId))
this.select({ kind: 'asset', assetId: s.assetId });
}
}
@@ -262,7 +268,12 @@ export function sameSelection(a: Selection, b: Selection): boolean {
if (!a || !b || a.kind !== b.kind) return false;
if (a.kind === 'asset' && b.kind === 'asset') return a.assetId === b.assetId;
if (a.kind === 'object' && b.kind === 'object') {
return a.sceneId === b.sceneId && a.objectId === b.objectId && a.objectIds.length === b.objectIds.length && a.objectIds.every((id, i) => id === b.objectIds[i]);
return (
a.sceneId === b.sceneId &&
a.objectId === b.objectId &&
a.objectIds.length === b.objectIds.length &&
a.objectIds.every((id, i) => id === b.objectIds[i])
);
}
if (a.kind === 'node' && b.kind === 'node') return a.assetId === b.assetId && a.nodeId === b.nodeId;
return false;