Classes — projetos e processo¶
O empreendimento que alguém declara, o quadro que a plataforma observa, e a iteração que pode ser uma coisa ou outra. Dezessete tabelas, em dois diagramas.
O nó deste subsistema é uma colisão de nome: o GitHub chama de "project" o que aqui é
quadro, e a SPO chama de projeto o empreendimento declarado por uma pessoa. São tabelas
diferentes, com donos diferentes, e confundi-las é o erro que os CHECK do banco existem para
impedir (activity_start_criterion.ex:15-21).
| Nome | Tabela | O que é | Quem cria |
|---|---|---|---|
| projeto | spo_projects |
o empreendimento — spo.project |
pessoa, na tela |
| quadro | observed_projects |
o Projects v2 do GitHub | a coleta |
1. O projeto declarado, e o que ele reúne¶
classDiagram
direction LR
class Project {
+uuid id
+uuid tenant_id
+string name
+date started_on
+date ended_on
+uuid parent_id
+uuid declared_by_user_id
+uuid updated_by_user_id
+datetime removed_at
+uuid removed_by_user_id
}
class ProjectOrganization {
+uuid id
+uuid project_id
+uuid organization_id
+datetime linked_at
+datetime unlinked_at
}
class ProjectTeam {
+uuid id
+uuid project_id
+uuid team_id
+datetime linked_at
+datetime unlinked_at
}
class ProjectRepository {
+uuid id
+uuid project_id
+uuid observed_repository_id
+datetime linked_at
+datetime unlinked_at
}
class ProjectBoard {
+uuid id
+uuid project_id
+uuid observed_project_id
+datetime linked_at
+datetime unlinked_at
}
class ActivityStartCriterion {
+uuid id
+uuid project_id
+uuid observed_project_id
+string event_type
+datetime declared_at
+datetime revoked_at
}
class ActivityDeadlineCriterion {
+uuid id
+uuid project_id
+uuid observed_project_id
+string source
+string field_name
+datetime declared_at
+datetime revoked_at
}
class PerformedProjectActivity {
+uuid id
+string internal_id
+uuid organization_id
+uuid project_id
+string activity_type
+string concept_id
+uuid performer_id
+string performer_login
+datetime occurred_at
+string subject_type
+uuid subject_id
+map payload
}
class Organization {
<<fronteira — EO>>
+uuid id
}
class Team {
<<fronteira — EO>>
+uuid id
}
class Person {
<<fronteira — EO>>
+uuid id
}
class ObservedRepository {
<<fronteira — ingestão>>
+uuid id
}
class ObservedProject {
<<do diagrama 2>>
+uuid id
}
Project "0..1" --> "0..*" Project : contém
Project "1" --> "0..*" ProjectOrganization : pertence a
Project "1" --> "0..*" ProjectTeam : é feito por
Project "1" --> "0..*" ProjectRepository : produz em
Project "1" --> "0..*" ProjectBoard : é acompanhado em
Organization "1" --> "0..*" ProjectOrganization : reúne
Team "1" --> "0..*" ProjectTeam : trabalha em
ObservedRepository "1" --> "0..*" ProjectRepository : recebe
ObservedProject "1" --> "0..*" ProjectBoard : acompanha
Project "0..1" --> "0..*" ActivityStartCriterion : declara início por
Project "0..1" --> "0..*" ActivityDeadlineCriterion : declara prazo por
ObservedProject "0..1" --> "0..*" ActivityStartCriterion : declara início por
ObservedProject "0..1" --> "0..*" ActivityDeadlineCriterion : declara prazo por
Person "0..1" --> "0..*" PerformedProjectActivity : realizou
Project "0..1" --> "0..*" PerformedProjectActivity : é contexto de
Os quatro elos são o mesmo desenho, quatro vezes¶
spo_project_organizations, spo_project_teams, spo_project_repositories e
spo_project_boards têm exatamente a mesma forma: project_id, o alvo, linked_by_user_id
+ linked_at, unlinked_by_user_id + unlinked_at, e um índice parcial
... WHERE unlinked_at IS NULL.
Isto não é repetição a corrigir: é a declaração com autoria e vigência aplicada quatro
vezes, e é o que permite perguntar "desde quando esta equipe trabalha neste projeto, e quem
disse". Um booleano ativo responderia à primeira metade e perderia a segunda.
PerformedProjectActivity não tem :updated¶
performed_project_activity.ex:10-15: os outros schemas têm três resultados porque descrevem
entidades que mudam. Uma ocorrência não muda — ela aconteceu. Reprocessar a mesma origem
devolve :unchanged, e o outcome virtual só admite [:created, :unchanged]
(performed_project_activity.ex:61).
É a tabela mais populosa do banco: 30 560 linhas em 2026-09-12.
E o nulo que significa mais aqui: concept_id nulo não é dado faltando — significa que a
rede de ontologias não nomeia este tipo de atividade. É o estado honesto de labeled e
cross-referenced (performed_project_activity.ex:16-18).
2. O quadro observado, a iteração, o sprint¶
classDiagram
direction TB
class ObservedProject {
+uuid id
+uuid tenant_id
+uuid connected_tool_id
+int number
+string title
+bool closed
+string source_external_id
+datetime no_longer_observed_at
}
class ProjectItem {
+uuid id
+uuid observed_project_id
+uuid collected_issue_id
+bool is_draft
+string source_external_id
+datetime no_longer_observed_at
}
class FieldDefinition {
+uuid id
+uuid observed_project_id
+string field_external_id
+string name
+string data_type
+map[] options
+datetime no_longer_observed_at
}
class FieldValue {
+uuid id
+uuid project_item_id
+uuid project_field_definition_id
+map raw_value
+string interpreted_as
+datetime last_observed_at
}
class Iteration {
+uuid id
+uuid observed_project_id
+string iteration_external_id
+string field_external_id
+string title
+date start_date
+int duration_days
+uuid sro_sprint_id
+uuid spo_intended_process_id
+datetime no_longer_in_configuration_at
}
class IterationFieldRole {
+uuid id
+uuid observed_project_id
+string field_name
+string role
+datetime declared_at
+datetime revoked_at
}
class Sprint {
+uuid id
+uuid tenant_id
+string internal_id
+uuid connected_tool_id
+int board_number
+string board_title
+string field_name
+string title
+date started_on
+int duration_days
+date ended_on
+bool completed
}
class SprintIssue {
+uuid id
+uuid sprint_id
+uuid collected_issue_id
+datetime observed_at
+datetime no_longer_observed_at
}
class IntendedProjectProcess {
+uuid id
+string internal_id
+string title
+date planned_start_on
+int duration_days
+datetime no_longer_observed_at
}
class CollectedIssue {
<<fronteira — trabalho>>
+uuid id
+int number
}
ObservedProject "1" --> "0..*" ProjectItem : lista
ObservedProject "1" --> "0..*" FieldDefinition : define
ObservedProject "1" --> "0..*" Iteration : configura
ObservedProject "1" --> "0..*" IterationFieldRole : tem o papel declarado de
ProjectItem "1" --> "0..*" FieldValue : carrega
FieldDefinition "1" --> "0..*" FieldValue : tipa
ProjectItem "0..1" --> "0..1" CollectedIssue : aponta para
Iteration "0..1" --> "0..1" Sprint : é lida como
Iteration "0..1" --> "0..1" IntendedProjectProcess : é lida como
Sprint "1" --> "0..*" SprintIssue : compromete
CollectedIssue "1" --> "0..*" SprintIssue : está em
A iteração vira uma de duas coisas, e quem decide é a organização¶
project_iterations tem sro_sprint_id e spo_intended_process_id, e o
CHECK project_iterations_exatamente_um_destino obriga exatamente um preenchido.
A escolha não é da plataforma: vem de smpo_iteration_field_roles, onde a organização declara
que o campo daquele quadro significa sprint ou planning_horizon
(iteration_field_role.ex:16). Sem a declaração, a iteração não é lida como nenhum dos dois —
e isso é o comportamento correto, não uma lacuna: a mesma coluna "Iteration" é sprint numa
organização e trimestre de planejamento noutra (iteration_field_role.ex:2-6).
O nulo que significa¶
| Campo nulo | Significa |
|---|---|
spo_projects.ended_on |
o projeto está em curso |
spo_projects.removed_at |
o projeto existe; remover marca, nunca apaga — e o índice de nome único só vale WHERE removed_at IS NULL |
spo_projects.parent_id |
projeto de topo, sem projeto que o contenha |
spo_project_*.unlinked_at |
o elo vale |
spo_activity_*_criteria.revoked_at |
o critério vale. Revogar marca e nunca apaga, ou "desde quando este critério vale" fica sem resposta (activity_start_criterion.ex:29-31) |
spo_performed_project_activities.concept_id |
a rede não nomeia este tipo de atividade — informação, não ausência |
spo_performed_project_activities.project_id |
a atividade não foi ligada a projeto declarado |
project_items.collected_issue_id |
o item é rascunho do quadro, e CHECK project_items_rascunho_sem_issue garante que rascunho e issue não coexistem |
project_iterations.no_longer_in_configuration_at |
a iteração continua na configuração do campo — nome diferente de no_longer_observed_at, e de propósito: uma iteração sai da configuração, não da observação |
sro_sprints.ended_on |
não derivado ainda de started_on + duration_days |
smpo_iteration_field_roles.revoked_at |
o papel do campo vale |
Classe → schema → tabela → conceito¶
| Classe | Schema | Tabela | Conceito |
|---|---|---|---|
Project |
...SPO.Schemas.Project |
spo_projects |
spo.project |
ProjectOrganization |
...SPO.Schemas.ProjectOrganization |
spo_project_organizations |
— elo declarado |
ProjectTeam |
...SPO.Schemas.ProjectTeam |
spo_project_teams |
— elo declarado |
ProjectRepository |
...SPO.Schemas.ProjectRepository |
spo_project_repositories |
— elo declarado |
ProjectBoard |
...SPO.Schemas.ProjectBoard |
spo_project_boards |
— elo declarado |
ActivityStartCriterion |
...SPO.Schemas.ActivityStartCriterion |
spo_activity_start_criteria |
spo.activity_start_criterion — social_object na UFO |
ActivityDeadlineCriterion |
...SPO.Schemas.ActivityDeadlineCriterion |
spo_activity_deadline_criteria |
declaração irmã; source ∈ {board_field, sprint, milestone} |
IntendedProjectProcess |
...SPO.Schemas.IntendedProjectProcess |
spo_intended_project_processes |
spo.specific_intended_project_process |
PerformedProjectActivity |
...SPO.Schemas.PerformedProjectActivity |
spo_performed_project_activities |
spo.performed_project_activity |
ObservedProject |
TheBand.Projects.Schemas.ObservedProject |
observed_projects |
— plataforma |
ProjectItem |
TheBand.Projects.Schemas.Item |
project_items |
— plataforma |
FieldDefinition |
TheBand.Projects.Schemas.FieldDefinition |
project_field_definitions |
— plataforma |
FieldValue |
TheBand.Projects.Schemas.FieldValue |
item_field_values |
— plataforma |
Iteration |
TheBand.Projects.Schemas.Iteration |
project_iterations |
— plataforma; o conceito sai do papel declarado |
IterationFieldRole |
...SMPO.Schemas.IterationFieldRole |
smpo_iteration_field_roles |
— declaração; role ∈ {sprint, planning_horizon} |
Sprint |
...SRO.Schemas.Sprint |
sro_sprints |
sro.sprint |
SprintIssue |
...SRO.Schemas.SprintIssue |
sro_sprint_issues |
— compromisso observado |
Invariantes que o diagrama não mostra¶
| Invariante | Forma |
|---|---|
| a iteração vira exatamente uma coisa | CHECK project_iterations_exatamente_um_destino |
| critério de início tem um alvo só | CHECK criterio_tem_um_alvo_so: num_nonnulls(project_id, observed_project_id) = 1 |
| critério de prazo idem | CHECK prazo_tem_um_alvo_so |
| campo só quando a origem do prazo é campo | CHECK campo_so_quando_a_origem_e_campo: (source = 'board_field') = (field_name IS NOT NULL) |
| rascunho não aponta issue | CHECK project_items_rascunho_sem_issue |
| nome de projeto único enquanto existir | UNIQUE spo_projects(tenant_id, name) WHERE removed_at IS NULL |
| um elo vigente por par, nos quatro elos | UNIQUE (tenant_id, project_id, <alvo>) WHERE unlinked_at IS NULL |
| um critério vigente por alvo | dois índices parciais por tabela, um para projeto e outro para quadro |
| um papel vigente por campo do quadro | UNIQUE smpo_iteration_field_roles(tenant_id, observed_project_id, field_name) WHERE revoked_at IS NULL |
Os índices de prazo usam NULLS NOT DISTINCT — sem isso, duas linhas com field_name nulo
não colidiriam, e "um critério vigente" deixaria de significar.
O que ficou de fora do diagrama, e por quê¶
inserted_at/updated_at,record_version, e os trios de proveniência (source_system,source_instance,source_external_id,collected_at,last_observed_at) — presentes em quase todas, e repetidos seriam ruído.spo_projects.phaseé virtual (project.ex:45,Ecto.Enumcom:simple/:complex): não é coluna, e por isso não está no ERD. Está aqui como campo da classe porque decide comportamento de tela.*_by_user_idem todos os elos e critérios:linked_by,unlinked_by,declared_by,revoked_by,removed_by,updated_by. O diagrama mostra as datas; a autoria está no ERD.project_field_definitions.options({:array, :map}) eitem_field_values.raw_value(map): o valor cru do campo do quadro, cuja forma varia pordata_typee não é derivável do schema.sro_sprints.internal_ide oboard_number/board_titledenormalizados.
Divergências encontradas¶
Nenhuma entre schema, migração e banco neste subsistema — todas as dezessete tabelas casam campo a campo (comparação automática schema × colunas, 2026-09-12).
Uma ausência declarada, e ela é do dado, não do modelo: spo_project_boards,
spo_project_organizations, spo_activity_start_criteria, spo_activity_deadline_criteria e
smpo_iteration_field_roles estão vazias no banco de desenvolvimento. spo_projects tem 1
linha e spo_project_teams tem 2. O modelo existe inteiro; o uso, quase nada — e um diagrama
que não dissesse isso faria o subsistema parecer mais exercitado do que está.