Classes — trabalho e mudança¶
O que a origem devolveu, o que a plataforma decidiu que aquilo é, e o que foi feito sobre isso. Dezessete tabelas: a issue e sua classificação, a solicitação de mudança e seus commits, a verificação contínua, a avaliação do artefato e o comentário.
É o subsistema mais populoso do banco — spo_performed_project_activities (30 560),
commit_files (30 120), verification_components (29 675), collected_commits (21 320),
medidos no banco de desenvolvimento em 2026-09-12.
Está quebrado em dois diagramas, porque num só as dezessete caixas não caberiam numa tela.
1. A issue, e o que a plataforma decide sobre ela¶
classDiagram
direction TB
class CollectedIssue {
+uuid id
+uuid tenant_id
+uuid observed_repository_id
+int number
+string title
+string state
+string state_reason
+string body
+string author_login
+uuid author_person_id
+string issue_type
+string external_parent_id
+int sub_issue_count
+string external_id
+datetime external_created_at
+datetime external_closed_at
+datetime no_longer_observed_at
}
class IssuePromotion {
+uuid id
+uuid tenant_id
+uuid collected_issue_id
+string declared_concept
+string derived_concept
+string target_table
+uuid target_id
+string rule_id
+int rule_version
+string evidence_source
+string confidence
+uuid mapping_rule_id
+string divergence_reason
+string divergence_kind
+string skip_reason
+string skip_detail
+datetime promoted_at
}
class MappingRule {
+uuid id
+uuid tenant_id
+uuid organization_id
+string where
+string how
+string pattern
+bool case_sensitive
+string target_concept
+int position
+bool active
+datetime deactivated_at
+int version
}
class UnmappedPatternDecision {
+uuid id
+uuid tenant_id
+uuid organization_id
+string pattern
+datetime decided_at
+datetime reverted_at
+string note
}
class DecompositionLink {
+uuid id
+uuid tenant_id
+uuid parent_issue_id
+uuid child_issue_id
+datetime observed_at
+datetime no_longer_observed_at
}
class RefusedLink {
+uuid id
+uuid tenant_id
+uuid parent_issue_id
+uuid child_issue_id
+string child_external_id
+string reason
+string cycle_path
+datetime refused_at
}
class IssueAssignee {
+uuid id
+uuid collected_issue_id
+string login
+uuid person_id
+datetime no_longer_observed_at
}
class IssueLabel {
+uuid id
+uuid collected_issue_id
+string name
+string color
+datetime no_longer_observed_at
}
class CollectedIssueComment {
+uuid id
+uuid collected_issue_id
+string body
+string author_login
+uuid author_person_id
+datetime external_published_at
+datetime external_edited_at
}
class Person {
<<fronteira — EO>>
+uuid id
+string login
}
CollectedIssue "1" --> "0..*" IssuePromotion : é classificada por
MappingRule "0..1" --> "0..*" IssuePromotion : sustentou
CollectedIssue "1" --> "0..*" IssueAssignee : é atribuída a
CollectedIssue "1" --> "0..*" IssueLabel : carrega
CollectedIssue "1" --> "0..*" CollectedIssueComment : recebe
CollectedIssue "1" --> "0..*" DecompositionLink : decompõe em
CollectedIssue "1" --> "0..*" RefusedLink : teve recusada
Person "0..1" --> "0..*" CollectedIssue : escreveu
Person "0..1" --> "0..*" IssueAssignee : é
issue_promotions é append-only, e isso é o modelo¶
Não há update_changeset nem updated_at (issue_promotion.ex:3-8). Uma issue que muda de
conceito entre coletas ganha linha nova, e a vigente é a última — atualizar reescreveria o
passado, e "como esta issue estava classificada em março" desapareceria.
inserted_at é em microssegundo, porque duas promoções do mesmo segundo empatariam e a
"vigente" passaria a depender do plano de execução.
declared_concept × derived_concept × divergence_kind¶
Três colunas, três afirmações diferentes, e é a razão de nenhuma ser um booleano:
| Coluna | Afirma |
|---|---|
declared_concept |
o que a origem declarou (o issue type do GitHub) |
derived_concept |
o que a plataforma concluiu, com evidence_source e confidence |
divergence_kind |
as duas discordam, e de que jeito — cinco valores no CHECK: epic_without_parts, composition_makes_epic, task_with_parts, user_story_without_parts, label_vs_structure |
skip_reason |
não foi possível concluir, e por quê |
E o invariante que os amarra: CHECK issue_promotions_promoted_xor_skipped — ou há
derived_concept e skip_reason é nulo, ou o contrário. Nunca os dois, nunca nenhum.
Uma promoção sem conclusão e sem motivo seria o sucesso silencioso em forma de linha.
CHECK issue_promotions_divergence_kind_needs_reason: divergência sem razão escrita não entra.
2. A mudança, o commit e a verificação¶
classDiagram
direction TB
class CollectedChangeRequest {
+uuid id
+uuid tenant_id
+uuid observed_repository_id
+int number
+string title
+string state
+string source_branch
+string target_branch
+int changed_files
+int commits_total
+int commits_collected
+string merged_head_sha
+string merged_check_state
+int merged_check_contexts
+string author_login
+uuid author_person_id
+uuid merged_by_person_id
+datetime external_merged_at
+datetime no_longer_observed_at
+int reviews_total
+int attended_issues_total
+string[] attended_issues_unresolved
}
class CollectedCommit {
+uuid id
+uuid observed_repository_id
+uuid change_request_id
+string sha
+string message_headline
+int additions
+int deletions
+int changed_files
+datetime external_committed_at
+datetime files_collected_at
}
class CommitFile {
+uuid id
+uuid collected_commit_id
+string path
+string change
+int additions
+int deletions
+string previous_path
}
class CommitAuthor {
+uuid id
+uuid collected_commit_id
+string author_login
+uuid author_person_id
+string author_name
+string author_email
+bool is_primary
}
class ChangeRequestIssue {
+uuid id
+uuid collected_change_request_id
+uuid collected_issue_id
+string source
+datetime no_longer_observed_at
}
class ArtifactEvaluation {
+uuid id
+uuid collected_change_request_id
+string state
+string body
+string author_login
+string author_type
+uuid author_person_id
+datetime external_submitted_at
}
class CollectedVerification {
+uuid id
+uuid observed_repository_id
+string workflow_name
+string workflow_path
+string head_sha
+string head_branch
+string trigger_event
+string run_status
+string conclusion
+string phase
+string[] process_kinds
+int attempt
+uuid actor_person_id
+datetime external_started_at
+datetime external_finished_at
}
class VerificationComponent {
+uuid id
+uuid collected_verification_id
+string job_name
+string conclusion
+string phase
+string[] components
+string[] step_names
}
class CollectedIssue {
<<do diagrama 1>>
+uuid id
+int number
}
CollectedChangeRequest "0..1" --> "0..*" CollectedCommit : reúne
CollectedCommit "1" --> "0..*" CommitFile : toca
CollectedCommit "1" --> "0..*" CommitAuthor : tem autoria
CollectedChangeRequest "1" --> "0..*" ChangeRequestIssue : fecha
CollectedIssue "1" --> "0..*" ChangeRequestIssue : é fechada por
CollectedChangeRequest "1" --> "0..*" ArtifactEvaluation : é avaliada em
CollectedVerification "1" --> "0..*" VerificationComponent : executa
Três campos de
CollectedChangeRequestestão no diagrama e não no schema Ecto.reviews_total,attended_issues_totaleattended_issues_unresolvedsão colunas reais, escritas e lidas por consultas sem schema —from c in "collected_change_requests", 13 ocorrências emlib/. Quem ler sócollected_change_request.exconclui que não existem. Detalhe e encaminhamento em Divergências.
O nulo que significa¶
| Campo nulo | Significa |
|---|---|
*.author_person_id (issue, comentário, commit, PR, verificação, avaliação) |
o login não casou com nenhuma pessoa observada. O author_login continua ali: a plataforma sabe quem escreveu, e não sabe quem é |
*.no_longer_observed_at |
o registro continua sendo visto na origem |
collected_issues.issue_type |
a origem não declarou tipo — e o campo fica cru, porque normalizar destruiria o dado que a lacuna precisa mostrar (collected_issue.ex:4-6) |
collected_issues.external_closed_at |
issue aberta |
collected_commits.change_request_id |
commit fora de qualquer solicitação coletada — ou a PR foi apagada (ON DELETE SET NULL) |
collected_commits.files_collected_at |
os arquivos deste commit ainda não foram coletados (etapa arquivos, balde REST) |
collected_change_requests.merged_check_state |
não se sabe se a verificação passou no momento do merge — não é "passou" |
collected_change_requests.external_merged_at |
não foi integrada (aberta ou fechada sem merge) |
issue_mapping_rules.deactivated_at |
a regra vale |
unmapped_pattern_decisions.reverted_at |
a decisão vale |
issue_promotions.divergence_kind |
origem e plataforma concordam |
Classe → schema → tabela → conceito¶
| Classe | Schema | Tabela | Conceito |
|---|---|---|---|
CollectedIssue |
TheBand.WorkItems.Schemas.CollectedIssue |
collected_issues |
— plataforma; o conceito sai da promoção |
IssuePromotion |
TheBand.WorkItems.Schemas.IssuePromotion |
issue_promotions |
— a decisão, com rule_id e rule_version da base |
DecompositionLink |
TheBand.WorkItems.Schemas.DecompositionLink |
decomposition_links |
— decomposição observada |
RefusedLink |
TheBand.WorkItems.Schemas.RefusedLink |
refused_links |
— a recusa, com o caminho do ciclo |
IssueAssignee |
TheBand.WorkItems.Schemas.IssueAssignee |
issue_assignees |
— plataforma |
IssueLabel |
TheBand.WorkItems.Schemas.IssueLabel |
issue_labels |
— plataforma |
CollectedIssueComment |
TheBand.Communication.Schemas.CollectedIssueComment |
collected_issue_comments |
cmo.comment |
CollectedChangeRequest |
TheBand.Changes.Schemas.CollectedChangeRequest |
collected_change_requests |
cmpo.change_request |
CollectedCommit |
TheBand.Changes.Schemas.CollectedCommit |
collected_commits |
cmpo.commit_artifact_copy |
CommitFile |
TheBand.Changes.Schemas.CommitFile |
commit_files |
cmpo.artifact_copy |
CommitAuthor |
TheBand.Changes.Schemas.CommitAuthor |
commit_authors |
cmpo.stakeholder_performed_commit |
ChangeRequestIssue |
TheBand.Changes.Schemas.ChangeRequestIssue |
change_request_issues |
— elo observado |
ArtifactEvaluation |
TheBand.Quality.Schemas.ArtifactEvaluation |
collected_artifact_evaluations |
qapo.artifact_evaluation |
CollectedVerification |
TheBand.Verification.Schemas.CollectedVerification |
collected_verifications |
ciro.continuous_integration_process |
VerificationComponent |
TheBand.Verification.Schemas.VerificationComponent |
verification_components |
— componente da execução |
MappingRule |
TheBand.Mapping.Schemas.MappingRule |
issue_mapping_rules |
— declaração do tenant |
UnmappedPatternDecision |
TheBand.Mapping.Schemas.UnmappedPatternDecision |
unmapped_pattern_decisions |
— declaração do tenant |
Invariantes que o diagrama não mostra¶
| Invariante | Forma |
|---|---|
| promoção ou conclui ou explica por que pulou | CHECK issue_promotions_promoted_xor_skipped |
| divergência sem razão escrita não entra | CHECK issue_promotions_divergence_kind_needs_reason |
evidence_source ∈ {declared_type, title, structure}, confidence ∈ {high, medium, low} |
dois CHECK |
| issue não é pai de si mesma | CHECK decomposition_links_no_self_parent |
recusa tem motivo fechado: cycle, out_of_scope, task_meets_epic |
CHECK refused_links_reason_check |
regra de mapeamento só olha declared_type ou title, e só por equals/starts_with/contains/regex |
dois CHECK |
| PR integrada com verificação vermelha é localizável | INDEX ... WHERE merged_check_state IN ('FAILURE','ERROR') — índice parcial, e a pergunta que ele serve |
refused_links.cycle_path guarda o caminho do ciclo, não um booleano: a recusa precisa
dizer por onde o ciclo passa, ou quem for corrigir não sabe onde cortar.
O que ficou de fora do diagrama, e por quê¶
inserted_at/updated_atem todas as dezessete tabelas — eissue_promotionsnão temupdated_at, de propósito (é append-only).raw_payload(map) emcollected_change_requests,collected_commits,collected_verifications,collected_artifact_evaluations,collected_issue_comments,cmpo_branches: o cru por linha, além doraw_payloadspor execução.source_system/source_instance/collected_at/last_observed_at: todas as tabelas coletadas os têm (Application Reference, FR-012); repeti-los em dezessete caixas esconderia o resto.collected_issues.milestone_*,project_titles,comment_count,reaction_count,issue_type_external_id— coletados, e não usados no desenho das relações.spo_performed_project_activitiesé a atividade realizada derivada destes registros, e pertence ao diagrama de projetos e processo.
Divergências encontradas¶
Obtidas por comparação campo a campo entre cada schema "..." do de lib/ e as colunas do
banco de desenvolvimento, em 2026-09-12 — e não por leitura visual.
1. Três colunas de collected_change_requests existem, são usadas, e o schema não as tem.
| Leitura | Fonte |
|---|---|
| as colunas existem | priv/repo/migrations/20260819030000_add_attended_issues_provenance.exs:37-38 (attended_issues_total, attended_issues_unresolved) e 20260819040000_create_artifact_evaluations.exs:82 (reviews_total) |
| o schema Ecto não as declara | collected_change_request.ex:19-63 — as 28 colunas restantes estão lá; estas três, não |
| e mesmo assim são escritas e lidas | quality/commands.ex:60-68 (Repo.update_all(... set: [reviews_total: total])), changes.ex:85, changes.ex:352-368, quality.ex:410 — todas por consulta sem schema, from c in "collected_change_requests" |
Uma consulta sem schema não passa pelo %CollectedChangeRequest{} e por isso nunca reclama de
campo inexistente. O efeito prático: %CollectedChangeRequest{} não tem reviews_total, e
quem tentar usá-lo pela struct descobre em tempo de execução. As duas saídas — declarar os três
campos no schema, ou registrar no moduledoc que a tabela é lida sem schema de propósito — são
de quem mantém Changes e Quality, não deste documento.
2. O moduledoc de CollectedVerification cita um campo que não existe.
| Leitura | Fonte |
|---|---|
"subtype e phase são a tradução para a CIRO" |
collected_verification.ex:5 |
não há subtype no schema, na migração nem no banco; há phase e process_kinds |
collected_verification.ex:18-53; colunas da tabela lidas em 2026-09-12 |
subtype é a única ocorrência do termo em todo o lib/ e priv/repo/migrations/. A leitura
mais provável é renomeação para process_kinds sem atualizar o moduledoc — mas provável não
é derivado, e por isso fica como achado. Levar a quem mantém a verificação contínua
(feature 037).
3. Tensão de nome, não resolvida aqui. collected_verifications carrega o conceito
ciro.continuous_integration_process, que na CIRO é o processo; a tabela guarda uma
execução dele (attempt, run_status, conclusion, external_started_at). O moduledoc
assume a leitura de execução de propósito. As duas leituras: ou o conceito da tabela deveria
ser o da execução, ou a CIRO trata o processo como o instanciado. Levar a quem mantém a base
de conhecimento — não é escolha deste documento.