mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
054b192e5d
Closes BUG-2813's old-binary half. S1 put the rule in the binary, which cannot help in the window this bug is about: an older binary serving the same SQLite file has no guard, so a rollback, a staged rollout or a second instance writes rows the invariant forbids. A trigger is enforced by the FILE. 194 BEFORE INSERT/UPDATE triggers over 97 columns — TASK-2825's 86-column census plus the second ring Ruling 2 admitted wholesale. ONE LIST, THREE CONSUMERS, which is the shape TASK-2825 asked for. The migration is GENERATED from internal/store/nulcolumns.go, a guard test compares that list against the LIVE migrated schema, and a second test pins the generated SQL against the list so neither can drift from the other by hand. The census guard is a BASELINE rather than a demand that all 405 text columns be classified. The census classified the 86 that can carry caller text and left ids, timestamps and hashes unenumerated; requiring an entry for each would be 300 lines nobody reads. The 301 known-outside columns are recorded, and ANY change to that set — added, renamed, removed — fails and asks for a decision. The predicate is TASK-2824's, measured on the driver Pad actually embeds: instr for the raw byte (length() C-truncates and cannot be used), and json_tree for the decoded escape, in values AND keys, with a json_valid guard because json_tree raises on a non-document. The doubled-backslash case stays literal, which is the false positive this predicate family exists to avoid. Trigger aborts are classified into the SAME typed error Layer A produces, so a caller cannot tell which layer refused and the handler's existing 400 mapping covers both. That discharges Ruling 2's condition for admitting the second ring: a header-derived user agent or IP hitting a trigger must not surface as a 500 or a broken login. THE FOURTH LEG IS LIT. S1 built the differential harness with three legs and left this one dark; all 17 corpus cases now agree across the HTTP gate, Layer A at the driver, Layer B via a real UNGUARDED SQLite write, and native Postgres. Four independent enforcers, one corpus — the property DOC-2823 named as the actual deliverable. Two consumers needed changes, both legitimate. The FTS trigger census excludes the new triggers by prefix, so adding a protected column does not require editing that test. And a timeline test that INJECTS a NUL to build its fixture now brackets the injection by dropping and restoring the two triggers — it needs a row that violates the invariant, because what it tests is the handling of legacy data, and Layer B exists to stop such a row being written. SQLite only. Postgres refuses a NUL in text natively and an escape decoding to one in jsonb, so it already owns the rule there. Full Go suite green on SQLite and Postgres 17; lint 0 issues. Claude-Session: https://claude.ai/code/session_01XLtX4dbjBpApbAv3SuBcTm