Text storage always equals rawSource; rendering is attribute-only. Never insert/delete display characters — hide delimiters, never strip them. | Display offset == raw offset (identity mapping) is what every selection, sync, and heal path assumes. Break it and every later edit drifts. | The delete-drift saga: six rounds over months, each recurrence traced to storage/rawSource divergence in some path. docs/investigations/delete-drift-investigation.md. |
TextKit 2 only. Never touch NSTextView.layoutManager; never store NSTextBlock/NSTextTable attributes. | Either one silently and permanently reverts the view to TextKit 1. A DEBUG tripwire asserts if TK1 engages — heed it. | ARCHITECTURE §2; the tripwire exists because the reversion is otherwise invisible. |
No NSTextAttachment. Images/icons are drawn as overlays. | TK2 only honors attachments on U+FFFC, which rawSource never contains (see rule 1). | ARCHITECTURE §2, §5. |
Never draw images on wrapping (multi-line) fragments; use stroked CGPaths instead. | A TK2 image on a wrapping fragment wedges layout — collapses the fragment to one line. Shapes don't trigger it. | The callout custom-title icon: docs/investigations/archives/callout-title-wrap-investigation.md; fix in ae61644 (stroked path, not image). |
Every storage-touching styling path guards !hasMarkedText() — including async paths scheduled before composition began. | Mutating storage mid-IME-composition strands the marked text; didChangeText then bails forever on its own guard and every later edit drifts. | Delete-drift rounds 1–2 (IME stranding cascade). docs/investigations/delete-drift-investigation.md. |
Never assume didChangeText follows every edit. Sync paths must survive a bypassed edit. | AppKit's drag-move-to-nowhere deletes via shouldChangeText → replaceCharacters and never calls didChangeText, silently freezing rawSource. The heal (scheduleBypassedEditSyncCheck in +EditFlow) exists for this. | Delete-drift round 4 (9f99795); rounds 5–6 hardened the heal itself. |
Attribute-only restyles that change geometry must invalidateLayout(for:) the range. | TK2 does not re-measure on attribute change; the fragment keeps a stale frame — empty bands, clipped lines. | ARCHITECTURE §8; recomposeDirty and the idle drain already do this — new paths must too. |
Undo restore is diff-based recomposeReplacing — never a full recompose. | Full recompose resets every fragment to a TK2 height estimate; the follow-up scroll lands wrong and the viewport drifts. | Undo/redo viewport drift — one of the costliest failures here. Fixed in 5bb2b40 (fix(undo): select + center the changed text; diff-based snapshot restore). |
Never blanket pkill -x edmd. pgrep -x edmd first, check start times, kill only the PID you launched. | The maintainer's daily-driver app shares the binary name. A blanket pkill kills their editor with their work in it. (ARCHITECTURE §1's pkill -x edmd shorthand predates this rule — don't copy it.) | Established after the maintainer's own instance was killed during a debugging session. |
| Never request macOS Computer Access (Screen Recording / Accessibility). | Both are already granted to the tools you use. Requesting again re-prompts the maintainer and can wedge TCC state. | CLAUDE.md "Environment"; the -debug.reproScript driver exists precisely so repros need no new TCC grants. |
| Visual judgments ("balance the padding", "is it centered") are MEASURED from screencapture pixels, not eyeballed. | Eyeballed "looks right" repeatedly shipped asymmetric spacing. Crop the window, count pixels, state the numbers. | Maintainer's explicit rule from UI-polish rounds (status-bar / table-padding branches). |
Files in test-files/ are the maintainer's manual test corpus. Never rewrite them for automation; test-files/todo.md especially is owner-edited. | They encode the maintainer's by-hand regression walk. Automation churn destroys that. Create your own fixtures in a scratch dir or Tests/. | Standing maintainer rule. |
| Never ship a fix for a live-input-layer bug (caret, IME, drag, selection timing) on reasoning alone. A frozen repro script must falsify the bug before and confirm the fix after. | This bug class does not reproduce headless (the test harness runs TK2's queued fixup synchronously). Reasoning about deferred AppKit machinery has a ~0% shipping record here. | Delete-drift rounds 1–5 each shipped a plausible fix; each came back. Round 6 finally held because the ReproScript driver reproduced the drift deterministically first. docs/dev-guides/live-repro-guide.md. |