feat(sim): F4.2 the sim protocol, the simulation room and LocalSimHost; M9 measured

packages/protocol holds the one list of /sim messages and checks every
field, naming what is wrong. packages/sim has the simulation room for F and
the web worker: the document's snapshot at start, the engine's clock on the
wall clock with Java's speed as cable delay, subscriptions to cable
activity, a card's new frames and a program's new window lines, capture and
clock kept over a stop, the command line as first action, a core failure
told to every client. LocalSimHost mirrors the document into the worker and
back without echo. M9 in Chromium: 17 examples give the same capture as in
Node, character for character; the room runs in a real worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 08:32:20 +00:00
co-authored by Claude Opus 5.5
parent 800e5de1d3
commit 0fd42f10e2
30 changed files with 1465 additions and 1 deletions
+48
View File
@@ -0,0 +1,48 @@
// M9 (plan §13): does the core give the same packet trace in Node and in a
// browser? One run of an example — read from its .fls, through the shared
// document, started and advanced tick by tick, with a few lines typed (a
// command line installed where the example has none) — as
// text: every card's capture in both views and every command line's window.
// Node and Chromium each compute it; the texts must be equal.
import * as Y from 'yjs';
import { NetworkRuntimeEngine } from '@mdg-filius/core';
import { docSnapshot, readProject, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import { traceFramesOf } from '../src/room.js';
export const M9_SECONDS = 30;
const SEED = 14;
export function traceText(archive: Uint8Array): string {
let counter = 0;
const opened = openProjectArchive(archive, () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
// An example without a command line gets one on its first PC or notebook, so that every run has traffic.
if (![...opened.installedPrograms.values()].some((programs) => programs.has('commandPrompt'))) {
const host = opened.graph.nodes.find((node) => node.kind === 'pc' || node.kind === 'notebook');
if (host !== undefined) opened.installedPrograms.set(host.id, new Set([...(opened.installedPrograms.get(host.id) ?? []), 'commandPrompt']));
}
const doc = new Y.Doc();
writeProject(doc, opened);
const project = readProject(doc);
const engine = new NetworkRuntimeEngine(SEED, undefined, { wallClock: () => 0 });
engine.handle({ type: 'start', snapshot: docSnapshot(doc), seed: SEED, initialTimeMilliseconds: 0 });
// The first command line pings the first other host with an address and asks for a name.
const terminal = project.graph.nodes.find((node) => project.installedPrograms.get(node.id)?.has('commandPrompt') === true);
const target = project.graph.nodes.map((node) => project.deviceConfigurations.get(node.id)?.ipAddress).find((ip) => ip !== undefined && ip !== '' && ip !== project.deviceConfigurations.get(terminal?.id ?? '')?.ipAddress);
const typed: [number, string][] = [
[5_000, 'ipconfig'],
[6_000, `ping ${target ?? '127.0.0.1'}`],
[15_000, 'host www.filius.de'],
];
for (let t = 200; t <= M9_SECONDS * 1_000; t += 200) {
engine.handle({ type: 'advance', toMilliseconds: t });
for (const [at, line] of typed) if (at === t && terminal !== undefined) engine.terminal(terminal.id)?.enter(line);
}
const cards = engine.capture.portIds().sort().map((portId) => ({ portId, frames: traceFramesOf(engine.capture.frames(portId)) }));
const windows = project.graph.nodes.flatMap((node) => {
const log = engine.terminal(node.id)?.log.all;
return log === undefined ? [] : [{ node: node.id, log }];
});
return JSON.stringify({ cards, windows });
}