Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
4 KiB
| name | description | metadata | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| schunix-qr-encoder | kernel/core/qr.c — the crash-report QR encoder, how it's verified, and the segno padding bug that is NOT ours. |
|
Built 2026-08-08. kernel/core/qr.c + include/qr.h, the Phase 8 crash channel
(see schunix-crash-screen and docs/roadmap.md).
Wired into the crash screen the same day. panic.c builds a compact payload
and paints a white card on the right of PSOGM. Verified end-to-end with
make crashshot && make crashqr, which decodes the symbol out of the captured
framebuffer — not out of the encoder. Payload format SXPSOGM1: magic, reason,
c<cpu> p<pid> t<ticks> <name>, b<kernel base>, then call-chain frames as
offsets from the kernel base (8 hex digits, not 16 — that is what makes a
useful number of frames fit in 134 bytes). The reason is written against a
buffer end held back by 34 bytes, so a long message costs frames but never all
of them. Deliberately not a URL: it encodes the report itself and reaches
nothing.
Scope, deliberately narrow: byte mode only, EC level L only, versions 1–6 only (max 134 bytes). Version 7 is where QR starts needing an 18-bit version-information block in two extra places; version 10 is where the character count grows to 16 bits and the alignment grid becomes 2-D. Stopping at 6 avoids both. If a payload ever needs more, shorten the report rather than add the tables. Freestanding: no allocation, no libc beyond memset/memmove, GF(256) multiply by shift-and-add so there is no table init to depend on — all of which matters because the caller is a panic handler.
How it is verified — this is the important part. A wrong QR still looks exactly like a QR, so "it renders" proves nothing.
make qrtest— self-checks (table arithmetic, RS generator polynomials, finder/timing placement) and writesbuild/qr.pngplus a terminal rendering a phone can read straight off the screen.-qsuppresses the ANSI art,-m TEXTdumps the module matrix,-f TEXTdumps the function-pattern map.make qrcheck(tools/qrcheck.py) — the real oracle. Decodes with OpenCV, and compares module-for-module against segno. Needspip install --target <dir> opencv-python-headless numpy segnoand PYTHONPATH; not wired intomake.
Two traps found doing this, both worth remembering:
- segno adds a spurious zero pad codeword to every byte-mode symbol. Its
write_padding_bits()doesextend([0] * (8 - (length % 8))), which emits a whole byte when the stream is already codeword-aligned — and in byte mode it always is (8L+16 bits after the terminator). ISO/IEC 18004 makes it conditional and segno quotes that exact sentence above the line. Harmless (decoders readcountbytes and ignore padding) but it means the two encoders cannot agree bit-for-bit on any padded payload. qrcheck compares full matrices only for exact-fill payloads and decodes the rest. Our behaviour is correct; do not "fix" it to match. - OpenCV's QRCodeDetector fails on perfectly valid symbols, depending on
scale and quiet-zone width. qrcheck tries six renderings, and if one still
fails it re-tests segno's symbol for the same payload — if that fails too it
reports
[detector], not[FAIL]. Without that step the check produces false failures, which is worse than not running it.
Debugging technique that cracked it: dump the module matrix and the function map, run the zigzag backwards in Python to recover the codewords from both encoders, and diff those. "First differing codeword index 18" localised it instantly; comparing matrices only ever said "108 of 441 modules differ". Note the mask must be read from the format bits and un-applied first — un-masking with the wrong mask makes your own output look like noise, which cost a round.
Windows note: subprocess cannot launch a relative path with forward slashes;
os.path.abspath it.