The trigger is “I did not know that”, not a calendar.
Here is a real one, from building this site. An agent writing the architecture editor copied
some Svelte state with structuredClone. The browser test crashed with DataCloneError. The fix took a minute, a JSON round trip, and the tests went
green.
The code was fixed. Nobody learned anything. The reason for the crash lived for one minute in a terminal and then scrolled away. That minute is the moment to write a note, while the thing that surprised you is still in front of you. A week later the reasoning is gone and all that is left is a workaround.
The one thing you did not know an hour ago, and the exact moment you found out.
A claim you could disagree with, before anything else.
Every note opens with one sentence, before any heading, that is the note compressed. Not a label and not a summary: a claim. “Notes on structuredClone” is a label. “This note explains cloning in Svelte” is a summary. Writing the claim is where the learning happens, because you cannot write it without deciding what is actually true.
A$stateobject is a Proxy, sostructuredClonethrows aDataCloneErroron it, and the quick escape, a JSON round trip, works only for as long as the data happens to be JSON-shaped.
That sentence is checkable, and most of it was checked: three browser tests measure the DataCloneError and both copies. That a $state object is a Proxy was
read in Svelte’s documentation, and the note says so. One quick test for the sentence itself:
if the claim line and the title say the same thing, the claim line is not finished.
Which one is the claim line?
One is a label, one a summary, one a claim. Pick before you compare.
The tracker only counts visible time, because the Page Visibility API reports hidden tabs and heartbeats stop while hidden.
One sentence, before any heading, that someone could disagree with, and that is neither the title again, a label, nor a summary.
A few short sections, and the kind decides how long it stays true.
The code already says what it does. A note that restates it is wrong the day the code changes, and nothing notices. Write only the part the code cannot say.
- ClaimOne sentence before any heading. The note, compressed into something you could disagree with.
- OriginWhat taught you this. It is how you will remember it.
- WhatWhat this is, for someone who has not read it.
- WhyThe reasoning, and the alternative that lost.
- GotchasOptional. What will bite, when you know it. Often the most valuable part, but a note can ship without it.
- Used inWhere it is actually used. Never omitted: it rots first, so it is what a checker can catch.
Then decide its kind, because the kind is its lifespan. This one is substrate: true of a dependency at a version. So it carries a stamp, “True of Svelte 5.57.0 in Chromium 153 · measured”, because it goes stale when someone else ships and no diff of yours will tell you.
| Kind | True of | Goes stale when |
|---|---|---|
| module | This file | The file changes |
| substrate | A dependency, at a version | Someone else ships, and nothing tells you |
| pattern | This architecture | You change architecture |
| technique | Broadly | Rarely |
| language | This language | A language release |
| concept | The domain | The domain changes |
The finished noteSubstrate · measured, one clause read
# structuredClone cannot copy Svelte state; take a snapshot first
A `$state` object is a Proxy, so `structuredClone` throws a `DataCloneError` on it, and the quick escape, a JSON round trip, works only for as long as the data happens to be JSON-shaped.
**True of Svelte 5.57.0 in Chromium 153 · measured 2026-09-12**: three browser tests measure the `DataCloneError` and both copies. That a `$state` object is a Proxy is read in Svelte’s documentation, not measured.
**Origin: a crash fixed in a minute, and not understood.** An architecture editor copied its declaration with `structuredClone` before changing it. The browser test died with `DataCloneError`. The fix that went in was `JSON.parse(JSON.stringify(…))`, the tests went green, and nothing about why was kept.
## What
`$state({…})` returns a deeply reactive Proxy over the object. Structured cloning copies plain data and refuses a Proxy; in the browser that throws `DOMException: DataCloneError`. `$state.snapshot` walks the proxy and returns ordinary data.
```
structuredClone(state) throws DataCloneError
$state.snapshot(state) a separate copy · keeps Date, Map, an undefined field
structuredClone(snapshot) works
JSON.parse(JSON.stringify(s)) a copy · drops undefined · Date becomes a string · Map becomes {}
```
## Why the JSON fix was the wrong one to keep
It passed because the editor's declaration held only strings, arrays and plain objects. The day a field holds a `Date`, a `Map`, or an `undefined` that means something, the copy changes the data and nothing fails. Writing this note meant either defending that shortcut or fixing it, so the editor now snapshots at the call site and clones plain data.
## Gotchas
- **Runes exist only in `.svelte` and `.svelte.ts` files.** A plain `.ts` helper cannot call `$state.snapshot`, so snapshot where the state lives and pass the result in.
- **`$state(…)` must initialize a declaration.** `return $state({…})` does not compile; declare it, then return it.
- **A snapshot honors `toJSON`.** If the state has one, the snapshot clones what `toJSON` returns instead.
## Used in
- `src/lib/features/architecture-as-rules/ArchitectureEditor.svelte`, which snapshots before every edit.
- `src/lib/content/lessons/notes-protocol/examples/probe/state-copy.svelte.spec.ts`, the measurement.
## Related
- [[svelte-state-is-a-proxy]]
Does this note meet the rules, and what is its claim?
It starts as the note most people would write after that crash. Edit it, or load the finished one, and watch the rules and the claim line update. Nothing is saved.
This note is about structuredClone and Svelte state.
Title: structuredClone and Svelte state5 of 10 rules met
- Met · title
A note starts with a # title.
- Met · sentence before any heading
One full sentence before any heading. The next three rules check that it is a claim.
- Met · claim is not the title
If the claim line and the title say the same thing, the claim line is not finished.
- Missing · claim is not a label
“Notes on zero values” is a label. Say what is true about them.
- Met · claim is not a summary
“This note explains zero values” is a summary. Say the thing it explains, as a sentence you could be wrong about.
- Met · what
A What section: what this is, for someone who has not read it.
- Missing · why
A Why section: the reasoning, and the alternative that lost.
- Missing · used in
A Used in section, never omitted: it is the part that rots first and the part a checker can catch.
- Missing · origin
An Origin line: what taught you this. It is how you will remember it.
- Missing · version stamp
A substrate note says which version it is true of, and whether that was measured or only read.
These are the structural rules a checker can hold. Whether the claim is true, and whether the code could have said it for you, is still a person’s call.
The measurement behind itThree tests in a real browser
import { describe, expect, it } from 'vitest';
import { makeState, snapshot } from './state-copy.svelte';
// The measurements behind the note on copying Svelte state. Runs in a real browser,
// because structuredClone and its DataCloneError are the browser's, not Node's.
describe('copying Svelte 5 $state', () => {
it('structuredClone refuses a state proxy with a DataCloneError', () => {
const state = makeState();
let error: unknown;
try {
structuredClone(state);
} catch (caught) {
error = caught;
}
expect(
error,
'a $state object is a Proxy, which structured cloning cannot copy'
).toBeInstanceOf(DOMException);
expect((error as DOMException).name).toBe('DataCloneError');
});
it('$state.snapshot returns a separate plain copy that keeps a Date, a Map and an undefined field', () => {
const state = makeState();
const copy = snapshot(state);
expect(copy).not.toBe(state);
expect(copy.seenAt).toBeInstanceOf(Date);
expect(copy.cache).toBeInstanceOf(Map);
expect('maybe' in copy).toBe(true);
const cloned = structuredClone(copy);
expect(cloned.cache.get('k'), 'a snapshot is plain data, so structuredClone accepts it').toBe(
1
);
});
it('a JSON round trip copies the proxy but drops undefined, turns a Date into a string and a Map into {}', () => {
const copy = JSON.parse(JSON.stringify(makeState()));
expect('maybe' in copy).toBe(false);
expect(typeof copy.seenAt).toBe('string');
expect(copy.cache).toEqual({});
});
});
$ npx vitest run --project client src/lib/content/lessons/notes-protocol/examples/probe/ # Svelte 5.57.0 · HeadlessChrome 153.0.8010.12 · 12 Sep 2026
RUN v4.1.11 /Users/sj/Desktop/dev/builds/heyrian
✓ |client (chromium)| src/lib/content/lessons/notes-protocol/examples/probe/state-copy.svelte.spec.ts:7:2 > copying Svelte 5 $state > structuredClone refuses a state proxy with a DataCloneError 1ms
✓ |client (chromium)| src/lib/content/lessons/notes-protocol/examples/probe/state-copy.svelte.spec.ts:22:2 > copying Svelte 5 $state > $state.snapshot returns a separate plain copy that keeps a Date, a Map and an undefined field 1ms
✓ |client (chromium)| src/lib/content/lessons/notes-protocol/examples/probe/state-copy.svelte.spec.ts:35:2 > copying Svelte 5 $state > a JSON round trip copies the proxy but drops undefined, turns a Date into a string and a Map into {} 0ms
Test Files 1 passed (1)
Tests 3 passed (3)
$ echo $?
0
A note with a claim line, a What, a Why, a Used in, an Origin line, and a kind that says when to doubt it. Gotchas when you have them.
Writing it found that the fix was a shortcut.
The Why section would not write. The honest version said the JSON copy works only because the data has no dates, maps, or meaningful undefined fields yet. A note that exists to defend a shortcut is the worst kind: either fix the code or record the condition that would make you revisit it.
So the code changed. The editor now takes a snapshot where the state lives and copies plain data. That is the quiet benefit of the habit. A shape you cannot explain in two sentences is usually a shape that is wrong.
// editor.ts: plain data in, a plain copy out
// Plain data in, a plain copy out. A Svelte $state object is a Proxy that structuredClone
// refuses, so the component takes $state.snapshot at the call site, where runes exist.
export const editable = (architecture: Architecture): Architecture => structuredClone(architecture);
// ArchitectureEditor.svelte: snapshot where the runes live
onchange={() => (architecture = toggleShared($state.snapshot(architecture), tier.id))} Either a fix, or the condition that would make you revisit the shortcut, written in the note’s Why.
The agent can file notes. The claim is yours to write.
This is what keeps it from becoming a second job. Let the agent do the parts that are clerical: at the end of a session, without being asked, it drafts or extends the note for the file it touched, fills in Used in, and adds the links.
Keep one thing for yourself. A note meant to outlive the project, a technique or a substrate note, makes a claim someone will act on in a different codebase. An agent that saw one package should propose that claim, not write it. Reading its proposal and writing the sentence yourself is five minutes, and it is the five minutes where you learned something.
Drafts module notes, fills in Used in, links notes both ways, and lists the lasting claims this work has earned.
Write or rewrite each lasting claim line, and delete the notes that only restate the code.
The agent’s half is one instruction in the file it reads before it starts. Here is one you could paste. It is an example written for this page, not a recording of the one this site uses.
At the end of every session, without being asked:
- For each file you changed, draft or extend its module note: a claim line first,
then What, Why, and Used in. Fill in Used in and link related notes both ways.
- List the lasting claims this work has earned, as proposals. Do not write a
technique or substrate claim line yourself; I write those. A standing instruction that makes the agent file notes, and a rule that lasting claim lines are yours.
Review a hundred sentences, not a hundred notes.
A pile of notes rarely gets re-read. So the claim lines are pulled into one generated list, and review is reading that list: the sentences you do not recognize are the notes to open. The extraction is a few lines: the first prose line after the title, before any heading.
/** The first prose line after the title, before any heading. Markdown emphasis removed. */
export function claimOf(source: string): string | null {
const after = source.split(/^#\s+.+$/m)[1] ?? '';
for (const raw of after.split('\n')) {
const line = raw.trim();
if (!line) continue;
if (line.startsWith('#')) return null;
if (line.startsWith('>') || line.startsWith('```') || line.startsWith('|')) continue;
return line
.replace(/\*\*/g, '')
.replace(/`/g, '')
.replace(/^WORKING\s*[—:-]\s*[^.]*\.\s*/, '');
}
return null;
} These are the notes this lesson carries, as that list shows them.
- substrate
os.tmpdir() hands you /var/folders/… while anything that resolves the path reports /private/var/folders/…, and because the short spelling sits inside the long one, redacting it alone leaves a stray /private behind.
a-macos-temp-directory-has-two-spellings - substrate
Without svelte/compiler resolvable from dependency-cruiser's own install, a cruise still lists every .svelte file and silently drops the components those files import, so a layering rule about components passes by seeing nothing.
dependency-cruiser-reads-svelte-files-without-their-imports - substrate
A $state object is a Proxy, so structuredClone throws a DataCloneError on it, and the quick escape, a JSON round trip, works only for as long as the data happens to be JSON-shaped.
structuredclone-cannot-copy-svelte-state - technique · working
An agent given every tool treats a procedure installed in its environment as part of its instruction, so the environment changes what "write tests" means.
an-installed-skill-is-part-of-the-prompt
One is marked working: it rests on a single observation, so it says so. A working note that nobody revisits becomes a rule by silence. The other half of coming back is retrieval, and it is indexed by intent: “I am about to copy reactive state” should lead straight to the note above.
The other notesThree more, from building the enforcement and spec lessons
# A macOS temp directory has two spellings
`os.tmpdir()` hands you `/var/folders/…` while anything that resolves the path reports `/private/var/folders/…`, and because the short spelling sits inside the long one, redacting it alone leaves a stray `/private` behind.
**True of macOS 26.3 with Node 22.21.1 · measured 2026-09-12.**
**Origin: a redaction that looked like it worked.** A script replaced a temp directory's path in recorded test output. The output read `RUN v4.1.11 /private<run>`: the replace had matched inside the longer spelling the test runner printed, and left its prefix.
## What
`/var` is a symbolic link to `private/var`. `mkdtemp` under `os.tmpdir()` returns the unresolved form. `realpath`, `pwd -P`, and any process that resolves its working directory report the `/private` form.
```
os.tmpdir() /var/folders/_t/…/T
mkdtemp result /var/folders/_t/…/T/probe-EYmqlI
realpathSync(result) /private/var/folders/_t/…/T/probe-EYmqlI
```
Redacting a line that holds both spellings:
```
short spelling only RUN … /private<run> and <run>
long spelling only the short spelling is left untouched
short, then long RUN … /private<run> and <run>
long, then short RUN … <run> and <run>
```
## Why the order matters
The short spelling is a substring of the long one. Replace it first and the long spelling no longer exists to be matched, so the second replace finds nothing and the prefix stays. Nothing errors, and the output looks redacted at a glance.
## Gotchas
- **Redact the resolved spelling first, then the short one.** `realpathSync(dir)`, then `dir`.
- **Compare paths after resolving both sides.** Two correct paths to the same folder are unequal as strings.
## Used in
- `src/lib/content/lessons/spec-before-code/examples/evidence/run-against.mjs.txt`, the recording runner, which redacts in that order.
## Related
- [[redaction-must-match-what-the-tool-prints]]
# dependency-cruiser reads a Svelte file without its imports unless Svelte sits beside it
Without `svelte/compiler` resolvable from dependency-cruiser's own install, a cruise still lists every `.svelte` file and silently drops the components those files import, so a layering rule about components passes by seeing nothing.
**True of dependency-cruiser 18.2.0 with Svelte 5.57.0 · measured 2026-09-12**: counted in recorded runs of the same tree, and the cause read in the tool's source.
**Origin: a green run I nearly used as evidence.** A miniature Svelte frontend reported 12 modules, 11 dependencies and no violations. With Svelte installed next to the tool, the same tree reported 14 modules and 25 dependencies, and with an agent's change applied, 26 dependencies and the component import it existed to catch, exit 1.
## What
dependency-cruiser does not parse `.svelte` itself. `src/extract/transpile/svelte-wrap.mjs` imports `svelte/compiler` with a try-import and, when that fails, marks the Svelte transpiler unavailable. The file is still collected as a module. What it imports is not.
```
without svelte/compiler beside the tool 12 modules · 11 dependencies · 0 violations
with it, before the change 14 modules · 25 dependencies · 0 violations
with it, after the change 14 modules · 26 dependencies · 1 violation · exit 1
```
## Why it passes silently
The tool warns loudly when TypeScript is missing. For Svelte it printed nothing. The exit code was 0 and the summary line looked like every other clean run. The only tell was the dependency count, which nobody reads unless they already suspect it.
## Gotchas
- **Installed in the project is not the same as beside the tool.** Running a copy of the tool from a scratch directory, the project's own Svelte was not resolvable from there.
- **Read the counts with the verdict.** A rule that fires on nothing and a rule that has nothing to fire on print the same line.
## Used in
- `src/lib/content/lessons/enforcement-layer/examples/README.md`, where the recorded runs live.
## Related
- [[a-check-that-can-pass-by-seeing-nothing]]
# An installed skill is part of the prompt
**WORKING: one observation.** An agent given every tool treats a procedure installed in its environment as part of its instruction, so the environment changes what "write tests" means.
**Origin: an agent that did more than it was asked.** Asked to implement a function and write its tests, an agent loaded the spec-tests skill installed on this machine, delegated the tests to a write-only writer, and ran its own mutation pass. Nothing in the ticket mentioned any of that.
## What
A skill or agent definition available to a session is visible to the model, and a capable model uses what fits the task. The ticket is not the whole instruction. The installed procedures are the rest of it.
## Why it matters
Two runs with the same ticket can differ because one machine has a skill installed. When comparing agents, or recording a run as evidence, the environment belongs in the record next to the prompt.
## Gotchas
- **It can help and still break an experiment.** The delegated tests were good. They were also not the author-written suite the comparison needed.
- **A self-report inherits the procedure's vocabulary.** The agent reported its own mutation score in the protocol's terms, which made a self-assessment read like an independent one.
## Used in
- `src/lib/content/lessons/spec-before-code/examples/evidence/spec-run/run-notes.md`
## Related
- [[a-self-report-is-a-receipt]]
The smallest note worth keeping.
- Claim
- The one sentence you could be wrong about.
- Origin
- The moment it surprised you.
- What
- What this is, for someone who has not read the note.
- Why
- What the code cannot say, and the option that lost.
- Used in
- Where it lives, so you notice when it moves.
Gotchas is optional; the checker above does not require it. Every note on this page comes from building this area. The crash happened while making the Architecture as rules editor, and its note was written up afterward, for this lesson, from that incident. The other three came out of making Enforcement layer and Spec before code.
A claim index you read, not a folder of notes you mean to reread.
Explain the habit without saying “notes protocol.”
“When something surprises me, I write one sentence I could be wrong about and the few lines the code cannot say. The agent does the filing. Later I read the sentences, not the code.” That is the habit. The name is only what you call it.
Before moving on, explain three things without the name: why the claim line comes before any heading, why a substrate note carries a version stamp, and why the agent may file a note but should not write its lasting claim. Then find the last thing that surprised you this week and write its claim line.
Connections to follow nextRelated lessons
- Draft directory is the other half of staying sharp: type the agent’s finished file, and note what the typing taught.
- Spec before code asks the same question of tests that the claim line asks of a note: what could say no?
- Architecture as rules holds the editor whose crash became this lesson’s note.
- Enforcement layer recorded the runs behind the dependency-cruiser note, and a check that can pass by seeing nothing.