Estados — a declaração revogável¶
Nove tabelas, uma máquina só. É o padrão mais repetido desta plataforma, e quem o entende uma vez entende nove partes do sistema de uma vez.
O que essas nove tabelas são¶
Toda uma família responde à mesma pergunta: o que esta organização afirma sobre o próprio processo? Não é dado observado — é decisão de gente, com nome e hora.
A razão de a decisão virar linha, e não constante no código, está escrita na migração da declaração de fase:
"A mesma coluna significa coisas diferentes em organizações diferentes, e nenhuma está errada. A plataforma registra a escolha, com quem a fez e quando." —
priv/repo/migrations/20260915120000_declaracao_de_fase_por_coluna.exs:18-19
E o dado que forçou isso: no quadro 43 da leds-conectafapes, em 2026-09-14, 459 cartões em
Done, dos quais 294 com a issue ainda ABERTA na origem. A plataforma definia "concluído"
como "issue fechada", e para aquela organização toda medida de entrega subcontava. A correção
não foi escolher por ela — foi registrar a escolha dela.
As nove tabelas¶
chave da vigência é o conjunto de colunas do índice único parcial — é ele que carrega a
invariante "uma declaração vigente por ...", e é a razão de o índice ser parcial.
| # | Tabela | O que a organização declara | Chave da vigência |
|---|---|---|---|
| 1 | spo_item_phase_declarations |
o que cada coluna do quadro significa | tenant, quadro, campo, opção |
| 2 | spo_event_concept_declarations |
o que cada evento da timeline materializa | tenant, event_type |
| 3 | spo_activity_end_criteria |
qual evento marca o fim do trabalho | tenant, quadro ou tenant, projeto |
| 4 | spo_activity_start_criteria |
qual evento marca o começo do trabalho | tenant, quadro ou tenant, projeto |
| 5 | spo_activity_deadline_criteria |
de que campo sai o prazo | tenant, quadro/projeto, origem, campo |
| 6 | smpo_iteration_field_roles |
que papel um campo de iteração cumpre | tenant, quadro, campo |
| 7 | eo_role_visibility_grants |
que papel organizacional enxerga o quê | tenant, papel, escopo |
| 8 | eo_role_structure_management_grants |
que papel organizacional gere a estrutura | tenant, papel, escopo |
| 9 | access_scope_grants |
que conta alcança que escopo | tenant, conta, nível, alvo |
As três primeiras são da feature 066 e entraram em 2026-09-15. As outras seis vieram antes, e a 066 copiou o desenho delas de propósito — a migração diz isso em voz alta:
"o mesmo desenho de
spo_activity_start_criteria(042) espo_activity_deadline_criteria(#368)" —20260915120000:19-20
O estado não é uma coluna¶
Não existe status em nenhuma das nove. A situação sai de uma data que pode ser nula:
| Estado | A regra, exatamente |
|---|---|
| vigente | revoked_at é nulo |
| revogada | revoked_at preenchido — sempre com revoked_by_user_id junto |
E há um terceiro estado que não é da linha, e sim da chave:
| Estado | A regra, exatamente |
|---|---|
| não declarado | nenhuma linha vigente para aquela chave |
Ele importa porque é o estado inicial de tudo, e porque a plataforma tem de dizer o que faz
sem declaração. É o que a tela da 066 faz: "sem ele, a plataforma diz o que assume"
(test/the_band_web/live/declarar_fase_test.exs:269).
A máquina¶
stateDiagram-v2
direction LR
[*] --> nao_declarado
nao_declarado --> vigente : declarar (autor + instante)
vigente --> revogada : revogar (autor + instante)
revogada --> vigente : redeclarar a MESMA chave
vigente --> vigente : redeclarar sobre a vigente\n(substituir — ver abaixo)
note right of nao_declarado
Nenhuma linha vigente para a chave.
A plataforma recorre ao padrao da casa
(YAML) ou diz o que assume.
end note
note right of revogada
A linha FICA. Revogar marca, e nunca apaga:
"desde quando este criterio vale" so tem
resposta se o encerramento preservar o comeco.
migracao 20260825140000:66-67
end note
Não há estado final. Uma chave revogada pode ser declarada de novo, e é por isso que os
índices únicos são parciais: um índice total sobre a chave impediria redeclarar depois de
revogar, e a migração do critério de início explica exatamente isso
(20260825140000:78-79). O teste que o prova é
criterio_de_inicio_test.exs:104 — "redeclarar depois de revogar é aceito".
A transição que parece duas, e é uma¶
vigente --> vigente é o gesto que a tela chama de Replace: declarar sobre uma chave que
já tem declaração vigente. Não é "editar a linha": são duas escritas numa transação,
revogando a anterior e criando a nova.
stateDiagram-v2
direction LR
state "declaração A — vigente" as A
state "declaração A — revogada" as Ar
state "declaração B — vigente" as B
A --> Ar : update_all(revoked_at, revoked_by)
Ar --> B : insert(B)
note right of B
As duas na MESMA transacao, porque sao um ato so:
revogar sem criar deixaria a coluna sem significado,
e criar sem revogar bateria no indice dos vigentes.
item_phase.ex:36-37
end note
Quem lê a tabela depois vê duas linhas: uma revogada e uma vigente. Isso é o desenho, não
duplicata — e a tela mostra a revogada sob a vigente, "porque 'revogar marca' precisa de
forma na tela e não só no banco" (item_phase.ex:114-115).
Onde cada transição acontece, e o que a prova¶
| # | Tabela | declarar |
revogar |
Teste da revogação |
|---|---|---|---|---|
| 1 | fase por coluna | spo/item_phase.ex:41 |
spo/item_phase.ex:80 |
live/declarar_fase_test.exs:135 |
| 2 | conceito por evento | spo/event_concept.ex:36 |
spo/event_concept.ex:67 |
live/declarar_fase_test.exs:252 |
| 3 | critério de fim | spo/end_criterion.ex:26 |
spo/end_criterion.ex:37 |
live/declarar_fase_test.exs:269 |
| 4 | critério de início | spo/start_criterion.ex:67 |
spo/start_criterion.ex:100 |
criterio_de_inicio_test.exs:63, 104 |
| 5 | critério de prazo | spo/deadline_criterion.ex:58 |
spo/deadline_criterion.ex:75 |
criterio_de_prazo_test.exs:267, 293, 312 |
| 6 | papel do campo | smpo/field_roles.ex:25 |
smpo/field_roles.ex:50 |
papel_do_campo_test.exs:149, 171, 176 |
| 7 | visibilidade do papel | eo/visibility.ex:130 |
eo/visibility.ex:145 |
visibilidade_test.exs:287, 372 |
| 8 | gestão da estrutura | eo/structure_grants.ex:93 |
eo/structure_grants.ex:120 |
concessao_de_gestao_test.exs:85 |
| 9 | escopo de acesso | tenants/access.ex:532 |
tenants/access.ex:568 |
access_test.exs:371 |
A lacuna de teste, declarada¶
As três declarações da feature 066 — fase por coluna, conceito por evento e critério de
fim — não têm teste de comando. Toda transição delas é provada só pelo teste de tela,
test/the_band_web/live/declarar_fase_test.exs, e a busca pelas tabelas em test/ volta vazia:
$ grep -rl "spo_item_phase_declarations" test/ → nenhum arquivo
$ grep -rl "spo_event_concept_declarations" test/ → nenhum arquivo
$ grep -rl "spo_activity_end_criteria" test/ → nenhum arquivo
As seis declarações anteriores têm teste no nível do comando, em
test/the_band/ontology/.... As três novas, não. Isto é material de trabalho para QA, e
está escrito aqui em vez de omitido porque transição sem teste no nível onde a regra vive é
lacuna, mesmo quando a tela passa verde.
As duas guardas que o banco impõe¶
Nenhuma delas é código de aplicação — são CHECK e índice, e por isso valem mesmo para quem
escrever direto no banco.
| Guarda | Onde | O que impede |
|---|---|---|
num_nonnulls(project_id, observed_project_id) = 1 |
spo_activity_start_criteria (20260825140000:75) |
um critério sem alvo valeria para tudo sem ninguém ter dito isso |
(project_id IS NULL) <> (observed_project_id IS NULL) |
spo_activity_end_criteria (20260915200000:63) |
a mesma coisa, escrita com o operador de diferença |
índice único parcial WHERE revoked_at IS NULL |
as nove | duas declarações vigentes para a mesma chave |
A precedência quadro → projeto. Nos critérios de início, fim e prazo, um quadro pode
discordar do projeto a que pertence: por isso há dois índices parciais por tabela, um para
cada alvo, e a leitura resolve quadro primeiro. A lista de quem discorda tem consulta própria,
start_criterion.ex:130 (boards_overriding/2).
Duas coisas que a máquina NÃO faz¶
Entram como nota, e não como seta, porque o código não as escreve.
- Revogada não vira "apagada". Não existe
deleteem caminho nenhum das nove. A linha revogada continua consultável, e a tela da 066 a mostra. - Revogar o que ninguém declarou não é sucesso. Devolve
{:error, :nao_encontrada}—item_phase.ex:89. Há teste dedicado a isso em duas das nove (criterio_de_prazo_test.exs:312epapel_do_campo_test.exs:171), e o nome do teste diz a razão: "revogar o que ninguém declarou não é sucesso silencioso". É o defeito que esta casa mais persegue, e a família inteira está desenhada para não cair nele.
A divergência de nomes, registrada¶
Oito das nove usam o par declared_by_user_id / declared_at. A nona, access_scope_grants,
usa granted_by_user_id / granted_at (lib/the_band/tenants/access/scope_grant.ex:28-29).
As duas leituras:
- é a mesma coisa com outro nome — conceder escopo é declarar algo, e a coluna de revogação
já é idêntica (
revoked_at/revoked_by_user_id) nas nove; - é outra coisa de propósito — conceder acesso a uma conta não é declarar um significado sobre o processo, e o vocabulário diferente marca a diferença.
Decidido em 2026-09-18: fica como está. A segunda leitura vence — conceder acesso a uma conta não é declarar um significado sobre o processo, e o vocabulário diferente marca uma diferença real. Renomear apagaria essa marca para ganhar uma busca.
O incômodo é de busca, e se resolve aqui: quem procurar declared_at não acha
access_scope_grants, que usa granted_at. As colunas de revogação são idênticas nas nove.
O que este modelo não mostra¶
inserted_at,updated_ateid— não decidem comportamento.- Os campos próprios de cada tabela (
event_type,target_concept,scope,level…). Eles estão no diagrama de classes e no ERD; aqui só a situação. - O décimo caso, que tem a mesma forma e não é tabela: o elo entre a conta e a pessoa
observada vive em
users.person_declared_at/users.person_revoked_at, um par de colunas dentro deusers. A máquina é esta mesma, e está em conta.md.