Saltar para o conteúdo
BeryxTechBeryxTech
Voltar às respostas para RFP

Documentação do BeryX

BeryX — Capacidades por critério de avaliação

Documento público. Organiza o que o BeryX faz pelos critérios com que os analistas avaliam ferramentas de qualidade de dados (profiling, monitorização, parsing e normalização, matching, limpeza, enriquecimento, gestão de metadados, orquestração e experiência para utilizadores de negócio). É a fonte técnica da página /rfp do site: cada secção diz o que existe, onde está no código e que testes o provam — e, quando é parcial, o que falta.

Segurança e conformidade estão em SEGURANCA.md. A cobertura por dimensão de qualidade (DAMA, ISO 8000, ISO/IEC 25012) e por área DAMA-DMBOK está em COBERTURA_QUALIDADE.md.

Profiling

Descoberta automática e perfil de cada tabela em todas as fontes ligadas: contagens, nulos, distintos, mínimo/máximo/média/desvio, cardinalidade, distribuições e amostras limitadas.

  • Mais de 20 tipos de fonte: warehouses (BigQuery, Snowflake, Databricks, Amazon Redshift, Fabric, Azure Synapse), bases de dados operacionais (PostgreSQL, MySQL, Oracle Database, SQL Server e Azure SQL — é nestes dois que correm os ERPs Primavera, PHC e Sage), MongoDB (schema inferido por amostra), aplicações de negócio (SAP S/4HANA e SAP Gateway por OData, Salesforce, Dynamics 365 e outros serviços OData v2 e v4), ficheiros, folhas Google (cada separador é um dataset), armazenamento de objetos (GCS, S3, Azure Blob e ADLS Gen2), SFTP, streaming (Kafka, Pub/Sub) e fontes documentais — lista e setup no guia de conectores.
  • Ficheiros Excel (.xlsx, .xlsm) numa pasta, num bucket ou num SFTP: cada folha é um dataset, ocultas incluídas, com o valor em cache das fórmulas e o aviso das que foram gravadas sem calcular. O .xls antigo fica fora.
  • Profiling incremental: em tabelas grandes com coluna temporal, o re-scan lê só a janela nova e combina-a com o perfil anterior (com um perfil completo periódico).
  • Documentos (SharePoint, Google Drive) têm um perfil próprio de qualidade documental.

Testes: backend/tests/test_incremental_profiling.py, backend/tests/test_profile_distributions.py.

Monitorização

  • Monitores automáticos por omissão em cada tabela da conexão (até 500 por conexão em cada scan), sem configuração e a partir dos metadados de cada fonte (nas bases de dados, só do catálogo): atualidade (aprende a cadência de atualização), volume (z-score modificado sobre o histórico) e mudanças de schema, conforme o que cada fonte expõe (método).

  • Deteção de drift e anomalias: colunas adicionadas/removidas, mudança de tipo, picos de nulos, quedas de volume, drift de distribuição por coluna (quantis e categorias já perfilados, método), anomalias sazonais e por segmento.

  • Metodologia publicada: fórmulas, pesos, limiares e limitações do score, das anomalias, dos duplicados, do trust score e da amostragem em METODOS.md, com uma secção «Como calculamos» na app.

  • Motor de regras com 16 tipos de regra: nulos (not_null), unicidade (unique), padrões (pattern), intervalos (range), atualidade (freshness), volume mínimo (row_count_min), validade por dígito de controlo (valid_identifier, registo de identificadores por país) — NIF/NIPC, NISS, NIB e Cartão de Cidadão; DNI, NIE e NIF de empresa de Espanha; CPF, CNPJ (também o alfanumérico) e PIS/PASEP do Brasil; AVS/AHV e UID da Suíça; NHS number e VAT GB do Reino Unido; IBAN com a estrutura e o comprimento do país (registo SWIFT e Angola); Luhn. Sem dígito de controlo publicado, só a estrutura: SSN, EIN e ITIN dos EUA, NINO e Companies House do Reino Unido, NIF e BI de Angola —, listas de referência (in_reference_set: as ISO incluídas ou as do registo de dados de referência do workspace, com versões, mapeamentos de códigos entre sistemas e uma verificação de coerência entre datasets), condições entre colunas da mesma linha numa linguagem de expressões fechada — «data_fim >= data_inicio», «total = soma(subtotal, iva)», «se pais = 'PT' então válido(nif, 'nif')» — (expression, com o dígito de controlo de NIF, NISS, CPF, CNPJ, DNI, NIE, NHS number, AVS/AHV e UID verificado na própria fonte), SQL à medida só de leitura — um SELECT das linhas violadoras, com zero ou até N linhas esperadas — (custom_sql), integridade referencial (referential: chaves órfãs face a outro dataset, por pushdown na mesma ligação ou anti-join em memória entre ligações, proposta pelo DETGEN para cada aresta do lineage), exatidão contra fontes autoritativas (accuracy, abaixo) e, nos streams, idade do evento (max_event_age), conformidade com o Schema Registry (schema_conformity), campos obrigatórios (required_fields) e mensagens ilegíveis (max_malformed_pct). As regras de expressão e de SQL correm na fonte, sobre a tabela inteira, e só nas fontes SQL; em ficheiros, APIs e streams ficam «não avaliáveis». O SQL do cliente passa por uma guarda — um só SELECT, sem escrita, só funções de uma lista e só tabelas da ligação — e corre numa transação sempre desfeita, com só-leitura e tempo máximo na sessão onde o motor os tem (DBA.md). Checks em Python à medida continuam fora. As regras chegam por packs por indústria (e-commerce, financeiro, saúde, SaaS), por sugestões a partir do perfil e do histórico de aceitação do workspace — só entram quando um administrador as aceita — ou pelo import de dbt (também os testes singulares, que viram regras de SQL à medida), Great Expectations, SodaCL e ODCS (o que converte). As suites legadas do Informatica Data Quality e do Collibra DQ entram pelo mesmo import, a partir dos exports de regras, com um relatório de cobertura da migração regra a regra (como migrar).

  • Exatidão contra fontes autoritativas (regra accuracy, cartão #266). Um NIF com o dígito de controlo certo pode não existir: a regra pergunta a uma fonte se o valor existe. Número de IVA no VIES da Comissão Europeia (Estados-Membros e Irlanda do Norte), código postal CP7 e localidade na GeoAPI.pt (dados dos CTT), telefone no plano de numeração do país com a biblioteca phonenumbers (E.164, tipo e país, sem rede), email por sintaxe e registo MX do domínio (DNS sobre HTTPS, sem enviar nada à caixa) e países e moedas nas listas ISO publicadas, descarregadas com atualização periódica. É opt-in: um admin autoriza cada fornecedor em PUT /api/v1/workspace/accuracy e, sem autorização, nenhum valor sai e a regra fica «não avaliada». Os pedidos são só GET pelo cliente HTTP só-leitura dos conectores, com a guarda anti-SSRF; as respostas ficam em cache no worker, separadas por workspace, cada fornecedor tem um intervalo mínimo entre pedidos e cada scan tem um teto de consultas (DATAVAULT_ACCURACY_MAX_LOOKUPS_PER_SCAN). O que falha abre um issue de exatidão com exemplos mascarados. Os números de IVA do Reino Unido e da Suíça ficam por verificar: os serviços oficiais (HMRC e UID) exigem POST (backend/app/domain/quality/accuracy.py, backend/app/domain/scans/accuracy.py).

  • Regras portuguesas só com dados de forma portuguesa. O que é próprio de Portugal deixa de se aplicar a dados com outra forma (backend/app/domain/quality/pt_shape.py):

    • a lista de CP4 exige o CP7 em maioria na amostra (NNNN-NNN, também com espaço ou sem separador). Uma coluna que só guarde o CP4 («1000») deixa de receber essa regra, porque não se distingue de um código suíço. Um código postal japonês escrito sem hífen («1000001») continua a passar por português;
    • o NIF e o código postal do pack de e-commerce em colunas vat_number, tax_id, zip ou postal_code exigem a mesma forma na amostra do último scan. Um VAT britânico, um EIN americano ou um UID suíço não têm a forma do NIF;
    • na correspondência de telefones, um número sem indicativo casa com o mesmo número com indicativo estrangeiro («923 456 789» com «+244 923 456 789», backend/app/domain/quality/matching.py). Sem indicativos estrangeiros na chave, o +351 aplica-se aos 9 dígitos. Com o país dos dados PT (a omissão), o caso espelho também casa: um telemóvel português «912 345 678» liga-se a «+244 912 345 678» quando é esse o único número da chave com os mesmos 9 dígitos. Com outro país num dos lados (ver o país dos dados, abaixo), o número sem indicativo desse lado leva o indicativo do país (+34 em Espanha) e nunca o +351;
    • na deteção de dados pessoais, um telefone de outro país conta como telefone: com +, com o DDD brasileiro («(11) 91234-5678») ou, numa coluna com nome de telefone, só pelos dígitos. O nome conta quando tem uma palavra de telefone (telefone_alt, phone_number, tlm) ou quando é só celular, numero_celular, contato, contacto ou whatsapp; canal_contacto, modelo_celular e is_mobile não contam. Datas, decimais, o CP7 e o CEP nunca contam como telefone. Deixa de passar por morada sem sair do PII (backend/app/domain/compliance/pii_map.py). Sem nome de telefone, também conta o número nacional escrito com separadores e com os grupos de um telefone de PT, Espanha, Brasil, Angola, EUA, Suíça ou Reino Unido («020 7946 0958», «612 34 56 78»), quando é 80% da amostra. Numa coluna de identificador (sku, order_ref) não conta (#552).

    Nas datas NN/NN/AAAA, a correção só lê o mês primeiro quando algum valor tem o 2.º campo acima de 12. Sem prova da ordem não há UPDATE, só o aviso.

  • País dos dados (#513). O workspace tem um país (ISO 3166-1 alfa-2, PT por omissão) e a conexão e o dataset podem sobrepô-lo. O scan usa o do nível mais baixo que o tenha (backend/app/domain/catalog/data_country_service.py). Não é a residência dos dados. Com um país que não seja PT (backend/app/domain/quality/data_country.py):

    • o tipo semântico deixa de dar phone_pt e postal_code_pt, e o DETGEN não propõe a lista de CP4 dos CTT nem o padrão do telefone português (backend/app/domain/quality/semantic_types.py, backend/app/domain/rules/generation.py);
    • os telefones passam a ser o tipo phone com a região do país, quando a maioria da amostra são números atribuídos no plano de numeração desse país. O plano vem do phonenumbers, sem rede (backend/app/domain/quality/phones.py, #516). Um valor com parte decimal nunca conta. Sem pista de telefone no nome da coluna, só contam os números escritos com «+», «00» ou separadores, e com os grupos de um telefone desse plano: o processo «2024/12345» não conta. Uma coluna de tipo numérico não é phone (backend/app/domain/quality/semantic_types.py);
    • a standardização não propõe o +351 nem o CP7, as moradas não se leem como portuguesas e não há enriquecimento por distrito e concelho (backend/app/domain/insights/standardization.py);
    • a standardização de telefones propõe o E.164 do país, por exemplo +34 num dataset espanhol. Um número escrito com «+» ou «00» só se converte com o indicativo do país. Os números de outro país ficam como estão, para revisão manual, e contam na tolerância da regra que protege a coluna. Em Portugal, o mesmo vale para o +351 (backend/app/domain/insights/phone_standardization.py);
    • na correspondência, cada telefone passa a E.164 no plano do país do seu lado. «923 456 789» de um dataset angolano casa com «+244 923 456 789» e não com «+351 923 456 789». Os números brasileiros com o 9.º dígito casam com «+55…», também com o 0 de tronco ou o código de operadora. No Reino Unido, na Suíça e nos EUA o formato nacional casa com o internacional;
    • NIF, NISS, CC e IBAN continuam a reconhecer-se, porque o dígito de controlo prova o identificador. O mapa de PII continua a apanhar um telefone numa coluna com nome de telefone, pelas amostras.

    Numa fonte SQL, uma tabela com uma coluna de país (pais, country…) com códigos ISO de dois países ou mais, um deles PT, recebe para a coluna de NIF ou NISS a proposta se pais = 'PT' então válido(nif, 'nif') quando a amostra mistura valores com e sem o dígito português (pelo menos 30 % com ele): o dígito de controlo só se pede às linhas portuguesas, na fonte.

  • SLO por dataset e incidentes por linhagem (um problema a montante agrupa os afetados a jusante).

  • Alertas in-app, email, Slack, Teams e webhook, com escalas de piquete e escalonamento, e SMS ou WhatsApp pela conta Twilio do cliente só para incidentes críticos e violações de SLA (mensagem curta com o link, horário silencioso e teto por número). Cada canal tem um âmbito opcional (domínios, conexões, datasets, tipos de problema, monitores automáticos), para cada equipa receber só os seus, com pré-visualização dos últimos 30 dias.

  • Varredura de engenharia no Databricks e no Snowflake (cartão #55). Opt-in por conexão e só de leitura: no Databricks só pedidos GET às APIs oficiais (notebooks, jobs, pipelines DLT e Lakeflow, Model Registry); no Snowflake só SHOW e SELECT de catálogo (views, materialized views, procedures, tasks, streams e warehouses). Abre um problema «Engenharia» por heurística, com os ativos afetados: credenciais escritas no código (o tipo e a linha, nunca o valor), SELECT *, escrita na Gold a partir da Bronze, jobs agendados sem retry, timeout ou alerta de falha, jobs e pipelines em falha, notebooks que escrevem tabelas sem job, tasks suspensas há mais de 30 dias, streams stale, modelos ML sem re-treino há mais de 90 dias e warehouses sem auto-suspend. O código não fica guardado. O resumo por IA é opcional e só leva metadados dos achados (metadata_only). Na app, ativa-se e pede-se no cartão da conexão em Conexões, e os achados abrem-se em Problemas com o filtro de engenharia (guia).

Testes: backend/tests/test_auto_monitors.py, backend/tests/test_schema_changes.py, backend/tests/test_anomalies.py, backend/tests/test_quality_rules.py, backend/tests/test_slo.py, backend/tests/test_oncall_escalation.py, backend/tests/test_alert_routing.py, backend/tests/test_engineering_scan.py, frontend/src/components/EngineeringScanPanel.dom.test.tsx.

Parsing e normalização

Tudo é proposta. O BeryX mede a amostra do perfil e gera o SQL no dialeto da conexão e o PySpark. Quem aplica é o cliente, e nada é escrito na fonte.

  • Tipos semânticos detetados automaticamente por coluna, a partir dos valores e do nome: email, URL, UUID, telefone, NIF, NISS, IBAN, Cartão de Cidadão, código postal, país, moeda, datas, percentagens, montantes, nomes e moradas (backend/app/domain/quality/semantic_types.py). Os tipos com formato fixo dão uma regra de padrão ou de dígito de controlo sugerida (backend/app/domain/rules/generation.py).

  • Standardização de formatos: telefones, códigos postais, NIF, IBAN e datas guardados em formatos mistos passam à forma canónica. O passo 1 conta as linhas por máscara e o passo 2 é um UPDATE proposto. Ao aceitar, o BeryX propõe a regra pattern que impede os formatos antigos de voltar (backend/app/domain/insights/standardization.py).

  • Decomposição de campos compostos por regras de locale (pt, es, en), sem serviços externos (backend/app/domain/quality/parsing.py):

    • moradas → rua, número, andar, código postal, localidade e país;
    • nomes completos → título, primeiro nome, apelidos (com «de/da/del/y») e último apelido, também na forma «Apelido, Nome»;
    • telefones → indicativo, número nacional, extensão e E.164;
    • NIF ou DNI/NIE/CIF junto com o nome → identificador validado pelo dígito de controlo e nome;
    • datas em texto livre (meses e dias da semana em português, espanhol e inglês) → data ISO;
    • montantes com moeda → valor decimal e código ISO da moeda.

    O locale e a ordem dia/mês leem-se dos próprios valores. Uma coluna com um tipo semântico atómico (NIF, IBAN, email, código postal, data ISO…) nunca é tratada como composta, e sem pista no nome um telefone só se reconhece com «+», «00» ou separadores (ATOMIC_SEMANTIC_KINDS em backend/app/domain/quality/parsing.py). Para cada coluna, o relatório dá a taxa de sucesso na amostra, a cobertura de cada componente e as máscaras dos valores que não se decompõem, nunca os valores. Num corpus de teste com formatos mistos, decompõem-se 30 de 30 moradas PT/ES, 30 de 30 nomes, 29 de 30 telefones e 30 de 30 datas.

  • Proposta de normalização (sugestão parse_standardize): um SELECT só de leitura com as colunas derivadas e a contagem das linhas reconhecidas, mais o PySpark equivalente. As colunas derivadas só se preenchem nas linhas que a contagem dá como reconhecidas e ficam a NULL nas outras. Nunca há UPDATE (backend/app/domain/insights/parsing_sql.py). Uma coluna que já tem standardização não recebe a decomposição. Como qualquer sugestão aceite, abre um pull request no repositório do cliente (limpeza e remediação).

  • Na app: o painel «Parsing» no detalhe do dataset mostra os componentes detetados, a cobertura e os formatos que falham. O botão «Propor normalização» mostra o SQL e o PySpark da sugestão. A API é GET /api/v1/datasets/{id}/parsing.

  • Deteção de variantes do mesmo valor numa coluna categórica (maiúsculas, espaços), com proposta de normalização em SQL e PySpark.

  • Contratos de dados ODCS com tipos e formatos, validáveis em CI (gate de qualidade em CI).

Limites: o SQL usa só funções que existem em PostgreSQL, BigQuery, Snowflake, Databricks e MySQL. SQL Server/Fabric e SQLite não têm REGEXP_REPLACE e recebem só o PySpark. A coluna data_iso sai em texto e só se preenche com dias que existem no calendário. No SQL, o NIF e o DNI/NIE reconhecem-se pela forma; o dígito de controlo só se valida no relatório da amostra. Nos telefones, os indicativos reconhecidos são os de uma lista fixa (Portugal, Espanha, Reino Unido, EUA, França, Alemanha, Itália, Suíça, Luxemburgo, Bélgica, Países Baixos, Irlanda, Brasil, Angola, Moçambique e Cabo Verde), e os outros não se decompõem.

Testes: backend/tests/test_parsing_composite.py (corpus e componentes), backend/tests/test_parsing_sql.py (SQL compilado em PostgreSQL e BigQuery, colunas derivadas a NULL fora das linhas reconhecidas, PySpark e API), backend/tests/test_postgres_only.py (o SQL executado em PostgreSQL no job backend-postgres), backend/tests/test_suggestions_standardization.py, backend/tests/test_suggestions.py, backend/tests/test_quality_rules.py.

Matching e deduplicação

Tudo é proposta. O BeryX pontua e agrupa os registos, a decisão de quem revê fica registada no BeryX e a consolidação sai em SQL e PySpark só de leitura. Nada é escrito na fonte.

  • Deteção de duplicados exatos (hash) e aproximados (semelhança textual com rapidfuzz) dentro de um dataset, agrupados em clusters que se reveem, fundem ou ignoram na UI, com proposta de consolidação em SQL (ROW_NUMBER de deduplicação) e PySpark.
  • Correspondência entre dois datasets, também de fontes diferentes (ex.: clientes no CRM e no ERP — resolução de entidades): chaves exatas, normalizadas (email, telefone, NIF, nome, código postal) ou aproximadas, com pares, linhas sem par, ambíguas e cobertura, na página «Correspondências» (backend/app/domain/quality/matching.py, método). Para cada par, o registo que ficaria (mais completo, depois mais recente, depois o lado A), com o motivo e um SELECT de diagnóstico.
  • Regras de correspondência configuráveis por campo, guardadas por par de datasets (backend/app/domain/quality/matching_rules.py): para cada campo, o comparador, o normalizador, o peso e a semelhança mínima. Os comparadores são exact, normalized, jaro_winkler, token (token_sort_ratio), numeric com tolerância e date com janela em dias (backend/app/domain/quality/matching_comparators.py). Nos normalizadores, «Nome de empresa» (company_name) tira a forma jurídica do fim do nome (Lda, Unipessoal, S.A., S.L., Ltda, EIRELI, Inc, LLC, Ltd, PLC, AG, GmbH, Sàrl) e «Nome de pessoa» (person_name) ignora as partículas («de», «da», «dos», «del», «van») e aceita um nome sem alguns apelidos com 0,9 (backend/app/domain/quality/entity_names.py). O blocking é por chave, por prefixo ou por um código fonético do português, e há dois limiares: correspondência (0,85 por omissão) e revisão (0,70). Na UI, as colunas de cada lado escolhem-se do schema dos dois datasets e os campos (até 12) e as regras de blocking (até 4) acrescentam-se e removem-se. Sem campos escolhidos, a regra nasce das chaves sugeridas.
  • Clusters revistos na UI: cada linha liga-se ao seu melhor candidato e as ligações formam clusters entre os dois datasets. Um cluster fica em «correspondência» quando a ligação mais fraca passa o limiar de correspondência, e em «revisão» quando fica entre os dois limiares. Na secção «Correspondências: regras e clusters» da vista «Entre datasets» de Duplicados, cada cluster mostra a semelhança de cada campo e marca-se como fundido ou ignorado. A decisão liga-se às linhas do cluster, e não à execução: vale nas execuções seguintes enquanto o cluster tiver as mesmas linhas, e fica no trilho de auditoria.
  • Regras de sobrevivência por campo (backend/app/domain/quality/survivorship.py): fonte prioritária, mais recente (pela coluna de atualização de cada dataset, que a sugestão deteta pelo nome; sem as duas, a regra é recusada), mais completo ou mais frequente, com os empates resolvidos pela ordem das fontes e depois pela das linhas. O registo consolidado é só proposto, e cada campo diz de que fonte e de que linha vem o valor e por que regra. Exporta-se num SELECT só de leitura ou em PySpark sem escrita, e pode seguir como sugestão que, aceite, abre um pull request no repositório do cliente (limpeza e remediação).
  • Métricas por conjunto de regras: pares candidatos e pontuados, clusters em cada faixa, taxa de correspondência, decisões de revisão (fundidos, ignorados, por rever) e a precisão observada nessas decisões.
  • Benchmark rotulado (sintético, português): com as regras sugeridas para CRM × ERP e os limiares por omissão, precisão 1,0 e recall 0,85 em 10 000 linhas, e precisão 1,0 e recall 0,83 em 2 000 (método).

As execuções das regras correm fora do scan, na fila do matching, sobre uma amostra determinística de até 50 000 linhas de cada dataset. Com colunas de dados pessoais nas regras, a execução pede confirmação, como na correspondência por chaves. Os clusters guardam os scores e um hash de cada linha. Os valores de exemplo ficam truncados, com os dados pessoais mascarados, e a retenção apaga-os.

Limites: o matching corre no BeryX sobre essa amostra, sem pushdown para a fonte. Cada conjunto de regras compara dois datasets. Os limiares não se afinam sozinhos a partir das decisões: a precisão observada fica à vista e quem gere as regras decide se os muda. Um bloco com mais de 200 linhas num dos lados não gera candidatos e fica contado. O código fonético é uma redução própria da ortografia portuguesa, e não um algoritmo publicado. No SQL exportado, o JOIN usa só os campos de igualdade e cada linha é um par A × B, com a mesma escolha que a app faz para esse par; os pares que dependem de campos de semelhança, e a escolha entre mais de dois registos de um cluster, estão nos clusters da app.

Por desenho, não somos um hub MDM: não mantemos um registo-mestre persistido nem escrevemos o golden record nas fontes (o que não fazemos).

Testes: backend/tests/test_duplicates.py, backend/tests/test_duplicate_constellation.py, backend/tests/test_matching.py, backend/tests/test_record_matching.py, backend/tests/test_matching_bench.py, backend/tests/test_matching_rules.py, backend/tests/test_matching_rules_api.py.

Limpeza e remediação

Por desenho: propomos, não escrevemos.

O BeryX propõe a correção — SQL e PySpark gerados, com poupança estimada e risco analisado (seguro vs breaking) — e o cliente aplica. Não pedimos credenciais de escrita e nunca escrevemos na fonte.

  • Remediação como pull request: a correção abre um PR no repositório GitHub/GitLab do cliente (modelos SQL/PySpark e testes dbt), para revisão pelo processo que já existe.
  • Fluxo governado: proposto → aceite → aplicado → verificado, com trilho de auditoria e verificação automática no scan seguinte.

Testes: backend/tests/test_git_remediation.py, backend/tests/test_remediation_lifecycle.py.

Enriquecimento

Tudo é proposta. O BeryX junta a cada valor de uma coluna o que uma fonte de referência diz dele, com a origem, e gera o SQL no dialeto da conexão e o PySpark. Quem aplica é o cliente, por pull request ou SQL depois de aprovar a sugestão, e nada é escrito na fonte.

  • Seis tipos de enriquecimento, escolhidos pelo tipo semântico da coluna no último perfil e, no código postal espanhol, pelo nome da coluna (backend/app/domain/quality/enrichment.py):
    • código postal português (CP7) → localidade, concelho e distrito, na GeoAPI.pt (dados dos CTT e do INE);
    • código postal espanhol → localidade e comunidade autónoma, no Nominatim público do OpenStreetMap, que recebe só o código postal;
    • NIF de empresa → nome e morada registados no VIES da Comissão Europeia. Só se consulta o NIF de pessoa coletiva (1.º dígito 5, 6 ou 9). O NIF de uma pessoa singular fica fora do âmbito e não sai;
    • morada → latitude e longitude, numa instância Nominatim do próprio cliente. As moradas não vão para o Nominatim público, cuja política de uso pede que não se lhe enviem dados pessoais;
    • país → código ISO 3166-1 alfa-2, e moeda → código ISO 4217, pelas listas ISO versionadas no BeryX, sem consulta externa.
  • Proveniência: cada valor proposto leva a fonte, a data da consulta e a confiança. Cada proposta dá a cobertura na amostra, ou seja, as linhas com valor proposto entre as linhas com valor na coluna. Um valor que a fonte não conhece, que passa o teto de consultas ou cuja fonte não respondeu fica sem proposta, com o motivo. No VIES, um campo publicado como --- conta como sem valor.
  • Proposta (sugestão enrichment, backend/app/domain/quality/enrichment_service.py): o passo 1 conta, só com leitura, as linhas a enriquecer, e pode ensaiar-se na fonte (ensaio de uma correção). O passo 2 é um UPDATE proposto numa coluna à escolha, que só preenche as linhas onde ela está vazia, ou, sem coluna escolhida, um SELECT que junta os campos propostos. A tabela de referência do SQL só leva valores que a fonte confirmou: códigos postais, NIF de empresas e códigos ISO. Na geocodificação, o SQL não leva moradas: o passo 2 junta uma tabela de coordenadas que o cliente carrega. Como qualquer sugestão aceite, pode seguir por pull request para o repositório do cliente (limpeza e remediação). Os scans não apagam uma proposta por aprovar: só uma nova proposta da mesma coluna e do mesmo tipo a atualiza. Depois de aplicada, a sugestão não passa sozinha a verificada, porque o scan não mede o enriquecimento: marca-se como verificada à mão, no cartão da sugestão.
  • Opt-in por fornecedor: nenhum valor sai antes de um administrador autorizar o fornecedor no workspace, em «Regras → Fornecedores de enriquecimento» ou em PUT /api/v1/workspace/enrichment. A autorização regista quem consentiu e quando. Sem autorização, a amostra nem se lê. É independente da autorização da regra accuracy. O que sai para cada fornecedor está em SEGURANCA.md.
  • Geocodificação só na instância do cliente (nominatim_self_hosted): para a autorizar, o administrador indica o URL da instância (nominatim_url) e confirma que as moradas podem ser dados pessoais (confirm_personal_data). Mudar o URL pede outra vez essa confirmação, e retirar a autorização apaga o URL. Sem instância configurada e autorizada, nenhuma morada sai. O URL passa pela guarda anti-SSRF ao gravar e em cada consulta. Uma instância atrás de autenticação não está suportada.
  • API: GET /api/v1/datasets/{id}/enrichment lista as colunas candidatas, com o tipo e o fornecedor de cada uma; POST /api/v1/datasets/{id}/enrichment/proposals lê uma amostra só dessa coluna, consulta o fornecedor e grava a sugestão (backend/app/api/v1/enrichment.py).

Limites: a proposta corre sobre uma amostra da coluna, e os valores que não aparecem nela ficam por propor. Cada proposta tem um teto de consultas externas, e o Nominatim público responde a 1 pedido por segundo. No código postal espanhol, o Nominatim público devolve um só resultado, por isso um código partilhado por várias localidades dá só uma delas. A GeoAPI.pt e o Nominatim público são serviços gratuitos sem nível de serviço contratado. Os valores do Nominatim público vêm do OpenStreetMap, com a licença ODbL, que pede atribuição a quem os publica. Fora da lista acima, o BeryX não acrescenta dados de terceiros aos registos: não há serviço postal pago, base comercial de empresas nem geocodificação fora da instância Nominatim do cliente.

Testes: backend/tests/test_enrichment.py.

Gestão de metadados

  • Catálogo automático de todas as fontes, com domínios/data products, dono e membros.

  • Linhagem: chaves estrangeiras, SQL das views (ao nível da coluna), manifest.json do dbt, eventos OpenLineage de terceiros — OpenLineage — e a linhagem do Unity Catalog e a Data Lineage API do Dataplex, lidas no import do catálogo (cartões #691 e #692).

  • Explorador de linhagem (página /linhagem, GET /lineage/graph, cartão #714): o grafo tabela → tabela do workspace e, ao expandir um dataset, coluna → coluna, com os dashboards e modelos como nós finais. Os nós pintam-se pela confiança (o trust score do catálogo, «Confiança B · 74», calculado para até 2.000 datasets, os mais ligados), pelo score de qualidade, pelos problemas abertos, pela sensibilidade ou pela certificação. Filtra por conexão e pelos domínios que quem pede lê, expande a montante e a jusante salto a salto, mostra o caminho mais curto entre dois datasets e exporta a vista em PNG, SVG ou JSON. A mesma vista existe em lista hierárquica, e a linhagem das colunas lê-se também em texto, no painel do dataset e na lista. O grafo usa-se por teclado e o layout corre num Web Worker. Um dataset invisível para quem pede só aparece como «Dataset restrito», ligado a um visível (app/domain/lineage/explorer.py).

  • Publicação no catálogo do cliente: veredictos de qualidade no OpenMetadata e no DataHub, e assets «Data Quality Metric» no Collibra (Import API v2). No Microsoft Purview, o veredito, o score, a data do scan e o link para o BeryX vão para atributos da tabela no Data Map. No Unity Catalog da Databricks, o trust score, o veredito, as regras falhadas, os issues por resolver e a certificação vão para tags beryx_* da tabela; o owner, as tags, os comentários das colunas e a linhagem são lidos para o catálogo leve. No Dataplex Universal Catalog (cartão #692), a entry da tabela recebe três aspects da BeryX: veredito e trust score, resultado das regras e issues abertas. O Dataplex entra sem chave JSON (a BeryX impersona uma service account do cliente) e dá também a leitura: descrições, glossário e linhagem. No Alation (cartão #693), a tabela e as colunas recebem campos de qualidade beryx_* pela Data Quality API: veredito e score, trust score, issues abertas, certificação e uma entrada por regra, cada uma GOOD, WARNING ou ALERT. Com o veredito FALHA, a tabela leva também uma trust flag Warning do utilizador da integração, que sai quando o veredito muda. A BeryX não escreve owners, termos nem descrições no Alation. No Atlan (cartão #695), a tabela recebe os atributos do custom metadata «BeryX», que um admin do Atlan cria: veredito, score e resumo, as regras, trust score e grau, issues abertas, certificação, link e data do scan, cada um no tipo que o atributo tem. Com o veredito FALHA, a tabela leva também um announcement da BeryX, só se não tiver outro, que sai quando o veredito muda. A BeryX não escreve owners, termos, tags nem a certificação do Atlan. No Informatica CDGC (cartão #694), cada regra e o veredito sobem como Data Quality Rule Occurrences BERYX-… ligadas à tabela, com o score 0-100, as linhas lidas e as falhadas, num import em lote por push. O login usa o utilizador e a password do IDMC, presos aos dois hosts do pod. A BeryX não escreve owners nem termos no CDGC. O Collibra, o Purview, o Unity Catalog, o Dataplex, o Alation, o Atlan e o Informatica CDGC estão validados com transporte simulado; falta a validação num tenant ou projeto do cliente — catálogo externo.

  • Classificação de sensibilidade por coluna (dados pessoais) e README por dataset gerado e aprovado por um humano.

  • Descrições sugeridas por IA (cartão #687) para o dataset e as colunas sem descrição, a partir dos metadados e do perfil agregado (tipo, chaves, % de nulos, distintos e sensibilidade, nunca valores das linhas), pela tarefa catalog_docs do routing do LLM. Sem modelo disponível saem de templates, em pt, en ou es. Todo o texto sugerido passa pela guarda de saída antes de se gravar, e uma edição com dados pessoais é recusada. Cada sugestão fica marcada «sugerida por IA» até uma pessoa a aceitar, tal como veio ou editada, ou a recusar, e só a aceite entra no catálogo, com histórico e auditoria. Decide o owner de um domínio do dataset ou um admin, no próprio dataset, e um admin em massa na fila de revisão (GET /description-suggestions). A ação «Sugerir descrições» das operações em massa gera para todos os datasets da seleção, e o rollback apaga as sugestões que ninguém decidiu. Datasets com docs do dbt ou do catálogo externo ficam de fora, porque o catálogo de registo do cliente continua a ganhar (backend/app/domain/catalog/descriptions.py, catálogo externo).

  • Catálogo pelo grafo (cartão #715, ecrã «Sugestões do grafo», só admins): um motor sem LLM lê a linhagem ao nível da tabela e da coluna, as chaves declaradas (também as do inventário), o perfil e o co-uso em consultas (pares de tabelas lidas pela mesma query no Snowflake e no BigQuery, só contagens). Propõe a descrição e o termo do glossário numa coluna copiada 1:1 a jusante, a classificação de uma coluna copiada ou derivada (um COUNT(email) não herda pii), chaves estrangeiras por declarar (pista no nome, tipo e perfil compatíveis), o domínio (a mesma regra das sugestões de domínio, e um «Ignorar» de lá conta como rejeição), o owner pela maioria estrita dos vizinhos e, só quando se pede, data products por área. Cada sugestão traz a confiança e o caminho no grafo que a justifica, em lista. Nada se escreve sem uma pessoa aceitar: aceitar aplica pelo serviço de sempre (o termo passa pelo fluxo de aprovação das ligações, se estiver ligado) e cada decisão fica na auditoria. Uma sugestão rejeitada só volta se mudar o que propõe (o texto da descrição a montante, por exemplo) ou, no domínio, os vizinhos que votaram nele. As que estão por decidir chegam aos admins na caixa «As minhas tarefas». Um tipo só corre por omissão com precisão ≥ 90 % num conjunto de teste com respostas conhecidas, pelo menos 5 casos e 3 armadilhas (backend/app/domain/catalog/graph_engine.py, backend/tests/graph_golden.py).

  • Qualidade do catálogo e dos metadados (KPIs de governação), por workspace e por domínio: % de datasets com owner, descrição, classificação, domínio, contrato e regras; descrições desatualizadas (o schema mudou depois), owners inativos ou que saíram, datasets sem uso e sem owner e docs de colunas que já não existem. Cada problema traz a sugestão pelo canal que o resolve (README gerado, import do catálogo externo), com metas, tendência diária e export CSV (GET /catalog/metadata-quality, backend/app/domain/catalog/metadata_quality.py).

  • Pesquisa do catálogo com facetas e ranking (cartão #681, vista «Pesquisa» dos Datasets, GET /catalog/search): os datasets que casam com o texto, com as regras da pesquisa transversal, ou todos sem texto, com oito facetas e as contagens de cada valor: domínio, owner efetivo, termo do glossário, sensibilidade (a mais alta das colunas), grade do trust score, nível de uso, certificação e os modelos e agentes de IA que consomem o dataset (cartão #685). Dentro de uma faceta os valores somam-se e entre facetas cortam-se, e cada faceta conta com os filtros das outras. Só entram os datasets que quem pesquisa vê, e os domínios restritos sem concessão não aparecem na faceta. A ordem dá pontos, até 100: texto até 50 (nome igual 50, palavras no nome 40, schema ou descrição 25, coluna ou termo 20, tag 15), trust score até 20, uso até 20 (escala logarítmica, 1000 queries/dia dão 20) e certificado 10. Desempata pelo texto, pelo trust, pelo uso e pelo nome, e cada resultado mostra os fatores que o puseram ali («coluna cliente_id (+20) · certificado (+10)»). O texto, os filtros e a página ficam no URL, que se copia para partilhar a pesquisa. Os filtros e as facetas contam com todos os datasets visíveis que casam. Só o trust score, calculado na leitura, fica limitado aos primeiros 2000 que passam os outros filtros, pela ordem sem o trust. Acima disso, a resposta diz-o (capped, trust_scored): a faceta e o filtro do trust só contam com esses, os restantes vêm no fim, sem os pontos do trust, e a contagem e o estado vazio avisam (backend/app/domain/catalog/faceted_search.py, frontend/src/components/CatalogSearch.tsx).

  • Eventos de metadados nos webhooks e pesquisa do catálogo no MCP (cartão #688): um canal pede, em event_kinds, os eventos de dataset criado ou removido, schema mudado, owner ou steward mudado, termo do glossário aprovado e certificação mudada. Cada evento grava-se na transação da mudança, e o worker entrega-o assinado, com Idempotency-Key, com o âmbito do canal e com o de uma equipa. Uma descoberta ou uma conexão apagada dá no máximo 100 eventos, e o último diz quantos ficaram de fora. A tool MCP search_catalog lê GET /catalog/search com as credenciais de quem chama e não leva o owner (backend/app/domain/catalog/metadata_events.py, backend/app/domain/alerts/metadata_dispatch.py, backend/app/mcp_catalog.py; ver WEBHOOKS e MCP_SERVER).

  • Glossário de negócio (plano Team ou superior): termos como «Receita líquida», com definição, sinónimos, domínio e owner, ligados aos datasets e às colunas que os implementam. Aprovar um termo ou uma ligação é de um admin ou do owner do domínio do termo, e fica na auditoria. O termo herda a qualidade das colunas aprovadas: o score dos datasets e os issues por resolver de cada coluna, com a explicação («está a 51.7 por causa de 2 issue(s)…»). O sugeridor propõe colunas pelo nome, pelos sinónimos, pelo tipo semântico e pela descrição dbt, sem chave de IA; com IA, a sugestão fica marcada como gerada por IA. Uma sugestão nunca nasce aprovada. Os termos aprovados entram no contrato ODCS (businessName e tags), no datavault.yaml, na pesquisa do catálogo, no assistente (search_glossary) e no MCP (glossary://terms) (backend/app/domain/catalog/glossary.py, backend/app/api/v1/glossary.py). Na app, a página Glossário pesquisa, cria, edita e aprova termos e revê as sugestões, e a tabela de colunas do dataset mostra os termos de cada coluna (aceite com borda sólida, proposta tracejada), com «+ termo» e o aviso das ligações por rever (frontend/src/pages/Glossario.tsx, frontend/src/components/DatasetTermChips.tsx).

  • Hierarquia, categorias e relações do glossário (cartão #677): um termo tem um termo mais largo (o pai) e uma categoria («Financeiro»), e relaciona-se com outros como sinónimo, «contém» (do todo para a parte) ou relacionado. A hierarquia não aceita ciclos nem mais de 6 níveis, e «contém» também não fecha ciclos. Num termo fora de rascunho, muda a estrutura quem o aprova, em todos os termos que a mudança toca: as duas pontas de uma relação, e o pai antigo e o novo. Um termo importado do catálogo do cliente é só de leitura, também como a outra ponta. Com o fluxo dos termos em modo retido, a estrutura de um termo aprovado não muda (409), porque o fluxo ainda não a leva (PUT /glossary/terms/{id}/hierarchy, POST /glossary/terms/{id}/relations, backend/app/domain/catalog/glossary_hierarchy.py). O dicionário para o catálogo do cliente leva só os termos aprovados e mantém a hierarquia (é um ficheiro: o BeryX publica, não substitui o catálogo, #407). Um antepassado por aprovar corta o caminho publicado: o termo fica por baixo do último aprovado da cadeia. No OpenMetadata, vai um CreateGlossaryTerm por termo, os pais antes dos filhos, com o parent e os relatedTerms pelo FQN, e as colunas citam o termo pelo FQN com o caminho. No DataHub, cada categoria e cada termo com filhos é um glossaryNode, e o termo leva o parentNode e as relações em glossaryRelatedTerms (backend/app/domain/catalog/dictionary.py). Na app, o termo mostra o caminho, os termos mais estreitos e as relações, e edita-os (frontend/src/components/GlossaryTermStructure.tsx).

  • Papéis, subdomínios e re-atestação do owner dos domínios (cartão #677): um domínio tem um domínio pai (sem ciclos, no máximo 5 níveis; muda-o um admin), stewards e custodians além do owner, e uma matriz RACI por atividade (definição, qualidade, acesso, classificação e ciclo de vida) com um só «A» por atividade. Os papéis não dão permissões. Os papéis e a RACI muda-os quem gere o domínio. O owner atesta de 180 em 180 dias que continua a responder pelo domínio, e outra vez logo que o owner muda (na app, na API, por GitOps ou pelo import do catálogo do cliente) ou que o IdP desativa a conta ou tira de uma equipa quem tem um papel no domínio (SCIM, apagar o grupo no SCIM, ou os grupos do IdP no login), ou que um admin desativa essa conta na app (PATCH /auth/users/{id}, motivo «conta desativada»). Só o owner atesta: um admin ou um token levam 403. A atestação vencida, ou a 14 dias do prazo, aparece na caixa do owner (ownership_attestation) e sai de lá quando ele atesta. A secção do pacote do conselho (GET /ownership-attestations/board-pack, também em Markdown) dá as contagens por estado e a lista das vencidas, com o domínio, o owner, o prazo, os dias de atraso e o motivo. A sugestão de owner pontua as pessoas pelos datasets do domínio de que são owner ou steward, com mais peso para os mais consultados nos últimos 30 dias, e pelos datasets a montante na linhagem de que são owner. Só contam os datasets que quem pede vê. A resposta não traz nomes de datasets, só contagens por tipo de evidência (GET /domains/{id}/stewardship, backend/app/domain/catalog/stewardship.py, backend/app/domain/governance/ownership_tasks.py). Na app, o domínio tem o painel «Papéis e ownership» (frontend/src/components/DomainStewardshipPanel.tsx). O pacote do conselho da app (em Governação) ainda não traz esta secção: lê-se pela API, como a dos agentes.

  • Dono fantasma (cartão #759): o estado de quem responde vale para todos os objetos governados com uma pessoa responsável: o dataset, o domínio, o termo local do glossário, a definição de métrica (o owner e a autoridade de definição), o elemento de dados crítico, a política e o agente ou modelo de IA (o owner e o owner do prompt). Ficam de fora os retirados, os termos descontinuados e os termos externos. Os estados são os do owner_inactive dos KPIs de metadados (saiu do workspace, conta desativada, sem sessão há 90 dias) e um sinal novo, «silencioso»: conta ativa e com sessão, sem atestações, aprovações nem decisões no trilho de auditoria há 180 dias, a cadência da atestação do owner de um domínio. Para o owner de uma política, a janela é o período de revisão mais longo das suas, se passa dos 180 dias (365 por omissão): quem cumpre a cadência que o produto lhe pede não é silencioso. Uma conta mais nova do que a janela também não. Com o responsável inativo, cada admin recebe na caixa a tarefa «Reatribuir o responsável» (owner_reassignment), por objeto e papel, com o aviso no sino e por email. A tarefa não tem prazo e abre a página do objeto. Fecha sozinha quando o responsável muda ou a conta volta a estar ativa. Acima de 500, o teto corta pelo pior estado: entram primeiro as contas desativadas, depois quem saiu e por fim quem está sem sessão. O silencioso não pede reatribuição: aparece só no relatório GET /governance/ghost-owners (só admins). O relatório lista os objetos, pior estado primeiro, com o estado, a última sessão e a última decisão de quem responde. As contagens por estado são de todos os objetos, e truncated diz que a lista passou do teto de 500 (backend/app/domain/governance/ghost_owners.py).

  • Carta do papel e «Os meus papéis» (cartão #762): cinco papéis têm carta, o owner, o steward e o custodian de um domínio, a autoridade de definição de uma métrica registada e o dono de um agente de IA. Cada carta diz o que o papel é e «a decisão que tomas sozinho», ligada a uma atividade em que o papel é o «A»: nos papéis do domínio, as da matriz RACI desse domínio; a autoridade é o «A» da definição da métrica, porque assina cada versão, e o dono do agente o da passagem, porque só o dono novo a aceita. Há cartas por omissão (o steward e o custodian sem decisão própria, porque a RACI por omissão não lhes dá nenhum «A»), e um admin muda-as em PUT /role-charters. Uma decisão só se liga a uma atividade em que o papel é o «A» na RACI por omissão ou na de algum domínio. Atestar o ownership de um domínio confirma a versão da carta do owner que está à vista. Sem ela, a atestação dá 422, e com uma versão que já mudou dá 409. A versão confirmada fica no domínio. Em «As minhas tarefas», o separador «Os meus papéis» (GET /tasks/my-roles) lista os objetos de que a pessoa responde, até 500, com a carta, as atividades em que é o «A», o papel que decide quando a RACI de um domínio dá a decisão da carta a outro e, no owner, a atestação e se confirmou a carta em vigor. Os objetos de domínios e datasets que a pessoa não vê ficam de fora, e um token não tem papéis (backend/app/domain/governance/role_charters.py, frontend/src/components/TarefasPapeis.tsx).

  • Fluxos de aprovação (plano Team ou superior): o admin configura, por workspace, o fluxo dos termos do glossário, o das ligações termo → coluna, o da certificação (ver «Certificação de datasets e data products»), o das exceções às políticas (ver «Políticas e normas») e o dos pedidos de acesso (ver «Pedidos de acesso a domínios, com prazo e recertificação»). Cada etapa tem aprovadores (um papel: admins, qualquer membro ou quem gere o domínio do termo; uma equipa; ou uma pessoa), uma regra (um aprova, todos aprovam ou a maioria aprova), um SLA opcional em horas e para quem escala (por omissão, os admins). Vencido o SLA, o worker escala o pedido uma vez por etapa e quem recebe o escalonamento decide a etapa sozinho. Quem sai de uma etapa (conta desativada, apagada ou passada a Leitor) deixa de contar para a regra, e, se ninguém sobrar por votar, os admins decidem a etapa (ou o pedido espera, se o fluxo o disser). Quem se ausenta delega as aprovações noutra pessoa por um período, na Governação, e o voto do delegado conta pela pessoa ausente. As tarefas de cada pessoa aparecem em «As minhas tarefas» (/tarefas), ao lado das outras, com um aviso no sino e por email quando chegam e quando o SLA da etapa vence. Com o fluxo das ligações ligado, entram nele as ligações propostas à mão, as do «Sugerir ligações» e as que já estavam propostas quando o fluxo foi ligado, e o termo mostra a etapa de cada uma. O histórico de cada pedido (quem decidiu, em que etapa, em nome de quem, com que comentário) fica na auditoria e no termo: o do termo numa secção própria, o de cada ligação na tabela «Onde é usado». Sem fluxo configurado, vale o de antes: um admin ou quem gere o domínio aprova (uma exceção, decide-a um admin). Cada pedido, e cada pedido de ação do assistente (o propose_action do MCP incluído), traz a pré-visualização do que muda se for aprovado, calculada sem IA (cartão #699): o diff campo a campo, os ativos e consumidores a jusante (agentes incluídos), se reinicia o histórico de uma regra ou detetor e o custo estimado por scan e de IA. Uma parte ou uma linha do diff que não se calcula aparece como «Impacto desconhecido», com o porquê (por exemplo, a definição de um termo aprovado fora do fluxo, que o histórico só guarda até 500 carateres, ou um objeto que mudou depois do pedido). Numa edição retida, o diff é a proposta face ao termo. «As minhas tarefas» mostra-a em «O que muda?» (backend/app/domain/governance/approvals.py, backend/app/domain/governance/approval_config.py, backend/app/domain/governance/request_preview.py, frontend/src/components/RequestPreview.tsx, backend/app/api/v1/approvals.py, frontend/src/pages/Tarefas.tsx, frontend/src/components/FluxosAprovacao.tsx, frontend/src/components/ApprovalHistory.tsx, frontend/src/components/DelegacaoAusencia.tsx).

    As garantias que uma auditoria pede:

    • Veto. Cada etapa tem um limiar de rejeição: por omissão, uma rejeição faz cair a etapa e o pedido. Com um limiar maior, a etapa só cai com esse número de rejeições, ou quando os votos que faltam já não chegam à regra (approvals._outcome).
    • Quem pede não decide. Quem pediu (quem submeteu o pedido e o autor do termo, da ligação, da exceção ou do pedido de acesso; numa edição retida, só quem editou) nunca decide o próprio pedido: não conta como aprovador da etapa, nem por delegação, escalonamento, fallback ou override. Se não sobrar outro aprovador, ou se os aprovadores da etapa não resolverem ninguém, o fluxo diz o que acontece: decidem os admins do workspace, menos quem pediu, ou o pedido espera (approval_config.barred_voters, approval_config.step_approvers, approvals._enter_step).
    • Agentes não aprovam. Um token de API (com ou sem ficha de agente), uma chamada de uma ferramenta MCP, o Microsoft 365 Copilot e o steward automático recebem 403 ao decidir. O MCP não tem nenhuma ferramenta que decida (backend/app/api/v1/approvals.py, approval_guard.is_agent).
    • Proposta com base. O pedido guarda o que se aprova, o valor de cada campo do objeto quando abriu e o SHA-256 dos dois. Antes de cada decisão, o objeto compara-se com essa base: se mudou, o pedido passa a «em conflito» e nada se aplica por cima (backend/app/domain/governance/approval_guard.py).
    • Decisões só de acréscimo. Aprovar, rejeitar e o override de um admin acrescentam uma linha ao histórico, que nunca se edita nem se apaga. O RGPD só retira a pessoa. O override decide o pedido por cima das etapas que faltam, pede sempre um motivo, e o motivo fica no histórico e na Auditoria (backend/app/domain/governance/approval_outcomes.py).
    • Edição retida. Com o modo retido no fluxo dos termos, a edição de um termo aprovado não se aplica logo: fica à espera do fluxo, o termo mostra a versão de antes e quem a propôs não a decide. Aprovada, entra no termo com o histórico de cada campo (glossary.update_term).
    • Expiração. Um fluxo com prazo em dias fecha o pedido como «expirado» quando ninguém o decide a tempo. O pedido expirado ou em conflito volta a quem pediu, como tarefa em «As minhas tarefas», até voltar a pedir (backend/app/domain/governance/approval_returns.py).
  • Tarefas geradas pela medição e ações em lote («As minhas tarefas», /tarefas): além do que espera uma decisão, a caixa mostra o que a medição sabe que falta, ao owner do dataset (o próprio ou o herdado do domínio) ou, sem owner, ao steward do dataset. São quatro tipos: os checks de cobertura dos KPIs que falham (owner, descrição, classificação), o issue crítico de uma regra sem pessoa nem equipa atribuída, o elemento de dados crítico sem responsável e a certificação a menos de 14 dias do fim. Num dataset sem owner nem steward, o owner em falta vai aos admins, e a descrição e a classificação não vão a ninguém até haver um dos dois. Não se gravam: cada leitura recalcula-as, por isso fecham sozinhas quando o dado é corrigido. As dos metadados não contam no menu nem avisam no sino ou por email, porque são o backlog da cobertura, sem prazo. A lista põe primeiro as vencidas e, a seguir, as críticas (um dataset com CDE, um issue crítico), pelo prazo. Aprovar, rejeitar com comentário e reatribuir fazem-se em lote, e a permissão verifica-se item a item: só entram as tarefas da caixa de quem pede, decididas pelo serviço da rota de origem, e o resto fica por fazer, com o motivo. Um agente não aprova nem rejeita em lote, como na rota da decisão, e um Leitor, mesmo com uma concessão editor, decide na origem. O separador «Criados por mim» lista os pedidos por decidir que a pessoa abriu, e o pedido de acesso e a mudança de uma atividade do RoPA (só admins) retiram-se dali. Com uma delegação em ausência ativa, o delegado recebe também as tarefas geradas da pessoa ausente, com o nome dela. As tarefas que vencem chegam também ao Slack e ao Teams, pelo outbox de colaboração: em cada tick dos avisos, um evento tasks.overdue por domínio, com o nome do domínio, quantas venceram, quantas são críticas e o link para a caixa filtrada por esse domínio, sem títulos nem pessoas. Só o recebem os canais com essa subscrição, e a política da página «Presença» lê-se antes de cada envio: a pausa salta-o e «só links» tira o nome do domínio (backend/app/domain/governance/generated_tasks.py, backend/app/domain/governance/task_actions.py, backend/app/domain/governance/task_channels.py, backend/app/api/v1/tasks.py).

  • Certificação de datasets e data products: o selo tem cinco estados (pedido, certificado, em revisão, revogado ou recusado). Cada certificado guarda quem certificou, a data, a validade (180 dias por omissão, até 730) e a evidência dos critérios. Os critérios são automáticos: dono (o do dataset ou o herdado do domínio), descrição, contrato ODCS ativo e não quebrado, regras críticas a passar e score de pelo menos 80. Num data product valem para todos os datasets dele, e conta o pior score. O pedido só abre com os critérios todos cumpridos e segue o fluxo de aprovação das certificações. Sem fluxo configurado, aprova um admin ou quem gere o domínio. A aprovação volta a conferir os critérios e só uma pessoa aprova: um token de API ou um agente pode pedir, mas não certifica. Quando a validade acaba, o worker põe o certificado em revisão e abre o pedido de recertificação, que aparece em «As minhas tarefas» e abre no dataset ou no domínio. No fim de cada scan, um certificado cujo contrato quebrou, cujo veredito passou a fail ou cujo score desceu do mínimo também vai a revisão. Aprovada, a recertificação dá validade nova; recusada, o certificado fica revogado. Um admin ou quem gere o domínio revoga, com motivo. O selo aparece na API (GET /certifications, GET /datasets/{id}/certification, GET /domains/{id}/certification), no MCP (get_certification, só leitura) e no catálogo do cliente: o caso datavault_quality publicado no OpenMetadata, no DataHub e no Collibra leva o estado, a validade e o selo no resumo. O Purview recebe só o veredito; o Unity Catalog, o estado na tag beryx_certification e a validade em beryx_certified_until. Na app, o selo, os critérios, o pedido e a revogação aparecem na página do dataset e na do domínio. A lista Datasets do catálogo e a dos domínios mostram o selo ao lado do nome quando o objeto está certificado ou em revisão. Quem não lê um domínio restrito vê o nome dele, mas não o estado da certificação do data product: a lista, as tarefas e o histórico deixam-no de fora. Apagar o domínio ou a conexão apaga a certificação e fecha o pedido aberto (backend/app/domain/governance/certification.py, backend/app/api/v1/certifications.py, backend/app/domain/integrations/catalog_sync.py, frontend/src/components/CertificacaoPanel.tsx, frontend/src/components/CertificationBadge.tsx).

  • Pedidos de acesso a domínios, com prazo e recertificação (página /acessos). Cada passo fica na auditoria, sem o texto do motivo (backend/app/domain/access/requests.py, backend/app/domain/access/recertification.py, backend/app/domain/access/access_policy.py, backend/app/api/v1/access_requests.py, frontend/src/pages/Acessos.tsx).

    • Pedido. Quem é membro de uma equipa (mesmo com o papel Leitor) ou um admin pede acesso dessa equipa a um domínio, de leitura ou de escrita, à versão mascarada ou aos valores sensíveis, com a finalidade (BI, agente, RAG ou treino), o motivo, o prazo em dias e, opcional, o ticket externo. Uma equipa com uma concessão sem prazo não pede: a equipa owner do domínio muda-a na matriz.
    • Aprovação. O pedido segue o fluxo de aprovação dos pedidos de acesso, que o admin configura como os outros. Sem fluxo configurado, aprova um admin ou a equipa owner do domínio, que é quem gere as concessões. O responsável indicado do domínio gere o conteúdo e não decide acessos: o fluxo dos pedidos de acesso não aceita o papel «quem gere o domínio». Um Leitor, mesmo na equipa owner, vê os pedidos do domínio mas não decide acessos, como nos outros fluxos. Um pedido aos valores sensíveis leva sempre mais uma etapa no fim, «Dados pessoais», decidida pelos aprovadores que a política indica (por omissão, os admins), e tem um prazo máximo mais curto (30 dias por omissão, contra 180). Ligada na política, a aprovação automática aprova logo um pedido de leitura, mascarado e com prazo curto. Um pedido aos valores sensíveis nunca é aprovado assim. O aprovador vê no pedido a decisão de confiança dos datasets do domínio para essa finalidade.
    • Prazo. Aprovado, o pedido cria a concessão da equipa com data de fim. Passado o prazo, a concessão deixa de contar no acesso e na clearance de dados pessoais, e o worker apaga-a.
    • Na fonte. As fontes do cliente são só de leitura. Por isso, a concessão que um pedido aprovou abre à equipa owner do domínio e aos admins a tarefa de a executar na fonte, em «As minhas tarefas». Quando sai, abre a de a retirar: no fim do prazo, no «Revogar acesso», na recertificação, ao apagá-la na matriz ou ao apagar a equipa. O pedido fecha então como expirado ou revogado.
    • Recertificação. Uma campanha, que um admin abre à mão ou que abre sozinha de N em N dias, dá à equipa owner de cada domínio e aos admins uma tarefa por concessão em vigor: confirmar ou revogar. No fim da campanha, o que ninguém confirmou é revogado, com a tarefa de o retirar na fonte.
  • Owner e steward por dataset: cada dataset pode ter owner e steward próprios. Sem owner próprio, herda o do primeiro domínio que tem owner. Esta regra é a mesma no detalhe, no dicionário, na pesquisa e nos KPIs de metadados (effective_owner em backend/app/domain/catalog/metadata.py). O steward não se herda. Muda o owner e o steward de um dataset quem pode escrever nesse dataset; o owner de um domínio só o muda um admin. O owner do dataset não dá permissões.

  • Tags por dataset e por coluna: o workspace escolhe uma política de tags. Em modo livre aceita qualquer tag; em modo controlado, só se acrescentam as da lista (as que o alvo já tinha ficam), e só um admin a muda. Editar as tags substitui as manuais e deixa as do catálogo externo. Mudar o owner, o steward ou as tags fica no trilho de auditoria (PATCH /datasets/{id}/owners, PUT /datasets/{id}/tags, PUT /catalog/tag-policy). Na app, o detalhe do dataset tem o painel «Owner, steward e tags», com o histórico e o diff entre datas, e Datasets › Metadados tem a política de tags.

  • Dicionário de dados exportável por dataset, domínio ou workspace, em CSV, JSON, Markdown, OpenMetadata e DataHub. O dicionário leva descrições, tipos, sensibilidade, termos aprovados, tags, owner, steward, regras ativas e o SLA dos domínios. Os termos só saem nos planos com glossário. O ficheiro OpenMetadata traz um corpo de PUT /api/v1/tables (CreateTableRequest) por tabela e, à parte, o que esse pedido não aceita: o owner e o steward por email (o pedido exige o id do utilizador no OpenMetadata), os domínios, o SLA e as regras. As tags e os termos vão na classificação e no glossário DataVault, que têm de existir no OpenMetadata. O ficheiro DataHub traz MCPs UPSERT para o datahub ingest, com o owner como BUSINESS_OWNER e o steward como DATA_STEWARD. O FQN ou URN vem do mapeamento da sincronização do catálogo, se a conexão o tiver. É um ficheiro: o BeryX não envia descrições nem tags para o catálogo do cliente. No CSV, as células que começam por =, +, - ou @ saem neutralizadas. Cada pedido leva até 2 000 datasets; com mais, a resposta traz X-Dictionary-Truncated: true (GET /datasets/{id}/dictionary, GET /domains/{id}/dictionary, GET /catalog/dictionary, backend/app/domain/catalog/dictionary.py).

  • Histórico de metadados: cada alteração fica registada com o quê, quem, quando, o antes e o depois. Entram as mudanças de owner, steward e tags, a sensibilidade posta à mão, as ligações de termos a colunas criadas ou decididas pela API e as edições dos campos de um termo. O histórico devolve as 500 alterações mais recentes da janela, com X-History-Truncated: true quando há mais; para recuar, passa-se em before o id da mais antiga devolvida. O diff entre duas datas conta com todas as alterações da janela e dá, por campo, o valor no início e no fim (GET /datasets/{id}/metadata/history, GET /datasets/{id}/metadata/diff, GET /catalog/metadata-history, GET /catalog/metadata-diff). Entra também no export do workspace. O histórico começa com esta versão: os eventos anteriores de auditoria e de sensibilidade não foram convertidos.

  • Pesquisa transversal (GET /search?q=): datasets, colunas, termos do glossário, tags, owners ou stewards e domínios (data products), estes pelo nome e pela descrição. Acentos e maiúsculas não contam («regiao» encontra «Região»). As colunas vivem numa tabela própria, dataset_columns, com uma linha por coluna. O perfil, o inventário e o import dbt atualizam-na em cada escrita (backend/app/infra/db/catalog_index.py). No Postgres a pesquisa usa um índice guardado: GIN sobre um tsvector com pesos (nome, schema, descrição) e trigramas do pg_trgm, que aceitam uma letra em falta ou a mais numa palavra de 4 ou mais letras («custmer» encontra customer_id). Uma letra errada ou duas letras trocadas de lugar nem sempre passam: «pagamneto» encontra pagamento, «emial» não encontra email. Com 50 000 colunas responde abaixo de 300 ms (teste pg_only). No SQLite casa por LIKE, sem tolerância a erros de escrita. Os datasets dos domínios restritos a quem pesquisa não aparecem em nenhuma das espécies (backend/app/domain/catalog/metadata_search.py). O domínio restrito aparece pelo nome e pela descrição, como no GET /domains. Nos datasets conta também a popularidade (cartão #686): as queries/dia do nível de uso somam ao score até 0,3 em escala logarítmica (1000/dia dão o máximo; o nome igual vale 1), e no SQLite desempatam antes do nome.

  • Uso a partir de Postgres, SQL Server e Redshift (cartão #686): a recolha da telemetria (POST /dashboard/finops/collect) dá também nível de uso às tabelas destas fontes, só com estatísticas, sem ler dados nem texto de queries (backend/app/infra/connectors/usage_stats.py). No Postgres lê os scans do pg_stat_user_tables, sem privilégios além de ligar à base. No SQL Server lê os seeks, scans e lookups do sys.dm_db_index_usage_stats, com VIEW SERVER STATE. No Redshift lê os passos scan do SYS_QUERY_DETAIL, sem as queries do próprio BeryX, com SYSLOG ACCESS UNRESTRICTED. A vista guarda uns 7 dias, por isso a média divide pelos dias que ela mostra e não por 30 (usage_log_keeps_window). Postgres e SQL Server só têm contadores acumulados, que não dizem desde quando contam (o Postgres guarda-os num reinício limpo, com o stats_reset vazio). Por isso a média sai da diferença entre duas recolhas, com pelo menos 7 dias entre elas, e sem as leituras dos scans do próprio BeryX, medidas no mesmo contador antes e depois de cada scan (uma amostra TABLESAMPLE não o sobe, um count(*) paralelo sobe-o uma vez por processo). As leituras do BeryX fora dos scans contam como de outros, e as de outros durante um scan, nas tabelas dele, como do BeryX (backend/app/domain/scans/service.py). A primeira recolha e um reset não dão nível novo nem mexem no aviso aberto. Uma tabela só fica «sem uso» com 80 dias seguidos sem leituras de outros. Com uma réplica de leitura (ou a intenção read-only do SQL Server), o BeryX liga só à secundária, nunca ao primário: os contadores contam só as leituras feitas nela, a recolha di-lo e nenhuma tabela fica «sem uso» por eles (backend/app/domain/finops/read_counters.py). Sem o privilégio ou com o track_counts do Postgres desligado, a conexão sai como indisponível, com o que falta, e os níveis anteriores ficam (backend/app/domain/finops/usage_priority.py). O painel Custo × qualidade mostra, por conexão, o que falta, o que a réplica não vê e quantas tabelas ainda esperam nível, com os textos da API na língua de quem recolhe (frontend/src/pages/DashboardExecutivo.tsx).

  • Pesquisa global na app (cartão #680): em qualquer página, ⌘K (Ctrl+K) ou o botão Pesquisar do cabeçalho abrem uma janela sobre o GET /search com até 5 resultados por tipo (datasets, domínios, termos e colunas) e as páginas da app que a conta vê, escolhidos com as setas e abertos com Enter. A página /pesquisa mostra até 20 por tipo, com filtro por tipo e o que casou sublinhado (frontend/src/components/CommandPalette.tsx, frontend/src/pages/Pesquisa.tsx). As tags e os owners não têm destino na app e ficam na API.

  • Dashboards e relatórios BI como ativos do catálogo (cartão #683): os dashboards do Power BI, do Tableau e do Looker (sync de BI) e os exposures dbt do tipo dashboard têm página própria (/dashboards/:id, GET /catalog/dashboards/{id}) e listagem (GET /catalog/dashboards), e aparecem na pesquisa global como tipo «Dashboards e relatórios BI», pelo nome, pelo rótulo e pela descrição da curadoria. A página mostra os datasets de origem com o trust de cada um, e o trust do dashboard é o da fonte mais fraca. Mostra também o uso que a ferramenta reportou no último sync (última visualização, total, «sem visualizações há N dias»). Editores e admins escrevem a curadoria (PATCH /catalog/dashboards/{id}): owner, descrição, tags com a política de tags e termos do glossário. Cada mudança fica na auditoria (dashboard_curated, só ids e nomes dos campos), e a curadoria sobrevive a um dashboard que sai de um sync e volta (backend/app/domain/catalog/dashboards.py). Um dashboard só com fontes em domínios restritos a quem pede não aparece. Ainda não sobem ao catálogo externo, nem entram na pesquisa com facetas (ver catálogo externo).

  • Modelos e agentes de IA como ativos do catálogo (cartão #685): o registo de agentes e modelos do #674 (GET /compliance/ai-agents) passa a ser também catálogo, com o mesmo objeto e sem cópia. Cada modelo ou agente tem página própria (/modelos-ia/:id, GET /catalog/ai-agents/{id}) e listagem (GET /catalog/ai-agents, com filtro kind). A página mostra o owner (a pessoa responsável ou, sem ela, o texto da ficha), o modelo, o sistema de IA do AI Act ligado, o estado e o nível de risco. Mostra também a linhagem para os datasets de treino (papéis treino, validação e teste) e para os de consumo em produção (inferência), com o trust de cada um. O trust do modelo ou agente é o do dataset mais fraco, e cada etapa tem o seu. Os datasets são os da ficha, os mesmos da ficha do Anexo IV, e só um admin os muda no registo. Na pesquisa global aparecem como tipo «Modelos e agentes de IA», pelo nome, pelo modelo e pela finalidade. Na vista «Pesquisa» dos Datasets, a faceta «Modelos e agentes de IA» filtra os datasets que cada um consome, e cada resultado diz por quem é usado (backend/app/domain/catalog/ai_assets.py, frontend/src/pages/ModeloIaDetalhe.tsx). Um modelo ou agente só com datasets em domínios restritos a quem pede não aparece. Num que também consome outros, só se veem os datasets visíveis. Não sobem ao catálogo externo (ver catálogo externo).

  • Pipelines como ativos do catálogo (cartão #684): os conectores do Fivetran e do Airbyte (o retrato do último sync da integração), os jobs OpenLineage do cliente (Airflow, Spark, dbt, Dagster, pela ferramenta do producer), os jobs do dbt Cloud e as DAGs do Airflow reportadas pelo beryx-airflow (o pipeline é a DAG; o estado é o do run mais recente, falhado se alguma task falhou) têm página própria (/pipelines/:id, GET /catalog/pipelines/{id}) e listagem (GET /catalog/pipelines), e aparecem na pesquisa global como tipo «Pipelines», pelo nome, pela ferramenta e pela descrição e tags da curadoria. A página mostra o estado do último run, a cadência declarada no integrador, os datasets que o pipeline lê e escreve (no dbt Cloud, os modelos cujo último run foi deste job), os issues abertos dos datasets que escreve (os de sync deste pipeline primeiro) e o impacto a jusante: os datasets depois dos que escreve, até 5 saltos e os primeiros 50, os consumidores afetados (os consumidores travados pela decisão de confiança à parte) e o alcance total. Editores e admins escrevem a curadoria (PATCH /catalog/pipelines/{id}): owner, descrição, agenda declarada e tags com a política de tags. Cada mudança fica na auditoria (pipeline_curated, só ids e nomes dos campos), e a curadoria sobrevive a um pipeline que sai de um sync e volta (backend/app/domain/catalog/pipelines.py). Um pipeline que só toca datasets de domínios restritos a quem pede não aparece; os restritos a jusante ficam «Dataset restrito». O que falta: o Azure Data Factory não tem leitor próprio (só entra pelo OpenLineage, se o cliente o emitir), a mesma DAG do Airflow reportada pelo beryx-airflow e pelo OpenLineage aparece duas vezes, e os pipelines não sobem ao catálogo externo nem entram na pesquisa com facetas (ver catálogo externo).

  • Montra dos data products (cartão #689): a página /produtos (GET /catalog/data-products) lista os domínios publicados por nome, com a descrição, o owner, a versão e a data da última publicação. Os descontinuados só aparecem a pedido e os rascunhos nunca. O data product é o domínio, sem entidade à parte. A quem lê o domínio, cada data product mostra também o trust (o do dataset visível mais fraco, como nos dashboards e nos modelos de IA), em quantos datasets há um contrato ODCS aceite, com o bundle para descarregar (GET /domains/{id}/contract), e o selo de certificação. Num domínio restrito sem concessão, ou retido pela ficha de um agente, só se veem o nome, a descrição e o owner. O botão «Pedir acesso» aparece sem leitura e, só com leitura, para pedir escrita. Abre o formulário de Acessos com o domínio escolhido (/acessos?dominio=) e, no pedido de escrita, com o tipo «Escrita» (&papel=editor). Quem não está em nenhuma equipa não pode pedir, porque o pedido é sempre de uma equipa, e a página indica-lhe o owner do data product. A montra só lê, com um número fixo de queries por página (backend/app/domain/catalog/marketplace.py, frontend/src/pages/Produtos.tsx). Não publica no catálogo externo (ver catálogo externo).

  • Inventário só de metadados (cartão #682): por conexão, agendado (de 1 em 1 hora a uma vez por semana), traz tudo o que a conexão vê sem perfilar nem ler linhas: esquemas, tabelas, vistas e colunas, com tipo, comentários, chaves primárias e estrangeiras, partições e clustering (BigQuery) e a contagem estimada quando o motor já a guarda (Postgres, MySQL, BigQuery). As fontes SQL leem o catálogo de cada esquema de uma vez. Salesforce, OData e SAP leem o describe ou o $metadata. Nas fontes em que as colunas só se inferem de linhas (ficheiros, folhas, tópicos, coleções, APIs REST, Dynamics), o inventário fica pelos nomes, com um objeto por ficheiro e sem abrir os livros Excel. As fontes documentais não têm inventário (inventory_mode em backend/app/infra/connectors/inventory.py). O inventário lista todos os ficheiros, tópicos, coleções e entity sets, sem o teto da descoberta (200 por omissão, inventory_tables). Cada consulta tem o teto de tempo do conector e, no BigQuery, o maximum_bytes_billed. A volta tem um prazo de 30 minutos e um teto de 50 000 objetos, e uma volta que para a meio não conta ausências como remoções. Os tenants suspensos ficam fora do agendamento, e cada conexão corre uma volta de cada vez. A 1.ª volta completa é a linha de base. Daí em diante, cada tabela nova, removida ou com o schema mudado fica num evento, com o diff das colunas e se parte (backend/app/domain/catalog/inventory.py). Os eventos e os objetos removidos ficam 90 dias. Os objetos que ainda não são datasets aparecem no GET /search na espécie inventory. Fica fora dos datasets monitorizados e do limite do plano: um objeto passa a dataset pela descoberta de sempre. API: GET /connections/{id}/inventory, GET /connections/{id}/inventory/objects, PUT /connections/{id}/inventory e POST /connections/{id}/inventory/run (admin). Para quem já tem um catálogo de registo, o BeryX continua a publicar lá (CATALOGO_EXTERNO.md).

  • Exportação única para um catálogo vazio (cartão #690, a exceção ao #407): quem adota um OpenMetadata ou um DataHub pode levar para lá, uma vez, os datasets das conexões do workspace (até 2000), com as colunas e o tipo, as descrições do dataset e das colunas, as tags, os termos aprovados do glossário com as ligações aprovadas e o owner efetivo. O owner só segue quando o catálogo tem um utilizador com o mesmo email. Só escreve num destino (tipo e URL) sem tabelas nem termos que um run para esse destino não tenha reclamado, e o worker volta a verificar isso antes da primeira escrita. Um admin pede-a e confirma com o nome do workspace, e o pedido fica na auditoria. Repetir faz upsert do que já criou e não duplica, também depois de uma tabela a meio ou de um worker que morreu: o run grava as referências do plano antes de escrever. Um run parado deixa de contar como ativo ao fim do prazo (2 h na fila, 30 min sem escrever). O relatório conta o que ficou criado e dá o mapeamento de cada conexão para o CATSYNC (o prefixo e as exceções dos datasets que ele não mapeia, como os sem schema numa conexão mista), sem emails (backend/app/domain/catalog/catalog_bootstrap.py). Fora disto, o #407 fica igual: o BeryX não escreve owners, tags nem termos no catálogo do cliente. API: POST /catalog-integrations/{id}/bootstrap/plan (só lê o destino), POST /catalog-integrations/{id}/bootstrap e GET /catalog-integrations/{id}/bootstrap (admin).

O que falta: o owner e as tags, o dicionário e o histórico ainda não têm página na app e usam-se pela API. O inventário só de metadados também ainda não tem ecrã: liga-se e lê-se pela API, e a pesquisa global da app não mostra a espécie inventory. A exportação única para um catálogo vazio também só existe na API. Ficam fora do histórico a criação de termos, as ligações sugeridas automaticamente, o que o datavault.yaml aplica e a classificação automática da sensibilidade. Com o catálogo externo ligado, contam-se os termos de glossário lidos no último import, mas esses termos não entram no glossário do BeryX. O import também não escreve tags do dataset.

Testes: backend/tests/test_column_lineage.py, backend/tests/test_dbt_manifest.py, backend/tests/test_openlineage_consume.py, backend/tests/test_catalog_sync.py, backend/tests/test_column_sensitivity.py, backend/tests/test_dataset_readme.py, backend/tests/test_metadata_quality.py, backend/tests/test_glossary.py, backend/tests/test_metadata_management.py, backend/tests/test_catalog_search_index.py, backend/tests/test_catalog_inventory.py, backend/tests/test_catalog_bootstrap.py, frontend/src/pages/Glossario.dom.test.tsx, frontend/src/components/DatasetTermChips.dom.test.tsx, frontend/src/components/CommandPalette.dom.test.tsx, frontend/src/pages/Pesquisa.dom.test.tsx, frontend/e2e/f12-pesquisa-global.spec.ts.

Que catálogo para que cliente

A BeryX não substitui o catálogo de registo do cliente (#407). Quem já tem um catálogo fica com ele e a BeryX publica lá a qualidade; o catálogo leve da BeryX serve quem não tem nenhum. A recomendação segue o que o cliente já usa:

  • Microsoft (Azure, Fabric): Microsoft Purview.
  • Databricks: Unity Catalog.
  • Google Cloud (BigQuery): Dataplex Universal Catalog.
  • Sem catálogo: o catálogo leve da BeryX. Quando o cliente precisar de 80 conectores ou mais, o OpenMetadata, alojado por ele ou no Collate.
  • Grande empresa regulada: o catálogo que já comprou (Collibra, Alation ou Informatica CDGC).
  • Atlan: Atlan.
  • DataHub, open source ou DataHub Cloud: DataHub.

A tabela diz o que a BeryX publica e lê em cada um e o que já foi validado. As linhas são as do registo dos publicadores (CATALOG_KINDS em backend/app/infra/catalog/client.py), e o backend/tests/test_docs_drift.py falha quando os dois deixam de bater. «Transporte simulado»: os testes substituem a API do catálogo por um servidor falso (httpx.MockTransport), e falta a validação num catálogo de um cliente. O detalhe de cada um está no catálogo externo.

Catálogo Para quem A BeryX publica A BeryX lê Estado
Microsoft Purview Microsoft O veredito, o score, a data do scan e o link, em atributos datavault_* da tabela, sem as regras — Transporte simulado; por confirmar no tenant
Unity Catalog Databricks O trust score, o veredito, as regras falhadas, as issues por resolver e a certificação, em tags beryx_* da tabela O owner do dataset, as tags, os comentários e a linhagem Transporte simulado; falta um workspace do cliente
Dataplex Universal Catalog Google Cloud Três aspects beryx-* na entry da tabela: o veredito e o trust score, uma entrada por regra e as issues abertas As descrições, o glossário e a linhagem Transporte simulado; por confirmar no projeto
Catálogo leve da BeryX Sem catálogo — (fica na BeryX) Os esquemas, as tabelas, as vistas e as colunas das conexões (inventário, cartão #682) Na BeryX; o inventário ainda só se usa pela API
OpenMetadata Sem catálogo, a precisar de 80 conectores ou mais O veredito, o score e uma entrada por regra, como test cases Os domínios, os owners, a sensibilidade das colunas, as descrições e o glossário Transporte simulado; nunca experimentado no Collate
Collibra Grande empresa regulada O veredito e uma entrada por regra, como assets «Data Quality Metric» — Transporte simulado; por confirmar no tenant
Alation Grande empresa regulada Campos beryx_*: o veredito, o trust score, as issues abertas, a certificação e uma entrada por regra; com o veredito FALHA, uma trust flag Warning — Transporte simulado; por confirmar no tenant
Informatica CDGC Grande empresa regulada O veredito com o score 0-100 e uma entrada por regra, com as linhas lidas e as falhadas, como Data Quality Rule Occurrences BERYX-… ligadas à tabela — Transporte simulado; por confirmar no tenant
Atlan Atlan O custom metadata «BeryX»: o veredito, o trust score, as issues abertas, a certificação e uma entrada por regra; com o veredito FALHA, um announcement — Transporte simulado; por confirmar no tenant
DataHub DataHub O veredito, o score e uma entrada por regra, como assertions Os domínios, os owners, a sensibilidade das colunas, as descrições e o glossário Transporte simulado

Elementos de dados críticos e dossiê regulatório

  • Elementos de dados críticos (CDE): um dataset ou uma coluna marcado para o BCBS 239, a Solvência II, o DORA ou outra regulação, com referência («Princípio 3», «art. 19.º», a linha do relatório ao supervisor), owner e racional. Declaram-no um admin ou o owner de um domínio do dataset.
  • Dossiê regulatório, por regulação e requisito: princípios 3 a 6 do BCBS 239; art. 82.º da Diretiva 2009/138/CE e art. 19.º do Regulamento Delegado (UE) 2015/35, para a Solvência II; art. 8.º, n.os 1 e 4, art. 9.º, n.º 3, alínea d), e art. 12.º, n.º 7, do Regulamento (UE) 2022/2554, para o DORA. Para cada elemento traz a definição (os termos do glossário com a ligação aprovada), os controlos de qualidade por dimensão exigida com a tolerância de cada regra e o último resultado, a tendência diária dos resultados na janela, os problemas abertos, o cumprimento do SLA do domínio, a linhagem a um salto (ao nível da coluna) e a fonte autoritativa (as origens a montante, salto a salto, até 10), o contrato ODCS e as lacunas: sem regra, sem resultado nos últimos 30 dias, sem owner, sem definição, sem SLA. A exatidão conta com a regra accuracy e com a última auditoria de exatidão por amostragem concluída na janela; a integridade com a regra referential. O KPI «cobertura de CDEs» conta as dimensões exigidas com controlo e resultado.
  • Página Elementos críticos (Governação → Elementos críticos): o dossiê filtrado por regulação e por domínio, com cada elemento e o estado de cada requisito e dimensão. Uma dimensão sem regras aparece «Não monitorizado», nunca a verde (frontend/src/pages/ElementosCriticos.tsx).
  • Exporta-se em Markdown, CSV e PDF, e partilha-se pelo link de leitura dos relatórios. É determinístico, sem IA generativa e só com metadados. Nenhum estado afirma conformidade: o dossiê aponta lacunas e não certifica (GET /api/v1/cde/dossier, backend/app/domain/compliance/regulatory_dossier.py, backend/app/domain/compliance/cde_evidence.py).
  • Ciclo de vida (cartão #703): um elemento marcado nasce candidato. Submetido com a justificação, que é obrigatória, entra no fluxo de aprovação cde (o configurado ou, sem ele, um admin ou quem gere um domínio do dataset aprova). Quem o marcou e quem o submeteu não o aprovam. Aprovado, passa a certificado enquanto o dataset tiver a certificação em vigor e volta a aprovado quando ela cai. Desmarcar um aprovado também é um pedido no fluxo, com o motivo, e só depois de aprovado fica retirado. Rejeitado, expirado ou em conflito, volta a candidato. A API (GET /api/v1/cde) e o dossiê mostram o estado; o dossiê não traz retirados (backend/app/domain/compliance/cde_lifecycle.py). Um retirado também deixa de classificar o dataset para os agentes de IA e sai da ficha do Anexo IV; um candidato ainda conta (backend/app/domain/compliance/ai_agents.py, backend/app/domain/compliance/annex_iv.py). Na pré-visualização do pedido (#699), quem decide vê o elemento, a justificação ou o motivo, e o que o dataset passa a herdar ou deixa de herdar, que fica de fora se outro elemento aprovado do mesmo dataset já o dá (backend/app/domain/governance/request_preview.py).
  • O que um elemento aprovado herda: cada issue de aviso ou crítico do dataset que dá alerta (de um scan, de um monitor, de um SLO, do dbt, de um pipeline, de BI ou de custos) leva o prazo de resolução da política dos CDEs (24 horas por omissão, configurável em PUT /api/v1/cde/policy) e o alerta sai crítico, marcado cde. Se o SLA de resolução da severidade (#396) der um prazo mais curto, o issue fica com esse. Um issue informativo não dá alerta e fica sem esse prazo (backend/app/domain/compliance/cde_lifecycle.py). Um canal de notificação com o âmbito «Só elementos de dados críticos» (cde_only) recebe só esses alertas: é a audiência própria dos CDEs (backend/app/domain/alerts/routing.py). O âmbito vai também no datavault.yaml e na IaC (backend/app/domain/gitops/workspace_config.py). Enquanto o dataset não está certificado, o owner do elemento tem a tarefa «Certificar o dataset de um CDE» em «As minhas tarefas», com prazo de 30 dias desde a aprovação, configurável (backend/app/domain/compliance/cde_tasks.py).

O que falta: o chip «CDE» no dataset, o KPI no painel do domínio, marcar, submeter ou desmarcar um elemento pela interface (hoje só pela API) e a marca «fora de SLA» pelo prazo do CDE (o tick do SLA mede pela política da severidade; o prazo do CDE fica no due_date do issue).

Testes: backend/tests/test_regulatory_dossier.py, backend/tests/test_cde.py, backend/tests/test_cde_lifecycle.py, frontend/src/pages/ElementosCriticos.dom.test.tsx.

Privacidade do cliente: registo de tratamentos, conservação e titulares

  • Registo de atividades de tratamento (RGPD, art. 30.º) do cliente, uma atividade por domínio. O catálogo pré-preenche-o em cada leitura: as categorias de dados vêm do mapa de PII com a classificação revista por cima (a categoria especial do art. 9.º fica marcada), a finalidade vem da descrição do domínio e o prazo de conservação, se a atividade não o declarar, vem da regra pii_retention_max_age da política de dados. O admin declara as finalidades (um catálogo partilhado com a decisão de confiança por finalidade), a base legal, os titulares, os destinatários, as transferências, o prazo, as medidas e o responsável. Os datasets com dados pessoais fora de domínios aparecem à parte. Exporta-se em CSV e JSON, com a postura de evidência mínima: a BeryX guarda contagens e diagnósticos e as linhas ficam no armazém do cliente (GET /api/v1/workspace/ropa, GET /api/v1/workspace/ropa/export, backend/app/domain/privacy/ropa.py).

  • Alertas de conservação: os datasets com dados pessoais cujo registo mais antigo, pela última atividade, passa o prazo mais curto das atividades dos seus domínios (ou o da política). É a mesma medida da regra de conservação: um perfil feito por amostra ou à janela de partições deixa o dataset «por medir», com o motivo, e a resposta conta os datasets sem prazo declarado (GET /api/v1/workspace/retention-alerts).

  • Pedido de acesso com descoberta nas fontes: o valor do titular (email, NIF, telefone, documento, IBAN) procura-se só nas colunas de identificador classificadas do tipo que casa com a forma do valor, com uma contagem na fonte por coluna. O valor vai como parâmetro e nunca no texto do SQL, na sessão só-de-leitura das regras SQL. A resposta diz onde há dados (dataset, coluna, quantas linhas) e nunca os valores; o trilho de auditoria guarda as contagens e os tipos procurados, sem o valor. Com o pedido do registo, traz o prazo dele, de um mês na UE e no Reino Unido (POST /api/v1/workspace/subject-data/discover, backend/app/domain/privacy/subject_discovery.py).

  • Página «Privacidade»: junta o registo, os alertas e os pedidos. Cada pedido aberto tem os passos que o estado deixa dar: concluir com o resultado, acusar a receção, prorrogar uma vez dentro do prazo inicial onde a lei o prevê, e pausar com o motivo ou retomar. Um passo recusado mostra a causa junto ao pedido (PATCH /api/v1/workspace/subject-requests/{id}, backend/app/domain/privacy/subject_requests.py).

  • Deriva do RoPA (cartão #704): onde o catálogo se afasta do registo, com a idade de cada desvio. Dois desvios, só com metadados do catálogo do workspace:

    • uma coluna com dados pessoais (o mapa de PII do profiling com a classificação revista por cima) num dataset fora de qualquer domínio, e por isso de qualquer atividade. Associa-se o dataset a uma atividade (entra no domínio) ou justifica-se;
    • um destino que a linhagem mostra e a atividade não declara. A partir dos datasets com dados pessoais do domínio, a travessia segue as arestas de linhagem pelos datasets do mesmo domínio; o primeiro dataset fora dele é um destino, tal como os exposures (dbt, dashboards de BI) e os consumidores agentes da Trust API (consumos dos últimos 30 dias) de um dataset dentro. Declara-se o destino como destinatário (fica na atividade, em declared_recipients) ou justifica-se.

    A comparação corre no fim de cada scan, de 15 em 15 minutos no worker (para a linhagem importada e a classificação feita à mão) e com «Verificar agora». Abre o desvio que apareceu, fecha o que desapareceu e reabre, com a idade a zero, o que voltou; um justificado fica justificado enquanto existir. Cada desvio aberto é uma tarefa dos admins em «As minhas tarefas» (o RoPA é só de admins), e o destino não declarado também é um alerta no sino. Nada sai do workspace: nem as fontes do cliente nem um LLM são chamados, e os canais (Slack, Teams, webhooks) não recebem nada. O export do registo traz os desvios abertos (GET /api/v1/workspace/ropa/drift, POST /api/v1/workspace/ropa/drift/check, POST /api/v1/workspace/ropa/drift/{id}/justify|associate|declare, backend/app/domain/privacy/ropa_drift.py, backend/app/domain/privacy/ropa_tasks.py).

  • Finalidade e base legal pelo motor de fluxos (cartão #704): mudar a finalidade ou a base legal já declaradas de uma atividade abre um pedido processing_activity com a mudança retida e só o fecho aprovado a aplica. Os outros campos gravam-se logo, e declarar pela primeira vez (o campo estava vazio) também. Sem fluxo configurado, decide outro admin; quem pediu nunca decide. A atividade mostra a mudança à espera, e qualquer admin a retira com «Retirar pedido», na atividade ou em «Criados por mim»: o pedido fica cancelado e a atividade como estava. Apagar o domínio também fecha o pedido (POST /api/v1/workspace/ropa/activities/{domain_id}/pending-change/cancel, backend/app/domain/privacy/ropa_approvals.py).

O que falta: a descoberta corre no pedido, para o valor não ir para a fila, por isso tem tetos (40 colunas, 15 s por contagem, 90 s no total) e diz o que ficou de fora. As fontes sem contagem na fonte (as APIs, como o Salesforce e o SAP, o MongoDB, os streams e os documentos) ficam «não suportadas». Num ficheiro acima do teto de leitura, a contagem cobre só as linhas que o perfil lê; numa tabela SQL perfilada à janela de partições, só essa janela. Essas colunas vêm à parte, com as linhas lidas, e a página dá o resultado como incompleto mesmo sem correspondências. Um email procura-se como foi escrito e em minúsculas: guardado com outras maiúsculas, não se encontra. Na deriva, os destinatários escritos em texto não contam (só os destinos do catálogo declarados), os nós externos do OpenLineage (S3, Kafka) e os consumidores que não são agentes não são destinos, e a travessia para no primeiro dataset fora da atividade. O pedido de mudança de finalidade ou de base legal guarda os aprovadores de quando abriu, e configurar o fluxo depois não lhe mexe. Num workspace com um só admin e sem fluxo configurado, ninguém o decide: decide-o um admin que entre depois, ou quem pediu retira-o e volta a propor, já com o fluxo configurado com outra pessoa.

Testes: backend/tests/test_privacy_ropa.py, backend/tests/test_ropa_drift.py, frontend/src/pages/Privacidade.dom.test.tsx.

Registo de definições de métricas

  • Uma só definição por métrica (página Negócio, secção «Definições de métricas»; /api/v1/metric-definitions, backend/app/domain/governance/metric_registry.py): nome, definição em linguagem de negócio, fórmula de referência (por exemplo, sum(amount - discount)), filtros, grão, dono, autoridade de definição (uma equipa ou uma pessoa), termos do glossário e datasets de origem. O nome normalizado é único no workspace: Receita Líquida e receita-liquida são a mesma métrica. O nome novo de uma mudança à espera também já tem dono até o pedido fechar: criar ou renomear com ele dá conflito, e o import do dbt conta-o como existente (_reserved_keys).
  • Versões e aprovação: um rascunho edita-se livremente. Submetê-lo abre um pedido metric_definition no motor de fluxos, e o fecho aprovado grava a versão 1. Numa definição aprovada nada se grava diretamente: a mudança fica retida no pedido, e só o fecho aprovado a aplica, com a versão seguinte, o pedido que a aprovou e quem o fechou. Assina o fluxo configurado para o tipo em Aprovações (sem ele, um admin) e, no fim, a autoridade de definição; sem fluxo configurado e com autoridade, assina só ela. Quem pede nunca decide, e quem pediu ou um admin retira um pedido aberto. Um before_flush recusa mudar um campo de uma definição aprovada sem a versão nova na mesma transação (refuse_unapproved_changes). Se, antes do fecho, o nome novo passar a ser de outra definição, ou a equipa ou a conta da mudança deixar de valer (apagada ou desativada), o pedido fica em conflito e nada se aplica (stale_changes). O histórico lê-se em GET /api/v1/metric-definitions/{id}/versions, com as assinaturas de cada versão.
  • Importar do dbt: POST /api/v1/metric-definitions/import-dbt (só admins) cria rascunhos das métricas do semantic layer já importadas que ainda não estão no registo. Quando os projetos divergem, usa a variante mais comum. Uma definição que já existe não muda, mesmo renomeada depois do import (reconhece-se pela métrica dbt de origem).
  • Metric drift contra a definição registada (backend/app/domain/lineage/metric_registry_drift.py, backend/app/domain/lineage/metric_conformance.py): cada implementação compara-se com a definição aprovada. As implementações são a métrica dbt com o mesmo nome ou label (ou a que deu origem à definição) e as métricas de negócio ligadas. Comparam-se a fórmula (average = avg, count_distinct(x) = count(distinct x)), os filtros, o grão (nas métricas de negócio) e a origem. Quando divergem, abre um issue metric_drift por definição, com a evidência por implementação: o aspeto, o valor registado e o implementado. O issue atualiza-se no import do manifest, no fecho de cada versão e quando uma métrica de negócio ligada é criada ou apagada (também pela cascata de uma conexão apagada). Resolve-se quando todas voltam a seguir a definição e fica silenciado se for ignorado. O detalhe da definição mostra a mesma comparação; as métricas dbt só aparecem lá em modo aberto ou a admins, como o relatório do metric drift. Uma definição importada do dbt dá por conforme a métrica de origem pela forma canónica do import só enquanto a fórmula e os filtros registados forem os dela (canonical_holds); a origem compara-se sempre.
  • Quem lê a evidência: o issue não tem dataset e lê-o todo o workspace, por isso só nomeia o que um membro sem concessões vê (every_reader). Uma métrica de negócio com uma fonte num domínio restrito, ou uma métrica dbt em modo restrito, entra só na contagem, e um dataset de origem que esse membro não vê aparece como «Dataset restrito». No detalhe da definição vale o acesso de quem pede. Quem não vê um dataset também não o liga como origem (o erro é o de um id que não existe) nem o tira ao editar a lista.
  • Métricas de negócio ligadas: uma métrica de negócio pode seguir uma definição aprovada (definition_id). O nome e a descrição passam a ser os da definição, também depois de cada versão, e a criação recusa uma agregação ou um grão que não batam com a fórmula e o grão da definição.

Testes: backend/tests/test_metric_registry.py, frontend/src/components/RegistoMetricas.dom.test.tsx.

Políticas e normas

  • Políticas e normas com ciclo de vida: cada política tem tipo (qualidade, privacidade, segurança, ciclo de vida e retenção, ética da IA ou definições), finalidade, texto, dono, versão e âmbito (domínios e camadas medallion). Quem a edita submete-a para revisão, e só com dono; um admin aprova-a ou devolve-a a rascunho e retira-a em qualquer estado. Em revisão não se edita, e editar uma aprovada abre a versão seguinte em rascunho. Edita-a o dono ou um admin, e, sem dono, quem escreve. Cada mudança fica na auditoria (backend/app/domain/governance/policies.py).
  • Revisão periódica: ao aprovar, a próxima revisão fica marcada (por omissão a 365 dias, entre 30 e 1095). Com a antecedência configurada (por omissão, 30 dias), o worker abre a tarefa de revisão ao dono, com aviso no sino e por email. O dono vê-a em «As minhas tarefas» (/tarefas) e fecha-a com «Marcar revisão feita»; a revisão seguinte conta a partir daí e volta a avisar (backend/app/domain/governance/policy_tasks.py).
  • Controlos ligados e cumprimento calculado: a política liga até 50 controlos que já correm: regras da política de dados por camada (POLICYCHK), regras de qualidade e o contrato de dados de um dataset. Ligar controlos não abre versão nova. O cumprimento calcula-se a cada leitura, controlo a controlo e dataset a dataset: a regra da política de dados efetiva de cada dataset do âmbito (a mesma do veredito), o último resultado da regra de qualidade e o estado do contrato, lido como no veredito e na certificação. Um contrato sem versão ativa, ou com uma coluna prometida que o dataset já não tem, falha; só com avisos, cumpre. Uma regra desligada, que nunca correu ou cujo último resultado ficou por avaliar (como em Regras) fica «não medida» e não entra na percentagem, que é (cumpre + com exceção) / (cumpre + com exceção + falha), da política e de cada controlo. Num dataset que nenhum scan analisou, as regras da política de dados que leem o score, o perfil ou a PII detetada também ficam «não medidas», e uma violação que já se vê conta. As regras require_owner, require_active_contract e require_readme contam sempre. Cada leitura avalia até 500 datasets do âmbito (backend/app/domain/governance/policy_compliance.py).
  • Exceções que expiram: quem escreve pede uma exceção a um controlo ligado, para um dataset (ou todos os do controlo) e uma coluna (ou o dataset inteiro), com justificação (10 a 2000 caracteres) e data de fim obrigatória, no máximo a 365 dias. Numa regra de qualidade ou num contrato, o dataset é o do controlo. Com o fluxo de aprovação das exceções ligado, o pedido entra no motor de fluxos e decide-se em «As minhas tarefas»; sem ele, decide um admin. Nos dois casos, quem a pediu nunca a decide, mesmo que outra pessoa a submeta ao fluxo: no fluxo não conta como aprovador da etapa, nem por delegação, escalonamento ou fallback, e, se era o único, decidem os outros admins (backend/app/domain/governance/approvals.py, backend/app/domain/governance/approval_config.py). Aprovada e por expirar, a exceção faz contar a falha que cobre como «com exceção»; numa falha em várias colunas, só cobre se todas tiverem exceção (num contrato partido, as colunas que já não existem). Sete dias antes do fim, o worker põe-na nas tarefas de quem a pediu e do dono da política, com aviso no sino e por email. Passada a data de fim, a verificação volta a contar como incumprimento logo na leitura seguinte, sem esperar pelo worker, que a marca expirada. Renova-se com um pedido novo (backend/app/domain/governance/policy_waivers.py).
  • API e MCP: /api/v1/policies (backend/app/api/v1/policies.py; com domínios restritos no workspace, só admins). O MCP lê as políticas e as exceções com get_policy e list_waivers, só de leitura (backend/app/mcp_policies.py). Na app, as políticas gerem-se na Governação, e as revisões e as exceções perto do fim aparecem em «As minhas tarefas» (/tarefas, frontend/src/pages/Tarefas.tsx), onde um pedido de exceção abre a política.

O que falta: o cumprimento calcula-se na leitura e não guarda histórico, e os avisos de revisão e de fim de uma exceção são pessoais (sino e email): não vão aos canais de notificação do workspace.

Testes: backend/tests/test_policies.py, backend/tests/test_mcp_policies.py.

Maturidade da gestão de dados pela evidência

  • Modelo por capacidade: as 11 áreas de conhecimento do DAMA-DMBOK2 (governação, arquitetura, modelação, armazenamento e operações, segurança, integração e interoperabilidade, documentos e conteúdos, dados de referência e mestres, data warehousing e BI, metadados e qualidade), cada uma com um nível de 0 (inexistente) a 5 (otimizado) (backend/app/domain/governance/maturity.py). O mapeamento DCAM/CDMC do EDM Council fica de fora até haver licença.
  • Pontuada pela evidência: cada capacidade com evidência no produto pontua-se com indicadores que o BeryX já mede: cobertura de owners, documentação, classificação, contratos e regras (os KPIs do painel de governação), datasets com linhagem, CDE aprovados e com responsável, problemas resolvidos dentro do prazo e cumprimento do SLO nos últimos 30 dias, e políticas aprovadas com a revisão em dia. Cada percentagem passa a nível por faixas (0 % → 0; abaixo de 25 % → 1; 25–49 % → 2; 50–74 % → 3; 75–89 % → 4; 90 % ou mais → 5) e a capacidade fica com a média dos níveis medidos. A nota calcula-se a cada leitura, por isso muda sozinha quando a evidência muda, e cada nota traz os indicadores que a sustentam, com valor, amostra, nível e a página onde se vê (backend/app/domain/governance/maturity_service.py).
  • Respostas manuais onde não há evidência: um admin dá um nível manual (com justificação obrigatória) e um alvo (1 a 5; por omissão, 3) a cada capacidade. A resposta só conta enquanto a capacidade não tem nenhum indicador medido: a evidência ganha sempre. Cada mudança fica na auditoria, sem o texto da justificação.
  • Histórico e mapa de calor: o worker escreve um fecho por trimestre e por workspace, reescrito uma vez por dia enquanto o trimestre corre. O mapa de calor cruza capacidades com os últimos 8 trimestres e o corrente, contra o alvo. Com domínios restritos, o histórico do workspace inteiro só aparece a quem vê todos os domínios, e a avaliação do workspace só a admins.
  • Pacote do conselho: GET /api/v1/governance/maturity/board-pack.md dá a avaliação em Markdown, com o resumo, o mapa de calor, a evidência ou a justificação de cada capacidade e o método, sem nomes de pessoas, datasets nem colunas. Na app, o painel fica na Governação (frontend/src/components/MaturidadeDados.tsx).

O que falta: as lacunas ainda não viram tarefas na caixa. O pacote do conselho (secção seguinte) leva o resumo e a nota de cada capacidade contra o alvo; o mapa de calor e a evidência ficam só neste Markdown.

Testes: backend/tests/test_maturity.py.

Pacote do conselho, registo de decisões e atas

  • Pacote do conselho por período: um admin gera o pacote de um período (até 366 dias, sem futuro). Os números calculam-se no pedido, sem IA, e ficam gravados à data de referência com o SHA-256 do JSON canónico. Cada leitura recalcula o hash e diz se ainda bate (hash_valid) (backend/app/domain/governance/board_pack.py, backend/app/domain/governance/board_service.py).
  • KPIs: cobertura dos CDE e o estado de cada um, pelo pior das dimensões exigidas (os mesmos do dossiê regulatório); problemas abertos, críticos, com o SLA ultrapassado, abertos e resolvidos no período, e o cumprimento dos prazos do painel de governação; exceções em vigor, pendentes, a expirar em 30 dias e a idade média e máxima; cobertura do catálogo (owner, documentação, classificação); pedidos de acesso decididos em que cada etapa do fluxo com SLA fechou dentro do prazo, refeito do início da etapa e do SLA copiado no pedido (a aprovação automática conta no prazo; sem SLA, fica fora da conta), e os pendentes fora do prazo. A maturidade é a avaliação da secção anterior à data de referência, com os alvos gravados por capacidade: a nota global de 0 a 5, as capacidades avaliadas, no alvo e com lacuna, e a nota, o alvo e o estado de cada capacidade, só pelo código. Ao LLM segue só o resumo; na app, as capacidades com lacuna aparecem pelo nome (frontend/src/components/PacoteConselhoDetalhe.tsx).
  • Top riscos: CDE em falha, problemas críticos abertos, exceções em vigor (e as que expiram) e acessos totais a dados sensíveis concedidos no período, também os que entretanto expiraram ou foram revogados, até 10 de cada.
  • Narrativa pela IA, só sobre agregados: a narrativa (síntese, riscos, decisões pedidas) escreve-se na fila board_pack.requested pela fachada LLM, com a guarda de saída. O prompt leva só contagens, percentagens e códigos: nenhum nome de dataset, coluna, política, equipa ou pessoa (board_pack.narrative_input). Sem modelo disponível, o template offline escreve as mesmas secções.
  • Decisões pedidas e registo de decisões: o pacote regista como pendentes as decisões que pede ao conselho: aprovar as políticas em revisão, aceitar o risco das exceções pendentes ou a expirar e fixar a tolerância dos CDE em falha. Uma decisão ainda aberta não se repete no pacote seguinte, e a que o conselho já aprovou ou rejeitou não volta a ser pedida (a de uma política volta quando a política muda de versão). Um admin também pede decisões à mão (uma igual ainda aberta dá conflito) e regista o resultado (aprovada, rejeitada ou adiada) com fundamentação obrigatória. Aprovada ou rejeitada, a decisão fecha. Cada decisão fica ligada à política, exceção ou CDE que decide, com o nome e a versão da política no momento da decisão, e cada passo fica na auditoria. Registar a decisão não muda a política nem a exceção, que seguem o seu ciclo (backend/app/domain/governance/board_decisions.py).
  • Atas: a ata da reunião fica no pacote e não se aprova enquanto a narrativa está a ser escrita. Aprovada, fecha com o seu SHA-256 e deixa de se editar, e o pacote fecha com ela: a narrativa já não se reescreve e nenhuma decisão se pede nem se toma nele (409 board_pack_locked); a que ficou em aberto continua no registo. Cada pedido de outra narrativa fica na auditoria (board_narrative_requested), com o SHA-256 da narrativa substituída (backend/app/domain/governance/board_service.py).
  • Na app: em Governação, só para admins, gera-se o pacote, leem-se os KPIs, os top riscos e a narrativa (com o rótulo de IA e o modelo, ou o aviso do modo offline), decide-se o que o pacote pede e escreve-se e aprova-se a ata (frontend/src/components/PacoteConselho.tsx). Os erros da API saem na língua do pedido (pt, en, es).
  • API: /api/v1/governance/board (backend/app/api/v1/board.py), só para admins.

O que falta: o pacote não tem exportação em PDF ou PPTX e não se agenda (gera-se a pedido). Registar uma decisão não aplica nada na política ou na exceção.

Testes: backend/tests/test_board_pack.py, frontend/src/components/PacoteConselho.dom.test.tsx.

Controlos-chave do CDMC: estado e evidência

  • Mapa dos 14 controlos-chave: os 14 controlos-chave do CDMC (EDM Council) ligam-se às verificações que a BeryX já mede: políticas e o cumprimento calculado, owner, certificação, registo de tratamentos, regra de conservação da política de dados, catálogo, classificação, domínios restritos e revisões de acessos, regras e resultados, tamanho e custo de compute, e linhagem. Só entram os datasets sensíveis, com colunas confidential ou pii (backend/app/domain/compliance/cdmc.py).
  • Estado calculado da evidência: uma verificação fica met só com evidência escrita (o que se observou e de onde veio). Um controlo fica evidenced num dataset só com todas as verificações met, e no workspace só com todos os datasets sensíveis evidenced. Com datasets sensíveis fora do teto de 500, que ficam por avaliar, nenhum controlo fica evidenced no workspace. Um workspace sem datasets sensíveis fica no_sensitive_data, que não é verde. Um scan ou um resultado com mais de 30 dias e uma revisão de acessos com mais de 365 dias já não contam. Os controlos das políticas só contam com todos medidos no dataset: uma exceção aprovada conta, mas fica dita à parte.
  • Lacunas assinaladas: cada verificação que falha diz o código da lacuna e quantos datasets a têm. O que a BeryX não mede fica product_gap e nunca conta: a localização da fonte, a cifra em repouso na fonte, a DPIA e o custo do movimento dos dados. Por isso os controlos 4, 9, 10 e 13 nunca ficam verdes.
  • Evidência exportável: GET /api/v1/compliance/cdmc em JSON, .md e .csv (uma linha por dataset × controlo × verificação, com a evidência e a lacuna), calculado na leitura, sem LLM e sem tocar nas fontes. Cada pessoa vê os datasets sensíveis a que tem acesso (backend/app/domain/compliance/cdmc_service.py). Na app, o painel fica na Governação (frontend/src/components/ControlosCdmc.tsx).

O que falta: a BeryX não está certificada no CDMC, e o mapa não é uma declaração de conformidade. A licença do enquadramento e o custo da avaliação são decisões do owner. O mapeamento, as lacunas do produto e o caminho para certificar estão em docs/compliance/cdmc.md.

Testes: backend/tests/test_cdmc.py.

Agentes e modelos de IA: registo, ficha do Anexo IV e acesso pelo MCP

  • Registo de agentes e modelos ligados aos datasets que consomem. A ficha de cada um diz o modelo, a finalidade, o nível de risco do AI Act (ou o sistema de IA do art. 10.º a que pertence), as ferramentas MCP e os domínios permitidos e o responsável. Estados: proposto, aprovado, suspenso e retirado. Aprovar exige finalidade, e mudar a finalidade, o modelo, as ferramentas ou os domínios de um agente aprovado devolve-o a proposto (GET/POST /api/v1/compliance/ai-agents, backend/app/domain/compliance/ai_agents.py).
  • Identidade por agente nos tokens do MCP: um token dvk_* criado com agent_id é a identidade desse agente, e o servidor MCP manda o nome da ferramenta em cada chamada (X-Beryx-MCP-Tool; read_resource nas leituras de resources). Em cada pedido, a API aplica a ficha: o kill-switch global (PUT /api/v1/compliance/ai-agents/kill-switch) e o do agente (suspenso ou retirado) param tudo; com ferramentas na ficha, uma chamada fora da lista é recusada; a ferramenta do cabeçalho só vale numa rota que ela chama, porque o cabeçalho é de quem chama e o token também serve a API REST (backend/app/domain/compliance/ai_agent_tools.py, cartão #741); um dataset classificado, com colunas PII ou confidenciais ou marcado como CDE, só o lê um agente aprovado, com finalidade declarada e com o go-live completo, se o dataset lhe estiver ligado e por uma ferramenta que a matriz de direitos deixa chegar a dados classificados; e, com domínios na ficha, só os datasets desses domínios. Um id que a ficha fecha, no path ou na query, dá 403 com o motivo, e um domínio que tenha um desses datasets também, /domains/{domain_id} incluído. As listagens sem filtro deixam esses datasets de fora e, dos domínios que os têm, o relatório, a certificação e a pontuação do domínio: a lista de domínios mostra-lhes o nome, como a de um domínio restrito. Enquanto a ficha lhe retiver datasets ou domínios, fecham-se-lhe as vistas do workspace inteiro (painéis, grafo do conhecimento, qualidade dos metadados) e as leituras que não filtram por dataset (o progresso de um scan, o sistema de IA do art. 10.º, as integrações, as listas de referência); ficam abertos os pedidos de ação do próprio token (backend/app/api/agent_access.py). No gate RAG, um corpus que a ficha fecha é «não encontrado». Um token de agente não leva o scope config, que aplica o datavault.yaml.
  • Go-live e operação de cada agente (cartão #725, backend/app/domain/compliance/ai_agent_golive.py):
    • Donos: o dono do agente e o dono do prompt são pessoas do workspace (PUT /api/v1/compliance/ai-agents/{agent_id}/owners). Trocar uma pessoa por outra abre uma passagem com um runbook de quatro passos: rever a matriz de direitos, o registo de risco, os tokens do agente e os ensaios. Só o novo dono marca os passos e aceita a passagem (POST /api/v1/compliance/ai-agents/handovers/{handover_id}/accept). Enquanto a passagem está aberta, o agente não lê dados classificados.
    • Matriz de direitos (PUT /api/v1/compliance/ai-agents/{agent_id}/rights): por ferramenta MCP, se chega a dados classificados, os domínios (dentro dos da ficha), o teto de pedidos por hora (acima dele, 429 agent_tool_limit) e os tipos de ação que um propose_action pode propor (agent_action_not_allowed). Com matriz, as ferramentas da ficha são as dela, e mudá-la num agente aprovado devolve-o a proposto.
    • Checklist de go-live (GET /api/v1/compliance/ai-agents/{agent_id}/golive): donos sem passagem aberta, matriz definida, datasets ligados todos certificados, classificação desses datasets verificada por uma pessoa ou pelo catálogo (nenhuma coluna só com a classificação automática ou por classificar), os três ensaios de falha passados dentro do intervalo da ficha (90 dias por omissão), avaliação de impacto registada e aprovação registada por uma pessoa. Aprovar exige os outros seis itens (409 ai_agent_golive_incomplete). Sem os sete, o agente não lê dados classificados. Com os sete, lê só os classificados ligados, os que a checklist verificou: um classificado que não lhe está ligado dá 403 agent_classified_denied e fica fora das listagens. A checklist lê-se em cada pedido do agente: um certificado que cai, uma coluna nova por classificar ou um ensaio que vence fecham-nos no pedido seguinte. Mudar a ficha apaga a avaliação de impacto. Ligar outro dataset a um agente aprovado conta como mudança da ficha: o agente volta a proposto e o dataset entra na checklist.
    • Kill switch por agente (POST /api/v1/compliance/ai-agents/{agent_id}/kill): suspende o agente e revoga todos os tokens dele, incluindo os que estão na graça de uma rotação. O pedido seguinte de qualquer um deles dá 401. Um stream aberto (/events/*) verifica o token a cada 15 s, antes da mensagem seguinte, e fecha. Como o sinal de vida sai de 15 em 15 s, o fecho pode levar até 30 s. Fica em admin_events (ai_agent_killed, com quem, o estado anterior e quantos tokens revogou). Reativar o agente não devolve os tokens revogados.
    • Registo de risco (GET /api/v1/compliance/ai-agents/risks, backend/app/domain/compliance/ai_agent_risks.py): por agente, cada risco tem dono, probabilidade e impacto de 1 a 5, a pontuação (o produto) e o nível (baixo, médio, alto, crítico), a mitigação, o estado e a data da próxima revisão. O dono do risco ou um admin regista a revisão (POST /api/v1/compliance/ai-agents/risks/{risk_id}/review), e a data seguinte conta a partir daí. Um risco por fechar com a data passada está em atraso.
    • Ensaios de falha (POST /api/v1/compliance/ai-agents/{agent_id}/drills, backend/app/domain/compliance/ai_agent_drills.py): três cenários contra as guardas reais, com valores sintéticos, sem ler a fonte nem chamar um LLM. Na fuga de PII, um email, um NIF e um IBAN passam pela guarda de saída dos LLM e os datasets classificados do workspace são pedidos pelo agente sem o go-live, e os que não lhe estão ligados também com o go-live completo. No dado envenenado, cinco pedidos dirigidos a um modelo passam pelo detetor de injeção. Na ação proibida, entram uma ferramenta fora da matriz, o teto por hora, um token config e um voto de aprovação por um token. Cada verificação fica gravada como passou, falhou ou não se aplica, e o ensaio falha se a guarda que exercita falhar (por exemplo, com a guarda de saída desligada). Cada cenário tem de voltar a passar dentro do intervalo, senão o item dos testes de guarda cai.
    • Pacote do conselho (GET /api/v1/compliance/ai-agents/board-pack, também em Markdown com board-pack.md): o registo de risco (por nível, em atraso, sem dono e a lista do maior para o menor) e os ensaios dos últimos 90 dias por cenário e desfecho, com os agentes a quem falta um cenário, falhou ou venceu. Só contagens, nomes de agentes, títulos e datas.
  • Registo de uso: cada pedido de um agente fica registado, aceite ou recusado, com o agente, a ferramenta, a rota, os datasets, se eram classificados, o motivo da recusa e o X-Request-ID. Numa listagem sem dataset no pedido, «classificado» diz se o que o agente vê inclui datasets classificados. Só ids e nomes, nunca o que foi lido (GET /api/v1/compliance/ai-agents/usage, filtros por agente, dataset, desfecho e data).
  • Ficha de dados do Anexo IV (ponto 2, alínea d)) por agente ou modelo, gerada a partir da proveniência (fonte, tabela, camada e datasets a montante), do perfil (linhas, colunas e colunas com valores em falta), da qualidade, da classificação e dos sinais de enviesamento do art. 10.º, com as lacunas escritas. Exporta-se em JSON e Markdown (GET /api/v1/compliance/ai-agents/{agent_id}/annex-iv.md, backend/app/domain/compliance/annex_iv.py).
  • IA sombra: os consumidores do tipo agente que registam consumos pela Trust API sem registo aparecem no registo como «não autorizados»; registá-los com data_consumer_id tira-os dessa lista.
  • Pré-verificação de um SQL antes de o correr (cartão #701): o agente manda a consulta e a finalidade e recebe a decisão de confiança de cada tabela que ela lê (joins, CTEs, subconsultas, nomes citados), com a pior a mandar no resumo. Uma tabela que não corresponde a um dataset que o agente vê é unknown, nunca trusted, e uma tabela fora do âmbito do token responde como uma que não existe. O SQL só se lê com o sqlglot, nunca corre (POST /api/v1/trust/check-sql, tool MCP check_sql_trust, backend/app/domain/quality/trust_sql.py).
  • Números que se defendem (cartão #726): o agente manda o número que vai responder e como o mediu (dataset, agregação, coluna, filtros e período) ou o SQL que o gerou. A BeryX recalcula-o na fonte, só por leitura, com o timeout e os tetos de custo da conexão, compara-o com uma tolerância e devolve verified, divergent (com o valor da fonte e a evidência) ou unverifiable (com o motivo). Um número que bate com uma fonte untrusted ou sem scan fica unverifiable. Das colunas sensíveis, a métrica só faz contagens. O SQL do agente passa a guarda das regras SQL e não as lê, nem com outro nome (um *, uma lista de aliases, mesmo no alias da própria tabela, uma UNION). Cada verificação fica no registo de saídas, por agente, e na auditoria (POST /api/v1/ai-outputs/verify, GET /api/v1/compliance/ai-agents/{agent_id}/outputs, tool MCP verify_claim, backend/app/domain/quality/claim_check.py).
  • A definição que vale, no MCP (cartão #760): o resource metrics://definitions dá a um agente as definições aprovadas do registo de métricas, com a fórmula, os filtros, o grão, a versão aprovada, o dono e a autoridade de definição (só o nome) e os datasets de origem que quem chama vê (backend/app/mcp_metrics.py). Com definition_id, o verify_claim recalcula o número com a fórmula e os filtros dessa versão, sobre um dataset de origem da definição, em vez da agregação que o agente manda; a consulta passa a guarda do SQL do agente, e uma mudança à espera de aprovação só conta depois de aprovada (backend/app/domain/quality/claim_definition.py).
  • O agente que perguntou e não leu fica registado (cartão #730): POST /trust/consumptions com consumed: false grava uma abstenção, com a decisão que o agente viu. Conta à parte dos consumos (abstentions_7d em «Consumidores» do dataset e em GET /consumers), não avisa os canais nem o sino como uma leitura em untrusted, e no impacto de um incidente o agente que não lê nenhum dataset do alcance sai travado («1 agente travado pela decisão de confiança») em vez de afetado. O SDK regista assim os datasets que atingem o fail_on (trust.check e o TrustGate quando recusa) e a tool MCP get_trust_decision com consumer_name regista-a numa decisão untrusted (backend/app/domain/lineage/data_consumers.py).
  • Os agentes nunca aprovam nem certificam: um token só escreve nas operações que a API lhe abre, e a única ação de um agente é propor (propose_action), que uma pessoa decide na app.
  • Conteúdo de origem marcado nos resultados do MCP: cada ferramenta e cada resource dizem no _meta (beryxtech.com/provenance) que campos trazem texto das fontes e das pessoas do workspace e quais têm a forma de um pedido a um modelo, sem apagar nem alterar o texto. As instruções do servidor lembram o agente de que esses campos são dados, nunca ordens (backend/app/mcp_provenance.py, servidor MCP).

O que falta: com domínios na ficha, a lista de domínios (GET /api/v1/domains) continua a mostrar os nomes dos de fora, sem os datasets nem a pontuação. Uma lista de referência importada de uma coluna guarda só o nome «dataset.coluna», sem o id do dataset, por isso fecha-se inteira ao agente com datasets retidos. A lista de ferramentas vale para os pedidos que chegam pelo servidor MCP: quem tem o token pode chamar a API com outro cabeçalho. A guarda da classificação e dos domínios não usa esse cabeçalho, mas a matriz de direitos sim: sem o cabeçalho de uma ferramenta da matriz, um pedido não chega a dados classificados. A aprovação é direta, por um admin; o fluxo de aprovação por etapas ainda não cobre agentes. Os ensaios de falha correm quando um admin os pede: nenhum agendador os corre sozinho, e um ensaio vencido fecha os dados classificados até voltar a passar. O pacote do conselho da app (em Governação) ainda não traz a secção dos agentes: lê-se pela API. Não há página na app: o registo gere-se pela API.

Testes: backend/tests/test_ai_agents.py, backend/tests/test_ai_agent_golive.py, backend/tests/test_mcp_server.py, backend/tests/test_mcp_provenance.py, backend/tests/test_trust_sql.py.

Orquestração e fluxo de trabalho

  • As minhas tarefas (página /tarefas, GET /api/v1/tasks): a caixa de cada owner e steward com o que espera uma decisão dessa pessoa — termos do glossário em rascunho e ligações propostas (quem gere o domínio do termo, ou um admin), pedidos dos fluxos de aprovação (quem decide a etapa atual: o aprovador, o delegado de alguém ausente, quem recebeu o escalonamento ou, sem ninguém por votar, um admin), propostas de contrato a que o produtor respondeu (admins), pedidos de ações da IA (o papel que a política exige), issues atribuídos e, desde o cartão #669, a revisão periódica das políticas de que a pessoa é dono e as exceções perto do fim (quem a pediu e o dono da política). Cada tarefa só aparece a quem a pode decidir, com a regra da rota de origem (um issue ou uma ligação pede permissão de escrita no dataset, e um pedido de aprovação deixa de fora as ligações a datasets que a pessoa não vê), com prazo e o SLA vencido primeiro; filtra-se por domínio e por tipo. Com o fluxo dos termos ligado, um termo em rascunho só chega à caixa depois de submetido, como pedido de aprovação. Com o das ligações, uma ligação sem pedido abre o glossário, onde «Aceitar» a põe no fluxo. Aprovar ou rejeitar na caixa chama a rota do ecrã de origem (mesmo serviço e mesma auditoria): num pedido de aprovação é o voto na etapa, com o motivo opcional da rejeição; um lote de ações da IA só abre em Equipa → Ações da IA, onde se vê o que executa. A contagem fica no menu e num resumo na página inicial. Um aviso no sino e um email por pessoa saem quando a tarefa chega e quando o prazo vence (app/domain/governance/tasks.py). Num pedido de aprovação, o prazo é o SLA da etapa, cada etapa avisa quem a decide e quem recebe o escalonamento também é avisado. A chegada de um issue que uma pessoa atribuiu só vai ao sino, porque essa atribuição já manda o email; a de um issue que o sistema atribui, como o de um SLO violado, segue por email. Um agente lê a caixa pela tool MCP list_my_tasks (backend/app/mcp_tasks.py, só de leitura): com as credenciais de uma pessoa vem a caixa dela e com um token vem vazia.
  • Orquestradores: webhook do dbt Cloud (scan no fim de cada job) e pacote Airflow com um gate de qualidade numa DAG — orquestradores.
  • Integradores: saúde dos pipelines Fivetran e Airbyte (só leitura, APIs oficiais) — sync falhado ou atrasado por dataset de destino, latência e reconciliação origem↔destino e o sync no diagnóstico de causa-raiz — integradores.
  • Ferramentas de BI: Power BI, Tableau e Looker (só leitura, APIs oficiais) — refreshes falhados ou atrasados como issues, dashboards na linhagem e no impacto das issues com o dono, e aviso de dashboards sem visualizações — ferramentas de BI.
  • CI/CD: gate de regras e contratos no pipeline, data-diff entre base e candidato — gate de qualidade em CI.
  • Agentes: servidor MCP para Claude e outros clientes MCP — servidor MCP.
  • Validação de registos no ponto de entrada: o integrador ou a pipeline mandam até 5 000 registos a POST /api/v1/datasets/{id}/validate antes de os escrever e recebem o veredito de cada um com as regras ativas do dataset (not_null, pattern, range, required_fields, valid_identifier com os identificadores do registo por país, in_reference_set com as listas ISO e as do workspace), o código do motivo de cada falha e o veredito do lote. As regras do dataset inteiro (unique, freshness…) vêm marcadas como não aplicáveis. Nada é guardado nem enviado a um LLM e a fonte não é lida: rejeitar ou pôr em quarentena fica do lado do cliente. Token dvk_* ou SDK (dv.validate.records) — validação de registos.
  • IDE: extensão VS Code com a qualidade inline nos ficheiros dbt/SQL — score, issues, contrato e PII por tabela, perfil por coluna no hover, alertas de política e data-diff do branch — extensão VS Code.
  • Configuração como código: export/import determinístico da configuração do workspace em YAML (datavault.yaml), para rever em code review e promover entre ambientes. O ficheiro leva as regras (rules), os domínios (domains), a política de dados (data_policy), a política de IA (llm_policy), os canais de alerta (channels), os relatórios agendados (report_schedules), o glossário de negócio com as ligações aprovadas (glossary) e, por conexão, a agenda de scans e a ativação dos monitores automáticos (scan_schedules, auto_monitors) e o ambiente da fonte (connection_environments) — sem segredos nem ids. O import mostra primeiro o plano (o diff do que muda) e aplica tudo ou nada. O cliente corre-o a partir do próprio CI com o SDK (beryx config plan no PR, beryx config apply no merge, sobre /api/v1/workspace/config) e um token «Configuração como código»; as mudanças às políticas de IA e de dados e ao ambiente das conexões ficam com um admin. O operador também o corre com backend/scripts/workspace_config.py, que corre junto do backend (no pipeline). Pela API, regras, domínios, canais e relatórios agendados gerem-se recurso a recurso em /api/v1/iac com o mesmo token — é o que usa o provider Terraform (integrations/terraform). Monitores definidos noutras ferramentas (ex.: montecarlo.yml) não se importam: as regras entram pelos formatos do import — dbt, Great Expectations, SodaCL e ODCS (o que converte), mais os exports das suites legadas do Informatica Data Quality e do Collibra DQ — e saem em dbt, SodaCL e GX: testes dbt, checks SodaCL ou uma suite do Great Expectations (GET /api/v1/datasets/{id}/rules/export?format=dbt|sodacl|gx; o workspace inteiro num só ficheiro SodaCL por GET /api/v1/rules/export). No SodaCL e no GX só sai o que o import lê de volta como a mesma regra; o que fica de fora (regras de streams, dígito de controlo, regras SQL fora do dbt, tolerâncias acima de zero) vem no ficheiro com o motivo, no cabeçalho do SodaCL e em meta.beryx.unsupported no GX (o que sai).
  • Fluxo de problemas com estados, responsáveis, escalas e agendamento de scans e relatórios.

Testes: backend/tests/test_orchestrators.py, backend/tests/test_data_diff.py, backend/tests/test_mcp_server.py, backend/tests/test_workspace_config_gitops.py, backend/tests/test_iac_api.py, backend/tests/test_ide.py, backend/tests/test_record_validation_api.py, integrations/vscode/test/.

Experiência para utilizadores de negócio

  • Relatórios por persona (engenharia, gestão, executivo) com impacto estimado em euros, em PDF, agendáveis e partilháveis por link de leitura.
  • Painel de governação para o CDO: a eficácia do programa num só sítio — cobertura (owner, documentação, classificação, contratos, regras), cycle time e MTTR dos críticos, cumprimento do SLO/SLA e dos prazos, adoção de sugestões e de regras «nunca mais», score por dimensão, incidentes críticos e uso —, cada KPI com meta por workspace ou organização, semáforo, tendência mensal e drill-down por domínio; export do scorecard executivo (CSV) e da evidência para o pack de procurement (Markdown) (GET /governance, backend/app/domain/governance/kpis.py).
  • Insights de negócio (página Negócio): métricas definidas pelo utilizador sobre os datasets, em linguagem de negócio (GET /business-metrics, backend/app/domain/insights/business_insights.py).
    • Definição: soma, contagem, contagem de distintos ou média; em euros, clientes, contagem ou número; por dia ou por semana. A coluna de data é do tipo data ou data e hora (no SQLite, também texto) e a soma e a média pedem uma coluna numérica. A criação recusa uma coluna classificada como dados pessoais ou confidenciais quando a métrica mostraria os valores dela (segmento, data, valor de uma soma ou média); a contagem de distintos aceita-a.
    • Cálculo: em cada scan, só em PostgreSQL, MySQL, SQL Server, Fabric, Snowflake, BigQuery, Databricks e SQLite (a criação recusa as outras fontes), com uma consulta agregada só de leitura: sem linhas, só o agregado de cada período e segmento. Uma coluna classificada depois deixa de sair: o segmento sai da leitura e do scan seguinte, e uma data ou um valor param a origem.
    • Insight: a métrica, o período, a variação face ao período anterior e ao habitual (o mesmo dia da semana ou as 8 semanas anteriores) e a evidência: os datasets, a consulta e os problemas abertos nos dados. Na soma e na contagem, a variação decompõe-se por origem e segmento (a média e a contagem de distintos têm uma só origem e a criação recusa-lhes o segmento, porque as partes não somam a variação do total), e a parte que está em origens com problemas abertos nos dados (frescura, volume, estrutura, regras) separa «rever os dados» de uma variação sem problemas nos dados, sem afirmar causas. Um período que alguma origem ainda não calculou fica «por atualizar», nunca a zero. Com a métrica em euros ou clientes (fora da média), mostra também uma estimativa do valor em linhas com regras a falhar no último scan.
    • Resumo: escrito por templates em português, inglês e espanhol, sem LLM, com as origens de cada métrica e o link para a página; segue por email, Slack ou Teams com frequência semanal ou diária.
  • Visões por equipa (GET /team-lens, backend/app/domain/access/team_lens.py): os dashboards de engenharia, gestão e executivo, o calendário de qualidade, o scorecard por dimensão e o SLO (com os CSV) aceitam ?team= (global, mine ou o id da equipa). O âmbito de uma equipa são os datasets dos domínios onde tem uma concessão em vigor e os que têm como owner um membro ou o gestor da equipa; sem team, quem vê o global recebe o global e os outros as suas equipas.
    • Política do admin (PUT /team-lens/policy, auditada em team_lens.policy_updated): open (por omissão, todos veem tudo), aggregates (o detalhe só das suas equipas, os KPIs das outras) ou isolated (as outras equipas saem do AccessScope: os datasets e domínios só delas dão 404 em toda a API, incluindo «As minhas tarefas»). A visão global e a comparação entre equipas são do admin e dos papéis em global_roles. Uma política gravada que não se reconhece vale isolated, sem global_roles (team_lens.policy_of).
    • KPIs da lente (GET /team-lens/kpis): score, problemas abertos, MTTR a 30 dias, correções aplicadas e cobertura de regras; GET /team-lens/comparison põe as equipas lado a lado, só para quem vê o global (backend/app/domain/scans/team_kpis.py).
    • Gestor da equipa: PUT /teams/{team_id}/manager (admin, auditado em team_manager_set); conta para a lente e para o modo isolado como um membro.
    • Relatório do workspace por equipa: POST /reports com team_id (só no workspace_overview) monta o contexto só com os datasets, problemas, regras, duplicados e sinais do âmbito da equipa, sem o custo LLM do workspace (backend/app/domain/insights/overview.py). O team_id tem de ser o id de uma equipa do workspace com datasets no âmbito (422 senão). Os datasets ficam gravados no pedido (AIReport.team_scope_json): o contexto e os gráficos do export usam só esses, o texto gravado de problemas e sinais que nomeia um dataset de fora sai «Dataset restrito», e lê o relatório quem tem read em todos eles, também depois de uma concessão retirada. O relatório de equipa não entra na base de conhecimento, onde ficaria sem dataset e legível a todo o workspace (backend/app/domain/knowledge/service.py, ingest_report). GET /issues, GET /suggestions, GET /reports, GET /tasks e GET /tasks/count aceitam o mesmo ?team=.
    • Scans e amostras: GET /scans, GET /scans/{scan_id}, GET /scans/{scan_id}/live, GET /duplicates e GET /rules/{rule_id}/failing-rows.sql aceitam o mesmo ?team=. Um scan está na lente quando leu um dataset da equipa (team_lens.scan_in_lens, a regra do worker); fora dela, o 404 de sempre.
    • Envio à equipa: o digest semanal dos canais de equipa leva os KPIs da equipa e segue também para o gestor, quando ele lê o que a equipa lê (backend/app/domain/alerts/team_channels.py). O relatório de equipa concluído segue por email aos membros e ao gestor com read em todos os datasets gravados no pedido (backend/app/domain/insights/team_reports.py).
    • Na app, o seletor «Lente de equipa» (frontend/src/components/TeamLensSelect.tsx) está nos três dashboards, em Problemas, Sugestões, Relatórios IA, SLO e «As minhas tarefas». A escolha fica no URL (?equipa=) e no browser (frontend/src/lib/teamLensStore.ts). A contagem de tarefas do menu e o resumo da página inicial seguem a escolha guardada. No modal «Relatório do workspace» escolhe-se o âmbito: todo o workspace, só para quem vê o global, ou uma equipa.
  • Assistente em linguagem natural sobre os dados de qualidade do workspace.
  • Scan ao vivo: o primeiro scan mostra-se passo a passo; primeiro resultado em minutos.
  • Interface em português, inglês e espanhol; badge público de qualidade por dataset.

Testes: backend/tests/test_reports_v2.py, backend/tests/test_report_pdf.py, backend/tests/test_assistant_chat.py, backend/tests/test_public_badge.py, backend/tests/test_governance.py, backend/tests/test_business_metrics.py, backend/tests/test_team_lens.py, backend/tests/test_team_lens_properties.py, backend/tests/test_team_lens_views.py, backend/tests/test_team_report_scope.py, backend/tests/test_team_digest_delivery.py.