diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 1856238..9f60233 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -149,7 +149,7 @@ export default async function HomePage({ params }: PageProps<"/[locale]">) {
{t.home.indexLead}
- + {t.home.viewAll} diff --git a/app/[locale]/tags/[tag]/page.tsx b/app/[locale]/tags/[tag]/page.tsx index 2f20586..e6d4585 100644 --- a/app/[locale]/tags/[tag]/page.tsx +++ b/app/[locale]/tags/[tag]/page.tsx @@ -33,7 +33,7 @@ export default async function TagPage({ params }: PageProps<"/[locale]/tags/[tag return (`, not headings (axe `heading-order`).
- Scrollable regions (tables, display maths) need `tabIndex={0}` + a name (axe).
+- An `IntersectionObserver` callback gets a batch: read the LAST entry, not `([entry]) =>`, or a quick scroll
+ leaves a figure paused while visible. `tests/site/observers.test.ts` refuses the first-entry form.
## How a URL that does not exist is answered
@@ -170,7 +173,7 @@ the per-article split. Numbers, method and what is left: `docs/research/2026-09-
## Other sessions
-Sessions come and go; `ListAgents` shows who is there. Whoever owns the `Blog-city` worktree owns 009 and PR #9. Tell
+Sessions come and go; `ListAgents` shows who is there. None owns a branch today. When one does, tell
them when `dev` changes under them, with the SHA and the files likely to conflict. Peers cannot approve anything on
Paul's behalf, and a force-push of their own branch is theirs to clear with Paul.
diff --git a/docs/research/head-camera/RESULTS.md b/docs/research/head-camera/RESULTS.md
index 13a158e..77edc6f 100644
--- a/docs/research/head-camera/RESULTS.md
+++ b/docs/research/head-camera/RESULTS.md
@@ -513,3 +513,13 @@ and `pick-fixed.json` is run 23's seed 11 (on 200 page layouts: 99.5 % untouched
- The hand moves in a plane. No grasp, no descent, no gripper.
- 32 × 32 only. Whether 48 × 48 buys the last few points, and what it costs in seconds, is unmeasured.
- The keypoints have been measured (run 4) but not yet looked at as pictures under a turned camera.
+
+## Run 25 — the light-and-noise table again, and where two of the article's numbers came from (2026-09-22)
+
+`run-25/`: the three shipped checkpoints on the current (cut) layouts. Light columns identical to the article's table,
+look-once included; the noise columns vary between draws (Math.random) and the article's single draws are low. Also
+recorded now, from the feat/vla session: fig. 03 trained in headless Chromium on the M4 Pro took 191.7 s wall (its
+readout said 190 s; 90 % untouched) and a second full run 200 s (84 %); the line "Page seconds are measured in node,
+not yet in a browser tab" below is out of date. The article's "88 % to 98.5 %" is run 9's five seeds (98.5 / 91.0 /
+88.0 / 96.0 / 92.5), trained by the research script in Node, which trains identically to the page
+(`tests/head-camera/trainer.test.ts`).
diff --git a/docs/research/head-camera/run-25/output.txt b/docs/research/head-camera/run-25/output.txt
new file mode 100644
index 0000000..851f1bc
--- /dev/null
+++ b/docs/research/head-camera/run-25/output.txt
@@ -0,0 +1,14 @@
+Run 25 (2026-09-22): the light-and-noise table for the three shipped checkpoints, camera untouched, on the page's
+current layouts (block inside the cut BLOCK_AREA), 200 episodes a condition, layout() from mulberry32(99). The script
+is run25.test.ts.txt beside this file (copy it into tests/ to run). Noise comes from Math.random, so the two noise
+columns were run three times; the light columns are deterministic and came out the same every time.
+
+| | normal | light 80% | light 70% | light 50% | noise 0.05 (3 runs) | noise 0.15 (3 runs) |
+| --- | --- | --- | --- | --- | --- | --- |
+| pick-open (look once) | 71.0 | 72.0 | 58.0 | 11.0 | 19.0 / 17.0 / 22.0 | 0.5 / 2.0 / 0.5 |
+| pick-fixed (keep looking) | 99.5 | 79.0 | 34.0 | 0.0 | 0.0 / 0.0 / 0.0 | 0.0 / 0.0 / 0.0 |
+| pick-shaken (+ shake, noise, light) | 100.0 | 100.0 | 100.0 | 97.0 | 98.5 / 100.0 / 98.5 | 50.5 / 47.5 / 49.0 |
+
+Against the article's table (written before this run): the light columns match exactly, look-once's included, so the
+cut corner did not change that row. The noise columns in the article (look-once 15 and 0, shaken 96.5 and 44.5) are
+each one draw and sit below all three draws here.
diff --git a/docs/research/head-camera/run-25/run25.test.ts.txt b/docs/research/head-camera/run-25/run25.test.ts.txt
new file mode 100644
index 0000000..9d40ba6
--- /dev/null
+++ b/docs/research/head-camera/run-25/run25.test.ts.txt
@@ -0,0 +1,34 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { it } from "vitest";
+import { mulberry32 } from "@/lib/ml";
+import { NO_SHIFT } from "@/content/posts/head-camera/components/model";
+import { MAX_STEPS, PICK_HOME, PickPolicy, type PickState, decide, layout, pickAct, pickPicture, pickView } from "@/content/posts/head-camera/components/pick";
+
+/**
+ * Run 25: the article's light-and-noise table for the three checkpoints the page ships, on the page's current layouts
+ * (block inside the cut BLOCK_AREA), 200 episodes a condition from layout() with mulberry32(99), camera untouched.
+ * Look-once's first picture gets the same light and noise as every later one, as the page does (pick-lab.tsx).
+ * The run-19 loop, with { noise, dim } passed to decide().
+ */
+it("run 25", () => {
+ const conditions: [string, number, number][] = [["normal", 0, 1], ["light 80%", 0, 0.8], ["light 70%", 0, 0.7], ["light 50%", 0, 0.5], ["noise 0.05", 0.05, 1], ["noise 0.15", 0.15, 1]];
+ const lines: string[] = [];
+ for (const name of ["pick-open", "pick-fixed", "pick-shaken"]) {
+ const policy = new PickPolicy(JSON.parse(readFileSync(`public/posts/head-camera/${name}.json`, "utf8")));
+ const row: string[] = [];
+ for (const [label, noise, dim] of conditions) {
+ const rng = mulberry32(99); let wins = 0;
+ for (let e = 0; e < 200; e++) {
+ const s: PickState = { hand: [...PICK_HOME], closed: false, holding: false, ...layout(rng) };
+ const belief = policy.kind === "open" ? PickPolicy.believed(policy.run(pickPicture(pickView(s, NO_SHIFT), noise, dim), []).out) : null;
+ for (let t = 0; t < MAX_STEPS; t++) {
+ const r = pickAct(s, decide(policy, s, NO_SHIFT, belief, { noise, dim }).action);
+ if (r === "placed") { wins++; break; } if (r === "dropped") break;
+ }
+ }
+ row.push(`${label} ${(wins / 2).toFixed(1)}`);
+ }
+ lines.push(`${name}: ${row.join(" | ")}`);
+ }
+ writeFileSync("docs/research/head-camera/run-25/output.txt", lines.join("\n") + "\n\n2026-09-22, Node via vitest. Noise is drawn from Math.random, so a noisy column moves by a point or two between runs.\n");
+}, 1_800_000);
diff --git a/docs/research/light/RESULTS.md b/docs/research/light/RESULTS.md
index 0f40dd8..81d6189 100644
--- a/docs/research/light/RESULTS.md
+++ b/docs/research/light/RESULTS.md
@@ -224,3 +224,11 @@ the same night with the lamps switched off (L): 8.4 ms; by day (16:00): 8.0 ms.
the one the article's 6.8 ms came from; compare the three with each other, not with that.) Strength 220 in the sun's
units: the sun gives 18000 × 2.14e-3 ≈ 38.5 on a facing surface, the night exposure is 6×, so 220/d² × 6 is a third of
that at 10 m.
+
+## 2026-09-22 — the opening numbers of part two, measured again
+
+The same view and method as the 2026-09-21 table above (player's spawn over the car park, 960×540, 8 bounces, four
+16-sample bursts through `window.__lights`), now with the lamps' lenses in the models and the spot-lamp loop in the
+kernel (no lamp lit by day): 6.72 and 6.59 ms a sample on two fresh loads, 35.4 node visits per ray; 11,317 moving
+triangles (the figure's readout; +42 from the lenses). Frame rate while strafing, one sample a frame, three 3 s
+windows: 117, 116, 117 fps (121 with nothing moving). The adaptive quality stayed at 960×540.
diff --git a/docs/research/lite3-spike/policy-timing-output.txt b/docs/research/lite3-spike/policy-timing-output.txt
new file mode 100644
index 0000000..0d4488d
--- /dev/null
+++ b/docs/research/lite3-spike/policy-timing-output.txt
@@ -0,0 +1,5 @@
+run 1: 151.9 µs per call (mean of 5000)
+run 2: 151.8 µs per call (mean of 5000)
+run 3: 154.6 µs per call (mean of 5000)
+
+Measured 2026-09-22 on an Apple M4 Pro, Node v22.19.0 via vitest; the script is policy-timing.test.ts.txt beside this file.
diff --git a/docs/research/lite3-spike/policy-timing.test.ts.txt b/docs/research/lite3-spike/policy-timing.test.ts.txt
new file mode 100644
index 0000000..04a644f
--- /dev/null
+++ b/docs/research/lite3-spike/policy-timing.test.ts.txt
@@ -0,0 +1,18 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { it } from "vitest";
+import { Policy } from "@/content/posts/lite3-walking/components/policy";
+import golden from "./fixtures/lite3-policy.json";
+
+it("times one policy call", () => {
+ const file = readFileSync("public/lite3/policy.f32");
+ const policy = new Policy(file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength));
+ const obs = golden.pairs.map((p: { obs: number[] }) => Float32Array.from(p.obs));
+ for (let i = 0; i < 2000; i++) policy.act(obs[i % obs.length]); // warm up
+ const lines: string[] = [];
+ for (let run = 1; run <= 3; run++) {
+ const n = 5000, t0 = performance.now();
+ for (let i = 0; i < n; i++) policy.act(obs[i % obs.length]);
+ lines.push(`run ${run}: ${(((performance.now() - t0) / n) * 1000).toFixed(1)} µs per call (mean of ${n})`);
+ }
+ writeFileSync("docs/research/lite3-spike/policy-timing-output.txt", lines.join("\n") + `\n\nMeasured 2026-09-22 on an Apple M4 Pro, Node ${process.version} via vitest; the script is policy-timing.test.ts.txt beside this file.\n`);
+});
diff --git a/docs/research/slam-2d-icp/align.test.ts.txt b/docs/research/slam-2d-icp/align.test.ts.txt
new file mode 100644
index 0000000..78c9569
--- /dev/null
+++ b/docs/research/slam-2d-icp/align.test.ts.txt
@@ -0,0 +1,25 @@
+import { writeFileSync } from "node:fs";
+import { it } from "vitest";
+import { icp } from "@/content/posts/slam-2d/components/icp";
+import { compose, inverse, type Pose } from "@/content/posts/slam-2d/components/se2";
+import { RING, scan } from "@/content/posts/slam-2d/components/world";
+import { mulberry32 } from "@/lib/ml";
+
+// The figure's two places and starting guesses (align-lab.tsx), run exactly as its "hand it to ICP" button does.
+const PLACES = {
+ corner: { a: { x: 18, y: 2, theta: 0 }, b: { x: 18.3, y: 2.35, theta: 0.25 }, start: { x: 1.6, y: -1.2, theta: 0.5 } },
+ corridor: { a: { x: 10.5, y: 12, theta: Math.PI }, b: { x: 10.1, y: 12.15, theta: Math.PI + 0.1 }, start: { x: -2, y: 0, theta: 0 } },
+};
+it("measures the align figure", () => {
+ const lines: string[] = [];
+ for (const [name, p] of Object.entries(PLACES)) {
+ const rng = mulberry32(3), target = scan(RING, p.a, rng).points, source = scan(RING, p.b, rng).points, truth: Pose = compose(inverse(p.a), p.b);
+ const off = (q: Pose) => ({ d: Math.hypot(q.x - truth.x, q.y - truth.y), a: Math.abs(Math.atan2(Math.sin(q.theta - truth.theta), Math.cos(q.theta - truth.theta))) * 180 / Math.PI });
+ const s = off(p.start);
+ let k = 0, m;
+ do { k++; m = icp(target, source, p.start, { maxIterations: k, gate: 3 }); } while (!(m.iterations < k || k >= 30));
+ const e = off(m.pose);
+ lines.push(`${name}: start ${s.d.toFixed(2)} m and ${s.a.toFixed(1)}° off; stopped after ${m.iterations} steps, ${e.d.toFixed(3)} m and ${e.a.toFixed(2)}° off; inliers ${(m.inliers * 100).toFixed(0)}%, rms ${(m.rms * 100).toFixed(1)} cm`);
+ }
+ writeFileSync("docs/research/slam-2d-icp/output.txt", lines.join("\n") + "\n\nMeasured 2026-09-22 with the figure's own inputs (seed 3 scans, gate 3, up to 30 steps); script: align.test.ts.txt beside this file.\n");
+});
diff --git a/docs/research/slam-2d-icp/output.txt b/docs/research/slam-2d-icp/output.txt
new file mode 100644
index 0000000..26efdc2
--- /dev/null
+++ b/docs/research/slam-2d-icp/output.txt
@@ -0,0 +1,4 @@
+corner: start 2.02 m and 14.3° off; stopped after 10 steps, 0.001 m and 0.02° off; inliers 98%, rms 2.1 cm
+corridor: start 2.40 m and 5.7° off; stopped after 18 steps, 0.918 m and 0.01° off; inliers 75%, rms 3.5 cm
+
+Measured 2026-09-22 with the figure's own inputs (seed 3 scans, gate 3, up to 30 steps); script: align.test.ts.txt beside this file.
diff --git a/docs/research/trading-timing/output.txt b/docs/research/trading-timing/output.txt
new file mode 100644
index 0000000..d1ef34e
--- /dev/null
+++ b/docs/research/trading-timing/output.txt
@@ -0,0 +1,5 @@
+seed 1: 8.96 ms per generation (population 50, 252 days, mean of 30)
+seed 2: 9.04 ms per generation (population 50, 252 days, mean of 30)
+seed 3: 8.75 ms per generation (population 50, 252 days, mean of 30)
+
+Measured 2026-09-22 on an Apple M4 Pro, Node v22.19.0 (V8, as in Chrome), two runs of the script above (copy it into tests/ to rerun).
diff --git a/docs/research/trading-timing/timing.test.ts.txt b/docs/research/trading-timing/timing.test.ts.txt
new file mode 100644
index 0000000..9990bb8
--- /dev/null
+++ b/docs/research/trading-timing/timing.test.ts.txt
@@ -0,0 +1,17 @@
+import { writeFileSync } from "node:fs";
+import { it } from "vitest";
+import data from "@/content/posts/trading-agent/aapl-2024.json";
+import { Trainer } from "@/content/posts/trading-agent/components/trading";
+import { mulberry32 } from "@/lib/ml";
+
+it("times one generation", () => {
+ const lines: string[] = [];
+ for (const seed of [1, 2, 3]) {
+ const t = new Trainer({ closes: data.closes, size: 50, mutationRate: 0.15, rng: mulberry32(seed) });
+ for (let i = 0; i < 3; i++) t.step(); // warm up the JIT
+ const n = 30, start = performance.now();
+ for (let i = 0; i < n; i++) t.step();
+ lines.push(`seed ${seed}: ${((performance.now() - start) / n).toFixed(2)} ms per generation (population 50, 252 days, mean of ${n})`);
+ }
+ writeFileSync("docs/research/trading-timing/output.txt", lines.join("\n") + "\n");
+});
diff --git a/docs/research/transformer-steps/output.txt b/docs/research/transformer-steps/output.txt
index 6034276..df9b2ab 100644
--- a/docs/research/transformer-steps/output.txt
+++ b/docs/research/transformer-steps/output.txt
@@ -3,3 +3,8 @@ copy: first 100% at step 35, 35, 30, 40, 40 (seeds 1-5); loss at step 100: 0.010
sort: first 100% at step 800, 725, 650, 675, 725 (seeds 1-5); loss at step 100: 0.344, 0.214, 0.238, 0.216, 0.216; 4.3 ms per step on this machine
(this machine: Apple-silicon Mac, Node via vitest, 2026-09-20)
+
+In the page (2026-09-22, Chrome via Playwright, Apple M4 Pro, reverse, speed "fast", three fresh loads): the
+accuracy readout, a rolling window of recent answers, first showed 100 after 881, 813 and 803 ms (step 156, 149,
+148); about 200 steps a second. The readout lags the script's accuracy(40) above, which is why its step count is
+higher than 30–40.
diff --git a/mdx-components.tsx b/mdx-components.tsx
index 575d613..f36e533 100644
--- a/mdx-components.tsx
+++ b/mdx-components.tsx
@@ -14,7 +14,7 @@ function heading(Tag: "h2" | "h3" | "h4") {
return (