CORTEX · doc

Identity Service

quem é a pessoa

16 regras · v0.5.0 · rascunho · owner: plataforma · 41 KB · identity-service · sumário em JSON

CORTEX — identity-service (quem é a pessoa)

Versão: 0.5.0 · Data: 2026-10-14 · Status: Rascunho · Owner: Plataforma Caminho canônico: cortex-docs/services/IDENTITY_SERVICE.md Documentos irmãos: cortex-docs/ECOSYSTEM.md (mapa e fronteiras) · cortex-docs/services/TRACKING_SERVICE.md (berço do lead_id e comportamento) · cortex-docs/standards/UNISCALE_ENGINEERING_STANDARDS.md (padrões técnicos)

O identity-service é o dono de quem é a pessoa e da trilha de presença (por onde passou). É o serviço murado do ecossistema: não fica exposto à internet, é escrito só por fila e lido de forma síncrona pelos demais.

Este documento fecha o desenho que o ECOSYSTEM.md §7 mantinha adiado ("Schemas de tracking e identity").

Leia junto com o TRACKING_SERVICE.md. O tracking cunha o lead_id (navegador); o identity cunha o person_id (pessoa). Cada regra de fronteira aqui tem a recíproca lá.


1. person_id nasce da PII, não da navegação (Obrigatório)

O person_id é cunhado pelo identity-service, em UUIDv7 (uniscale.columns.uuid-generation), no momento em que um lead_id se torna identificável por PII — e não antes.

  • Navegação anônima não gera pessoa. Um navegador que nunca se identificou tem lead_id e nenhum person_id. Pessoa é um conceito de PII; criar pessoa para tráfego anônimo povoaria o domínio com registros que em maioria nunca serão gente.
  • Cardinalidade: 1 person_id para N lead_id. A mesma pessoa se identificando no celular, no desktop e no computador do trabalho amarra os três lead_id ao mesmo person_id.
  • Uma PII distinta é uma pessoa. A mesma pessoa identificando-se de novo não cunha person_id novo: o matching (§3) encontra o existente e amarra mais um lead_id. Cunhar por evento de identificação, e não por pessoa distinta, quebraria a cardinalidade acima.
  • O person_id é o handle de pessoa do ecossistema. Fora do par tracking/identity, os serviços referenciam pessoa por person_id — nunca por lead_id, que é identificador de origem de navegação (ecosystem.services.tracking-lead-birth).
  • O person_id é opaco e satisfaz o identity_ref exigido por ecosystem.services.ai-wallet-lead-pii.

2. Registro do lead anônimo, sem pessoa (Obrigatório)

O identity consome tracking.lead.created e grava em identity.lead_registry: lead_id, tenant_id, matching_key_hash, consent_state, first_seen_at. Sem person_id (§1).

  • Isso não é uma pessoa — é um sinal esperando virar pessoa.
  • Existe por uma razão operacional concreta: quando a identificação chega, o matching precisa alcançar os lead_id anteriores daquela origem. Sem o registro, a identificação só enxergaria o lead_id do instante, e a amarração multi-navegador de §1 nunca aconteceria.
  • Emenda ao ECOSYSTEM §5: o texto atual diz que o identity "cria seu registro anônimo", o que se mantém — mas esse registro não tem person_id, ao contrário do que a leitura de "um registro por pessoa que evolui de anônimo a conhecido" sugere. O registro anônimo e a pessoa são tabelas distintas.

3. Matching: confiança é do par identificador × modo de aquisição (Obrigatório)

O ECOSYSTEM §4 fixa "≥90% = mesma pessoa". Esta regra dá a esse número significado auditável.

A confiança não pertence ao tipo do identificador, e sim ao par (identificador, modo de aquisição). O mesmo telefone vale coisas diferentes conforme tenha sido digitado num formulário ou usado para falar conosco. Ignorar isso é o erro que produz tanto fusão errada quanto fusão que deixa de acontecer.

Modos de aquisição (vocabulário fechado, coluna acquisition_mode em §5):

  • verified_possession — a pessoa provou controlar o identificador ao usá-lo: mensagem inbound de WhatsApp, clique em link de double opt-in de email.
  • verified_transaction — o identificador foi validado por um terceiro autoritativo numa transação real (gateway de pagamento, jacad).
  • declared — foi digitado num formulário e não foi verificado por ninguém.
Identificador Modo de aquisição Confiança Funde sozinho?
Documento que identifica pessoa (CPF, passaporte, SSN) verified_transaction 1.00 sim
Documento que identifica pessoa (CPF, passaporte, SSN) declared 0.97 sim, salvo veto
RG, foreign_id, other qualquer ≤ 0.70 não
CNPJ qualquer ≤ 0.70 não
Documento inválido qualquer — não entra como identificador
Telefone verified_possession 0.95 sim, salvo veto
Email verified_possession 0.95 sim, salvo veto
Email declared 0.93 sim, salvo veto
Telefone declared + nome muito similar — 0.92 sim, salvo veto
Telefone declared 0.70 não
Nome, device, IP, geo qualquer ≤ 0.60 nunca

Documento não é um tipo só (Obrigatório). O vocabulário de uniscale.types.personal-documents mistura coisas com propriedades diferentes, e tratá-las igual funde estranhos:

  • Validação por tipo é obrigatória na aplicação, como a própria §22.2 exige: CPF e CNPJ com dígito verificador e rejeição dos repetidos; SSN com as faixas nunca emitidas; passaporte pelo padrão ICAO, e brasileiro com 2 letras + 6 dígitos. Documento inválido é guardado — recusar perderia informação de um lead que já entrou — mas fica marcado (is_valid = false) e não vira identificador: um CPF digitado errado que caia no CPF de outra pessoa real fundiria dois estranhos com confiança 0.97.

  • Só documento único de pessoa física funde e veta. O RG não é único no Brasil: é emitido por órgão estadual e dois estados podem repetir o número. Se ele vetasse, separaria quem tirou segunda via em outro estado; se fundisse, juntaria homônimos de estados diferentes.

  • O país entra na decisão, nunca na chave de busca. Passaporte é único dentro do país emissor: AB123456 do Brasil e de Portugal são documentos distintos. A tentação é pôr o país no hash — e é erro, porque aí os dois registros nunca se enxergam e a colisão fica invisível: o sistema não separou, ele não viu. A chave de busca é tipo:valor; o país é atributo comparado na decisão:

    • mesmo número, mesmo país → funde;
    • mesmo número, país diferente → não funde e vira candidato a revisão (§16).

    Assim nem se funde um português com um brasileiro, nem se perde de vista que alguém pode ter apenas errado o país no formulário — o split acontece, mas fica visível e reparável.

  • CNPJ é validado, guardado e nunca funde pessoas. O titular é uma empresa. Ele existe no modelo porque a venda B2B (empresa comprando curso para funcionários) vai precisar dele — e é exatamente esse cenário que o proíbe no matching: todos os funcionários de um cliente informam o mesmo número, e fundi-los colapsaria o quadro inteiro num person_id.

  • Unicidade no banco, por índice único parcial, sobre os tipos que identificam pessoa física. Duas pessoas com o mesmo CPF é estado que o matching deveria ter impedido; o índice transforma isso em falha alta (DLQ, visível) em vez de duplicata silenciosa. Sem ele, duas réplicas do consumidor podem cunhar duas pessoas sem uma enxergar a outra.

Na dúvida, separa (Obrigatório). Os dois erros possíveis não têm o mesmo peso. Fusão errada junta duas pessoas: a PII de uma aparece para quem consulta a outra, o histórico vai para o CRM e para a IA misturado, e desfazer não desfaz o que já vazou. Split errado parte uma pessoa em duas: incomoda, e se conserta com uma fusão quando o sinal chegar. Toda regra abaixo é ordenada por isso — na ausência de certeza, o serviço separa e registra a suspeita, nunca junta.

Contra-sinal: um sinal forte sozinho não basta contra tudo o mais (Obrigatório). O veto trata da contradição (documentos diferentes). Falta o simétrico: um documento declarado que bate enquanto nome, e-mail e telefone discordam é, quase sempre, documento digitado errado — não uma pessoa que trocou de nome.

  • Documento declared que casa, mas com nome muito divergente (similaridade abaixo de ~0.45) e o candidato já tendo nome: o sinal é rebaixado, não funde, e o par vai para revisão (§16).
  • Vale só para declared. Documento de verified_transaction é autoritativo: ali um nome divergente significa que o nome está errado, não o documento.
  • Documento em disputa. Quando o documento não funde mas já é reivindicado por outra pessoa, ele é gravado com is_disputed e fica fora do índice único e fora do matching. Isso separa duas perguntas que é tentador colapsar numa flag só: is_valid é sobre formato (dígito verificador), is_disputed é sobre titularidade. Sem essa separação, o contra-sinal e o índice único colidem — o fato não pode fundir nem ser gravado, vai para a DLQ, e o lead se perde por causa de um erro de digitação.

Contaminação é detectada, não desfeita automaticamente (Obrigatório). O mesmo contato ativo em duas pessoas distintas vira candidato a revisão (sinal shared_contact). Pode ser legítimo — família dividindo um e-mail — ou rastro de anexação errada. O serviço não decide: registra.

Uma submissão posterior nunca remove atributo de pessoa existente. Formulário é entrada pública e não autenticada; permitir que ele desatrele dados seria vetor de envenenamento — bastaria enviar formulários com o documento de alguém para ir arrancando contatos da ficha dela. Correção cria a pessoa certa; a limpeza da errada é ato humano na fila.

  • Limiar: 0.90.
  • Veto por contradição (Obrigatório): se os dois candidatos têm documento pessoal diferente ou data de nascimento diferente, não funde — qualquer que seja o outro sinal, inclusive um de posse verificada. Sinal forte contraditório anula sinal forte concordante.
  • Telefone declared sozinho nunca funde. Em captação educacional o telefone do formulário é frequentemente do responsável, não do candidato: a mesma mãe preenchendo a LP de dois filhos produziria a fusão de três pessoas distintas.
  • Telefone verified_possession funde. Quando o lead nasce de uma IA atendendo no WhatsApp, o número é a identidade daquela conversa — a pessoa está escrevendo dele. Nesse nascimento o telefone é o único identificador que existe, e tratá-lo como declared inviabilizaria o canal inteiro. O veto continua valendo: se depois aparecer documento contraditório, não funde.
  • Abaixo do limiar: trata como pessoa nova, e o enriquecimento continua em background (§4).
  • Os pesos são configuração versionada, não código. Moram em identity.matching_weights (§5) e são ajustáveis sem deploy. Toda fusão registra qual sinal, com que confiança e sob qual versão de pesos (§7) — sem a versão, não se explica seis meses depois por que aquela fusão aconteceu.
  • Os pesos são do domínio de identidade, não regra comercial: ficam aqui, não no institutional-service.

4. Nunca interroga o usuário (Obrigatório)

Aplicação literal do ECOSYSTEM §4: abaixo do limiar, o identity trata como novo, enriquece em background e nunca interroga.

  • Proibido qualquer fluxo que devolva ao usuário perguntas de desambiguação ("este é você?", "confirme seu CPF") por iniciativa do identity.
  • A reavaliação do matching é assíncrona: quando um identificador novo chega, o worker reprocessa os candidatos e pode fundir depois — sem interação humana.
  • Uma IA especializada pode, por sua própria regra de negócio, pedir um dado ao usuário. Isso é decisão dela; o identity não solicita.

5. Modelo de dados (Obrigatório)

Banco próprio identity_db, schema identity (uniscale.naming.databases). Sem banco vetorial.

Tabela Papel
identity.lead_registry Lead anônimo, sem pessoa (§2)
identity.people id = person_id, tenant_id, status, first_identified_at
identity.person_leads Vínculo person_id ↔ lead_id (N navegadores por pessoa)
identity.person_contacts N por pessoa: contact_type (email|phone), valor cifrado, hash, acquisition_mode, source_system, is_primary, verified_at, bound_at, unbound_at
identity.matching_weights Pesos de confiança por (identificador, acquisition_mode), versionados (§3)
identity.person_documents personal_document_type / _value cifrado / _value_hash / _country
identity.person_names Nome tem versão e fonte; não é coluna única
identity.person_aliases person_id absorvido → sobrevivente (fusão, §7)
identity.person_merges Diário append-only: sinal, confiança, weights_version, event_id
identity.presences Por onde passou: service, first_seen_at, last_seen_at (§6)
identity.external_refs Pessoa ↔ id externo (aluno no jacad, card no CRM)
identity.pii_access_log Append-only, toda leitura de PII (§9)

Decisões que o modelo fixa:

  • Contato é tabela, não coluna. Uma pessoa tem dois emails e três telefones — isso é a norma, não a exceção. email como coluna em people obrigaria a eleger um e perder o resto.
  • acquisition_mode é obrigatório em todo contato e documento. Sem ele o matching (§3) não tem como pesar o sinal, e um telefone de formulário fica indistinguível de um telefone de WhatsApp inbound.
  • Documento segue o trio obrigatório personal_document_type / _value / _country (uniscale.types.personal-documents), valor sem máscara, vocabulário fechado.
  • status é campo único com vocabulário fechado (uniscale.service-internals.state-machine): identified → enrolled → erased. Não existe anonymous: pessoa anônima não existe (§1). enrolled é setado por consumo de fato do order-service — a verdade da matrícula continua sendo do jacad, o identity apenas reflete.
  • lead_id é referência externa ao tracking-service: sem FK local, com COMMENT ON COLUMN obrigatório (uniscale.constraints.foreign-keys — FK não cruza fronteira de serviço).
  • Colunas base em toda tabela (uniscale.columns.base); tenant_id em toda tabela de negócio; soft delete nas tabelas de negócio, com dispensa explícita nas de linhagem (identity.person_merges, identity.pii_access_log, que são append-only).

6. Presença diz por onde passou, não o que aconteceu (Obrigatório)

Aplicação literal do ECOSYSTEM §3: o identity registra que a pessoa passou pela wallet, pelo sales, pelo payment — e não o que ela fez lá.

  • identity.presences guarda service, first_seen_at, last_seen_at. Nada de conteúdo, pontuação, oferta, valor ou transcript.
  • O contexto rico é puxado na fonte pelo interessado (ECOSYSTEM §5; ecosystem.services.ai-wallet-source-context): o ai-sales resolve o person_id aqui e lê o contexto direto da wallet.
  • O identity não é cache de contexto de outros serviços. Duplicar contexto aqui recria a redundância que a fronteira tracking × identity existe para evitar.

7. Fusão de pessoas: apelido, convergente e reversível (Obrigatório)

Quando o matching (§3) descobre que dois person_id são a mesma pessoa, o identity funde — e é o único serviço autorizado a decidir isso.

  • Vencedor determinístico: sobrevive o person_id de first_identified_at mais antigo (com UUIDv7 ordenado no tempo, o menor id). Sem empate, sem dependência de ordem de chegada.
  • Apelido, não reescrita: o absorvido vira linha em identity.person_aliases; contatos, documentos e vínculos migram para o sobrevivente. O person_id absorvido continua resolvível para sempre — outros serviços guardaram essa referência e não são reescrevíveis.
  • Profundidade 1: o apelido aponta sempre direto para o sobrevivente atual. Ao fundir B em C, todos os membros do cluster de B (inclusive A) são reapontados na mesma transação. Consequência: o estado final independe da ordem de chegada e reprocessar a fila inteira dá o mesmo resultado — idempotente por construção, não por deduplicação (uniscale.events.ordering).
  • Desfazer é de primeira classe: o identity publica identity.person.unmerged ao reverter. A ≥90% de confiança, falso positivo é esperado na fronteira; sem reversão, o primeiro caso vira intervenção manual em produção.
  • identity.person_merges é append-only e preserva sinal, confiança e event_id de origem.

8. Escrita só por fila; leitura síncrona; nunca exposto (Obrigatório)

  • Não existe rota HTTP de escrita. O identity se popula consumindo fatos (ECOSYSTEM §4/§5), nunca por escrita externa direta.
  • Consome, entre outros: tracking.lead.created (topic), tracking.lead.identified (canal direct dedicado, ecosystem.services.tracking-pii-handoff), e fatos de payment, order e connectors de canal que revelem identificadores.
  • Consumer idempotente por event_id (uniscale.lineage.idempotency), ack manual, retry com limite e backoff, DLQ obrigatória, prefetch explícito (uniscale.service-internals.queue-config).
  • Não fica exposto à internet pública (ECOSYSTEM §4). É o serviço murado — o relaxe de borda vale só para o comportamento pseudônimo do tracking.

9. Leitura em dois níveis, e a de PII é auditada (Obrigatório)

  • resolve(ref) — recebe person_id ou lead_id, devolve o person_id sobrevivente e o status. Sem PII. Barato, chamável por qualquer serviço interno, não auditado.
  • list_display_names(person_ids[], purpose) — nível intermediário, para lista (a tela de Leads e a fila de fusão do institutional). Devolve apenas o nome de exibição de cada pessoa — nunca contato nem documento — e aceita a lista de uma vez. Grava uma única linha em identity.pii_access_log por consulta, com a finalidade e a quantidade de pessoas. Sem ele, uma lista de 50 leads seria 50 chamadas de get_person e 50 linhas de log, e o registro que existe para responder "quem acessou este titular" viraria ruído por volume. Nome é PII, mas de exibição: o meio-termo mantém o acesso auditado sem inflar o log em uma ordem de grandeza.
  • get_person(person_id, purpose) — devolve a PII completa. O chamador declara a finalidade — vocabulário fechado billing | enrollment | contact | support | erasure | merge_review —, e toda chamada grava linha em identity.pii_access_log (append-only: quem, quando, qual pessoa, qual finalidade, quais campos).

O log não é burocracia: a LGPD exige registro das operações de tratamento. Sem ele, a resposta a "quem acessou os dados deste titular" é "não sei" — que é a resposta que gera sanção.

  • Toda resposta de PII vem acompanhada do consent_state, para o chamador não poder alegar desconhecimento do que pode fazer com o dado (ecosystem.services.tracking-consent-tiers).
  • O identity nunca devolve PII completa em lote. O list_display_names é a única leitura em lote, e ainda assim só de nome de exibição, com finalidade e uma linha de log. Exportação massiva de PII completa é operação administrativa registrada, não rota de serviço.

10. PII em repouso: cifrada, e buscável por hash com pepper (Obrigatório)

  • Valor de PII cifrado em repouso por coluna (AES-256-GCM, chave de envelope).
  • Busca por igualdade sem índice em texto claro: coluna *_hash com HMAC-SHA256 + pepper guardado em KMS/vault, nunca no banco. Sem pepper, hash de CPF é quebrável por força bruta trivialmente — só existem ~10¹¹ CPFs. O pepper é o que torna o índice inútil para quem obtiver um dump do banco.
  • Normalização antes do hash, para que o matching funcione: email trim + lowercase; telefone em E.164; documento apenas com caracteres significativos, sem máscara (uniscale.types.personal-documents).
  • Proibida normalização "esperta" (remover pontos de endereço Gmail, tirar dígitos do telefone, apelidar nomes): gera fusão errada, que é o dano mais caro deste serviço.
  • PII nunca em log (uniscale.mongo.forbidden-content) e nunca em evento (§11).

11. Fatos publicados não carregam PII (Obrigatório)

Exchange identity, tipo topic; envelope padrão (uniscale.events.envelope).

Fato Quando Consumidores
identity.person.identified person_id cunhado ou novo lead_id amarrado tracking (grava o vínculo), cortex-service, crm-connector
identity.person.merged duas pessoas fundidas (§7) tracking, cortex-service, crm-connector
identity.person.unmerged fusão revertida (§7) idem
identity.person.erased apagamento do titular (§12) tracking, cortex-service, demais detentores
identity.merge_candidate.applied resolução da fila aplicada — fusão feita (§16) institutional (reflete na tela)
identity.merge_candidate.rejected_by_rule resolução recusada por regra dura — já resolvido, cross-tenant, já fundido (§16) institutional
  • Nenhum desses fatos carrega PII — só ids, status e metadados de decisão (sinal, confiança). Por isso vão no topic público, sem a restrição de canal dedicado que vale no sentido inverso (ecosystem.services.tracking-pii-handoff).
  • Publicação via outbox transacional: o fato e a escrita local commitam juntos.
  • Nomenclatura e versionamento seguem uniscale.events.naming e uniscale.events.versioning.

12. Apagamento do titular é executado aqui e propagado por fato (Obrigatório)

O direito ao esquecimento atravessa serviços: a PII está no identity, o comportamento no tracking, e cópias podem existir em quem consumiu.

  • O identity-service é o ponto único de execução. Ao receber o pedido: apaga fisicamente os valores de PII, mantém a casca da pessoa com status = 'erased' para que as referências de person_id espalhadas não fiquem penduradas, e publica identity.person.erased.
  • Cada detentor obedece ao fato: o tracking purga comportamento, sessões e vínculos (ecosystem.services.tracking-retention); os demais apagam suas cópias. Nenhum serviço decide apagamento por conta própria.
  • A execução é registrada em identity.pii_access_log (§9) — o apagamento é, ele próprio, uma operação de tratamento auditável.
  • O canal do pedido é fronteira aberta. Como o identity é escrito só por fila (§8), não existe rota HTTP para solicitar apagamento: hoje o gatilho é operação administrativa executada no próprio serviço, com o ator registrado no log. Quando o fluxo do DPO for desenhado, o pedido vira consumo de fato e esta regra é atualizada — a execução e a propagação acima não mudam.

13. Vínculo e fusão nunca atravessam tenant (Obrigatório)

  • Constraint dura: pessoa, contatos, documentos e vínculos só existem dentro de um tenant_id.
  • A mesma pessoa física em dois tenants são duas pessoas, com person_id distintos e sem qualquer ligação. Não há fusão cross-tenant, e o matching (§3) nunca compara candidatos de tenants diferentes.
  • Fato que atravesse tenant não é aplicado: DLQ retida e alerta.
  • Se esta validação falhar, o vazamento é entre clientes — a falha mais grave possível neste serviço.

14. Referências externas são do identity (Obrigatório)

identity.external_refs guarda o vínculo entre a pessoa e o identificador dela em plataforma externa (aluno no jacad, card no CRM): system, external_id, linked_at.

  • Isso é identidade de pessoa, e por isso mora aqui. Não confundir com a tradução de IDs de produto/oferta para terceiros, que é do institutional-service (ecosystem.services.institutional-rule-source).
  • Populado por consumo de fato dos connectors (§8), nunca por escrita direta.
  • A referência do card no CRM chega pela volta do connector. Quando o card nasce, o crm-connector devolve o fato de criação (crm.card.created, CRM_CONNECTOR.md §5); o identity o consome e grava pessoa ↔ card apenas quando a pessoa já é conhecida (o fato traz o person_id). Se o card nasceu de um lead ainda anônimo, não há pessoa para vincular aqui — o vínculo lead ↔ card fica no tracking (TRACKING_SERVICE.md §26), que é por onde a volta do card se resolve. Como todo write do identity, é por fila (§8), idempotente, nunca escrita direta.
  • A verdade acadêmica continua sendo do jacad (ECOSYSTEM §1): aqui fica a correspondência, não o cadastro.

15. Contato de posse verificada é exclusivo e datado (Obrigatório)

Um identificador adquirido por verified_possession (§3) prova que alguém controla aquele canal naquele momento — não para sempre, e não em exclusividade eterna.

  • Exclusividade no tempo: dentro de um tenant_id, um mesmo contato de posse verificada está atribuído a uma pessoa por vez. A atribuição tem bound_at e unbound_at.
  • Reciclagem de número: operadora devolve número inativo ao mercado. Quando um contato de posse verificada passa a ser exercido por outra pessoa (documento contraditório, veto de §3), a atribuição anterior é encerrada com unbound_at e a nova é aberta — nunca apagada. O histórico é o que permite explicar por que uma mensagem de 2026 foi para outra pessoa que não a de 2028.
  • Encerrar atribuição não funde nem separa pessoas. São operações distintas: a pessoa antiga continua existindo, apenas deixa de ter aquele canal.
  • O peso do sinal decai com a idade da posse. Posse verificada há duas semanas e posse verificada há quatro anos não valem o mesmo; a curva de decaimento é parâmetro em identity.matching_weights (§5), não constante em código.
  • Contato com unbound_at preenchido não participa do matching (§3) e não é usado para contato — mas permanece consultável para auditoria.

16. Controle de fusão: candidatos, asserção negativa e fusão manual (Obrigatório)

O matching automático decide os casos claros. O que fica entre o limiar de suspeita e o de fusão hoje é silenciosamente descartado — e é exatamente ali que mora o split reparável de §3. Essa informação precisa sobreviver.

  • Fila de candidatos. identity.merge_candidates guarda o par de pessoas, o sinal, a confiança e o status (pending | merged | rejected). Alimentam a fila: confiança entre a suspeita (~0.70) e o limiar (0.90); vetos por contradição (§3); mesmo documento com país diferente; contra-sinal de nome; documento em disputa; e mesmo contato ativo em duas pessoas (shared_contact).
  • Asserção negativa é permanente. rejected vale para aquele par, para sempre: o matching consulta antes de sugerir de novo. Sem isso a fila devolve o mesmo par a cada sinal novo, e em poucas semanas ninguém mais olha para ela. A decisão de que não são a mesma pessoa é tão informação quanto a de que são.
  • Fusão manual existe e é de primeira classe. Um operador que enxerga a duplicata precisa poder dizer isso. Grava em identity.person_merges com actor e motivo, é auditada em pii_access_log (é operação sobre PII) e continua reversível como qualquer fusão (§7).
  • Quem pode fundir é decidido no painel. A autorização (qual operador pode aprovar/fundir) é do institutional (§8 não abre escrita HTTP nem para operador). O identity garante o que é dele: só dentro do mesmo tenant_id — fusão cruzando tenant não existe, §13 não abre exceção —, rejeitando por regra dura o que cruzar.
  • A tela é do institutional; a resolução chega por fato. O cockpit é do institutional-service (ECOSYSTEM §4), que já existe. O identity expõe a leitura da fila — merge_candidates com o par, sinal, confiança, status, o motivo do rejected (a asserção negativa é permanente e precisa ser visível) e paginação por cursor — e consome a decisão como fato:
    • Entrada (o institutional publica, o identity consome): institutional.merge_candidate.resolved (candidate_id, decision = approved|rejected, tenant_id, actor_user_id, actor_name, reason); e institutional.person_merge.requested (person_id_a, person_id_b, tenant_id, actor_user_id, actor_name, reason) para a fusão manual de §16.
    • O identity não revalida papel. A autorização é gateada no painel antes de o fato sair; um consumidor de fila não autentica quem publicou, então revalidar role no consumo dá sensação de controle sem o controle. O identity registra actor_user_id/actor_name na autoria (person_merges, pii_access_log) e aplica. A autenticação entre serviços — para que o fato seja de quem diz ser — é regra de plataforma (uniscale.service-internals.service-auth).
    • Saída (o identity publica de volta): o canal é assíncrono, então o operador clica e não sabe o resultado. O identity publica identity.merge_candidate.applied (fusão aplicada) ou identity.merge_candidate.rejected_by_rule (recusada por regra dura — candidato já resolvido, par cross-tenant §13, pessoa já fundida) para o institutional refletir na tela. Sem esse retorno, a tela só consegue dizer "enviado" e mente ao operador.

Fronteiras a definir

Não inventar. Cada item entra como pacote próprio.

  • Papéis e vínculos entre pessoas (person_relations) — fora do v1 por decisão de negócio: cada pessoa está atrelada ao próprio produto (curso, assinatura, pós, graduação) e não se compra para terceiros, portanto pagador ≡ estudante. Se o pacote de payment/order precisar distinguir quem paga de quem estuda, esta fronteira reabre e traz person_relations + vocabulário de papéis.
  • Provisionamento de tenant — fechado: o institutional-service é o registro de tenants e os distribui por fato (institutional.tenant.created/.updated); o identity consome como qualquer config e resolve o tenant_id dele. Regra em ecosystem.services.institutional-tenant-registry.
  • Retenção do lead_registry — por quanto tempo guardar sinal anônimo que nunca virou pessoa. Depende da política de retenção do tracking (§12 de lá) e do parecer do DPO.
  • Migração da PII de v1 da ai-wallet — ecosystem.services.ai-wallet-lead-pii prevê que, na entrada do Cortex, a PII guardada pela wallet migre para cá. Por decisão atual, o CORTEX gera seus próprios dados e a adaptação da wallet é responsabilidade dela, no tempo dela. Sem trabalho de migração no escopo deste serviço.
  • Gestão e rotação do pepper e das chaves de envelope (§10) — depende do plano de infra (KMS/vault).
  • Parecer jurídico/DPO sobre bases legais, retenção e o fluxo de apagamento (§12).
  • Procedência por atributo — hoje um contato ou documento não registra de qual submissão veio. Sem isso, desfazer uma anexação errada só consegue apagar no braço: não há como saber que aquele e-mail entrou na mesma leva do documento digitado errado. Enquanto não existir, a fila de revisão repara removendo item a item.
  • Canal do pedido de resolução da fila (§16) — administrativo por ora, pelo mesmo motivo de §12.
  • Materializar pessoa ↔ card de card nascido anônimo (§14) — quando um card nasce de lead anônimo, o vínculo com a pessoa não existe aqui (só lead ↔ card no tracking). Se depois o lead for identificado, se e como o identity deve preencher o pessoa ↔ card retroativamente (backfill olhando o mapa do tracking) não está regrado — entra como decisão nova, não se supõe.

Impactos em documentos irmãos

  • ECOSYSTEM.md §3, §4 e §5 — o texto chama o lead_id de "handle universal que todos os serviços passam a usar". Com este documento, o handle de pessoa é o person_id; o lead_id é handle de origem de navegação, confinado ao par tracking/identity. Emenda necessária.
  • ECOSYSTEM.md §5 — "o identity consome e cria seu registro anônimo" permanece verdadeiro, mas esse registro não tem person_id (§2). Esclarecer.
  • ECOSYSTEM.md §4 e §7 — adicionar ponteiro para este documento e retirar identity da fronteira adiada de schemas.
  • AI_WALLET_SERVICE.md §12 — o identity_ref opaco corresponde ao person_id (§1).
  • CRM_CONNECTOR.md §5 — a recíproca (o identity consumir crm.card.created e gravar pessoa ↔ card) passa a estar escrita como regra dona aqui (§14). O item de impacto em IDENTITY do doc do CRM está realizado; ao promover, pode marcá-lo fechado.

CHANGELOG

Versão Data Resumo
0.5.0 2026-10-14 Fecha o par do §5 do crm-connector (recíproca da volta do card, junto com TRACKING 0.10.0). §14 (emenda): nomeia a origem da referência do card no CRM — chega pela volta do connector (crm.card.created, CRM §5), e o identity grava pessoa ↔ card em external_refs só quando a pessoa já é conhecida (person_id no fato); card de lead anônimo não tem pessoa a vincular aqui — o par lead ↔ card fica no tracking (§26 de lá). Por fila, idempotente, nunca escrita direta (§8). Nova fronteira: materializar pessoa ↔ card retroativamente quando um lead anônimo é identificado depois (backfill) não está regrado — decisão nova. Escrito no estilo conceito, não mecanismo. MINOR.
0.4.0 2026-09-24 Fecha o canal de resolução da fila de fusão agora que o institutional existe, e acrescenta um nível de leitura para lista. §16: a resolução deixa de ser operação administrativa e vira fato — o institutional publica institutional.merge_candidate.resolved e institutional.person_merge.requested (com actor_user_id/actor_name), o identity não revalida papel (autorização gateada no painel; consumidor de fila não autentica quem publicou) e publica de volta identity.merge_candidate.applied / .rejected_by_rule para o painel não mentir ao operador num canal assíncrono; a API de leitura da fila expõe status/motivo do rejected e paginação por cursor; "quem pode fundir" é autorização do painel, o identity só garante o tenant (§13). §9: novo nível intermediário list_display_names(person_ids[], purpose) — só nome de exibição, em lote, uma linha de pii_access_log por consulta (uma lista de 50 leads era 50 chamadas e 50 linhas, corroendo a auditoria); vocabulário de finalidade ganha merge_review. §11: os dois fatos de resultado (sem PII). Fronteira de tenant fechada: o institutional é o registro de tenants e distribui por fato (ecosystem.services.institutional-tenant-registry). Origem: pedidos da sessão do institutional-service (23–24/09). MINOR.
0.3.0 2026-09-10 Corrige um defeito de desenho e acrescenta o controle de fusão. §3: o país sai da chave de busca e passa a ser atributo da decisão — com ele no hash, dois registros do mesmo número de passaporte em países diferentes nunca se enxergavam e a colisão ficava invisível; agora eles se encontram, mesmo país funde e país diferente vira candidato a revisão. Explicita o princípio "na dúvida, separa" que ordena todas as regras de matching: fusão errada é irreversível na prática (PII já vazou entre as duas pessoas), split errado se conserta. Acrescenta §16 (identity-merge-review): fila de candidatos alimentada pelo que hoje é descartado em silêncio, asserção negativa permanente por par, e fusão manual com autoria, perfil restrito do próprio tenant, auditada e reversível. A tela é do institutional; aqui nascem só os dados e a API — ler é API, resolver é operação administrativa, porque aprovar/rejeitar é escrita e §8 não abre exceção nem para operador. §3 ganha ainda o contra-sinal: documento declarado que bate com nome radicalmente divergente é rebaixado e vai para revisão (erro de digitação é mais provável que troca de nome), e o documento em disputa (is_disputed) separa formato de titularidade — sem essa separação o contra-sinal colide com o índice único, o fato vai para a DLQ e o lead se perde. Acrescenta shared_contact (mesmo contato em duas pessoas) e fixa que submissão posterior nunca remove atributo de pessoa existente, para não virar vetor de envenenamento. Procedência por atributo fica registrada como fronteira. MINOR.
0.2.2 2026-09-10 Desdobra o sinal "documento" da §3 por tipo, motivado por aluno estrangeiro e por venda B2B. Fixa: validação por tipo obrigatória na aplicação (dígito verificador de CPF/CNPJ, faixas de SSN, padrão ICAO de passaporte), com documento inválido guardado e marcado, fora do matching; só documento único e de pessoa física funde e veta — RG fica de fora por não ser único nacionalmente (emissão estadual repete número); o escopo entra na chave de hash, porque passaporte só é único dentro do país emissor; e CNPJ é validado e guardado mas nunca funde pessoas, já que o titular é a empresa e o B2B faria todos os funcionários de um cliente colapsarem num person_id. Acrescenta unicidade no banco por índice único parcial sobre os tipos que identificam pessoa física. MINOR.
0.2.1 2026-09-10 Reconcilia §12 com a implementação: o canal do pedido de apagamento passa a ser declarado como fronteira aberta. Como §8 proíbe escrita HTTP, o gatilho hoje é operação administrativa com ator registrado no pii_access_log; quando o fluxo do DPO existir, vira consumo de fato. A execução e a propagação por identity.person.erased não mudam. PATCH.
0.2.0 2026-08-31 Corrige a modelagem do matching: a confiança pertence ao par (identificador, modo de aquisição), não ao tipo do identificador (§3). Introduz o vocabulário verified_possession / verified_transaction / declared, e com ele resolve o conflito entre os dois nascimentos reais: telefone declarado em formulário não funde sozinho (é frequentemente do responsável), telefone de posse verificada funde (a IA no WhatsApp conversa com quem escreve daquele número, e ali o telefone é o único identificador existente). Torna os pesos configuração versionada em identity.matching_weights, com weights_version gravada em cada fusão para auditoria retroativa. Adiciona acquisition_mode como obrigatório em contatos e documentos (§5). Acrescenta §15: contato de posse verificada é exclusivo por tenant e datado, com bound_at/unbound_at para tratar reciclagem de número pela operadora, e decaimento do peso com a idade da posse. MINOR.
0.1.0 2026-08-31 Documento inicial. Promove a @rule endereçável (área services, slugs ecosystem.services.identity-*) o desenho do identity-service, adiado em ECOSYSTEM §7. Fixa: person_id cunhado só na identificação por PII, 1 pessoa para N navegadores, e o person_id como handle de pessoa do ecossistema (§1); registro do lead anônimo sem pessoa, com emenda ao ECOSYSTEM §5 (§2); matching com escala de sinal, limiar 0.90 e veto por contradição, com telefone explicitamente insuficiente para fundir — o caso do responsável em captação educacional (§3); proibição de interrogar o usuário (§4); modelo de dados com contato e documento multivalorados (§5); presença registra passagem, não conteúdo (§6); fusão por apelido, determinística, convergente e reversível (§7); escrita só por fila e serviço não exposto (§8); leitura em dois níveis com auditoria obrigatória de acesso a PII (§9); PII cifrada com busca por HMAC + pepper e proibição de normalização "esperta" (§10); fatos publicados sem PII (§11); apagamento executado aqui e propagado por fato (§12); nunca cross-tenant (§13); referências externas de pessoa (§14).