Banco — as declarações da organização¶
Nove tabelas de 66, e o ERD que faltava: três delas entraram em 2026-09-15 com a feature
066 e não apareciam em diagrama nenhum. Recorte declarado em
mapa-das-tabelas.md.
Este documento atravessa os recortes dos outros ERDs de propósito: seis destas nove já
aparecem em projetos-e-processo.md e
eo-e-acesso.md, pelo contexto de escrita. Aqui elas aparecem juntas por
forma, que é o que se quer ver quando a pergunta é "como se declara alguma coisa nesta
plataforma?".
O que estas tabelas têm em comum¶
Todas guardam uma decisão de gente sobre o próprio processo, e todas têm o mesmo rodapé:
declared_by_user_id → users (quem decidiu)
declared_at (quando)
revoked_by_user_id → users (quem desfez)
revoked_at (quando — NULO = vigente)
E todas têm um índice único parcial sobre revoked_at IS NULL, que é a invariante do
modelo: uma declaração vigente por chave. A máquina de estados delas está em
estados/declaracao-revogavel.md.
O diagrama¶
Só as colunas que decidem comportamento. id, inserted_at e updated_at ficam fora e estão
nomeados no fim.
FK = chave estrangeira declarada na migração. Colunas sem marca são dado comum. As
colunas cruas (:binary_id sem references) estão marcadas cru e listadas
abaixo.
erDiagram
tenants ||--o{ spo_item_phase_declarations : delimita
tenants ||--o{ spo_event_concept_declarations : delimita
tenants ||--o{ spo_activity_end_criteria : delimita
tenants ||--o{ spo_activity_start_criteria : delimita
tenants ||--o{ spo_activity_deadline_criteria : delimita
tenants ||--o{ smpo_iteration_field_roles : delimita
tenants ||--o{ eo_role_visibility_grants : delimita
tenants ||--o{ eo_role_structure_management_grants : delimita
users ||--o{ spo_item_phase_declarations : declara
users ||--o{ spo_event_concept_declarations : declara
users ||--o{ spo_activity_end_criteria : declara
users ||--o{ spo_activity_start_criteria : declara
users ||--o{ spo_activity_deadline_criteria : declara
users ||--o{ smpo_iteration_field_roles : declara
users ||--o{ eo_role_visibility_grants : declara
users ||--o{ eo_role_structure_management_grants : declara
users ||--o{ access_scope_grants : recebe
observed_projects ||--o{ spo_item_phase_declarations : "tem coluna declarada em"
observed_projects ||--o{ spo_activity_end_criteria : "tem fim declarado em"
observed_projects ||--o{ spo_activity_start_criteria : "tem inicio declarado em"
observed_projects ||--o{ spo_activity_deadline_criteria : "tem prazo declarado em"
observed_projects ||--o{ smpo_iteration_field_roles : "tem campo declarado em"
spo_projects ||--o{ spo_activity_end_criteria : "tem fim declarado em"
spo_projects ||--o{ spo_activity_start_criteria : "tem inicio declarado em"
spo_projects ||--o{ spo_activity_deadline_criteria : "tem prazo declarado em"
eo_organizational_roles ||--o{ eo_role_visibility_grants : confere
eo_organizational_roles ||--o{ eo_role_structure_management_grants : confere
spo_item_phase_declarations {
uuid tenant_id FK
uuid observed_project_id FK "o quadro"
string field_external_id "o campo de selecao unica"
string option_external_id "a IDENTIDADE da opcao"
string option_name_at_declaration "o nome de entao"
string target_concept "conceito da rede, ou nao_diz_fase"
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
spo_event_concept_declarations {
uuid tenant_id FK
string event_type "cru, como a origem nomeia"
string target_concept "conceito da rede, ou nao_nomeado"
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
spo_activity_end_criteria {
uuid tenant_id FK
uuid project_id FK "exatamente UM dos dois"
uuid observed_project_id FK "exatamente UM dos dois"
string event_type
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
spo_activity_start_criteria {
uuid tenant_id FK
uuid project_id FK "exatamente UM dos dois"
uuid observed_project_id FK "exatamente UM dos dois"
string event_type
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
spo_activity_deadline_criteria {
uuid tenant_id FK
uuid project_id FK "exatamente UM dos dois"
uuid observed_project_id FK "exatamente UM dos dois"
string source "de onde vem a data"
string field_name "qual campo"
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
smpo_iteration_field_roles {
uuid tenant_id FK
uuid observed_project_id FK
string field_name
string role
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
eo_role_visibility_grants {
uuid tenant_id FK
uuid organizational_role_id FK
string scope
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
eo_role_structure_management_grants {
uuid tenant_id FK
uuid organizational_role_id FK
string scope
uuid declared_by_user_id FK
datetime declared_at
uuid revoked_by_user_id FK
datetime revoked_at "nulo = vigente"
}
access_scope_grants {
uuid tenant_id "CRU — sem FK"
uuid user_id FK
string level "team, project ou organization"
uuid target_id "CRU — polimorfico, depende de level"
uuid granted_by_user_id "CRU — sem FK"
datetime granted_at
uuid revoked_by_user_id "CRU — sem FK"
datetime revoked_at "nulo = vigente"
}
access_scope_grants aparece solta no diagrama, ligada só a users. Não é erro de desenho:
é o que a migração escreveu, e a razão está abaixo.
Os índices parciais, que carregam a invariante¶
Nenhum diagrama mostra um índice, e é aqui que a regra de verdade mora. Todos são
WHERE revoked_at IS NULL, e a razão é sempre a mesma: "um índice total impediria redeclarar
depois de revogar" (20260825140000:78-79).
| Tabela | Colunas | Nome | Fonte |
|---|---|---|---|
spo_item_phase_declarations |
tenant, quadro, campo, opção |
spo_fase_vigente_da_opcao_index |
20260915120000:70-76 |
spo_event_concept_declarations |
tenant, event_type |
spo_conceito_vigente_do_evento_index |
20260915180000:49-52 |
spo_activity_end_criteria |
tenant, quadro |
spo_fim_vigente_do_quadro_index |
20260915200000:66-70 |
spo_activity_end_criteria |
tenant, projeto |
spo_fim_vigente_do_projeto_index |
20260915200000:71-75 |
spo_activity_start_criteria |
tenant, projeto |
spo_activity_start_criteria_projeto_vigente_index |
20260825140000:80-84 |
spo_activity_start_criteria |
tenant, quadro |
spo_activity_start_criteria_quadro_vigente_index |
20260825140000:85-89 |
spo_activity_deadline_criteria |
tenant, quadro, origem, campo |
spo_prazo_vigente_do_quadro_index |
20260827040000:89-93 |
spo_activity_deadline_criteria |
tenant, projeto, origem, campo |
spo_prazo_vigente_do_projeto_index |
20260827040000:97-101 |
smpo_iteration_field_roles |
tenant, quadro, campo |
smpo_papel_vigente_do_campo_index |
20260827020000:67-71 |
eo_role_visibility_grants |
tenant, papel, escopo |
eo_concessao_vigente_do_papel_index |
20260827060000:69-73 |
eo_role_structure_management_grants |
tenant, papel, escopo |
eo_concessao_de_gestao_vigente_index |
20260907190000:71-75 |
access_scope_grants |
tenant, conta, nível, alvo |
access_scope_grants_vigente_index |
20260828160856:38-40 |
Doze índices para nove tabelas: três tabelas têm dois cada, porque o alvo pode ser o quadro ou o projeto, e o quadro prevalece.
As restrições CHECK¶
Duas, e ambas dizem a mesma regra com sintaxe diferente:
| Tabela | CHECK | Fonte |
|---|---|---|
spo_activity_start_criteria |
num_nonnulls(project_id, observed_project_id) = 1 |
20260825140000:75 |
spo_activity_end_criteria |
(project_id IS NULL) <> (observed_project_id IS NULL) |
20260915200000:63 |
"o critério do quadro prevalece sobre o do projeto, e um critério sem alvo valeria para tudo sem ninguém ter dito isso." —
20260915200000:44-45
spo_activity_deadline_criteria não tem CHECK equivalente na sua migração — os dois índices
parciais dela exigem IS NOT NULL do alvo, o que impede a linha sem alvo de ser vigente, mas
não impede a linha revogada sem alvo. Fica registrado como diferença entre as três irmãs, para
quem mantém a SPO avaliar.
O que é FK declarada, e o que é cru¶
A medida, feita sobre as 93 migrações reconstruídas:
| Quantas | |
|---|---|
colunas com references(...) — FK declarada |
201 |
colunas :binary_id / :uuid sem references (fora das PKs) |
12 |
chaves primárias :binary_id / :uuid |
66 |
Correção de uma premissa comum. No banco, a esmagadora maioria das referências é FK declarada — 201 contra 12. Quem espera encontrar ligação frouxa está pensando nos schemas Ecto, onde a história é o oposto: 214 campos
:binary_ide 3 associações declaradas. A ligação existe no esquema e não é modelada no Ecto. Isso está medido emclasses/mapa-dos-schemas.md.
As 12 colunas cruas do banco inteiro¶
Nesta família estão quatro delas, todas em access_scope_grants:
| Tabela | Coluna | Por que é cru |
|---|---|---|
access_scope_grants |
target_id |
polimórfico — aponta para equipe, projeto ou organização conforme level; FK é impossível |
access_scope_grants |
tenant_id |
sem razão escrita — ver o achado abaixo |
access_scope_grants |
granted_by_user_id |
sem razão escrita — user_id, na mesma tabela, é FK |
access_scope_grants |
revoked_by_user_id |
idem |
As outras oito, fora desta família, para completar o censo:
| Tabela | Colunas cruas | Por quê |
|---|---|---|
account_disablements |
tenant_id, disabled_by_user_id, enabled_by_user_id |
sem razão escrita; user_id é FK |
issue_promotions |
target_id |
polimórfico, com target_table ao lado |
spo_performed_project_activities |
subject_id |
polimórfico, com subject_type ao lado — "um commit não tem issue, e a coluna dedicada ficaria nula em metade das linhas" (20260814160000:55-57) |
spo_performed_project_activities |
board_id |
a resolução para o quadro observado pode não existir ainda (performed_project_activity.ex:53-55) |
spo_performed_project_activities |
project_id |
sem razão escrita — ver o achado abaixo |
users |
password_set_by_user_id |
sem razão escrita |
Achado 1 — duas tabelas com tenant_id sem FK · ✅ RESOLVIDO em 2026-09-18¶
Corrigido em
20260918120000_as_chaves_estrangeiras_que_faltavam. As duas passaram a declarar a chave, comON DELETE RESTRICT— que é o que 61 das 65 chaves detenant_idjá usavam, e o único compatível com a coluna serNOT NULL. Zero órfãos, conferido antes de aplicar.A leitura de "é descuido" venceu, pelo argumento que está escrito abaixo:
user_id, na mesma tabela e na mesma migração, era declarada.
63 das 66 tabelas de domínio declaram FK de tenant_id para tenants. As exceções:
tenants, que é a raiz — correto;access_scope_grantseaccount_disablements, que têmtenant_idcru.
As duas são tabelas de acesso, e são justamente onde um tenant errado tem o efeito mais caro. Nenhuma migração explica a escolha. As duas leituras:
- é deliberado — as duas nasceram na mesma leva de autenticação (
20260828160856e20260910050000), e talvez se quisesse evitarON DELETEem cascata num caminho de acesso; - é descuido —
user_id, na mesma tabela e na mesma migração, é FK declarada, o que torna a inconsistência interna difícil de explicar como intenção.
Não escolho entre as duas. Levar a quem mantém Tenants.Access. O efeito prático é que o
banco não impede uma concessão apontando para tenant inexistente; a aplicação impede, em
Access.grant/5 (lib/the_band/tenants/access.ex:545, :548).
Achado 2 — organization_id é FK e project_id não · ✅ RESOLVIDO em 2026-09-18¶
Corrigido na mesma migração, com
ON DELETE SET NULL— a regra deorganization_id, a irmã declarada na linha de cima, porque esta coluna aceita nulo de propósito.A leitura da ordem de migração venceu, e ela está abaixo:
spo_projectsnasceu um dia depois. Não era decisão; era o que dava para fazer naquele dia.
Em spo_performed_project_activities, o comentário da migração trata as duas colunas juntas:
"Estão no critério de identidade e aceitam nulo: nem toda origem futura conhece organização ou projeto." —
20260814160000:29-32
E logo abaixo, uma é declarada e a outra não:
add :organization_id, references(:eo_organizations, type: :uuid, on_delete: :nilify_all)
add :project_id, :uuid
20260814160000:33-34
As duas leituras: project_id aponta para spo_projects, que ainda não existia quando esta
migração rodou (spo_projects nasce em 20260815160000, um dia depois) — o que explicaria a
ausência como ordem de migração, não como decisão; ou é intencional, porque o projeto do ato
pode vir de origem que não temos. Levar a quem mantém a SPO.
O que o diagrama não mostra¶
id,inserted_at,updated_at— presentes nas nove, e não decidem comportamento.- As colunas
_atde relógio da coleta — não existem aqui: nenhuma destas nove é coletada. Toda linha foi escrita por uma pessoa. - A resolução na leitura. Nenhuma destas declarações regrava nada: a fase do item, o
conceito do evento e o instante de fim são resolvidos na consulta, e o motivo está em
lib/the_band/ontology/seon/spo/item_phase.ex:7-9— "Gravar a fase no item faria revogar uma declaração deixar para trás milhares de linhas afirmando o que ninguém mais declara". Um ERD não mostra isso, e quem lê o modelo precisa saber.