Classes — EO, a estrutura organizacional¶
Um subsistema só: quem é a organização, quem é a pessoa, o que é a equipe, e o que liga uma à
outra. O que mede trabalho (SPO, WorkItems, Quality), o que descreve perfil
(eo_person_profiles) e o que guarda credencial ficam de fora — cada um pede o seu diagrama.
O diagrama¶
classDiagram
direction LR
class Organization {
+uuid id
+uuid tenant_id
+string internal_id
+string name
+string login
+uuid parent_organization_id
+string source_system
+string source_instance
+string external_id
+datetime collected_at
}
class Person {
+uuid id
+uuid tenant_id
+string internal_id
+string name
+string email
+string login
+string account_type
+string external_id
+datetime last_observed_at
+datetime no_longer_observed_at
}
class Team {
+uuid id
+uuid tenant_id
+string internal_id
+string type
+string name
+string slug
+uuid organization_id
+uuid declared_by_user_id
+string source_system
+string source_instance
+string external_id
+datetime last_observed_at
+datetime no_longer_observed_at
}
class OrganizationalRole {
+uuid id
+uuid tenant_id
+uuid organization_id
+string code
+string name
+string catalog_concept_id
+uuid declared_by_user_id
+uuid updated_by_user_id
+datetime hidden_at
}
class TeamMembership {
+uuid id
+uuid tenant_id
+string internal_id
+uuid person_id
+uuid team_id
+uuid organizational_role_id
+datetime started_at
+datetime ended_at
+uuid declared_by_user_id
+datetime invalidated_at
+uuid invalidated_by_user_id
+text invalidation_reason
}
class TeamMembershipEvidence {
+uuid id
+uuid tenant_id
+uuid person_id
+uuid team_id
+string person_external_id
+string team_external_id
+string platform_access_level
+string source_system
+datetime observed_at
+datetime last_observed_at
+datetime no_longer_observed_at
+uuid promoted_membership_id
}
class TeamComposition {
+uuid id
+uuid tenant_id
+uuid part_team_id
+uuid whole_team_id
+datetime started_at
+datetime ended_at
+uuid declared_by_user_id
+uuid ended_by_user_id
}
class RoleVisibilityGrant {
+uuid id
+uuid tenant_id
+uuid organizational_role_id
+string scope
+uuid declared_by_user_id
+datetime declared_at
+uuid revoked_by_user_id
+datetime revoked_at
}
class User {
+uuid id
+uuid person_id
}
Organization "0..1" <-- "0..*" Organization : e parte de
Organization "1" <-- "0..*" Team : pertence a
Organization "1" <-- "0..*" OrganizationalRole : reconhece
Person "1" <-- "0..*" TeamMembership : aloca
Team "1" <-- "0..*" TeamMembership : na equipe
OrganizationalRole "0..1" <-- "0..*" TeamMembership : para desempenhar
Person "1" <-- "0..*" TeamMembershipEvidence : observa
Team "1" <-- "0..*" TeamMembershipEvidence : no time
TeamMembership "0..1" <-- "0..1" TeamMembershipEvidence : promovida em
Team "1" <-- "0..*" TeamComposition : e a parte
Team "1" <-- "0..*" TeamComposition : e o todo
OrganizationalRole "1" <-- "0..*" RoleVisibilityGrant : concedida a
User "0..1" <-- "0..*" TeamMembership : declarado por
User "0..1" <-- "0..*" TeamComposition : declarada por
User "1" <-- "0..*" RoleVisibilityGrant : concedida por
Person "0..1" <-- "0..1" User : e a conta de
TeamMembership e TeamComposition são relatores: reificam uma relação e carregam o período
e o autor dela (organizational_structure.yaml:142; migração 20260901230000:12-18).
TeamMembershipEvidence é observação, não domínio: guarda o que a origem mostrou.
RoleVisibilityGrant é declaração sobre o que um papel permite. User entra só como
fronteira — é da feature 045, e cinco chaves estrangeiras desta EO apontam para ela.
O nulo que significa¶
Metade deste modelo está nas colunas vazias. Cada linha abaixo é uma afirmação que a plataforma faz por ausência, e que a tela precisa dizer em texto.
| Campo nulo | Classe | O que significa | Fonte |
|---|---|---|---|
organizational_role_id |
TeamMembership | papel não declarado — é o vínculo observado, criado pela coleta | team_membership.ex:59,99-102; migração 20260906230000:15-20,37 |
declared_by_user_id |
TeamMembership | ninguém afirmou — veio da coleta, não de alguém digitando; autor falso mente mais que autor ausente | team_membership.ex:36-41,64; migração 20260814140000:11-13 |
started_at |
TeamMembership | desde quando não se sabe — nunca "começou hoje" nem observed_at |
team_membership.ex:30-31; commands.ex:723-725,1362-1369 |
ended_at |
TeamMembership | vigente — a pessoa está na equipe | team_membership.ex:33-34; queries.ex:445-453 |
invalidated_at |
TeamMembership | não houve equívoco — o vínculo vigeu de verdade | migração 20260901230000:20-32,104-112 |
promoted_membership_id |
TeamMembershipEvidence | a origem mostra a pessoa e não há vínculo: a organização já declarou saída ou equívoco, e a coleta não cria por cima | commands.ex:648-654; team_membership_evidence.ex:113-116 |
no_longer_observed_at |
TeamMembershipEvidence, Person, Team | a origem ainda lista — a observação é contínua | commands.ex:793-826; team_membership_evidence.ex:41 |
platform_access_level |
TeamMembershipEvidence | a origem não conhece este vínculo (equipe derivada); gravar MEMBER para preencher tornaria "observado como membro comum" e "a origem ignora" indistinguíveis |
team_membership_evidence.ex:94-111; migração 20260810140200:44-58 |
ended_at |
TeamComposition | composição vigente | team_composition.ex:8; migração 20260901230000:63-68 |
ended_by_user_id |
TeamComposition | vigente, ou encerrada por caminho que não gravou autor | team_composition.ex:11 |
revoked_at |
RoleVisibilityGrant | concessão vigente — revogar é marcar, nunca apagar | migração 20260827060000:36-40,69-74 |
hidden_at |
OrganizationalRole | papel visível na lista; ocultar não é apagar, e vínculos que já o usam continuam válidos | organizational_role.ex:16-17 |
catalog_concept_id |
OrganizationalRole | papel declarado pela organização, não vindo do catálogo SRO | organizational_role.ex:11-12; role_catalog.ex:7,46 |
parent_organization_id |
Organization | organização raiz — não é parte de outra | organization.ex:27 |
person_id (em users) |
User | conta sem pessoa declarada — bloqueio próprio, distinto de "sem concessão" | visibility.ex:18-19; specs/060-tela-da-equipe/data-model.md §2 |
O nulo que não existe, e devia: eo_team_compositions.started_at é null: false
(migração 20260901230000:52) e compose_teams/4 grava agora (commands.ex:233). "Faz parte desde quando não se
sabe" não é expressável hoje — é a lacuna que a FR-037 da spec 060 fecha na PR 2
(specs/060-tela-da-equipe/spec.md FR-037; data-model.md §5).
Classe → schema → tabela → conceito¶
| Classe | Schema | Tabela (migração) | Conceito da ontologia |
|---|---|---|---|
| Organization | schemas/organization.ex:20-39 |
eo_organizations (20260809120200:25) |
eo.organization (organizational_structure.yaml:16-21) |
| Person | schemas/person.ex:28-59 |
eo_people (20260809120200:53) |
eo.person (:72-82) |
| Team | schemas/team.ex:29-55 |
eo_teams (20260809120200:119; 20260816210000:24-25) |
eo.team (:94-99); type decide entre eo.organizational_team (:101-107) e eo.project_team (:109-115) |
| OrganizationalRole | schemas/organizational_role.ex:44-63 |
eo_organizational_roles (20260809120200:102, reescrita em 20260824180000:53-77) |
eo.organizational_role (:84-92) |
| TeamMembership | schemas/team_membership.ex:52-79 |
eo_team_memberships (20260809120200:157; 20260814140000:33; 20260901230000:76-112; 20260906230000:37-46) |
eo.team_membership (:134-147) |
| TeamMembershipEvidence | schemas/team_membership_evidence.ex:28-50 |
eo_team_membership_evidence (20260809120300:17) |
sem conceito de ontologia — declarada como observed_link na regra github_team_membership_evidence.yaml:77-94 |
| TeamComposition | schemas/team_composition.ex:32-45 |
eo_team_compositions (20260901230000:43) |
sem conceito — a ontologia declara a relação eo.team_part_of_team (:165), que esta tabela reifica com autor e período |
| RoleVisibilityGrant | schemas/role_visibility_grant.ex:32-43 |
eo_role_visibility_grants (20260827060000:45) |
não declarado na base — lacuna da #369 |
| — | — | eo_organizational_units (20260809120200:87 como eo_sectors, renomeada em 20260810100000:21) |
eo.organizational_unit (:23-43) |
| — | — | — | eo.team_member (:117-132) — papel, sem tabela de propósito: a identidade fica em eo.person |
| — | — | — | eo.organizational_part (:45-70) — role_mixin, sem tabela |
Invariantes que o diagrama não mostra¶
O modelo de classes não desenha índice parcial nem CHECK, e é neles que moram as regras.
| Invariante | Onde | O que impede |
|---|---|---|
eo_team_memberships_vigente_index em (tenant_id, person_id, team_id, organizational_role_id) WHERE ended_at IS NULL AND invalidated_at IS NULL |
20260901230000:95-100 |
a mesma pessoa alocada duas vezes ao mesmo papel vigente. Permite dois papéis diferentes, e permite o mesmo papel em períodos distintos |
eo_team_memberships_observado_vigente_index em (tenant_id, person_id, team_id) WHERE ended_at IS NULL AND invalidated_at IS NULL AND organizational_role_id IS NULL |
20260906230000:40-46 |
o vínculo observado duplicado. Nulos não colidem em índice único — sem este, a coleta duplicaria |
eo_equivoco_do_vinculo_completo (CHECK) |
20260901230000:104-112 |
equívoco pela metade: ou os três nulos, ou os três preenchidos |
eo_composicao_vigente_de_equipe_index em (tenant_id, part_team_id, whole_team_id) WHERE ended_at IS NULL |
20260901230000:63-68 |
a mesma composição vigorando duas vezes. O ciclo não é impedido aqui — índice não vê caminho; a recusa é da aplicação (commands.ex:285-328) |
eo_concessao_vigente_do_papel_index em (tenant_id, organizational_role_id, scope) WHERE revoked_at IS NULL |
20260827060000:69-74 |
a segunda concessão vigente para o mesmo papel e alcance |
eo_nome_unico_da_equipe_declarada_index em (tenant_id, organization_id, name) WHERE source_instance = 'declared' |
20260902010000:27-30 |
duas equipes declaradas de mesmo nome. As observadas ficam de fora: recusar o que a origem afirma seria o oposto do que a plataforma deve fazer |
únicos em eo_organizational_roles (tenant_id, organization_id, code) |
20260824180000:69 |
código repetido na mesma organização; o mesmo código em outra organização não é conflito |
papel_tem_uma_origem_so (CHECK) |
20260824180000:77 |
papel que é do catálogo e declarado ao mesmo tempo |
eo_teams_organizational_team_has_organization (CHECK) |
20260810150000:39-43 |
equipe organizacional sem organização |
eo_teams_type_check, eo_people_account_type_check (CHECK) |
20260809120200:149, :82 |
type fora de organizational_team/project_team; account_type fora de person/bot/app |
eo_evidence_github_has_access_level (CHECK) |
20260810140200:47-58 |
evidência do GitHub sem nível de acesso — e permite o nulo onde a origem não conhece o vínculo |
únicos em users (person_id) WHERE person_id IS NOT NULL AND person_revoked_at IS NULL |
20260827050000:69-71 |
duas contas vigentes apontando para a mesma pessoa — é o elo que pode_ver_equipe/3 lê |
O que ficou de fora do diagrama, e por quê¶
inserted_at,updated_at,record_version— em todas as classes. Não decidem comportamento nem delimitam vigência.outcome(:created | :updated | :unchanged) em Organization, Person, Team e TeamMembershipEvidence — campo virtual: descreve o que aconteceu na chamada, não o registro (organization.ex:36,person.ex:29,team.ex:24,team_membership_evidence.ex:47). Não é coluna, e é de propósito.eo_person_profiles(schemas/person_profile.ex) — perfil demonstrado; subsistema de perfis, não de estrutura.internal_idaparece só onde carrega regra: em TeamMembership o prefixoobserved_<id da evidência>é determinístico, e é o que faz o reprocessamento reconhecer em vez de duplicar (commands.ex:727-728; migração20260906230000:29-31).access_scope_grants— escopos por conta, feature 045. A EO decide por papel; a conta é outra conversa, e misturá-las foi recusado (plan.mdda 060, D5).spo_project_teams(vínculo equipe ↔ projeto) — é da SPO; entra no diagrama de projetos.eo_sync_*,eo_organizations.collected_atdas entidades derivadas e o resto da proveniência de coleta — cabem no diagrama de ingestão.
Divergências encontradas¶
Cada uma tem duas leituras, e nenhuma é escolhida aqui.
-
eo_role_visibility_grantsexiste no código e não está declarada na base. A tabela nasce em20260827060000:45,visibility.exa lê, epriv/knowledge_base/ontology/seon/eo/não tem conceito para ela. Leitura A: é lacuna herdada da #369 — a decisão foi tomada e o YAML nunca escrito. Leitura B: concessão de visibilidade não é conceito da EO de referência e por isso não teria lugar no módulo. A PR 1 da feature 060 resolve pela leitura A e declara as duas concessões juntas emrole_grants.yaml(specs/060-tela-da-equipe/data-model.md§3.1;plan.md, tabela do princípio IV). A quem levar: quem mantém a base — a categoria UFO proposta (normative_description/kind) é a pergunta aberta 4 doplan.md. -
eo.membership_to_play_roletem cardinalidadeonee a coluna admite nulo.organizational_structure.yaml:268declara que todo vínculo tem papel;20260906230000:37tornouorganizational_role_idanulável. Leitura A: é desvio consciente da ontologia de referência, registrado na ADR 0008 (docs/adr/0008-vinculo-observado.md:92-99) — o relator é materializado com o papel declaradamente ausente. Leitura B: a cardinalidade do YAML mente sobre o que o banco aceita, e deveria virarzero_or_onecom nota. A quem levar: quem mantém a base. A ADR registra o desvio; o YAML não. -
eo_team_compositionsnão tem conceito, só relação. A tabela reificaeo.team_part_of_team(:165) comstarted_at,ended_at,declared_by_user_ideended_by_user_id— quatro atributos que a relação não tem. Leitura A: a relação basta, e a proveniência é da plataforma. Leitura B: onde há período e autor há relator, e relator pede conceito — foi o argumento que crioueo.team_membership. A quem levar: quem mantém a base. -
eo_team_membership_evidenceestá declarada em regra, não em módulo.github_team_membership_evidence.yaml:77-94a declara comoobserved_linkcompersisted_as: team_membership_evidence. É coerente com "fonte externa não é domínio" (princípio II), e fica registrado que procurareo.team_membership_evidencena ontologia não acha nada — de propósito. -
eo_organizational_unitsé tabela sem schema e sem leitor. Criada comoeo_sectors(20260809120200:87), renomeada em20260810100000:21, egrep -rn eo_organizational_units lib/devolve zero. O conceitoeo.organizational_unitexiste na base (:23-43). Leitura A: é estrutura prevista e ainda não usada. Leitura B: é tabela morta que confunde quem lê o banco. A quem levar: Software Architect. -
eo.team_membershipno YAML declara dois atributos; a tabela tem doze.organizational_structure.yaml:143-145listastarted_ateended_at. Ficam de foradeclared_by_user_ide o trio do equívoco. A posição declarada é que são proveniência da declaração, não semântica do relator (specs/060-tela-da-equipe/data-model.md§5). Fica registrada como posição, não como achado — mas quem lê o YAML para saber o que o vínculo carrega não encontra o equívoco.
[NEEDS CLARIFICATION]¶
- A quarta combinação de (papel, autor) não tem nome. O changeset recusa autor sem papel
(
team_membership.ex:115-122) e não recusa papel sem autor — que é o queEO.allocate/2grava quando o chamador não passadeclared_by_user_id, como emtest/the_band_web/live/equipe_composta_test.exs:70-78. A leitura de origem do roster édeclared_by_user_idnulo ou preenchido (data-model.md§4.1), então esse vínculo apareceria como observado carregando um papel declarado — e a tela não tem palavra para isso. Pergunta: é estado legítimo, e a tela ganha um quarto rótulo, ou é chamada incompleta a fechar no changeset? A quem: Software Architect e Product Owner.