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.mdDocumentos irmãos:cortex-docs/ECOSYSTEM.md(mapa e fronteiras) ·cortex-docs/services/TRACKING_SERVICE.md(berço dolead_ide 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. Otrackingcunha olead_id(navegador); oidentitycunha operson_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_ide nenhumperson_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_idpara Nlead_id. A mesma pessoa se identificando no celular, no desktop e no computador do trabalho amarra os trêslead_idao mesmoperson_id. - Uma PII distinta é uma pessoa. A mesma pessoa identificando-se de novo não cunha
person_idnovo: o matching (§3) encontra o existente e amarra mais umlead_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 partracking/identity, os serviços referenciam pessoa porperson_id— nunca porlead_id, que é identificador de origem de navegação (ecosystem.services.tracking-lead-birth). - O
person_idé opaco e satisfaz oidentity_refexigido porecosystem.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_idanteriores daquela origem. Sem o registro, a identificação só enxergaria olead_iddo 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 |
verified_possession |
0.95 | sim, salvo veto | |
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:
AB123456do 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
declaredque 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 deverified_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_disputede 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
declaredsozinho 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_possessionfunde. 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 comodeclaredinviabilizaria 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.
emailcomo coluna empeopleobrigaria 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 existeanonymous: pessoa anônima não existe (§1).enrolledé setado por consumo de fato doorder-service— a verdade da matrícula continua sendo do jacad, o identity apenas reflete.lead_idé referência externa aotracking-service: sem FK local, comCOMMENT ON COLUMNobrigatório (uniscale.constraints.foreign-keys— FK não cruza fronteira de serviço).- Colunas base em toda tabela (
uniscale.columns.base);tenant_idem 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.presencesguardaservice,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): oai-salesresolve operson_idaqui 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_iddefirst_identified_atmais 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. Operson_idabsorvido 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.unmergedao 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 eevent_idde 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 depayment,ordere 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)— recebeperson_idoulead_id, devolve operson_idsobrevivente e ostatus. 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 doinstitutional). 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 emidentity.pii_access_logpor consulta, com a finalidade e a quantidade de pessoas. Sem ele, uma lista de 50 leads seria 50 chamadas deget_persone 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 fechadobilling|enrollment|contact|support|erasure|merge_review—, e toda chamada grava linha emidentity.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
*_hashcom 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.namingeuniscale.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 comstatus = 'erased'para que as referências deperson_idespalhadas não fiquem penduradas, e publicaidentity.person.erased. - Cada detentor obedece ao fato: o
trackingpurga 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_iddistintos 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-connectordevolve o fato de criação (crm.card.created,CRM_CONNECTOR.md§5); oidentityo consome e gravapessoa ↔ cardapenas quando a pessoa já é conhecida (o fato traz operson_id). Se o card nasceu de um lead ainda anônimo, não há pessoa para vincular aqui — o vínculolead ↔ cardfica notracking(TRACKING_SERVICE.md§26), que é por onde a volta do card se resolve. Como todo write doidentity, é 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 tembound_ateunbound_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_ate 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_atpreenchido 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_candidatesguarda o par de pessoas, o sinal, a confiança e ostatus(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.
rejectedvale 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_mergescomactore motivo, é auditada empii_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). Oidentitygarante o que é dele: só dentro do mesmotenant_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 é doinstitutional-service(ECOSYSTEM §4), que já existe. Oidentityexpõe a leitura da fila —merge_candidatescom o par, sinal, confiança,status, o motivo dorejected(a asserção negativa é permanente e precisa ser visível) e paginação por cursor — e consome a decisão como fato:- Entrada (o
institutionalpublica, oidentityconsome):institutional.merge_candidate.resolved(candidate_id,decision=approved|rejected,tenant_id,actor_user_id,actor_name,reason); einstitutional.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
identitynã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 revalidarroleno consumo dá sensação de controle sem o controle. Oidentityregistraactor_user_id/actor_namena 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
identitypublica de volta): o canal é assíncrono, então o operador clica e não sabe o resultado. Oidentitypublicaidentity.merge_candidate.applied(fusão aplicada) ouidentity.merge_candidate.rejected_by_rule(recusada por regra dura — candidato já resolvido, par cross-tenant §13, pessoa já fundida) para oinstitutionalrefletir na tela. Sem esse retorno, a tela só consegue dizer "enviado" e mente ao operador.
- Entrada (o
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 depayment/orderprecisar distinguir quem paga de quem estuda, esta fronteira reabre e trazperson_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); oidentityconsome como qualquer config e resolve otenant_iddele. Regra emecosystem.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 dotracking(§12 de lá) e do parecer do DPO. - Migração da PII de v1 da
ai-wallet—ecosystem.services.ai-wallet-lead-piiprevê 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 ↔ cardde 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 ↔ cardnotracking). Se depois o lead for identificado, se e como oidentitydeve preencher opessoa ↔ cardretroativamente (backfill olhando o mapa dotracking) 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 olead_idde "handle universal que todos os serviços passam a usar". Com este documento, o handle de pessoa é operson_id; olead_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 temperson_id(§2). Esclarecer.ECOSYSTEM.md§4 e §7 — adicionar ponteiro para este documento e retiraridentityda fronteira adiada de schemas.AI_WALLET_SERVICE.md§12 — oidentity_refopaco corresponde aoperson_id(§1).CRM_CONNECTOR.md§5 — a recíproca (oidentityconsumircrm.card.createde gravarpessoa ↔ card) passa a estar escrita como regra dona aqui (§14). O item de impacto emIDENTITYdo 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). |