Files
pad/internal/server
xarmian 178b6b5010 fix(server,store): two more from codex rounds 3 and 4 (BUG-2810)
**The import repair could silently change what gets imported.** It decodes into
map[string]any, where a repeated object member keeps only the LAST value. The
TYPED decode that runs next does not agree: encoding/json unmarshals members in
order into the same struct field, so two `"workspace"` objects MERGE there and
collapse here. A body with duplicate members would therefore import differently
with --repair-nul than without, which is outside what a flag by that name may
do.

It now DECLINES such a body: returns it untouched, lets the gate judge it
exactly as it would without the flag, and says why in the refusal — "the
payload repeats the member X, and repairing it would change which value is
imported". Detection is a token walk, because a decode is what loses the
information: by the time there is a map the duplicate is gone. The detector's
own test carries the false positive that matters — the same member name in
SIBLING objects is not a duplicate, and a single shared set of names would
decline every real export, since items all carry `id`, `title`, `slug`.

Rewriting such a body faithfully wants a token-preserving pass, which is
BUG-2812's token-walk and not a rider on this. A real export cannot contain
duplicate members (json.Marshal does not emit them), so declining costs nothing
an operator meets by accident.

The tally now owns the repair — decodeJSONRepairingNUL takes it and calls
Apply — so the count and the declined reason come back through one object
instead of a return value a caller has to remember to record. That is the same
mistake this branch already made once, when the JSON path dropped the count and
the header reported 0 for an import that had rewritten a value.

**A row the repair could not address was reported as a failure.** A NUL in a
key column the list does not protect, on a row whose violation is elsewhere,
makes the address unbindable: Layer A inspects every bound parameter, including
a WHERE clause's, so the lookup is refused before SQLite is asked to find the
row. It landed in Failed carrying "invalid text parameter: parameter 2" — the
same information phrased as a fault in the repair rather than a property of the
row. Now detected up front and reported as a skip with the reason, alongside
the two skips that already existed.

**One finding NOT fixed, deliberately, and recorded instead.** Round 3 raised
that the scan misses a NUL in a value shadowed by a LITERAL duplicate key, so
such a row passes the migrate-to-pg preflight and then fails during the copy —
the exact failure the preflight replaces, surviving for one shape. That is
textguard.KnownGaps: a blind spot every layer shares on purpose, which DOC-2823
forbids closing in one layer alone, because layers disagreeing about one value
is the defect this cluster is made of. So it is named in ScanNUL's doc comment,
in the preflight's, and in docs/backup.md for the operator, and
TestScanNULInheritsTheRecordedKnownGaps pins the miss and FAILS when it stops
being one — the notification that BUG-2812 has landed and those three prose
sites need updating. The consequence is recorded on BUG-2812's trail.

Round 2's single finding was refuted rather than fixed: it predicted
TestRepairFlagReachesTheNestedAndObliqueForms would fail, on a mechanism that
describes the raw-byte scanner this branch had already replaced. The test
passes; the outer decode resolves the oblique spelling before the walk sees it.
2026-09-02 16:24:25 +00:00
..