introspect-dev.mjs: guard that asserts every view body literal in schema.ts still matches pg_get_viewdef
MEASURED by db03 2026-09-19, DEV, drizzle-kit 0.31.4: of 109 views in public+rutap, 633 single-quoted literals taken from pg_get_viewdef(oid,true) and searched as substrings in the generated schema.ts body, 27 do NOT appear, across 3 views. (1) v_monitor_historial_anomalias loses an interior ';' inside a label string - the installed view has it in DEV and PROD, the mirror does not, and the TERMINATING ';' survives, so the splitter does not respect string literals. (2) v_rutap_rows_norm and v_rutap_latest_norm differ in 26 literals of the form ' \t\n' / '[\n]' - regex whitespace classes whose representation changes; whether meaning is preserved was NOT evaluated and that is the part worth measuring first. The other 106 views: 0 mismatches. Limits of that measurement: literals only, structure and non-literal text not compared. WHAT TO BUILD: a check inside tools/introspect-dev.mjs that, after drizzle-kit generates, compares each view body's literals against pg_get_viewdef and REPORTS the mismatches. It must NOT repair them. A post-process that re-inserts text into a generated string is armed against every future edit of that object and fails CLOSED - that is exactly the ux_carrocerias patch deleted in d45b8483 two days after it broke introspect for an unrelated DDL change. The instrument is an assertion, not a patch. DESIGN NOTES: the 26 whitespace-class literals are known and would drown the signal, so they need a named allowlist with the reason written next to them, and the allowlist itself has to be checked for entries that no longer mismatch (a stale allowlist hides the next real case). The guard must be seen to FAIL before it is trusted: force a mismatch against the current mirror and watch it fire, per the standing rule that a control never seen failing is a green line and not a control. Introspect already holds a DEV connection, so no new credential path. WHY IT MATTERS: the loss is silent, so the mirror keeps asserting a body it does not have. The standing rule (pg_get_viewdef is the only source for an installed body; schema.ts is a TYPES mirror) is now written in docs/reference/view-catalog.md by db03 in 8524794c - this ticket is the mechanical check behind that rule, because a convention loses to the primitive every time.
Activity
-
CONTRADICTION between the two blast-radius measurements, and it is not a conflict - the instruments have different blind spots. db03: 633 single-quoted literals from pg_get_viewdef searched as substrings in the mirror body -> 27 missing across 3 views (1 semicolon + 26 whitespace-class literals in v_rutap_rows_norm/v_rutap_latest_norm). db02: whole bodies, WHITESPACE-COLLAPSED and trailing-semicolon-stripped, compared char by char -> 1 differing body out of 122, the semicolon. Both are correct on their own terms: db02's normalization collapses whitespace, which is precisely the dimension in which db03's 26 literals differ, so that method CANNOT see them. Therefore db02's conclusion 'nothing that changes MEANING across the entire corpus' is NOT established for those 26 - it is an artifact of the normalizer, and a re-encoded regex whitespace class ' \t\n' -> '[\n]' is exactly the kind of change that alters matching behaviour while surviving a whitespace-insensitive diff. OPEN and unmeasured: do the 26 preserve semantics. The guard must compare literals WITHOUT whitespace normalization, or it inherits db02's blind spot. Also carried over from db02, and it generalizes past this ticket: his first run used the DIRTY working copy of drizzle/schema.ts and reported 5 differing bodies, 4 of which were pure staleness that LOOKED like drizzle-kit dropping expressions. A dirty shared checkout does not only corrupt what you execute, it fabricates findings when you only READ it - any mirror-vs-DB comparison names the COMMIT it read, never the path.
-
DESIGN INPUT from db02 (2026-09-19), folded in, and it changes how the guard must compare. (1) THE ESCAPE TRAP: the question is whether the mirror SOURCE holds a two-character backslash-t or a real tab byte. Inside a TS template literal a two-character backslash-t evaluates to a tab at load time, so it is MEANING-EQUAL to a real tab in the DB; a doubled backslash would not be. That distinction is invisible in a grep or a terminal printout - it has to be read as bytes (od -c on the extracted literal, or the escape count in the parsed AST). A guard that compares the printed form will call meaning-equal literals different, or the reverse, and be believed either way. (2) THE INSTRUMENT: do NOT whitespace-normalise the whole body in either direction. Split each body into string literals and non-literal text, whitespace-normalise ONLY the non-literal parts, and compare literals byte-exact AFTER evaluating TS escapes. That answers both failure modes in one pass and hides neither - normalising everything hid the 26, normalising nothing would drown the signal in deparse noise. db02's scratchpad mirror_blast.py already does the body parse and the per-relation diff; the literal split is the piece it lacks. db02 also withdrew the headline 'nothing that changes meaning across the corpus' as unsupported by his own method, and restated it correctly as 'no difference OUTSIDE whitespace' - which still rules out truncation, reordering and rewritten expressions, the original blast-radius question.
-
SETTLED, and it REVERSES the proposed disposition. db03 read the bytes (committed blob, drizzle-kit 0.31.4): the DB literals hold REAL tab and LF bytes - btrim(x, ' <TAB><LF>'::text || chr(160)) in v_rutap_rows_norm (12) and v_rutap_latest_norm (12), plus '[<LF>]' (2). The mirror body contains ZERO tab bytes and ZERO LF bytes; the same btrim reads btrim(x, ' '::text || chr(160)). So it is not an escape question - the bytes are gone, not escaped - and the installed view trims space+TAB+LF+NBSP while anyone reading the mirror sees space+NBSP. db02's 'no difference outside whitespace' is literally true and the whitespace IS the meaning. The 26 are MEANING-DIFFERENT. THEREFORE: NO ALLOWLIST for these 26. An allowlist is for differences known to be benign; these are known to be HARMFUL, and suppressing them would make the guard certify a mirror that is wrong about what the view trims - a control that hides its only true positives. They are the guard's first real findings and they stay RED. The allowlist mechanism still gets built, because a benign case will appear, but it starts EMPTY. THE DURABLE FIX, which removes the class rather than reporting it: write the trim set with chr() instead of literal whitespace bytes - btrim(x, chr(32)||chr(9)||chr(10)||chr(160)) - which is the form this repo already uses elsewhere (the carrocerias and fabricantes normalisation expressions) and which survives a generator that collapses whitespace, because there is no whitespace left in the source to collapse. That is a DDL change to two views and needs its own file, rehearsal and GO; it is not part of 513, but 513's report stays red until it lands or someone shows the difference is harmless. Flagging here so the two are not separated. Also consequent: any regex or trim-set reading of schema.ts is WRONG for those two views, not merely unverified.
-
RUN, not inferred. All three blackboard respondents converged on the same proof step - invoke node_modules/.bin/drizzle-kit introspect directly into a scratch out dir, bypassing tools/introspect-dev.mjs, then read the emitted bytes - so I ran it instead of voting on it. drizzle-kit 0.31.4, DEV, 122 views fetched, exit 0, output in scratchpad dk-raw/schema.ts (286199 bytes). RESULT (a): THE LOSS IS INSIDE DRIZZLE-KIT. The RAW output, which our wrapper never touched, already has 0 tab bytes and 0 LF bytes inside the v_rutap_rows_norm body - its btrim already reads btrim(r.adas, ' '::text || chr(160)) - and already reads 'por diseno ' with no semicolon in v_monitor_historial_anomalias. tools/introspect-dev.mjs is exonerated by measurement, not by reading it. The pg_get_viewdef read was already exonerated by db03's viewdefs.txt holding the real TAB bytes. Note the file as a whole has 4353 tab bytes (indentation), so nothing strips tabs globally - the loss is specific to view-body literals. RESULT (d), and it is STRONGER than the 26 instances: across ALL 122 emitted bodies - 0 contain a tab byte, 0 contain an LF byte, 0 contain a double space, 0 contain an interior ';'. Exactly 1 of 122 ends in ';'. So the whitespace collapse is UNCONDITIONAL for every view body, not 26 scattered cases; the 26 are simply the only literals whose MEANING depends on the whitespace. That is the claim db02 asked for and it is now measured. RESULT (b): TWO MECHANISMS, as db02 predicted from the shape. The 26 are a COLLAPSE (tab/LF/double-space -> single space, unconditional across the corpus). The ';' is a DELETION, which no whitespace collapser has a reason to do. Still 1/1 on the ';': only one installed body has an interior one. His probe-view idea (one DEV view with tab, LF, ';' and a double space in a single literal) would settle whether the ';' deletion is also unconditional; it is DDL in DEV and cheap, but it is not needed to act. CONSEQUENCE FOR THE GUARD: the guard cannot compare literals byte-exact against pg_get_viewdef, because the collapse is unconditional and every body would go red. It must normalise whitespace inside literals the SAME way drizzle-kit does and then flag the literals where that normalisation CHANGES MEANING - a trim/character-class set is the known class - plus any non-whitespace difference such as the ';'. Reporting 122 red lines is the same failure as reporting none.
-
Ticket premise sharpened by measurement. The literal-substring instrument in the description finds 27 mismatches but cannot say WHY; reading drizzle-kit 0.31.4 itself (node_modules/drizzle-kit/api.mjs:32002) gives the whole rule in one line: definition.replace(/\s+/g,' ').replace(';','').trim(). Two mutations: whitespace collapse that reaches INSIDE string literals (JS \s also matches U+00A0), and - the non-obvious one - a STRING pattern in replace, so only the FIRST ';' of the body dies. That is the complete explanation of v_monitor_historial_anomalias: interior ';' deleted, terminator survives. Reimplementing that line reproduces all 122 installed bodies exactly, so the guard asserts FULL-BODY fidelity (INFIDELITY=0 over 122), not literal presence - stronger than the ticket asked for.
-
The ticket asked for an ALLOWLIST plus a staleness check on it; shipped a BASELINE instead, and the difference is not cosmetic. An allowlist only silences and only grows. The baseline fails in BOTH directions: a new lossy literal aborts, and so does one that DISAPPEARS - so when the v_rutap_*_norm trim sets are rewritten to chr(32)||chr(9)||chr(10)||chr(160) the build stops and forces the entry to be deleted by hand, instead of going quietly green over a file still asserting a defect that is gone. That is the ticket's own 'stale allowlist hides the next real case' requirement, enforced by construction rather than by a second check. Keyed on view+literal bytes+count, never on offset: an offset-keyed baseline churns on unrelated edits and gets deleted rather than maintained. Today 4 entries / 27 regions / 3 views.
-
Shipped bdc638c5 v5.5.247 + fefece55 v5.5.248 (audit fixes). COVERAGE, stated as limits not as a verdict: INFIDELITY is proven at UNIT level only - forcing it live needs a DEV write and this lane is SELECT-only. Live forced-failure covered the three LOSSY directions against the real pipeline and the real DEV database: dropped a baseline entry -> LOSSY_NEW exit 1; added a phantom -> LOSSY_GONE; bent a count 12->11 -> LOSSY_COUNT; restored -> exit 0. 19 unit controls, each meaning-changing case paired with its meaning-preserving twin (a lone space inside a literal must NOT fire; whitespace outside a literal must NOT fire). NOT exercised: the guard runs only inside a manual pnpm db:introspect - no deploy step calls it, so a view changed in DEV without reintrospecting does not wake it, and that is written in OPERACIONES.md rather than left to be inferred. Tokenizer does not model SQL comments or E'...' (pg_get_viewdef emits neither); a whitespace value assembled at runtime is invisible. audit reviewed from the committed blobs and accepted the design; its three findings are all fixed in fefece55.