Review CODE-REVIEW-220-r1.
H1: markersNeedingPlacement decided who depends on the order by reading
marker.area alone, while resolveExplicitMarkerPlacement reads
`marker.area || <area of the HA device>`. The ordinary marker — bind an HA
device, store neither field — is anchored by the registry and never depended
on the order, yet it was being written a space it never asked for. Dormant
today, and the day that HA area changes it moves the marker to whatever space
used to be first. The resolver now asks for the area actually in force.
M1: a mouse released past the panel left the gesture stuck, swallowing the
next click. Pointer capture is the usual answer and is now taken, but it is
not a guarantee — the browser grants it only for a live pointer. The window
listener is what actually closes the gesture.
M2: the fifth mutant from the spec is registered, plus a sixth for the stuck
drag above.
Writing the smoke for M1 turned up why the first attempt passed against
broken code: synthetic PointerEvents default to composed:false and never
leave the shadow root, so nothing outside the panel could ever hear them.
Real pointer events are composed; the smoke now says so.
Issue: #220
User-Visible: no
The order of config.spaces used to be whatever order the spaces were created
in, and there was no way back other than deleting a space and drawing it
again.
The gesture is deliberately narrow — mouse, editors only. The same tabs are
the primary way to switch spaces in View, where touch is first class, so a
drag there would compete with the tap that switches. Recorded in the spec as
"Touch editor: not exposed".
The part that needed care is not the drag. Position in the array feeds three
things: the marker placement fallback, the swipe neighbour and a positional
`floor`. So the write that stores the new order also writes down the
placement that used to depend on it: a marker with neither an explicit space
nor an area that names one gets the space it has right now. Both changes go in
one save; splitting them would leave a window in which markers move on their
own. The positional `floor` cannot be fixed from here, so the card says so
once.
Issue: #220
User-Visible: yes
The assumptions section is explicitly labelled "technical, free to change",
and it held a requirement that AC3 and a mutant already test as a fact. Read
literally, it invited splitting the write in two — reopening the very window
in which markers move. The point now states the opposite: everything else in
that section is free, this one is normative and lives in section 8.3.
Issue: #220
User-Visible: no
M1: the spec now carries the touch classification TOUCH-SUPPORT.md asks every
editor feature for — "Touch editor: not exposed", with the reason it is a
decision rather than an omission.
M2: the first draft denied adding a config field in one section while planning
to store an anchor in settings in another. Resolved by dropping the anchor:
reordering materialises the placement that was implicit, giving those markers
an explicit space in the same write. No new field, no schema change, and the
marker stays exactly where the user saw it.
Issue: #220
User-Visible: no
Written on the owner's product decisions of 2026-08-20: mouse only and only in
the editor modes, one warning about the positional `floor` from #210, no
keyboard alternative.
The spec carries the part that is easy to miss — the order of `config.spaces`
is not decoration. It feeds the marker placement fallback, the swipe
neighbour and the numeric `floor`, so reordering tabs must not move a single
marker. That is a named acceptance criterion with a mutant behind it.
Issue: #220
User-Visible: no
Import of a backup holding PDF attachments: the content resolver parses a url
as a url, and the three mutants guarding it are registered. The user-visible
change is documented in 4a84734, which carries both changelog entries — this
merge adds no behaviour of its own.
Code review r2 green (docs/reviews/CODE-REVIEW-225-r2.md). The third pass was
a rebase over #226, not a fix — owner arbitration on the review-4 the cycle
counter raised for it (PROCESS.md §4; counter defect filed as #227).
Issue: #225
User-Visible: no
Review CODE-REVIEW-225-r1.
M1: urlsplit(url).path was trusted even when the url carried a scheme or an
authority, so "https://evil.example/houseplan_files/files/m1/doc.pdf"
resolved onto a local file while _looks_internal kept calling it external —
the mirror image of the inconsistency this resolver exists to prevent. Only a
same-document reference is resolved by its path now.
M2: the three mutants the spec described are registered in
scripts/mutation-gate.mjs instead of living as a one-off manual run. The
traversal entry drops both structural checks at once on purpose: taken one at
a time the defence is layered (sanitize_marker_id turns ".." into "misc") and
the mutant would be equivalent — established by running it.
Issue: #225
User-Visible: no
A backup holding a PDF attachment could not be imported back: legacy links
carry a cache-buster (".../files/m1/doc.pdf?v=1783170649"), and the resolver
compared the raw tail with its sanitized form, so the query made the name
differ from itself. The reference then read as internal by prefix and
non-canonical by name, which is exactly the combination _content_state must
refuse — every such document failed with invalid_content.
Parse the url as a url: the path addresses the file, the query and the
fragment address the transfer. Path segments keep doing the guarding, so
dropping the query cannot widen what a segment is allowed to be.
Issue: #225
User-Visible: yes