Skip to content

Commit d00aaf6

Browse files
committed
book: The Three Anchors
1 parent 2b41c2f commit d00aaf6

22 files changed

Lines changed: 1008 additions & 0 deletions

‎.github/ISSUE_TEMPLATE/tweak-book.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ body:
1616
description: The dropdown stays in sync with the shelf automatically.
1717
options:
1818
# BOOKS:START (auto-generated by generator/sync-issue-books.mjs — do not edit by hand)
19+
- "The Three Anchors · How complementary continual-learning mechanisms preserve memory — composecl-memory"
1920
- "Four Tokens Ahead — ncp-archpreview"
2021
- "The Imagined Experiment · World Model Reinforcement Learning — world-model-rl"
2122
- "The Routing Harness · How agent traces become the next training mixture — neohorse-routing-harness"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import { Player } from '../../core';
3+
import { Render, vizScene } from './chapter-1';
4+
5+
function Chapter() { const sc = vizScene(); return <div style={{ padding: '4vh 4vw' }}><Player timeline={sc.tl} loop>{(s) => <Render s={s} />}</Player></div>; }
6+
const meta: Meta<typeof Chapter> = { title: 'Books/The Three Anchors/Chapter 1', component: Chapter };
7+
export default meta;
8+
export const OneHundredWaysToForget: StoryObj<typeof Chapter> = {};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Grounding: arXiv:2609.06986 Sections 1 and 3; official source README.md,
2+
// data/*/manifest.json, evaluate.py, and analysis/summarize.py.
3+
import { CAMERA_HOME, Camera, Timeline, cameraInterp, colors, ease } from '../../core';
4+
import type { CameraState, SceneState } from '../../core';
5+
6+
const clamp01 = (v: number) => Math.max(0, Math.min(1, v));
7+
const TASKS = Array.from({ length: 100 }, (_, i) => ({ i, x: 106 + i * 10.7 }));
8+
const DATASETS = [
9+
{ label: 'Symbol-QA', detail: '100 × 100 arbitrary associations', color: colors.ACCENT },
10+
{ label: 'LLM-QA', detail: '100 × 100 fictional facts', color: colors.SECONDARY },
11+
{ label: 'Real-QA', detail: '100 × 50 filtered questions', color: colors.POSITIVE },
12+
];
13+
14+
export function buildScene() {
15+
const tl = new Timeline();
16+
const cam = tl.channel<CameraState>('camera', CAMERA_HOME, cameraInterp);
17+
const memoryU = tl.channel('first memory', 0);
18+
const tapeU = tl.channel('hundred task tape', 0);
19+
const overwriteU = tl.channel('sequential overwrite', 0);
20+
const datasetsU = tl.channel('three datasets', 0);
21+
const noIdU = tl.channel('no task identifier', 0);
22+
const foldU = tl.channel('tape folds into matrix', 0);
23+
const matrixU = tl.channel('temporal accuracy matrix', 0);
24+
const diagonalU = tl.channel('immediate acquisition diagonal', 0);
25+
const finalU = tl.channel('final retention row', 0);
26+
const metricU = tl.channel('paper metrics', 0);
27+
const closeU = tl.channel('closing question', 0);
28+
29+
tl.caption({ at: 0.4, dur: 5.5, text: 'Teach a language model one new answer, then keep teaching. The first memory now has ninety-nine chances to disappear.' });
30+
tl.tween(memoryU, 1, { at: 0.8, dur: 0.7, ease: ease.pop });
31+
tl.tween(tapeU, 0.12, { at: 1.6, dur: 1.5, ease: ease.draw });
32+
tl.tween(cam, { x: 300, y: 300, k: 1.08 }, { at: 2.0, dur: 1.2, ease: ease.move });
33+
34+
tl.caption({ at: 6.4, dur: 5.5, text: 'The paper turns that worry into a one-hundred-task memorization setting, learned in sequence with no old examples kept.' });
35+
tl.tween(tapeU, 1, { at: 6.9, dur: 4.0, ease: ease.linear });
36+
tl.tween(overwriteU, 1, { at: 8.0, dur: 3.0, ease: ease.linear });
37+
tl.tween(cam, CAMERA_HOME, { at: 9.8, dur: 1.2, ease: ease.move });
38+
39+
tl.caption({ at: 12.4, dur: 5.5, text: 'The model receives only the question at inference. No task label tells it which chapter of its past to search.' });
40+
tl.tween(noIdU, 1, { at: 12.9, dur: 1.0, ease: ease.enter });
41+
42+
tl.caption({ at: 18.4, dur: 5.5, text: 'Three streams make the test harder to fake: random symbols, invented facts, and natural questions the base model could not answer.' });
43+
tl.tween(datasetsU, 3, { at: 18.9, dur: 2.0, ease: ease.enter });
44+
tl.tween(cam, { x: 640, y: 350, k: 1.06 }, { at: 19.5, dur: 1.2, ease: ease.move });
45+
46+
tl.caption({ at: 24.4, dur: 5.5, text: 'After every task, evaluation revisits the whole learned stream. The tape folds into a temporal accuracy matrix.' });
47+
tl.tween(datasetsU, 0, { at: 24.7, dur: 0.6, ease: ease.enter });
48+
tl.tween(foldU, 1, { at: 25.0, dur: 1.4, ease: ease.move });
49+
tl.tween(matrixU, 1, { at: 26.1, dur: 2.2, ease: ease.draw });
50+
tl.tween(cam, { x: 640, y: 350, k: 1.05 }, { at: 26.2, dur: 1.2, ease: ease.move });
51+
52+
tl.caption({ at: 30.4, dur: 5.5, text: 'Each new row asks what the current checkpoint remembers about every task it has already seen.' });
53+
tl.tween(diagonalU, 1, { at: 31.0, dur: 1.6, ease: ease.enter });
54+
55+
tl.caption({ at: 36.4, dur: 5.5, text: 'The diagonal measures immediate acquisition. Bright cells there only prove that each answer was learned once.' });
56+
tl.tween(cam, { x: 600, y: 350, k: 1.08 }, { at: 37.0, dur: 1.2, ease: ease.move });
57+
58+
tl.caption({ at: 42.4, dur: 5.5, text: 'The final row is the expensive claim. Its mean is final retention after the hundredth update.' });
59+
tl.tween(finalU, 1, { at: 42.9, dur: 1.3, ease: ease.pop });
60+
tl.tween(cam, { x: 640, y: 360, k: 1.08 }, { at: 43.0, dur: 1.2, ease: ease.move });
61+
62+
tl.caption({ at: 48.4, dur: 5.5, text: 'The same matrix yields forgetting by comparing each task at its best moment with that last row.' });
63+
tl.tween(metricU, 1, { at: 49.0, dur: 1.5, ease: ease.enter });
64+
tl.tween(cam, CAMERA_HOME, { at: 51.0, dur: 1.2, ease: ease.move });
65+
66+
tl.caption({ at: 54.4, dur: 5.5, text: 'Naive sequential fine-tuning leaves almost nothing at this horizon. One mechanism at a time is not enough.' });
67+
tl.tween(overwriteU, 1.7, { at: 55.0, dur: 1.5, ease: ease.move });
68+
69+
tl.caption({ at: 60.4, dur: 5.8, text: 'So the paper asks a sharper question: which protections preserve different parts of memory, and what happens when they work together?' });
70+
tl.tween(closeU, 1, { at: 61.0, dur: 1.3, ease: ease.move });
71+
tl.hold(66.2, 1.0);
72+
73+
return { tl, cam, memoryU, tapeU, overwriteU, datasetsU, noIdU, foldU, matrixU, diagonalU, finalU, metricU, closeU };
74+
}
75+
76+
const scene = buildScene();
77+
78+
function MemoryCard({ u, fade }: { u: number; fade: number }) {
79+
return <g transform={`translate(118 112) scale(${0.86 + 0.14 * u})`} opacity={u}>
80+
<rect width="262" height="92" rx="24" fill="#151f31" stroke={colors.WARM} strokeWidth="3" />
81+
<text x="22" y="32" fill={colors.MUTED} fontSize="12" fontFamily={colors.font.mono}>Question: oHBvRP</text>
82+
<text x="22" y="65" fill={colors.WARM} fontSize="18" fontWeight="800" fontFamily={colors.font.mono} opacity={fade}>Answer: OIvG</text>
83+
</g>;
84+
}
85+
86+
export function Render({ s }: { s: SceneState }) {
87+
const close = s.get(scene.closeU);
88+
const tape = s.get(scene.tapeU);
89+
const overwrite = s.get(scene.overwriteU);
90+
const fold = s.get(scene.foldU);
91+
const matrix = s.get(scene.matrixU);
92+
const diagonal = s.get(scene.diagonalU);
93+
const final = s.get(scene.finalU);
94+
const memoryFade = Math.max(0.08, 1 - 0.82 * clamp01(overwrite));
95+
const tapeFade = 1 - fold;
96+
97+
return <Camera {...s.get(scene.cam)}>
98+
<g opacity={1 - close}>
99+
<text x="640" y="60" textAnchor="middle" fill={colors.TEXT} fontSize="34" fontWeight="850">the hundred-task horizon</text>
100+
<g opacity={tapeFade}>
101+
<MemoryCard u={s.get(scene.memoryU)} fade={memoryFade} />
102+
<path d="M112 292 H1172" stroke={colors.GRID} strokeWidth="7" strokeLinecap="round" />
103+
{TASKS.map((task) => {
104+
const u = clamp01(tape * 100 - task.i);
105+
const old = task.i === 0;
106+
return <g key={task.i} opacity={u}>
107+
<rect x={task.x} y={270} width="7" height={old ? 44 : 28} rx="3.5" fill={old ? colors.WARM : colors.ACCENT} opacity={old ? memoryFade : 0.25 + 0.55 * u} />
108+
{(task.i === 0 || task.i === 9 || task.i === 49 || task.i === 99) && <text x={task.x + 3.5} y="335" textAnchor="middle" fill={old ? colors.WARM : colors.MUTED} fontSize="10">{task.i + 1}</text>}
109+
</g>;
110+
})}
111+
<g opacity={s.get(scene.noIdU)}>
112+
<rect x="432" y="158" width="416" height="70" rx="22" fill="#251823" stroke={colors.NEGATIVE} strokeWidth="2.5" />
113+
<text x="640" y="187" textAnchor="middle" fill={colors.NEGATIVE} fontSize="14" fontWeight="800">NO TASK IDENTIFIER AT INFERENCE</text>
114+
<text x="640" y="211" textAnchor="middle" fill={colors.MUTED} fontSize="12">one question must identify one answer globally</text>
115+
</g>
116+
<g>
117+
{DATASETS.map((d, i) => {
118+
const u = clamp01(s.get(scene.datasetsU) - i);
119+
return <g key={d.label} transform={`translate(${152 + i * 360} 430)`} opacity={u}>
120+
<rect width="312" height="106" rx="26" fill="#101827" stroke={d.color} strokeWidth="2.5" />
121+
<circle cx="38" cy="53" r="19" fill={d.color} opacity="0.28" />
122+
<text x="72" y="44" fill={d.color} fontSize="17" fontWeight="800">{d.label}</text>
123+
<text x="72" y="70" fill={colors.MUTED} fontSize="12">{d.detail}</text>
124+
</g>;
125+
})}
126+
</g>
127+
</g>
128+
129+
<g opacity={matrix} transform={`translate(${fold * 135} ${fold * 8})`}>
130+
<text x="368" y="116" fill={colors.MUTED} fontSize="12" fontFamily={colors.font.mono}>evaluate.py → accuracy_matrix</text>
131+
{Array.from({ length: 10 }, (_, row) => Array.from({ length: 10 }, (_, col) => {
132+
if (col > row) return null;
133+
const idx = row * 10 + col;
134+
const u = clamp01(matrix * 100 - idx);
135+
const isDiag = row === col;
136+
const isFinal = row === 9;
137+
const x = 338 + col * 48;
138+
const y = 142 + row * 42;
139+
const dim = isDiag ? 0.48 + 0.52 * diagonal : isFinal ? 0.24 + 0.76 * final : 0.22;
140+
const color = isDiag ? colors.ACCENT : isFinal ? colors.WARM : colors.MUTED;
141+
return <rect key={`${row}-${col}`} x={x} y={y} width="38" height="32" rx="7" fill={color} opacity={u * dim} stroke={color} strokeWidth={isFinal && final > 0 ? 2.5 : 0.5} />;
142+
}))}
143+
<text x="590" y="590" textAnchor="middle" fill={colors.WARM} fontSize="13" opacity={final}>final row → mean final retention</text>
144+
<text x="826" y="340" textAnchor="middle" fill={colors.ACCENT} fontSize="12" transform="rotate(-90 826 340)" opacity={diagonal}>diagonal → immediate acquisition</text>
145+
<g opacity={s.get(scene.metricU)}>
146+
<rect x="865" y="190" width="282" height="226" rx="30" fill="#101827" stroke={colors.SECONDARY} strokeWidth="2.5" />
147+
<text x="1006" y="232" textAnchor="middle" fill={colors.TEXT} fontSize="18" fontWeight="800">the matrix answers</text>
148+
{['final retention', 'immediate acquisition', 'forgetting', 'forward transfer'].map((label, i) => <g key={label} transform={`translate(902 ${270 + i * 34})`}>
149+
<circle r="6" fill={[colors.WARM, colors.ACCENT, colors.NEGATIVE, colors.POSITIVE][i]} />
150+
<text x="18" y="5" fill={colors.MUTED} fontSize="13">{label}</text>
151+
</g>)}
152+
</g>
153+
</g>
154+
</g>
155+
156+
<g opacity={close}>
157+
<rect x="180" y="122" width="920" height="444" rx="54" fill={colors.BG} stroke={colors.WARM} strokeWidth="4" />
158+
<MemoryCard u={1} fade={0.16} />
159+
<text x="640" y="252" textAnchor="middle" fill={colors.TEXT} fontSize="46" fontWeight="880">one memory · one hundred updates</text>
160+
<path d="M286 360 H994" stroke={colors.GRID} strokeWidth="8" strokeLinecap="round" />
161+
{['data', 'function', 'weight', 'allocation'].map((label, i) => <g key={label} transform={`translate(${330 + i * 205} 360)`}>
162+
<circle r="42" fill="#101827" stroke={[colors.ACCENT, colors.SECONDARY, colors.WARM, colors.POSITIVE][i]} strokeWidth="4" />
163+
<text y="5" textAnchor="middle" fill={colors.TEXT} fontSize="12">{label}</text>
164+
</g>)}
165+
<text x="640" y="474" textAnchor="middle" fill={colors.MUTED} fontSize="16">which protections compose?</text>
166+
</g>
167+
</Camera>;
168+
}
169+
170+
export const vizScene = () => scene;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import { Player } from '../../core';
3+
import { Render, vizScene } from './chapter-2';
4+
5+
function Chapter() { const sc = vizScene(); return <div style={{ padding: '4vh 4vw' }}><Player timeline={sc.tl} loop>{(s) => <Render s={s} />}</Player></div>; }
6+
const meta: Meta<typeof Chapter> = { title: 'Books/The Three Anchors/Chapter 2', component: Chapter };
7+
export default meta;
8+
export const ThreeAnchorsOneUpdate: StoryObj<typeof Chapter> = {};

0 commit comments

Comments
 (0)