ADR 0093 — The dbre role becomes two scripts: migration linter and backup report
- Status: Accepted
- Date: 2026-08-17
- Context: the product owner's decision to bring forward part of the
dbrerole declared indocs/fluxo.yml(ADR 0085), without waiting for the trigger ("real data volume") to fire - Revises:
docs/fluxo.yml, blockid: dbre
Context
docs/fluxo.yml (ADR 0085) already declared dbre as proposto, absorbed
by dev-lead (migration review) and platform (tuning, once activated),
with the separation criterion: "real data volume in the managed project
(today the risk is SCHEMA risk, not load risk)".
That sentence already contained the answer that was missing execution. Of
the four target deliverables for the role — parecer-de-migracao,
plano-de-capacidade, backup-restore-testado, tuning —, only two
genuinely depend on real data volume:
plano-de-capacidadeandtuningrequire real load to mean anything. Simulating them without it would be inventing a number — the same class of error that ADR 0042 already refuses for model rating and ADR 0077 refuses for capability ranking.parecer-de-migracaois STATIC analysis of SQL text: a risk pattern (DROP COLUMN,ALTER COLUMN ... TYPE,ADD COLUMN ... NOT NULLwithoutDEFAULT) is risky regardless of whether the managed database has ten or ten million rows.backup-restore-testadois already real and tested TODAY — the backup CronJob has been running since Phase 5, writes tobackup_runs, and the restore procedure has been EXECUTED for real (docs/runbook.md#restore, real RTO ~40s against a ~108 KB database). What was missing was just a way to READ that state on demand, formatted as a report.
Neither one calls for an LLM agent (there's no natural-language judgment
to make — it's pattern recognition over SQL text and reading a table) nor
an engine GenServer (there's no long-lived state or loop — each run is a
point-in-time read, triggered by a human or by CI). The decision was to
treat them for what they are: two mechanical scripts, of the same genre as
scripts/ci/pr-police.ts and apps/api/scripts/medir-execucao.ts.
Decision
apps/api/scripts/lint-migracao.tsscans ALL ofapps/api/src/db/migrations/*.sql(no--projeto— it's analysis of the repository, not of a run) and flags five risk patterns, line by line:DROP TABLE,TRUNCATE,DROP COLUMN,ALTER COLUMN ... TYPE/SET DATA TYPE, andADD COLUMN ... NOT NULLwithoutDEFAULT. The logic is PURE (lintarConteudo, receives name + SQL text, returns findings) separate from the I/O adapter (lintarDiretorio/principal), the same design aspr-police.ts(avaliarPr). Comment lines are ignored, because comments in this repository cite these very patterns in prose to explain why they were AVOIDED (real case:0042_tough_captain_midlands.sql, line 3). Exits!= 0if it finds any occurrence.apps/api/scripts/relatorio-backup.tsreadsbackup_runswith the SAME logic asDomainGaugesCollector.collectBackup()(last SUCCESS — age, size — and how the LAST run ended), formatted as an on-demand report — not a Prometheus gauge, a point-in-time read for whoever wants the answer now. It cites the already-tested restore procedure instead of re-executing it. The classification logic (avaliarBackup) is pure, testable with a mockedbackup_runs.- Neither one enters CI for now — see Consequences.
docs/fluxo.yml:dbremoves fromstatus: propostotostatus: active.entregaveis_alvo(flat list) becomesentregaveis(list of objects withstatus):parecer-de-migracaoandbackup-restore-testadomarkedreal, with the mechanism that proves them;plano-de-capacidadeandtuningkept aslacuna, with the explicit reason.hoje_absorvido_por/criterio_de_separacaoremain in the block, now describing only what's left — the rule of ONE migration per wave (meta/_journal.json) is NO LONGER called the "mechanized version of the role": it prevents snapshot CONFLICTS between agents running in parallel, a concern orthogonal to "this SQL has a risky pattern", which now has its own mechanism.
Consequences
- The linter scans the ENTIRE repository, not a PR's diff. Running it
against today's real migrations FINDS three occurrences in migrations
already merged and accepted —
0006_whole_princess_powerful.sql:22and0034_quick_saracen.sql:33(DROP COLUMN),0007_groovy_bullseye.sql:2(ALTER COLUMN ... SET DATA TYPE). This isn't a defect to fix: these are decisions already made and accepted, and fixing them in passing would erase the evidence of why they existed (the same rule CLAUDE.md states for findings Z/AD/AE). This is exactly why the script did not enter as a CI step: a gate that scans the whole repository would fail every PR, forever, for findings that aren't the PR's. Turning it into a real gate requires the same technique aspr-police.ts— scoping to the diff against the PR's base — left for whendbreactually needs to BLOCK a merge; today it's a report, not a verdict, and runs manually (pnpm --filter api lint:migracao). - The backup report doesn't re-execute the restore. It reuses
exclusively the read that
collectBackup()already does; the restore procedure itself is already tested and documented (docs/runbook.md#restore). Conflating the two would inflate the script's scope into reimplementing something that already exists and already works. - The "backup overdue" threshold (26h) is duplicated, not imported,
from the
brabo-backup-atrasadoalert (deploy/k8s/observability/alerts/brabo-alerts.yaml) — the Grafana YAML isn't read by the script's Node process. The two numbers can drift apart if someone changes one side and forgets the other; accepted consciously, the same cost any duplicated threshold in code would have. plano-de-capacidadeandtuningremain a GAP, declared indocs/fluxo.yml, with no deadline. The trigger for splitting them off is still real data volume in the MANAGED project — not thetoken_usage/session_eventsvolume of Brabo itself, which is already large, but that of the product the agents build.