Rename-shuffle fix: label-only channel + three hygiene items

The live-rename interlude routed label changes through a fresh
snapshot object, which flowed setData -> geometry rebuild - and the
rebuild treats the last-fetched position ledger as authoritative and
cancels any pending drag commit, so a rename visibly rewound the
whole graph whenever display had drifted (recent drag, mid-anneal,
physics re-anneal).

- Renderer gains setClusterLabels (a setPhysicsTuning sibling): pure
  applyClusterLabels patches the data-side cluster records and the
  built region meta (re-wrapping the changed entries with the same
  measure the build uses), markDirty only. Never touches nodes, the
  sim, positions, or the pending commit. The hull-silence guard
  travels inside it: a null-label cluster is a gone record - no
  rename can reach it.
- The lh:chat-renamed listener stops building snapshot objects:
  renameSessionsPatch writes the sessions entry in place (pure
  display state - the detail card's provenance line), the cluster
  label goes through the new channel, and a tick bump re-renders the
  view. Identity preserved everywhere -> the [snapshot]-keyed
  setData feed cannot fire.
- Hygiene (a): a fresh sim starts at alpha 1 and never decays while
  stopped - the first post-rebuild drag re-annealed the whole graph
  at full strength. buildSim now initializes alpha at the settle
  floor, so first-drag matches steady-state drag. setPhysicsTuning
  still rehearses alpha(1) when a full re-anneal is the point.
- Hygiene (b): nodes missing from the position ledger used to dump
  at (0,0) via the ?? 0 fallback. seedMissingNodePositions seeds
  them on a deterministic ring around the known positions' centroid
  (FNV-1a hashed angle, golden-angle spiral offset).
- Hygiene (c) is store-band (writePositionMap's writeFileSync ->
  tmp+rename): flagged for Platform, not crossed.

Tests: applyClusterLabels suite (patch + re-wrap, hull silence,
no-op, explicit null, unknown ids), renameSessionsPatch identity
pins (sessions map and clusters array identity preserved - the
contract that keeps setData from firing), seedMissingNodePositions
determinism + ring math. Live-verified: mid-anneal rename shows a
4-world-unit max disc jump (no teleport), the commit PUT survives
the rename, rest renames leave the disc build byte-identical, and a
held first drag displaces discs ~14 units instead of re-forming the
graph.
This commit is contained in:
amosblomqvist
2026-09-23 20:20:36 +03:00
parent 976be50a81
commit e826088cc8
4 changed files with 362 additions and 19 deletions
+79
View File
@@ -12,6 +12,7 @@
import assert from "node:assert/strict";
import { describe, it, it as test } from "node:test";
import {
applyClusterLabels,
arrowGeometry,
boxBlurRound,
fitTransform,
@@ -314,3 +315,81 @@ describe("wrapRegionLabel (wave 7: session names / file paths)", () => {
assert.ok(measure(lines[1]!) <= W, "ellipsized tail fits");
});
});
describe("applyClusterLabels (the label-only rename channel)", () => {
const measure = (s: string) => s.length * 5;
const W = 220;
const MAXL = 3;
const fixture = () => ({
clusters: [
{ id: "chat_a", label: "Old title" },
{ id: "chat_b", label: null },
{ id: "seedplan", label: "Orbital mechanics" },
],
regionMeta: [
{ id: "chat_a", label: "Old title", lines: ["Old title"], cx: 0, cy: 0, hue: 10 },
{ id: "chat_b", label: null, lines: null, cx: 1, cy: 1, hue: 20 },
{ id: "seedplan", label: "Orbital mechanics", lines: ["Orbital mechanics"], cx: 2, cy: 2, hue: 30 },
],
});
it("patches the matching cluster and re-wraps its region meta", () => {
const f = fixture();
// 5px/char and W=220: a 45-char title just overflows one line
// ("...for the" is 40 chars, appending " graph" reads 46).
const longTitle = "A much longer new session title for the graph";
const changed = applyClusterLabels(
f.clusters, f.regionMeta, new Map([["chat_a", longTitle]]),
W, MAXL, measure,
);
assert.equal(changed, true);
assert.equal(f.clusters[0]!.label, longTitle);
assert.deepEqual(f.regionMeta[0]!.lines, ["A much longer new session title for the", "graph"]);
// untouched entries stay byte-identical
assert.equal(f.clusters[1]!.label, null);
assert.equal(f.clusters[2]!.label, "Orbital mechanics");
assert.deepEqual(f.regionMeta[2]!.lines, ["Orbital mechanics"]);
});
it("hull silence: a null-label cluster never gains a label", () => {
const f = fixture();
const changed = applyClusterLabels(
f.clusters, f.regionMeta, new Map([["chat_b", "Resurrected?"]]),
W, MAXL, measure,
);
assert.equal(changed, false);
assert.equal(f.clusters[1]!.label, null);
assert.equal(f.regionMeta[1]!.lines, null);
});
it("a label already matching is a no-op (no redraw)", () => {
const f = fixture();
const changed = applyClusterLabels(
f.clusters, f.regionMeta, new Map([["seedplan", "Orbital mechanics"]]),
W, MAXL, measure,
);
assert.equal(changed, false);
});
it("an explicit null clears a labeled cluster's lines", () => {
const f = fixture();
const changed = applyClusterLabels(
f.clusters, f.regionMeta, new Map([["chat_a", null]]),
W, MAXL, measure,
);
assert.equal(changed, true);
assert.equal(f.clusters[0]!.label, null);
assert.equal(f.regionMeta[0]!.lines, null);
});
it("unknown ids in the map change nothing", () => {
const f = fixture();
const changed = applyClusterLabels(
f.clusters, f.regionMeta, new Map([["chat_ghost", "Nope"]]),
W, MAXL, measure,
);
assert.equal(changed, false);
});
});