From 248576191ed1f6a5fef3fcb89355224457767f80 Mon Sep 17 00:00:00 2001 From: developmentconexus Date: Wed, 11 Nov 2026 19:40:54 -0300 Subject: [PATCH 1/5] =?UTF-8?q?docs(conectores):=20o=20cart=C3=A3o=20da=20?= =?UTF-8?q?pol=C3=ADtica=20de=20dados=20pessoais=20dos=20conectores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/steps/a-politica-de-dados-pessoais.md | 561 +++++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 docs/steps/a-politica-de-dados-pessoais.md diff --git a/docs/steps/a-politica-de-dados-pessoais.md b/docs/steps/a-politica-de-dados-pessoais.md new file mode 100644 index 00000000..fbce238b --- /dev/null +++ b/docs/steps/a-politica-de-dados-pessoais.md @@ -0,0 +1,561 @@ +# A política de dados pessoais dos conectores + +O cartão parte do estudo "dados pessoais de terceiros nos conectores" (seções 1, 7 e 9) e do estudo +anterior do caminho dos dados (seções 5, 8 e 9), com as respostas do fundador de 2026-11-11: "A1, mas +seria uma capabilidade para criar e selecionar POR EXEMPLO PARA DAR PARA OUTRO cargo"; "B1, okay, mas +como vamos tratar isso [...] Qual que seria a lógica [...] Vale a pena? É uma gambiarra? Referências +fazem?"; "`C1` porém mesma coisa de B"; "`D1`"; "E Só vai vir depois". Cada linha do `main` foi lida em +`7c8b156f`, com `better-auth` 1.7.8, `@mastra/core` 1.82.0, `zod` 4.6.5, +`@brazilian-utils/brazilian-utils` 2.5.0 e PostgreSQL 18. As referências: n8n `2e855356`, twenty +`b76d0217`, Airbyte `8aed3123`, PostgREST `d42ae9d`. + +Nenhuma peça do Hub, das telas ou do Builder fala de um conector pelo nome. O que é de um sistema (em +que caminho da resposta mora o comprador, que coluna do Sankhya é o CPF) mora na pasta do conector, +escrito por quem escreve o conector. + +## A página do fundador + +**No fim desta etapa** você abre **Papéis**, marca no papel **Vendas** que ele vê **contato** e +**endereço** dos clientes, e pede ao Builder um app "quem comprou da gente" do Mercado Livre: o app +mostra nome e cidade do comprador. Pergunta ao Assistente "quem mais comprou este mês?" e ele responde +com nomes, sem nunca mandar CPF ao modelo. Uma pessoa do papel **Membro** pergunta o mesmo e recebe os +pedidos sem nome. + +**A lógica, em uma frase.** O conector diz **onde** está cada dado pessoal e de **que tipo** ele é; a +empresa diz, por papel, **quem** vê cada tipo; o Hub aplica isso **num ponto só**, a leitura comum que +serve ao app, ao Assistente, ao Builder e à cópia; e cada leitura com dado pessoal fica registrada, +sem o valor. + +**A sua primeira pergunta (A1): "uma capabilidade para criar e selecionar, para dar para outro +cargo".** É isso. Os quatro tipos (**contato**, **endereço**, **documento**, **sensível**) viram +permissões do papel, no mesmo editor de **Papéis** onde você já marca "usa a leitura Pedidos". Você +marca em qualquer papel, inclusive num papel que você criou ("Vendas", "Financeiro"). Dono e +Administrador já vêm com contato, endereço e documento, e não mudam; Desenvolvedor, Membro e os +papéis novos começam sem nenhum; **sensível** não aparece para ninguém (`D1`). Como: o better-auth já +guarda a permissão de cada papel como "recurso: ações" e já responde "este papel pode?" a cada +leitura (`apps/server/src/hub/identity/roles.ts:68-82`, `may-use.ts:88-112`); os tipos entram como +mais um recurso, igual ao "publicar agente que escreve sozinho" que você aprovou na 24.5 +(`company-roles.ts:237-244`). É nativo, sem tabela nova. + +**A sua segunda pergunta (B1 e `C1`): "qual seria a lógica para vários conectores? Vale a pena? É +gambiarra? Referências fazem?"** A lógica não é marcar o CPF campo a campo na mão. São quatro peças, +iguais para todo conector: + +1. **Uma lista pequena e fixa de tipos**, no kit dos conectores: contato (nome, e-mail, telefone), + endereço, documento (CPF), sensível (saúde, salário, sindicato). Ninguém inventa um tipo por + conector. +2. **Cada leitura declara os seus caminhos pessoais**, escritos por quem escreve o conector (a equipe, + ou o Builder quando ele escrever um): "nos pedidos, `buyer` é contato"; "no envio, + `destination.shipping_address` é endereço"; "na busca de parceiros do Sankhya, `documento` é + documento". É o que o n8n faz: cada nó declara os caminhos sensíveis da saída dele + (`packages/workflow/src/interfaces.ts:3245-3251`, declarado no próprio nó, + `FormTriggerV2.node.ts:100`, provado em `FormTriggerV2.node.test.ts:651-661`); e o que a Shopify + faz com os níveis de campo do cliente (nome, endereço, telefone e e-mail num nível à parte, que só + vem a quem tem a liberação; documentado). +3. **Uma conferência automática** que não deixa um conector entrar errado: cada caminho declarado tem + de existir nas respostas do dublê e nas gravadas; e se aparecer nelas um CPF, um e-mail ou um + telefone num caminho que **não** foi declarado, o teste falha. Assim, o conector novo que esquece o + CPF não passa no CI. +4. **Um ponto só de aplicação**, a leitura comum (`readThrough`, `apps/server/src/hub/connectors/read.ts:139`): + quem não tem o tipo recebe a linha sem aquele campo. É o que o twenty faz: tira do resultado o campo + que o papel não lê, num lugar só (`common-select-fields-builder.ts:46-52`). + +Com isso, **B1** é uma linha da regra: o tipo documento nunca vai a quem é um modelo (Assistente, +Builder), mesmo que a pessoa tenha. **`C1`** é a mesma regra na cópia: a cópia só guarda a coluna +pessoal de um tipo que quem a liga pode ver, e a apaga ao desligar. + +**Vale a pena?** Sim. O corte de hoje (`omit` no Mercado Livre) já é um mecanismo por caminho; a +política troca a lista fixa por "caminho + tipo" e põe a decisão no papel. São cerca de 1.100 linhas, +e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo +(`sankhya/operations/buscas.ts:30-33`, `cadastros.ts:35`), entra na mesma regra. + +**É gambiarra? O que as referências fazem e o que é nosso, sem rodeio.** + +- Declarar no conector: o n8n faz, mas para dados que equivalem a credencial (cabeçalho de + autorização), sempre ocultos e nunca revelados; ele não tem tipos nem liberação por papel. Usar a + declaração para dado pessoal de cliente, com tipo, é extensão nossa. +- Liberar por papel e por campo: o twenty faz, em campos do próprio CRM; não em respostas de + conector, e sem tipos. +- Tipos liberados à parte: a Shopify faz, por app, não por papel. +- A conferência que acha CPF, e-mail ou telefone não declarado numa resposta gravada: **nenhuma + referência faz**. É nossa, roda só no teste e usa os validadores da biblioteca que já instalamos + (`isValidCpf`, `isValidEmail`, `isValidPhone`). +- A detecção por modelo de IA (o `PIIDetector` do Mastra, `strategy` e `redactionMethod`, + `@mastra/core/dist/processors/processors/pii-detector.d.ts:97,105`) fica de fora: nós **sabemos** + onde o dado está; o detector é caro e não determinístico. Ele só faria sentido para texto livre + (mensagens), e fica como rede de segurança opcional, depois. +- A combinação (tipos fixos + declaração no conector + liberação no papel + um ponto + registro) não + existe pronta em nenhuma referência; cada peça existe em uma. Não é gambiarra: é a forma do n8n, do + twenty e da Shopify juntas, e nenhuma peça é um corte escondido. + +**Exemplo (inventado).** A Loja Exemplo liga o Mercado Livre. Ana é dona; Beto é do papel +"Estoque", sem tipos. + +| | Hoje | No fim desta etapa | +| --- | --- | --- | +| Ana pergunta ao Assistente "quem comprou 3 vezes?" | "não tenho o comprador" | "Maria Fictícia (3 pedidos)"; o CPF não vai ao modelo | +| Beto pergunta o mesmo | o mesmo | pedidos sem nome | +| App "Quem comprou", publicado pela Ana com contato | só "pedido 123, R$ 89,90" | "Maria Fictícia, Campinas/SP" para quem você deu acesso ao app | +| Clientes do Sankhya, perguntados pelo Beto | CPF e nome | sem CPF e sem nome | +| O registro | só escalares | mais `personal: ['contato']`, nunca o valor | + +**O roteiro de uso.** Você é dono, com o Mercado Livre e o Sankhya ligados. + +1. Em **Papéis**, abra **Membro**: a seção nova **Dados pessoais de clientes** mostra contato, + endereço e documento, desmarcados. Abra **Dono**: os três marcados, sem poder mudar. +2. Crie o papel **Vendas**, marque **contato** e **endereço**, salve, e dê esse papel a um segundo + e-mail seu. +3. Com o segundo e-mail, pergunte ao Assistente "quais clientes compraram hoje no Mercado Livre?": + ele responde com nomes e cidades. Pergunte o CPF de um cliente do Sankhya: ele diz que não pode ver + documentos. +4. Com o seu e-mail, peça ao Builder um app "quem comprou da gente" do Mercado Livre e publique: a + publicação diz que o app mostra contato e endereço; o app mostra nome e cidade. +5. Volte o papel do segundo e-mail para **Membro** e repita a pergunta do passo 3: os pedidos vêm sem + nome. + +**O que você faz, e quando.** Antes: nada novo. No começo: aprova esta página e responde F, G e H. +No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. + +**Fica de fora.** + +- **E, o advogado** (contrato de operador, suboperadores, cláusulas para o modelo no exterior): vem + depois, como você disse. Não bloqueia o código. +- **Sensível (`D1`)**: o tipo existe no kit e na regra, mas nenhum papel o recebe e a tela não o + mostra; as tabelas de RH do Sankhya ficam recusadas pela porta genérica (G). +- **Agentes publicados**: a 24.6 está congelada; uma versão de agente não declara tipos e lê sem dado + pessoal até a volta dos agentes. +- **Prazo de guarda escolhido pela empresa** (a parte opcional de `C1`) e **apagar um titular a + pedido**: cartão próprio, com a decisão do prazo. +- **Máscara** ("M***a F***a") no lugar de tirar o campo: o twenty e a Shopify tiram; nós também. + +**As decisões suas nesta etapa.** O estudo não as trouxe; elas aparecem ao desenhar o app e a porta. + +- **F. Num app, de quem é o "pode ver"?** Hoje o app usa as leituras que a versão declara, conferidas + contra o papel de quem publica (sua decisão `C1` da etapa 20, `app-calls.ts:103-109`), e quem abre o + app é quem você deu acesso. + - **F1 (recomendada):** igual para os tipos. A versão declara "mostro contato e endereço"; publicar + confere que quem publica tem esses tipos; quem abre o app vê o que ele mostra. Você controla quem + vê pelo acesso ao app. Exemplo: o vendedor da Capacidade 4, que entra só no app e não tem papel + na empresa, vê os clientes dele com nome. É a forma do Retool, do Appsmith e da Shopify (a + liberação é do app); vale igual para a cópia lida pelo app, que não pode filtrar por pessoa (o + Projeto lê o banco com um login só). Veredito: nativo (estende a `C1`). + - **F2:** o tipo segue o papel de quem abre. O Beto não vê nome no app da Ana; o vendedor da + Capacidade 4, sem papel na empresa, nunca vê nome; e uma cópia com coluna pessoal não pode ser + liberada a Projeto nenhum. +- **G. A porta genérica do Sankhya** (`consultar`, SQL livre; `carregar`). Ela lê qualquer coluna de + qualquer tabela, com qualquer apelido, então não dá para declarar caminhos. + - **G1 (recomendada):** três travas: (1) o conector declara as colunas pessoais do dicionário do + Sankhya pelo nome (`CGC_CPF` documento, `NOMEPARC` contato e as de endereço e contato que o + construtor achar no dicionário); (2) para quem não tem o tipo, cada célula da porta que for um CPF + válido, um e-mail ou um telefone sai, mesmo com outro nome de coluna; (3) a consulta que nomeia + uma tabela de RH (`TFP...`) é recusada (`D1`). Um nome ou endereço com apelido inventado ainda + passa: está dito. Veredito: remendo (nenhuma referência tem SQL livre com política de campo; o + nativo seria o `GRANT` de coluna do banco, que no Sankhya não é nosso). + - **G2:** a porta não entrega nada a quem não tem os três tipos: o Assistente do Membro perde a + porta. +- **H. Onde entra no roteiro.** Pelo método (delivery L5), esta etapa é da trilha principal e espera + o Sync fechar, num pull request rascunho. + - **H1 (recomendada):** logo depois do Sync e **antes da Capacidade 4**, porque a 4 traz pessoas + que só entram no app e veem clientes, e hoje o Sankhya entrega CPF a todo papel. O Sync não precisa + esperar: a cópia já nasce certa (veja "O Sync" abaixo). + - **H2:** depois da Capacidade 4. + +**O Sync.** O cartão do Sync supõe o corte: a linha 47 ("a leitura do conector já o corta") e o +critério 10 ("o `omit` da leitura vale para a cópia"). Com esta política, a cópia lê pela mesma +leitura comum como uma versão que não declara tipo nenhum, então continua sem dado pessoal: o critério +10 passa a ser "a cópia sem tipo liberado não recebe valor pessoal" e muda de dono na U3 desta etapa +(delivery L7). A coluna pessoal na cópia (`C1`) entra na U6, depois do motor do Sync. + +## As unidades + +| Unidade | O que entrega | Pastas | Guia além do de código | Depende de | +| --- | --- | --- | --- | --- | +| U1 A declaração no kit | `PersonalKind` e `personal` na operação; `findUndeclared` (as formas conhecidas); a conformidade dos caminhos; as declarações do Mercado Livre (ao lado do `omit`, que ainda fica) e do Sankhya; as colunas pessoais da porta | `packages/connectors/src/{kernel,mercado-livre,sankhya}/`, `packages/connectors/package.json` | [testing](../guides/testing.md) | esta página; a U2b do Sync no `main` (pasta do Mercado Livre, L1) | +| U2 Os tipos nos papéis | o recurso `personal` nos `Statements`; dono e administrador com três tipos; `RoleView.personal`, `RoleCreate.personal`, `RoleChange.personal`; `personalKinds` na identidade; o registro da troca | `apps/server/src/hub/identity/`, `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts`, `packages/hub-api/src/{company-roles,roles}.ts` | [security](../guides/security.md) | U1 | +| U3 A aplicação num ponto | `shapePersonal` em `readThrough`; `Speaker` `VERSION` com `personal`; menos documento para `assistant` e `builder`; o escalar `personal` em `CONNECTOR_CALLED`; a porta do Sankhya (G1); o `omit`, o `BUYER` e o `without` apagados | `apps/server/src/hub/connectors/`, `packages/connectors/src/{mercado-livre,sankhya}/` | [security](../guides/security.md) | U1, U2; entra junto com a U4 (o mesmo pull request) | +| U4 O app declara os tipos | `personal` no manifesto por conector; a conferência de Publicar; a versão anterior ao campo; a skill `conexus-data` | `packages/app-contract/src/manifest.ts`, `apps/server/src/hub/connectors/{publish-check,app-calls}.ts`, `packages/app/skills/conexus-data/` | [security](../guides/security.md) | U3 | +| U5 A tela de Papéis | a seção **Dados pessoais de clientes** no editor, criar e abrir | `apps/web/src/hub/identity/` | [web](../guides/web.md) | U2 | +| U6 A coluna pessoal na cópia | o tipo de cada coluna, derivado do caminho; Ligar confere o papel; o `GRANT SELECT (colunas)` ao Projeto; Desligar apaga as colunas pessoais | `apps/server/src/hub/copies/`, `apps/server/src/hub/connectors/copy-port.ts`, `apps/server/src/platform/copy-sql.ts` | [data](../guides/data.md) | U3; as U3 e U4 do Sync no `main` | + +U2 e U1 não dividem pasta, mas U2 lê o tipo de U1. U5 corre ao lado de U3 e U4. U6 espera o Sync. + +**Orçamento de código alterado.** U1: 230. U2: 180. U3: 200. U4: 140. U5: 150. U6: 200. Total: +cerca de 1.100 linhas, abaixo de delivery L2. + +## Os critérios de aceite + +Cada um falha hoje: não há `personal` no kit, nem recurso `personal` nos papéis, nem conferência. + +| # | Unidade | Critério, observável de fora, com a recusa | Teste que o prova | Nível | +| --- | --- | --- | --- | --- | +| 1 | U1 | Uma operação que declara um caminho pessoal ausente da resposta do dublê falha a conformidade, com o id e o caminho | `kernel/personal.test.ts`, "caminho declarado que não existe" | must | +| 2 | U1 | Uma resposta do dublê com um CPF válido, um e-mail ou um telefone num caminho não declarado falha a conformidade, com o caminho e o tipo, nunca o valor | `kernel/personal.test.ts`, "forma conhecida não declarada" | must | +| 3 | U1 | O Mercado Livre e o Sankhya passam a conformidade com as declarações; tirar `buyer` de `pedidos` a faz falhar | `mercado-livre/mercado-livre.test.ts`, `sankhya/sankhya.test.ts` | must | +| 4 | U1 | Um `kind` fora dos quatro não compila nem passa em `defineOperation` | `kernel/kernel.test.ts`, "tipo desconhecido" | must | +| 5 | U2 | `GET /api/companies/{companyId}/roles` dá a dono e administrador `personal: ['contato','endereco','documento']`, e a desenvolvedor, membro e papel novo `[]` | `identity/roles-personal.test.ts`, "os tipos de cada papel" | must | +| 6 | U2 | `PUT .../roles/member` com `personal: ['contato']` é 200, e a leitura seguinte do membro recebe contato; com `['sensivel']` é 422 `REQUEST_MALFORMED` (`PERSONAL_KIND_UNKNOWN`); em `owner` é 409 `ROLE_FIXED` | `identity/roles-personal.test.ts`, "marcar contato no membro", "sensível recusado" | must | +| 7 | U2 | `POST .../roles` cria "Vendas" com `personal: ['contato','endereco']` e a resposta os traz | `identity/roles-personal.test.ts`, "papel novo com tipos" | must | +| 8 | U3 | Pela porta do Assistente, a mesma leitura `pedidos` do dublê: o dono recebe `buyer` com o nome plantado; o membro recebe a linha sem `buyer` | `connectors/personal-read.test.ts`, "dois papéis, duas respostas" | must | +| 9 | U3 | Pelo Assistente e pelo Builder, `notaFiscalDoPedido` nunca traz `recipient` nem o CPF plantado, nem para o dono (B1); pelo app de uma versão que declara documento, traz | `connectors/personal-read.test.ts`, "documento não vai ao modelo" | must | +| 10 | U3 | `buscarParceiros` pelo Assistente de um membro não traz `documento` nem `nome` | `connectors/personal-read.test.ts`, "Sankhya pelo membro" | must | +| 11 | U3 | `consultar` com `SELECT CGC_CPF AS X FROM TGFPAR` pelo Assistente de um membro não traz o CPF plantado; com `FROM TFPFUN` é recusado (`REFUSED`, `INPUT_INVALID`) antes da chamada | `connectors/personal-read.test.ts`, "porta com apelido", "RH recusado" | must | +| 12 | U3 | Cada `CONNECTOR_CALLED` de uma leitura que declara tipos traz `personal` (os tipos entregues) e `withheld` (os tirados), e nenhuma linha do registro traz o valor plantado | `connectors/personal-read.test.ts`, "o registro sem o valor" | must | +| 13 | U3 | Uma página de cópia (`by: 'copy'`) de `pedidos` não traz o nome plantado em célula nenhuma (o critério 10 do Sync, na forma nova) | `connectors/copy-port.test.ts`, "a cópia sem tipo" | must | +| 14 | U3 | `grep -rn "omit" packages/connectors/src/mercado-livre` não acha nada | revisão | must | +| 15 | U4 | Publicar uma versão que declara `personal: ['contato']` num conector, por quem não tem contato, é recusado com o tipo; por quem tem, publica | `connectors/publish.test.ts`, "tipo que o papel não tem" | must | +| 16 | U4 | Uma versão publicada antes do campo lê com os tipos do papel de quem a publicou, lido a cada chamada | `connectors/app-calls.test.ts`, "versão anterior ao campo" | must | +| 17 | U5 | Em Papéis, Membro mostra os três tipos desmarcáveis; Dono mostra os três marcados e travados; salvar marca o tipo e a lista seguinte o mostra; sensível não aparece | `apps/web/src/hub/identity/roles-personal.test.tsx` | must | +| 18 | U6 | Uma cópia ligada por quem tem contato guarda a coluna `buyer.nickname`; o Projeto liberado por quem não tem contato não lê essa coluna (42501); desligar a ligação apaga a coluna | `copies/personal-columns.test.ts` | must | +| 19 | U6 | Ligar uma proposta com coluna de um tipo que o papel de quem liga não tem é recusado com o tipo | `copies/accept-v2.test.ts`, "tipo que quem liga não tem" | must | + +## A matriz de estados + +| Tela | Primeira vez | Vazio | Carregando | Erro | Não encontrado | Sem permissão | Vencido ou recusado | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Papéis, seção Dados pessoais de clientes | os três desmarcados em Desenvolvedor, Membro e papel novo | não existe: os três tipos sempre aparecem | a do editor de hoje | o `FormAlert` de hoje, com `PERSONAL_KIND_UNKNOWN` | `ROLE_NOT_FOUND` de hoje | quem não gere papéis não chega (`manageRoles`) | Dono e Administrador: marcados e travados | + +| Campo de contrato | Quem escreve | Quem lê | +| --- | --- | --- | +| `Operation.personal` | o autor do conector | a conformidade (U1), `readThrough` (U3), a cópia (U6) | +| `RoleView.personal` | `readRoles` (U2) | a tela de Papéis (U5) | +| `RoleCreate.personal`, `RoleChange.personal` | a tela (U5) | `permissionOf` (U2) | +| `Speaker` `VERSION` `.personal` | `app-calls.ts` pela versão (U4); `copy-port.ts` pela cópia (U6, `[]` antes) | `personalKindsOf` (U3) | +| manifesto `connectors[].personal` | o Builder | Publicar (U4), `app-calls.ts` (U4) | +| `CONNECTOR_CALLED.personal`, `.withheld` | `readThrough` (U3) | quem lê o registro (o "registro de acesso" que o Mercado Livre pede) | + +A tela muda uma seção de um editor que existe; o desenho segue `role-unattended.tsx`, a seção +"Agentes" do mesmo editor, com uma caixa por tipo. + +## A tabela de falhas + +| Gravação ou comando, da etapa ou da biblioteca | Duas vezes ao mesmo tempo | Recusada no meio | Uma dependência fora | O que o processo imprime | Critérios | +| --- | --- | --- | --- | --- | --- | +| `updateOrgRole` e `createOrgRole` com `personal` (better-auth) | a trava da empresa de hoje (`lockCompany`) | a biblioteca recusa: nada gravado, a transação desfaz | `DATABASE_UNAVAILABLE` de hoje | `ROLE_UPDATED` (`company-routes.ts:257-265`), com os tipos em `added`/`removed` | 6, 7 | +| `GRANT SELECT (colunas)` ao Projeto (U6) | repete sem efeito | a liberação desfeita, como `undoRelease` (`copies/grants.ts:33-48`) | `COPY_DATABASE_UNAVAILABLE` | `COPY_GRANT_FAILED` de hoje | 18 | +| `ALTER TABLE ... DROP COLUMN` das colunas pessoais ao Desligar (U6) | a trava da cópia | a coluna fica e é apagada no próximo Desligar ou Ligar, como `copy_table_left` do Sync | anotada para depois | `COPY_PERSONAL_DROP_FAILED` | 18 | + +As leituras não gravam nada novo; o escalar do registro não é gravação. + +## A tabela de referência + +| Peça | Unidade | Hoje no main | Referência | Como a referência faz | Veredito | Prova, se não for nativo | Palavra do fundador | Arquivo novo | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| A lista fixa de tipos | U1 | nada | Shopify (níveis de campo do cliente, documentado); LGPD art. 5º II e art. 11 (sensível à parte) | poucos níveis fixos, liberados à parte | embrulhado: `PERSONAL_KINDS` no kit | conformidade (crit. 4) | A1, `D1` | `packages/connectors/src/kernel/personal.ts` | +| O conector declara caminho e tipo | U1 | `mercado-livre/operations/shared.ts:47` (`omit`, sem tipo); o Sankhya sem nada | n8n `interfaces.ts:3245-3251`, `FormTriggerV2.node.ts:100`, `FormTriggerV2.node.test.ts:651-661` | o nó declara caminhos com ponto; a plataforma oculta | embrulhado: `personal` na `Operation` no lugar do `omit` | crit. 1, 3 | B1 ("a lógica") | — | +| A conferência das formas não declaradas | U1 | nada | nenhuma (procurado em n8n, Airbyte, nango, twenty) | — | remendo: só no teste, com `isValidCpf`, `isValidEmail`, `isValidPhone` da biblioteca instalada | crit. 2, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | B1 ("vale a pena? referências fazem?"): esta página | — | +| Os tipos como permissão do papel | U2 | `identity/roles.ts:68-82` (recursos e ações), `role-statements.ts`, `company-roles.ts:237-244` | better-auth 1.7.8 `createAccessControl`, `hasPermission`, `createOrgRole`/`updateOrgRole` (o editor de hoje); twenty `field-permission.entity.ts:65-66` | uma ação no papel, lida a cada chamada | nativo: recurso `personal` com as ações `contato`, `endereco`, `documento`, `sensivel` | — | A1 ("para dar para outro cargo") | — | +| Aplicação num ponto | U3 | o corte dentro de `mercado-livre/answers.ts:101-126`, antes do papel | twenty `common-select-fields-builder.ts:46-52`; n8n `node-defined-field-redaction.strategy.ts:44-69` | um filtro único sobre o resultado | embrulhado: `shapePersonal`, função pura, chamada em `readThrough` depois de `answerOf` | crit. 8 | — | `apps/server/src/hub/connectors/personal.ts` | +| Documento nunca ao modelo | U3 | `cut.ts` (só tamanho) | Mastra `PIIDetector` (`pii-detector.d.ts:97,105`), por modelo; LGPD art. 33 | detecção por LLM | embrulhado: o leitor `assistant`/`builder` perde `documento` | crit. 9 | B1 | — | +| A porta genérica do Sankhya | U3 | `sankhya/door.ts:69-145` (qualquer coluna); `sankhya/skill/SKILL.md:21` (RH só por texto) | n8n `unknownNodes` oculta tudo do que não conhece (`node-defined-field-redaction.strategy.ts:52-58`); Postgres `GRANT SELECT (colunas)` (PostgREST `test/spec/fixtures/privileges.sql:55`) | fechar o que não se conhece; ou o banco libera por coluna | remendo: colunas pelo nome, formas conhecidas nas células, `TFP` recusado | crit. 11, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | G | — | +| O registro sem o valor | U3 | `read.ts:189-204` (`CONNECTOR_CALLED`, escalares) | n8n `execution-redaction.service.ts:133,145,168`; Mercado Livre "registro de acesso" (documentado) | um evento por revelação | embrulhado: dois escalares na linha que já existe (security S41) | crit. 12 | — | — | +| O app declara os tipos | U4 | `app-contract/src/manifest.ts:46-57`; `publish-check.ts:35-61` (`C1`) | Shopify (a liberação é do app); a `C1` da etapa 20 | o app pede o nível; a plataforma confere | nativo: estende o manifesto e a conferência de Publicar | — | F | — | +| A tela de Papéis | U5 | `apps/web/src/hub/identity/role-editor.tsx:153`, `role-unattended.tsx` | a seção Agentes do mesmo editor | uma caixa por ação | nativo | — | A1 | `apps/web/src/hub/identity/role-personal.tsx` | +| A cópia guarda só o liberado | U6 | o critério 10 do Sync; `copies/grants.ts` (`GRANT` da tabela) | Airbyte `docs/platform/using-airbyte/mappings.md:7` (por conexão); PostgREST `privileges.sql:55` | por conexão; coluna liberada por `GRANT` | nativo: `GRANT SELECT (colunas)` | — | `C1` | — | +| Apagar ao desligar | U6 | o Desligar do Sync U3 (`copy_table_left`) | Mercado Livre (termos e segurança, documentado) | apagar as cópias e a PII | embrulhado: no mesmo Desligar | crit. 18 | `C1` | — | + +**Ficam fora da tabela, com a razão.** RH e dados sensíveis (`SKILL.md:21`, LGPD art. 11): `D1`; a porta recusa `TFP` (G, U3). A detecção por modelo (o `PIIDetector` do Mastra): a declaração é determinística e mais barata; o detector fica como rede opcional para texto livre, num cartão futuro. + +**A medida do transplante.** Nada vem do repositório antigo. + +**A dívida.** O corpo de uma recusa do provedor vai inteiro ao modelo (`read.ts:37-40`); se um +provedor devolver dado pessoal numa recusa, ele passa. Fica para a seção 11 da arquitetura. + +## U1 A declaração no kit + +**Base e modelo.** `main` do dia, com a U2b do Sync. Sonnet: contrato pequeno dentro do kit. + +### As linhas da tabela + +A lista fixa, a declaração, a conferência das formas, e a parte de declaração da porta (G1, item 1). + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `packages/connectors/src/kernel/` | cria `personal.ts`, muda `operation.ts`, `conformance.ts`, `index.ts` | todo conector; o servidor (U3) | +| `packages/connectors/src/mercado-livre/operations/` | muda as leituras que têm `omit` | o kit | +| `packages/connectors/src/sankhya/` | muda `operations/{buscas,cadastros}.ts`, `door.ts` | o kit | +| `packages/connectors/package.json` | acrescenta `@brazilian-utils/brazilian-utils` (já no lock, pelo `hub-api`) | — | + +### Os contratos que a unidade cria + +```ts +// packages/connectors/src/kernel/personal.ts (dono dos tipos, C26) +export const PERSONAL_KINDS = ['contato', 'endereco', 'documento', 'sensivel'] as const +export type PersonalKind = (typeof PERSONAL_KINDS)[number] +/** Um caminho com ponto relativo a cada linha de `linhas`, ou ao valor quando ele não tem `linhas`. */ +export type PersonalField = Readonly<{ path: string; kind: PersonalKind }> +/** As formas conhecidas achadas num valor fora dos caminhos dados: caminho e tipo, nunca o valor. */ +export function findUndeclared(value: unknown, declared: readonly PersonalField[]): readonly Readonly<{ path: string; kind: 'documento' | 'contato' }>[] +// packages/connectors/src/kernel/operation.ts, em Operation: personal?: readonly PersonalField[] +// packages/connectors/src/kernel/definition.ts, em ReadDoor: personalColumns?: readonly Readonly<{ column: string; kind: PersonalKind }>[] +``` + +`defineOperation` confere cada `kind` com `z.enum(PERSONAL_KINDS)`. O id de tipo é texto com forma +nos contratos (`hub-api`, `app-contract`) e é conferido contra o kit no servidor, como `ConnectorId` +(`hub-api/src/company-roles.ts:7`, C23). + +### As skills e a leitura + +`docs/reference/stack.md` para `packages/connectors`: `zod`; `@brazilian-utils/brazilian-utils` +(README). + +### A ordem do trabalho + +1. `personal.ts`: os tipos e `findUndeclared` (`isValidCpf`; `isValidEmail`; `isValidPhone` só em + texto de 10 a 13 dígitos, para não pegar ids). Regras: C21, C26. +2. `operation.ts`: `personal` opcional; `defineOperation` confere. +3. `conformance.ts`: para cada probe com resposta, cada caminho declarado existe em ao menos uma linha + (`valueAt`, `operation.ts:103`), e `findUndeclared` volta vazio. +4. Mercado Livre: `pedidos`, `pedido` `{buyer: contato}`; `envio` `destination.receiver_name`, + `destination.receiver_phone` contato e `destination.shipping_address` endereço; `perguntas` + `from` contato; `mensagensDoPacote` `from.email`, `from.name`, `to.email`, `to.name` contato; + `notaFiscalDoPedido` `recipient` documento; `conta` os campos do `omit` de hoje com o tipo. O `omit` + fica até a U3. As leituras sem `omit` (`pacote`, `mensagensNaoLidas`, `reclamacoes`) passam pela + conformidade: se o dublê não planta nada, nada se declara. +5. Sankhya: `buscarParceiros` `documento` documento e `nome`, `razaoSocial` contato; `clientes` pelos + nomes que o dublê traz; `sankhyaDoor` com `personalColumns`. + +### Como se prova + +Critérios 1 a 4: `node --test` nos arquivos da unidade; o crit. 3 tira `buyer` de `pedidos` num teste +e espera a falha. Falha hoje: `personal` não existe. + +### Parar se + +O dublê de uma leitura que declara um caminho não o planta: plantar no dublê (T29), nunca tirar a +declaração. Uma forma pega um id que não é pessoal: estreitar a forma e dizer no relatório. + +## U2 Os tipos nos papéis + +**Base e modelo.** U1. Sonnet: segue o padrão de `UNATTENDED`. + +### As linhas da tabela + +Os tipos como permissão do papel. + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `apps/server/src/hub/identity/` | muda `roles.ts`, `role-statements.ts`, `company-roles.ts`, `may-use.ts`, `public.ts` | `connectors/read.ts` (U3), `publish-check.ts` (U4) | +| `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts` | o kit entrega os tipos à identidade (S36) | a identidade | +| `packages/hub-api/src/company-roles.ts` | `personal` em `RoleView`, `RoleCreate`, `RoleChange` | a tela (U5) | + +### Os contratos que a unidade cria + +```ts +// roles.ts: RoleKit ganha `personalKinds: readonly string[]` (do kit, pelo main.ts) +// createAccessControl({... , personal: ['contato','endereco','documento','sensivel'] }) +// owner e admin: personal: ['contato','endereco','documento']; developer, member: nada +// hub-api company-roles.ts +RoleView: { ...; personal: z.array(PersonalKindText) } // dono e admin, sempre os três +RoleCreate, RoleChange: { ...; personal: z.array(PersonalKindText).max(3).optional() } +// may-use.ts +personalKinds(companyId: CompanyId, speaker: PersonSpeaker): Promise +``` + +`PersonalKindText` é `z.string().regex(/^[a-z]{1,20}$/)`; `permissionOf` confere contra o kit e recusa +`sensivel` e o desconhecido com `malformed('PERSONAL_KIND_UNKNOWN')`. `personalKinds` faz uma leitura +de `hasPermission` por tipo, com `useMemoryCache` como `mayUseEach` (`may-use.ts:88-112`). +`actionsOf` passa a somar os tipos dados e tirados. Nenhuma migração: os papéis guardados não têm o +recurso e respondem "não" (A1); os de dono e administrador vêm do código. + +### As skills e a leitura + +`better-auth-best-practices`, `organization-best-practices` (`stack.md`, linha `better-auth`). + +### A ordem do trabalho + +1. `roles.ts`: o recurso e os papéis fixos. 2. `role-statements.ts` e `company-roles.ts` +(`permissionOf`, `readRoles`, `actionsOf`). 3. `may-use.ts`: `personalKinds`. 4. `hub-api`. Regras: +C23, C26, S36, S41. + +### Como se prova + +Critérios 5 a 7, pela rota pública (T5). Falha hoje: `RoleView` não tem `personal`. + +### Parar se + +A biblioteca recusa criar um papel com um recurso que o criador não tem e o dono não tiver +`sensivel`: é o comportamento esperado (`D1`); se recusar `contato`, parar e relatar. + +## U3 A aplicação num ponto + +**Base e modelo.** U1 e U2. Opus: desenho entre módulos (kit, identidade, leitores) e a porta. + +### As linhas da tabela + +Aplicação num ponto; documento nunca ao modelo; a porta (G1, itens 2 e 3); o registro. + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `apps/server/src/hub/connectors/` | cria `personal.ts`; muda `read.ts`, `rows.ts` (`Deps.identity`) | `app-calls.ts`, `builder-port.ts`, `assistant-port.ts`, `copy-port.ts`, `notes.ts` | +| `apps/server/src/hub/identity/may-use.ts` | `Speaker` `VERSION` ganha `personal: readonly PersonalKind[]` | `builder/agents/{tools,agent,frozen}.ts` (passam `[]`), `copy-port.ts` (`[]`) | +| `packages/connectors/src/mercado-livre/` | apaga `omit`, `BUYER` e `without` (`answers.ts:78-126`, `shared.ts:47,56-61`) | o kit | +| `packages/connectors/src/sankhya/{door,read-only-sql}.ts` | a recusa de `TFP` | o kit | + +### Os contratos que a unidade cria + +```ts +// apps/server/src/hub/connectors/personal.ts +export function shapePersonal(value: unknown, personal: readonly PersonalField[], allowed: ReadonlySet): Readonly<{ value: unknown; given: PersonalKind[]; withheld: PersonalKind[] }> +export function shapeDoor(value: unknown, columns: readonly Readonly<{ column: string; kind: PersonalKind }>[], allowed: ReadonlySet): ReturnType +/** Os tipos de quem a resposta serve: o papel da pessoa, ou o que a versão declara; menos documento para um modelo. */ +async function personalKindsOf(deps: Deps, a: ReadRequest): Promise> +``` + +`shapePersonal` tira o caminho (o twenty tira, não mascara). Em `readThrough`, depois de `answerOf` +(`read.ts:181`) e só para `OK`. `by: 'assistant' | 'builder'` tira `documento`; `sensivel` nunca está +num papel. A porta: `shapeDoor` tira as colunas declaradas e as células que `findUndeclared` acha. + +### As skills e a leitura + +`stack.md` para `apps/server/src/hub`: `better-auth-best-practices`; o kit pela U1. + +### A ordem do trabalho + +1. `personal.ts` e os seus testes de unidade. 2. `Speaker` `VERSION` com `personal`; todo chamador +passa `[]` (um caminho que esquece não compila). 3. `readThrough`: `personalKindsOf`, +`shapePersonal` ou `shapeDoor`, os escalares em `CONNECTOR_CALLED`. 4. Apagar `omit` e o resto (C75, +apagar antes de somar no mesmo commit). 5. `read-only-sql.ts`: recusar `\bTFP\w*` no texto e +`carregar` de entidade de RH. Regras: S40, S41, C21, T5. + +### Como se prova + +Critérios 8 a 14, pelas portas públicas do Assistente, do Builder e da cópia, com o dublê que planta o +comprador (`mercado-livre/fake-data.ts:21-42`). Falha hoje: o membro e o dono recebem a mesma linha. + +### Parar se + +Uma porta chama `readThrough` sem uma pessoa nem uma versão: parar. O teste do critério 10 do Sync +quebra por outro motivo que não o `omit`: parar e relatar. + +## U4 O app declara os tipos + +**Base e modelo.** U3, no mesmo pull request (sem U4, os apps do Sankhya publicados perderiam nomes). +Sonnet. + +### As linhas da tabela + +O app declara os tipos. + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `packages/app-contract/src/manifest.ts` | `personal` em cada `connectors[]` | o Builder, `hub-api` | +| `apps/server/src/hub/connectors/{publish-check,app-calls}.ts` | a conferência e o `Speaker` | Publicar, o clique do app | +| `packages/app/skills/conexus-data/` | a frase de quando declarar | o Builder | + +### Os contratos que a unidade cria + +```ts +// manifest.ts, em cada connectors[]: personal: z.array(z.string().regex(/^[a-z]{1,20}$/)).max(3).optional() +// UngrantedBy ganha reason 'PERSONAL_NOT_IN_ROLE' com `kind` +``` + +Uma versão sem o campo lê com os tipos do papel de quem a publicou, lidos a cada chamada (como +`builder/agents/tools.ts:221` trata o campo novo de uma versão antiga). + +### As skills e a leitura + +`stack.md` para `apps/server/src/hub` e `packages/app`. + +### A ordem do trabalho + +1. O manifesto. 2. `publish-check.ts`: depois das operações, os tipos contra `personalKinds` de quem +publica. 3. `app-calls.ts`: o `Speaker` com os tipos da versão. 4. A skill. Regras: C23, S10. + +### Como se prova + +Critérios 15 e 16, pela rota de Publicar e pelo clique do app. + +### Parar se + +A versão publicada não guarda quem a publicou: parar e relatar (a regra da versão antiga depende disso). + +## U5 A tela de Papéis + +**Base e modelo.** U2. Sonnet. + +### As linhas da tabela + +A tela de Papéis. + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `apps/web/src/hub/identity/` | cria `role-personal.tsx`; muda `role-editor.tsx`, `role-text.ts` | Papéis | + +### Os contratos que a unidade cria + +Nenhum: lê `RoleView.personal` e manda `personal` em `RoleCreate` e `RoleChange`. Textos: título +"Dados pessoais de clientes"; "Contato (nome, e-mail, telefone)", "Endereço", "Documento (CPF)"; a +dica "Quem tem o papel vê esse tipo de dado nas leituras dos conectores. Documento nunca vai ao +Assistente nem ao Builder." + +### As skills e a leitura + +`stack.md` para `apps/web/src`: `react-hook-form`, `@base-ui/react`, `shadcn`; [Impeccable](../reference/impeccable.md). + +### A ordem do trabalho + +1. `role-personal.tsx` no modelo de `role-unattended.tsx`, um `Checkbox` por tipo. 2. No editor de +criar e de abrir, para todo papel; travado em Dono e Administrador. Regras: W23. + +### Como se prova + +Critério 17; capturas a 1366 e 390 px, nos dois temas (conexus-qa). + +### Parar se + +Um texto da tela muda o sentido da página: volta ao fundador. + +## U6 A coluna pessoal na cópia + +**Base e modelo.** U3 e as U3 e U4 do Sync no `main`. Opus: banco da empresa, `GRANT` de coluna e +Desligar. + +### As linhas da tabela + +A cópia guarda só o liberado; apagar ao desligar. + +### O raio da mudança + +| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | +| --- | --- | --- | +| `apps/server/src/hub/copies/` | `accept.ts` (Ligar), `grants.ts` (liberar), o Desligar | a página Cópias, o Builder | +| `apps/server/src/hub/connectors/copy-port.ts` | o `Speaker` da cópia com os tipos dela | a rodada | +| `apps/server/src/platform/copy-sql.ts` | `GRANT SELECT (colunas)`, `DROP COLUMN` | `grants.ts` | + +### Os contratos que a unidade cria + +```ts +/** O tipo de cada coluna da definição versão 2: o do caminho pessoal que contém o `path` dela, ou nenhum. */ +export function personalColumnsOf(definition: CopyDefinitionV2, operation: Operation): ReadonlyMap +``` + +Ligar confere os tipos das colunas contra `personalKinds` de quem liga (recusa com o tipo). A rodada +fala como `VERSION` com esses tipos. Liberar ao Projeto dá `GRANT SELECT` nas colunas sem tipo e nas +de tipo que quem libera tem. O Desligar apaga as colunas pessoais (o resto segue a regra do Sync). + +### As skills e a leitura + +[data](../guides/data.md); `@neondatabase/serverless` pelo `stack.md`. + +### A ordem do trabalho + +1. `personalColumnsOf`. 2. Ligar. 3. `copy-port.ts`. 4. `grants.ts` e `copy-sql.ts`. 5. Desligar. +Regras: D11, D22. + +### Como se prova + +Critérios 18 e 19, com o PostgreSQL de teste (o login do Projeto lê e recebe 42501 na coluna negada). + +### Parar se + +O motor do Sync no `main` não guarda o `path` de cada coluna: parar; a U6 volta ao cartão. From 30e2b034ca27eebb0f9559e3052b942d1eb06fcd Mon Sep 17 00:00:00 2001 From: developmentconexus Date: Wed, 11 Nov 2026 19:48:06 -0300 Subject: [PATCH 2/5] =?UTF-8?q?docs(conectores):=20os=20achados=20do=20lei?= =?UTF-8?q?tor=20no=20cart=C3=A3o=20da=20pol=C3=ADtica=20de=20dados=20pess?= =?UTF-8?q?oais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/steps/a-politica-de-dados-pessoais.md | 336 ++++++++++----------- 1 file changed, 167 insertions(+), 169 deletions(-) diff --git a/docs/steps/a-politica-de-dados-pessoais.md b/docs/steps/a-politica-de-dados-pessoais.md index fbce238b..77517826 100644 --- a/docs/steps/a-politica-de-dados-pessoais.md +++ b/docs/steps/a-politica-de-dados-pessoais.md @@ -65,9 +65,9 @@ Builder), mesmo que a pessoa tenha. **`C1`** é a mesma regra na cópia: a cópi pessoal de um tipo que quem a liga pode ver, e a apaga ao desligar. **Vale a pena?** Sim. O corte de hoje (`omit` no Mercado Livre) já é um mecanismo por caminho; a -política troca a lista fixa por "caminho + tipo" e põe a decisão no papel. São cerca de 1.100 linhas, +política troca a lista fixa por "caminho + tipo" e põe a decisão no papel. São cerca de 1.150 linhas, e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo -(`sankhya/operations/buscas.ts:30-33`, `cadastros.ts:35`), entra na mesma regra. +(`sankhya/operations/buscas.ts:30-34`, `cadastros.ts:35`), entra na mesma regra. **É gambiarra? O que as referências fazem e o que é nosso, sem rodeio.** @@ -106,10 +106,11 @@ e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo 2. Crie o papel **Vendas**, marque **contato** e **endereço**, salve, e dê esse papel a um segundo e-mail seu. 3. Com o segundo e-mail, pergunte ao Assistente "quais clientes compraram hoje no Mercado Livre?": - ele responde com nomes e cidades. Pergunte o CPF de um cliente do Sankhya: ele diz que não pode ver - documentos. + ele responde com nomes e cidades. Pergunte o CPF de um cliente do Sankhya: a resposta vem sem o + CPF, porque documento nunca chega ao Assistente. 4. Com o seu e-mail, peça ao Builder um app "quem comprou da gente" do Mercado Livre e publique: a - publicação diz que o app mostra contato e endereço; o app mostra nome e cidade. + publicação diz que o app mostra contato e endereço; o app mostra o nome do comprador (dos pedidos) e + a cidade da entrega (dos envios). 5. Volte o papel do segundo e-mail para **Membro** e repita a pergunta do passo 3: os pedidos vêm sem nome. @@ -122,8 +123,8 @@ No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. depois, como você disse. Não bloqueia o código. - **Sensível (`D1`)**: o tipo existe no kit e na regra, mas nenhum papel o recebe e a tela não o mostra; as tabelas de RH do Sankhya ficam recusadas pela porta genérica (G). -- **Agentes publicados**: a 24.6 está congelada; uma versão de agente não declara tipos e lê sem dado - pessoal até a volta dos agentes. +- **Agentes publicados**: a 24.6 está congelada; hoje um agente publicado só escreve pelos conectores, + não lê. Quando os agentes voltarem, a versão deles declara tipos como a do app. - **Prazo de guarda escolhido pela empresa** (a parte opcional de `C1`) e **apagar um titular a pedido**: cartão próprio, com a decisão do prazo. - **Máscara** ("M***a F***a") no lugar de tirar o campo: o twenty e a Shopify tiram; nós também. @@ -142,14 +143,20 @@ No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. - **F2:** o tipo segue o papel de quem abre. O Beto não vê nome no app da Ana; o vendedor da Capacidade 4, sem papel na empresa, nunca vê nome; e uma cópia com coluna pessoal não pode ser liberada a Projeto nenhum. +- **Os apps já publicados**, de antes desta etapa, não declaram tipos. Eles passam a ler com os tipos + do papel de quem os enviou, lidos a cada clique: um app que você enviou continua mostrando os clientes + do Sankhya, e passa a mostrar também o comprador do Mercado Livre. Se quem enviou sair da empresa, o + app deixa de mostrar dado pessoal até ser publicado de novo. Não é uma decisão nova: segue F1. Se + escolher F2, esta linha some. - **G. A porta genérica do Sankhya** (`consultar`, SQL livre; `carregar`). Ela lê qualquer coluna de qualquer tabela, com qualquer apelido, então não dá para declarar caminhos. - **G1 (recomendada):** três travas: (1) o conector declara as colunas pessoais do dicionário do Sankhya pelo nome (`CGC_CPF` documento, `NOMEPARC` contato e as de endereço e contato que o construtor achar no dicionário); (2) para quem não tem o tipo, cada célula da porta que for um CPF válido, um e-mail ou um telefone sai, mesmo com outro nome de coluna; (3) a consulta que nomeia - uma tabela de RH (`TFP...`) é recusada (`D1`). Um nome ou endereço com apelido inventado ainda - passa: está dito. Veredito: remendo (nenhuma referência tem SQL livre com política de campo; o + uma tabela de RH (`TFP...`) é recusada (`D1`). O que ainda passa, dito: um nome ou endereço com + apelido inventado; uma tabela de RH lida por uma visão ou por `carregar` (a entidade do + dicionário não tem o nome `TFP`); os campos `AD_` que cada empresa cria, que o conector não conhece. Veredito: remendo (nenhuma referência tem SQL livre com política de campo; o nativo seria o `GRANT` de coluna do banco, que no Sankhya não é nosso). - **G2:** a porta não entrega nada a quem não tem os três tipos: o Assistente do Membro perde a porta. @@ -163,24 +170,24 @@ No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. **O Sync.** O cartão do Sync supõe o corte: a linha 47 ("a leitura do conector já o corta") e o critério 10 ("o `omit` da leitura vale para a cópia"). Com esta política, a cópia lê pela mesma leitura comum como uma versão que não declara tipo nenhum, então continua sem dado pessoal: o critério -10 passa a ser "a cópia sem tipo liberado não recebe valor pessoal" e muda de dono na U3 desta etapa -(delivery L7). A coluna pessoal na cópia (`C1`) entra na U6, depois do motor do Sync. +10 passa a ser "a cópia sem tipo liberado não recebe valor pessoal" e muda de dono na U2 desta etapa +(delivery L7). A coluna pessoal na cópia (`C1`) entra na U5, depois do motor do Sync. ## As unidades | Unidade | O que entrega | Pastas | Guia além do de código | Depende de | | --- | --- | --- | --- | --- | -| U1 A declaração no kit | `PersonalKind` e `personal` na operação; `findUndeclared` (as formas conhecidas); a conformidade dos caminhos; as declarações do Mercado Livre (ao lado do `omit`, que ainda fica) e do Sankhya; as colunas pessoais da porta | `packages/connectors/src/{kernel,mercado-livre,sankhya}/`, `packages/connectors/package.json` | [testing](../guides/testing.md) | esta página; a U2b do Sync no `main` (pasta do Mercado Livre, L1) | -| U2 Os tipos nos papéis | o recurso `personal` nos `Statements`; dono e administrador com três tipos; `RoleView.personal`, `RoleCreate.personal`, `RoleChange.personal`; `personalKinds` na identidade; o registro da troca | `apps/server/src/hub/identity/`, `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts`, `packages/hub-api/src/{company-roles,roles}.ts` | [security](../guides/security.md) | U1 | -| U3 A aplicação num ponto | `shapePersonal` em `readThrough`; `Speaker` `VERSION` com `personal`; menos documento para `assistant` e `builder`; o escalar `personal` em `CONNECTOR_CALLED`; a porta do Sankhya (G1); o `omit`, o `BUYER` e o `without` apagados | `apps/server/src/hub/connectors/`, `packages/connectors/src/{mercado-livre,sankhya}/` | [security](../guides/security.md) | U1, U2; entra junto com a U4 (o mesmo pull request) | -| U4 O app declara os tipos | `personal` no manifesto por conector; a conferência de Publicar; a versão anterior ao campo; a skill `conexus-data` | `packages/app-contract/src/manifest.ts`, `apps/server/src/hub/connectors/{publish-check,app-calls}.ts`, `packages/app/skills/conexus-data/` | [security](../guides/security.md) | U3 | -| U5 A tela de Papéis | a seção **Dados pessoais de clientes** no editor, criar e abrir | `apps/web/src/hub/identity/` | [web](../guides/web.md) | U2 | -| U6 A coluna pessoal na cópia | o tipo de cada coluna, derivado do caminho; Ligar confere o papel; o `GRANT SELECT (colunas)` ao Projeto; Desligar apaga as colunas pessoais | `apps/server/src/hub/copies/`, `apps/server/src/hub/connectors/copy-port.ts`, `apps/server/src/platform/copy-sql.ts` | [data](../guides/data.md) | U3; as U3 e U4 do Sync no `main` | +| U1 Os tipos nos papéis | a lista `PERSONAL_KINDS` no kit; o recurso `personal` nos `Statements`; dono e administrador com três tipos; `RoleView.personal`, `RoleCreate.personal`, `RoleChange.personal`; `personalKinds` na identidade; o registro da troca | `packages/connectors/src/kernel/`, `apps/server/src/hub/identity/`, `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts`, `packages/hub-api/src/{company-roles,failures}.ts` | [security](../guides/security.md) | esta página | +| U2 A declaração e a aplicação num ponto | `personal` na operação e `personalColumns` na porta; `findUndeclared`; a conformidade; as declarações do Mercado Livre e do Sankhya, com o `omit`, o `BUYER` e o `without` apagados; `shapePersonal` em `readThrough`; `Speaker` `VERSION` com `personal`; menos documento para `assistant` e `builder`; os escalares em `CONNECTOR_CALLED`; a porta do Sankhya (G1) | `packages/connectors/src/{kernel,mercado-livre,sankhya}/`, `packages/connectors/package.json`, `apps/server/src/hub/connectors/`, `apps/server/src/hub/identity/may-use.ts`, `apps/server/src/builder/agents/` | [security](../guides/security.md) | U1; a U2b do Sync no `main` (pasta do Mercado Livre, L1); entra junto com a U3 | +| U3 O app declara os tipos | `personal` no manifesto por conector; a conferência de Publicar; `sentBy` na chamada do app e a versão anterior ao campo; a skill `conexus-data` | `packages/app-contract/src/manifest.ts`, `packages/hub-api/src/{apps,failures}.ts`, `apps/server/src/hub/apps/`, `apps/server/src/hub/connectors/{publish-check,app-calls}.ts`, `packages/app/skills/conexus-data/` | [security](../guides/security.md) | U2 | +| U4 A tela de Papéis | a seção **Dados pessoais de clientes** no editor, criar e abrir | `apps/web/src/hub/identity/` | [web](../guides/web.md) | U1 | +| U5 A coluna pessoal na cópia | o tipo de cada coluna, derivado do caminho; Ligar confere o papel; o `GRANT SELECT (colunas)` ao Projeto; Desligar apaga as colunas pessoais | `apps/server/src/hub/copies/`, `apps/server/src/hub/connectors/copy-port.ts`, `apps/server/src/platform/copy-sql.ts` | [data](../guides/data.md) | U2; as U3 e U4 do Sync no `main` | -U2 e U1 não dividem pasta, mas U2 lê o tipo de U1. U5 corre ao lado de U3 e U4. U6 espera o Sync. +U4 corre ao lado de U2 e U3, porque não divide pasta com elas. U5 espera o Sync. Todas entram no +mesmo pull request da etapa, então o `main` nunca vê a U2 sem a U3. -**Orçamento de código alterado.** U1: 230. U2: 180. U3: 200. U4: 140. U5: 150. U6: 200. Total: -cerca de 1.100 linhas, abaixo de delivery L2. +**Orçamento de código alterado.** U1: 200. U2: 430. U3: 160. U4: 150. U5: 200. Total: +cerca de 1.140 linhas, abaixo de delivery L2. ## Os critérios de aceite @@ -188,25 +195,25 @@ Cada um falha hoje: não há `personal` no kit, nem recurso `personal` nos papé | # | Unidade | Critério, observável de fora, com a recusa | Teste que o prova | Nível | | --- | --- | --- | --- | --- | -| 1 | U1 | Uma operação que declara um caminho pessoal ausente da resposta do dublê falha a conformidade, com o id e o caminho | `kernel/personal.test.ts`, "caminho declarado que não existe" | must | -| 2 | U1 | Uma resposta do dublê com um CPF válido, um e-mail ou um telefone num caminho não declarado falha a conformidade, com o caminho e o tipo, nunca o valor | `kernel/personal.test.ts`, "forma conhecida não declarada" | must | -| 3 | U1 | O Mercado Livre e o Sankhya passam a conformidade com as declarações; tirar `buyer` de `pedidos` a faz falhar | `mercado-livre/mercado-livre.test.ts`, `sankhya/sankhya.test.ts` | must | -| 4 | U1 | Um `kind` fora dos quatro não compila nem passa em `defineOperation` | `kernel/kernel.test.ts`, "tipo desconhecido" | must | -| 5 | U2 | `GET /api/companies/{companyId}/roles` dá a dono e administrador `personal: ['contato','endereco','documento']`, e a desenvolvedor, membro e papel novo `[]` | `identity/roles-personal.test.ts`, "os tipos de cada papel" | must | -| 6 | U2 | `PUT .../roles/member` com `personal: ['contato']` é 200, e a leitura seguinte do membro recebe contato; com `['sensivel']` é 422 `REQUEST_MALFORMED` (`PERSONAL_KIND_UNKNOWN`); em `owner` é 409 `ROLE_FIXED` | `identity/roles-personal.test.ts`, "marcar contato no membro", "sensível recusado" | must | -| 7 | U2 | `POST .../roles` cria "Vendas" com `personal: ['contato','endereco']` e a resposta os traz | `identity/roles-personal.test.ts`, "papel novo com tipos" | must | -| 8 | U3 | Pela porta do Assistente, a mesma leitura `pedidos` do dublê: o dono recebe `buyer` com o nome plantado; o membro recebe a linha sem `buyer` | `connectors/personal-read.test.ts`, "dois papéis, duas respostas" | must | -| 9 | U3 | Pelo Assistente e pelo Builder, `notaFiscalDoPedido` nunca traz `recipient` nem o CPF plantado, nem para o dono (B1); pelo app de uma versão que declara documento, traz | `connectors/personal-read.test.ts`, "documento não vai ao modelo" | must | -| 10 | U3 | `buscarParceiros` pelo Assistente de um membro não traz `documento` nem `nome` | `connectors/personal-read.test.ts`, "Sankhya pelo membro" | must | -| 11 | U3 | `consultar` com `SELECT CGC_CPF AS X FROM TGFPAR` pelo Assistente de um membro não traz o CPF plantado; com `FROM TFPFUN` é recusado (`REFUSED`, `INPUT_INVALID`) antes da chamada | `connectors/personal-read.test.ts`, "porta com apelido", "RH recusado" | must | -| 12 | U3 | Cada `CONNECTOR_CALLED` de uma leitura que declara tipos traz `personal` (os tipos entregues) e `withheld` (os tirados), e nenhuma linha do registro traz o valor plantado | `connectors/personal-read.test.ts`, "o registro sem o valor" | must | -| 13 | U3 | Uma página de cópia (`by: 'copy'`) de `pedidos` não traz o nome plantado em célula nenhuma (o critério 10 do Sync, na forma nova) | `connectors/copy-port.test.ts`, "a cópia sem tipo" | must | -| 14 | U3 | `grep -rn "omit" packages/connectors/src/mercado-livre` não acha nada | revisão | must | -| 15 | U4 | Publicar uma versão que declara `personal: ['contato']` num conector, por quem não tem contato, é recusado com o tipo; por quem tem, publica | `connectors/publish.test.ts`, "tipo que o papel não tem" | must | -| 16 | U4 | Uma versão publicada antes do campo lê com os tipos do papel de quem a publicou, lido a cada chamada | `connectors/app-calls.test.ts`, "versão anterior ao campo" | must | -| 17 | U5 | Em Papéis, Membro mostra os três tipos desmarcáveis; Dono mostra os três marcados e travados; salvar marca o tipo e a lista seguinte o mostra; sensível não aparece | `apps/web/src/hub/identity/roles-personal.test.tsx` | must | -| 18 | U6 | Uma cópia ligada por quem tem contato guarda a coluna `buyer.nickname`; o Projeto liberado por quem não tem contato não lê essa coluna (42501); desligar a ligação apaga a coluna | `copies/personal-columns.test.ts` | must | -| 19 | U6 | Ligar uma proposta com coluna de um tipo que o papel de quem liga não tem é recusado com o tipo | `copies/accept-v2.test.ts`, "tipo que quem liga não tem" | must | +| 1 | U2 | Uma operação que declara um caminho pessoal ausente da resposta do dublê falha a conformidade, com o id e o caminho | `kernel/personal.test.ts`, "caminho declarado que não existe" | must | +| 2 | U2 | Uma resposta do dublê com um CPF válido, um e-mail ou um telefone num caminho não declarado falha a conformidade, com o caminho e o tipo, nunca o valor | `kernel/personal.test.ts`, "forma conhecida não declarada" | must | +| 3 | U2 | O Mercado Livre e o Sankhya passam a conformidade com as declarações; tirar `buyer` de `pedidos` a faz falhar | `mercado-livre/mercado-livre.test.ts`, `sankhya/sankhya.test.ts` | must | +| 4 | U2 | Um `kind` fora dos quatro não compila nem passa em `defineOperation` | `kernel/kernel.test.ts`, "tipo desconhecido" | must | +| 5 | U1 | `GET /api/companies/{companyId}/roles` dá a dono e administrador `personal: ['contato','endereco','documento']`, e a desenvolvedor, membro e papel novo `[]` | `identity/roles-personal.test.ts`, "os tipos de cada papel" | must | +| 6 | U1 | `PUT .../roles/member` com `personal: ['contato']` é 200, e a leitura seguinte do membro recebe contato; com `['sensivel']` é 422 `REQUEST_MALFORMED` (`PERSONAL_KIND_UNKNOWN`); em `owner` é 409 `ROLE_FIXED` | `identity/roles-personal.test.ts`, "marcar contato no membro", "sensível recusado" | must | +| 7 | U1 | `POST .../roles` cria "Vendas" com `personal: ['contato','endereco']` e a resposta os traz | `identity/roles-personal.test.ts`, "papel novo com tipos" | must | +| 8 | U2 | Pela porta do Assistente, a mesma leitura `pedidos` do dublê: o dono recebe `buyer` com o nome plantado e sem `billing_info`; o membro recebe a linha sem `buyer`; em `envio`, o membro não recebe `destination` (nem `receiver_id` nem `comments`) | `connectors/personal-read.test.ts`, "dois papéis, duas respostas" | must | +| 9 | U2 | Pelo Assistente e pelo Builder, nem `pedidos` (`buyer.billing_info`) nem `notaFiscalDoPedido` (`recipient`) trazem o CPF plantado, nem para o dono (B1); pelo app de uma versão que declara documento, trazem | `connectors/personal-read.test.ts`, "documento não vai ao modelo" | must | +| 10 | U2 | `buscarParceiros` pelo Assistente de um membro não traz `documento` nem `nome` | `connectors/personal-read.test.ts`, "Sankhya pelo membro" | must | +| 11 | U2 | `consultar` com `SELECT CGC_CPF AS X FROM TGFPAR` pelo Assistente de um membro não traz o CPF plantado; com `FROM TFPFUN` é recusado (`REFUSED`, `INPUT_INVALID`) antes da chamada | `connectors/personal-read.test.ts`, "porta com apelido", "RH recusado" | must | +| 12 | U2 | Cada `CONNECTOR_CALLED` de uma leitura que declara tipos traz `personal` (os tipos entregues) e `withheld` (os tirados), e nenhuma linha do registro traz o valor plantado | `connectors/personal-read.test.ts`, "o registro sem o valor" | must | +| 13 | U2 | Uma página de cópia (`by: 'copy'`) de `pedidos` não traz o nome plantado em célula nenhuma (o critério 10 do Sync, na forma nova) | `connectors/copy-port.test.ts`, "a cópia sem tipo" | must | +| 14 | U2 | `grep -rn "omit" packages/connectors/src/mercado-livre` não acha nada | revisão | must | +| 15 | U3 | Publicar uma versão que declara `personal: ['contato']` num conector, por quem não tem contato, é recusado com o tipo; por quem tem, publica | `connectors/publish.test.ts`, "tipo que o papel não tem" | must | +| 16 | U3 | Uma versão publicada antes do campo lê com os tipos do papel de quem a enviou (`sentBy`), lido a cada chamada; se essa pessoa saiu da empresa, sem tipo nenhum | `connectors/app-calls.test.ts`, "versão anterior ao campo" | must | +| 17 | U4 | Em Papéis, Membro mostra os três tipos desmarcáveis; Dono mostra os três marcados e travados; salvar marca o tipo e a lista seguinte o mostra; sensível não aparece | `apps/web/src/hub/identity/roles-personal.test.tsx` | must | +| 18 | U5 | Uma cópia ligada por quem tem contato guarda a coluna `buyer.nickname`; o Projeto liberado por quem não tem contato não lê essa coluna (42501); desligar a ligação apaga a coluna | `copies/personal-columns.test.ts` | must | +| 19 | U5 | Ligar uma proposta com coluna de um tipo que o papel de quem liga não tem é recusado com o tipo | `copies/accept-v2.test.ts`, "tipo que quem liga não tem" | must | ## A matriz de estados @@ -216,12 +223,14 @@ Cada um falha hoje: não há `personal` no kit, nem recurso `personal` nos papé | Campo de contrato | Quem escreve | Quem lê | | --- | --- | --- | -| `Operation.personal` | o autor do conector | a conformidade (U1), `readThrough` (U3), a cópia (U6) | -| `RoleView.personal` | `readRoles` (U2) | a tela de Papéis (U5) | -| `RoleCreate.personal`, `RoleChange.personal` | a tela (U5) | `permissionOf` (U2) | -| `Speaker` `VERSION` `.personal` | `app-calls.ts` pela versão (U4); `copy-port.ts` pela cópia (U6, `[]` antes) | `personalKindsOf` (U3) | -| manifesto `connectors[].personal` | o Builder | Publicar (U4), `app-calls.ts` (U4) | -| `CONNECTOR_CALLED.personal`, `.withheld` | `readThrough` (U3) | quem lê o registro (o "registro de acesso" que o Mercado Livre pede) | +| `Operation.personal` | o autor do conector | a conformidade (U2), `readThrough` (U2), a cópia (U5) | +| `ReadDoor.personalColumns` | o autor do conector (U2) | `shapeDoor` (U2) | +| `ConnectorCall.sentBy` | `handoff.ts` (U3) | `app-calls.ts` (U3), só para a versão anterior ao campo | +| `RoleView.personal` | `readRoles` (U1) | a tela de Papéis (U4) | +| `RoleCreate.personal`, `RoleChange.personal` | a tela (U4) | `permissionOf` (U1) | +| `Speaker` `VERSION` `.personal` | `app-calls.ts` pela versão (U3); `copy-port.ts` pela cópia (U5, `[]` antes) | `personalKindsOf` (U2) | +| manifesto `connectors[].personal` | o Builder | Publicar (U3), `app-calls.ts` (U3) | +| `CONNECTOR_CALLED.personal`, `.withheld` | `readThrough` (U2) | quem lê o registro (o "registro de acesso" que o Mercado Livre pede) | A tela muda uma seção de um editor que existe; o desenho segue `role-unattended.tsx`, a seção "Agentes" do mesmo editor, com uma caixa por tipo. @@ -231,8 +240,8 @@ A tela muda uma seção de um editor que existe; o desenho segue `role-unattende | Gravação ou comando, da etapa ou da biblioteca | Duas vezes ao mesmo tempo | Recusada no meio | Uma dependência fora | O que o processo imprime | Critérios | | --- | --- | --- | --- | --- | --- | | `updateOrgRole` e `createOrgRole` com `personal` (better-auth) | a trava da empresa de hoje (`lockCompany`) | a biblioteca recusa: nada gravado, a transação desfaz | `DATABASE_UNAVAILABLE` de hoje | `ROLE_UPDATED` (`company-routes.ts:257-265`), com os tipos em `added`/`removed` | 6, 7 | -| `GRANT SELECT (colunas)` ao Projeto (U6) | repete sem efeito | a liberação desfeita, como `undoRelease` (`copies/grants.ts:33-48`) | `COPY_DATABASE_UNAVAILABLE` | `COPY_GRANT_FAILED` de hoje | 18 | -| `ALTER TABLE ... DROP COLUMN` das colunas pessoais ao Desligar (U6) | a trava da cópia | a coluna fica e é apagada no próximo Desligar ou Ligar, como `copy_table_left` do Sync | anotada para depois | `COPY_PERSONAL_DROP_FAILED` | 18 | +| `GRANT SELECT (colunas)` ao Projeto (U5) | repete sem efeito | a liberação desfeita, como `undoRelease` (`copies/grants.ts:33-48`) | `COPY_DATABASE_UNAVAILABLE` | `COPY_GRANT_FAILED` de hoje | 18 | +| `ALTER TABLE ... DROP COLUMN` das colunas pessoais ao Desligar (U5) | a trava da cópia | a coluna fica e é apagada no próximo Desligar ou Ligar, como `copy_table_left` do Sync | anotada para depois | `COPY_PERSONAL_DROP_FAILED` | 18 | As leituras não gravam nada novo; o escalar do registro não é gravação. @@ -241,41 +250,41 @@ As leituras não gravam nada novo; o escalar do registro não é gravação. | Peça | Unidade | Hoje no main | Referência | Como a referência faz | Veredito | Prova, se não for nativo | Palavra do fundador | Arquivo novo | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | A lista fixa de tipos | U1 | nada | Shopify (níveis de campo do cliente, documentado); LGPD art. 5º II e art. 11 (sensível à parte) | poucos níveis fixos, liberados à parte | embrulhado: `PERSONAL_KINDS` no kit | conformidade (crit. 4) | A1, `D1` | `packages/connectors/src/kernel/personal.ts` | -| O conector declara caminho e tipo | U1 | `mercado-livre/operations/shared.ts:47` (`omit`, sem tipo); o Sankhya sem nada | n8n `interfaces.ts:3245-3251`, `FormTriggerV2.node.ts:100`, `FormTriggerV2.node.test.ts:651-661` | o nó declara caminhos com ponto; a plataforma oculta | embrulhado: `personal` na `Operation` no lugar do `omit` | crit. 1, 3 | B1 ("a lógica") | — | -| A conferência das formas não declaradas | U1 | nada | nenhuma (procurado em n8n, Airbyte, nango, twenty) | — | remendo: só no teste, com `isValidCpf`, `isValidEmail`, `isValidPhone` da biblioteca instalada | crit. 2, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | B1 ("vale a pena? referências fazem?"): esta página | — | -| Os tipos como permissão do papel | U2 | `identity/roles.ts:68-82` (recursos e ações), `role-statements.ts`, `company-roles.ts:237-244` | better-auth 1.7.8 `createAccessControl`, `hasPermission`, `createOrgRole`/`updateOrgRole` (o editor de hoje); twenty `field-permission.entity.ts:65-66` | uma ação no papel, lida a cada chamada | nativo: recurso `personal` com as ações `contato`, `endereco`, `documento`, `sensivel` | — | A1 ("para dar para outro cargo") | — | -| Aplicação num ponto | U3 | o corte dentro de `mercado-livre/answers.ts:101-126`, antes do papel | twenty `common-select-fields-builder.ts:46-52`; n8n `node-defined-field-redaction.strategy.ts:44-69` | um filtro único sobre o resultado | embrulhado: `shapePersonal`, função pura, chamada em `readThrough` depois de `answerOf` | crit. 8 | — | `apps/server/src/hub/connectors/personal.ts` | -| Documento nunca ao modelo | U3 | `cut.ts` (só tamanho) | Mastra `PIIDetector` (`pii-detector.d.ts:97,105`), por modelo; LGPD art. 33 | detecção por LLM | embrulhado: o leitor `assistant`/`builder` perde `documento` | crit. 9 | B1 | — | -| A porta genérica do Sankhya | U3 | `sankhya/door.ts:69-145` (qualquer coluna); `sankhya/skill/SKILL.md:21` (RH só por texto) | n8n `unknownNodes` oculta tudo do que não conhece (`node-defined-field-redaction.strategy.ts:52-58`); Postgres `GRANT SELECT (colunas)` (PostgREST `test/spec/fixtures/privileges.sql:55`) | fechar o que não se conhece; ou o banco libera por coluna | remendo: colunas pelo nome, formas conhecidas nas células, `TFP` recusado | crit. 11, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | G | — | -| O registro sem o valor | U3 | `read.ts:189-204` (`CONNECTOR_CALLED`, escalares) | n8n `execution-redaction.service.ts:133,145,168`; Mercado Livre "registro de acesso" (documentado) | um evento por revelação | embrulhado: dois escalares na linha que já existe (security S41) | crit. 12 | — | — | -| O app declara os tipos | U4 | `app-contract/src/manifest.ts:46-57`; `publish-check.ts:35-61` (`C1`) | Shopify (a liberação é do app); a `C1` da etapa 20 | o app pede o nível; a plataforma confere | nativo: estende o manifesto e a conferência de Publicar | — | F | — | -| A tela de Papéis | U5 | `apps/web/src/hub/identity/role-editor.tsx:153`, `role-unattended.tsx` | a seção Agentes do mesmo editor | uma caixa por ação | nativo | — | A1 | `apps/web/src/hub/identity/role-personal.tsx` | -| A cópia guarda só o liberado | U6 | o critério 10 do Sync; `copies/grants.ts` (`GRANT` da tabela) | Airbyte `docs/platform/using-airbyte/mappings.md:7` (por conexão); PostgREST `privileges.sql:55` | por conexão; coluna liberada por `GRANT` | nativo: `GRANT SELECT (colunas)` | — | `C1` | — | -| Apagar ao desligar | U6 | o Desligar do Sync U3 (`copy_table_left`) | Mercado Livre (termos e segurança, documentado) | apagar as cópias e a PII | embrulhado: no mesmo Desligar | crit. 18 | `C1` | — | - -**Ficam fora da tabela, com a razão.** RH e dados sensíveis (`SKILL.md:21`, LGPD art. 11): `D1`; a porta recusa `TFP` (G, U3). A detecção por modelo (o `PIIDetector` do Mastra): a declaração é determinística e mais barata; o detector fica como rede opcional para texto livre, num cartão futuro. +| O conector declara caminho e tipo | U2 | `mercado-livre/operations/shared.ts:47` (`omit`, sem tipo); o Sankhya sem nada | n8n `interfaces.ts:3245-3251`, `FormTriggerV2.node.ts:100`, `FormTriggerV2.node.test.ts:651-661` | o nó declara caminhos com ponto; a plataforma oculta | embrulhado: `personal` na `Operation` no lugar do `omit` | crit. 1, 3 | B1 ("a lógica") | — | +| A conferência das formas não declaradas | U2 | nada | nenhuma (procurado em n8n, Airbyte, nango, twenty) | — | remendo: só no teste, com `isValidCpf`, `isValidEmail`, `isValidPhone` da biblioteca instalada | crit. 2, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | B1 ("vale a pena? referências fazem?"): esta página | — | +| Os tipos como permissão do papel | U1 | `identity/roles.ts:68-82` (recursos e ações), `role-statements.ts`, `company-roles.ts:237-244` | better-auth 1.7.8 `createAccessControl`, `hasPermission`, `createOrgRole`/`updateOrgRole` (o editor de hoje); twenty `field-permission.entity.ts:65-66` | uma ação no papel, lida a cada chamada | nativo: recurso `personal` com as ações `contato`, `endereco`, `documento`, `sensivel` | — | A1 ("para dar para outro cargo") | — | +| Aplicação num ponto | U2 | o corte dentro de `mercado-livre/answers.ts:101-126`, antes do papel | twenty `common-select-fields-builder.ts:46-52`; n8n `node-defined-field-redaction.strategy.ts:44-69` | um filtro único sobre o resultado | embrulhado: `shapePersonal`, função pura, chamada em `readThrough` depois de `answerOf` | crit. 8 | — | `apps/server/src/hub/connectors/personal.ts` | +| Documento nunca ao modelo | U2 | `cut.ts` (só tamanho) | Mastra `PIIDetector` (`pii-detector.d.ts:97,105`), por modelo; LGPD art. 33 | detecção por LLM | embrulhado: o leitor `assistant`/`builder` perde `documento` | crit. 9 | B1 | — | +| A porta genérica do Sankhya | U2 | `sankhya/door.ts:69-145` (qualquer coluna); `sankhya/skill/SKILL.md:21` (RH só por texto) | n8n `unknownNodes` oculta tudo do que não conhece (`node-defined-field-redaction.strategy.ts:52-58`); Postgres `GRANT SELECT (colunas)` (PostgREST `test/spec/fixtures/privileges.sql:55`) | fechar o que não se conhece; ou o banco libera por coluna | remendo: colunas pelo nome, formas conhecidas nas células, `TFP` recusado | crit. 11, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | G | — | +| O registro sem o valor | U2 | `read.ts:189-204` (`CONNECTOR_CALLED`, escalares) | n8n `execution-redaction.service.ts:250` (o evento da revelação; as recusas em `:133,145,168`); Mercado Livre "registro de acesso" (documentado) | um evento por revelação | embrulhado: dois escalares na linha que já existe (security S41) | crit. 12 | — | — | +| O app declara os tipos | U3 | `app-contract/src/manifest.ts:46-57`; `publish-check.ts:35-61` (`C1`) | Shopify (a liberação é do app); a `C1` da etapa 20 | o app pede o nível; a plataforma confere | nativo: estende o manifesto e a conferência de Publicar | — | F | — | +| A tela de Papéis | U4 | `apps/web/src/hub/identity/role-editor.tsx:152`, `role-unattended.tsx` | a seção Agentes do mesmo editor | uma caixa por ação | nativo | — | A1 | `apps/web/src/hub/identity/role-personal.tsx` | +| A cópia guarda só o liberado | U5 | o critério 10 do Sync; `copies/grants.ts` (`GRANT` da tabela) | Airbyte `docs/platform/using-airbyte/mappings.md:7` (por conexão); PostgREST `privileges.sql:55` | por conexão; coluna liberada por `GRANT` | nativo: `GRANT SELECT (colunas)` | — | `C1` | — | +| Apagar ao desligar | U5 | o Desligar do Sync U3 (`copy_table_left`) | Mercado Livre (termos e segurança, documentado) | apagar as cópias e a PII | embrulhado: no mesmo Desligar | crit. 18 | `C1` | — | + +**Ficam fora da tabela, com a razão.** RH e dados sensíveis (`SKILL.md:21`, LGPD art. 11): `D1`; a porta recusa `TFP` (G, U2). A detecção por modelo (o `PIIDetector` do Mastra): a declaração é determinística e mais barata; o detector fica como rede opcional para texto livre, num cartão futuro. **A medida do transplante.** Nada vem do repositório antigo. **A dívida.** O corpo de uma recusa do provedor vai inteiro ao modelo (`read.ts:37-40`); se um provedor devolver dado pessoal numa recusa, ele passa. Fica para a seção 11 da arquitetura. -## U1 A declaração no kit +## U1 Os tipos nos papéis -**Base e modelo.** `main` do dia, com a U2b do Sync. Sonnet: contrato pequeno dentro do kit. +**Base e modelo.** `main` do dia. Sonnet: segue o padrão de `UNATTENDED`. ### As linhas da tabela -A lista fixa, a declaração, a conferência das formas, e a parte de declaração da porta (G1, item 1). +A lista fixa de tipos (só a constante); os tipos como permissão do papel. ### O raio da mudança | Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | | --- | --- | --- | -| `packages/connectors/src/kernel/` | cria `personal.ts`, muda `operation.ts`, `conformance.ts`, `index.ts` | todo conector; o servidor (U3) | -| `packages/connectors/src/mercado-livre/operations/` | muda as leituras que têm `omit` | o kit | -| `packages/connectors/src/sankhya/` | muda `operations/{buscas,cadastros}.ts`, `door.ts` | o kit | -| `packages/connectors/package.json` | acrescenta `@brazilian-utils/brazilian-utils` (já no lock, pelo `hub-api`) | — | +| `packages/connectors/src/kernel/personal.ts`, `index.ts` | cria a lista `PERSONAL_KINDS` | o servidor, pelo `RoleKit` | +| `apps/server/src/hub/identity/` | muda `roles.ts`, `role-statements.ts`, `company-roles.ts`, `may-use.ts`, `public.ts` | `connectors/read.ts` (U2), `publish-check.ts` (U3) | +| `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts` | o kit entrega os tipos à identidade (S36) | a identidade | +| `packages/hub-api/src/{company-roles,failures}.ts` | `personal` em `RoleView`, `RoleCreate`, `RoleChange`; o texto de `PERSONAL_KIND_UNKNOWN` | a tela (U4) | ### Os contratos que a unidade cria @@ -283,68 +292,6 @@ A lista fixa, a declaração, a conferência das formas, e a parte de declaraç // packages/connectors/src/kernel/personal.ts (dono dos tipos, C26) export const PERSONAL_KINDS = ['contato', 'endereco', 'documento', 'sensivel'] as const export type PersonalKind = (typeof PERSONAL_KINDS)[number] -/** Um caminho com ponto relativo a cada linha de `linhas`, ou ao valor quando ele não tem `linhas`. */ -export type PersonalField = Readonly<{ path: string; kind: PersonalKind }> -/** As formas conhecidas achadas num valor fora dos caminhos dados: caminho e tipo, nunca o valor. */ -export function findUndeclared(value: unknown, declared: readonly PersonalField[]): readonly Readonly<{ path: string; kind: 'documento' | 'contato' }>[] -// packages/connectors/src/kernel/operation.ts, em Operation: personal?: readonly PersonalField[] -// packages/connectors/src/kernel/definition.ts, em ReadDoor: personalColumns?: readonly Readonly<{ column: string; kind: PersonalKind }>[] -``` - -`defineOperation` confere cada `kind` com `z.enum(PERSONAL_KINDS)`. O id de tipo é texto com forma -nos contratos (`hub-api`, `app-contract`) e é conferido contra o kit no servidor, como `ConnectorId` -(`hub-api/src/company-roles.ts:7`, C23). - -### As skills e a leitura - -`docs/reference/stack.md` para `packages/connectors`: `zod`; `@brazilian-utils/brazilian-utils` -(README). - -### A ordem do trabalho - -1. `personal.ts`: os tipos e `findUndeclared` (`isValidCpf`; `isValidEmail`; `isValidPhone` só em - texto de 10 a 13 dígitos, para não pegar ids). Regras: C21, C26. -2. `operation.ts`: `personal` opcional; `defineOperation` confere. -3. `conformance.ts`: para cada probe com resposta, cada caminho declarado existe em ao menos uma linha - (`valueAt`, `operation.ts:103`), e `findUndeclared` volta vazio. -4. Mercado Livre: `pedidos`, `pedido` `{buyer: contato}`; `envio` `destination.receiver_name`, - `destination.receiver_phone` contato e `destination.shipping_address` endereço; `perguntas` - `from` contato; `mensagensDoPacote` `from.email`, `from.name`, `to.email`, `to.name` contato; - `notaFiscalDoPedido` `recipient` documento; `conta` os campos do `omit` de hoje com o tipo. O `omit` - fica até a U3. As leituras sem `omit` (`pacote`, `mensagensNaoLidas`, `reclamacoes`) passam pela - conformidade: se o dublê não planta nada, nada se declara. -5. Sankhya: `buscarParceiros` `documento` documento e `nome`, `razaoSocial` contato; `clientes` pelos - nomes que o dublê traz; `sankhyaDoor` com `personalColumns`. - -### Como se prova - -Critérios 1 a 4: `node --test` nos arquivos da unidade; o crit. 3 tira `buyer` de `pedidos` num teste -e espera a falha. Falha hoje: `personal` não existe. - -### Parar se - -O dublê de uma leitura que declara um caminho não o planta: plantar no dublê (T29), nunca tirar a -declaração. Uma forma pega um id que não é pessoal: estreitar a forma e dizer no relatório. - -## U2 Os tipos nos papéis - -**Base e modelo.** U1. Sonnet: segue o padrão de `UNATTENDED`. - -### As linhas da tabela - -Os tipos como permissão do papel. - -### O raio da mudança - -| Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | -| --- | --- | --- | -| `apps/server/src/hub/identity/` | muda `roles.ts`, `role-statements.ts`, `company-roles.ts`, `may-use.ts`, `public.ts` | `connectors/read.ts` (U3), `publish-check.ts` (U4) | -| `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts` | o kit entrega os tipos à identidade (S36) | a identidade | -| `packages/hub-api/src/company-roles.ts` | `personal` em `RoleView`, `RoleCreate`, `RoleChange` | a tela (U5) | - -### Os contratos que a unidade cria - -```ts // roles.ts: RoleKit ganha `personalKinds: readonly string[]` (do kit, pelo main.ts) // createAccessControl({... , personal: ['contato','endereco','documento','sensivel'] }) // owner e admin: personal: ['contato','endereco','documento']; developer, member: nada @@ -355,11 +302,14 @@ RoleCreate, RoleChange: { ...; personal: z.array(PersonalKindText).max(3).option personalKinds(companyId: CompanyId, speaker: PersonSpeaker): Promise ``` -`PersonalKindText` é `z.string().regex(/^[a-z]{1,20}$/)`; `permissionOf` confere contra o kit e recusa -`sensivel` e o desconhecido com `malformed('PERSONAL_KIND_UNKNOWN')`. `personalKinds` faz uma leitura -de `hasPermission` por tipo, com `useMemoryCache` como `mayUseEach` (`may-use.ts:88-112`). -`actionsOf` passa a somar os tipos dados e tirados. Nenhuma migração: os papéis guardados não têm o -recurso e respondem "não" (A1); os de dono e administrador vêm do código. +`PersonalKindText` é `z.string().regex(/^[a-z]{1,20}$/)`: o id de tipo é texto com forma nos +contratos e é conferido contra o kit no servidor, como `ConnectorId` (`hub-api/src/company-roles.ts:7`, +C23); `permissionOf` recusa `sensivel` e o desconhecido com `malformed('PERSONAL_KIND_UNKNOWN')`. +`personalKinds` faz uma leitura de `hasPermission` por tipo, com `useMemoryCache` como `mayUseEach` +(`may-use.ts:88-112`). `actionsOf` passa a somar os tipos dados e tirados. Nenhuma migração: os +papéis guardados não têm o recurso e respondem "não" (A1); os de dono e administrador vêm do código. +Modelo: `company-roles.ts` cumpre C21 (`RoleView.kind`), C23 (`Role`, `ConnectorId`) e C26 +(`UNATTENDED_ACTION` num dono). ### As skills e a leitura @@ -367,9 +317,9 @@ recurso e respondem "não" (A1); os de dono e administrador vêm do código. ### A ordem do trabalho -1. `roles.ts`: o recurso e os papéis fixos. 2. `role-statements.ts` e `company-roles.ts` -(`permissionOf`, `readRoles`, `actionsOf`). 3. `may-use.ts`: `personalKinds`. 4. `hub-api`. Regras: -C23, C26, S36, S41. +1. `personal.ts` com a lista. 2. `roles.ts`: o recurso e os papéis fixos. 3. `role-statements.ts` +e `company-roles.ts` (`permissionOf`, `readRoles`, `actionsOf`). 4. `may-use.ts`: `personalKinds`. +5. `hub-api`. Regras: C23, C26, S36, S41. ### Como se prova @@ -380,26 +330,40 @@ Critérios 5 a 7, pela rota pública (T5). Falha hoje: `RoleView` não tem `pers A biblioteca recusa criar um papel com um recurso que o criador não tem e o dono não tiver `sensivel`: é o comportamento esperado (`D1`); se recusar `contato`, parar e relatar. -## U3 A aplicação num ponto +## U2 A declaração e a aplicação num ponto -**Base e modelo.** U1 e U2. Opus: desenho entre módulos (kit, identidade, leitores) e a porta. +**Base e modelo.** U1, e a U2b do Sync no `main` (pasta do Mercado Livre, L1). Opus: desenho entre +módulos (kit, identidade, leitores) e a porta. A declaração e a aplicação são uma unidade porque a +conformidade lê o valor que `operation.answer` devolve (`conformance-session.ts:134-139`), e esse +valor já sai sem o `omit` (`mercado-livre/answers.ts:124-126`): enquanto o `omit` corta, um caminho +declarado não existe no valor; e tirar o `omit` sem a aplicação entregaria o comprador a todos. ### As linhas da tabela -Aplicação num ponto; documento nunca ao modelo; a porta (G1, itens 2 e 3); o registro. +A declaração; a conferência das formas não declaradas; a aplicação num ponto; documento nunca ao +modelo; a porta do Sankhya (G1); o registro. ### O raio da mudança | Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | | --- | --- | --- | -| `apps/server/src/hub/connectors/` | cria `personal.ts`; muda `read.ts`, `rows.ts` (`Deps.identity`) | `app-calls.ts`, `builder-port.ts`, `assistant-port.ts`, `copy-port.ts`, `notes.ts` | -| `apps/server/src/hub/identity/may-use.ts` | `Speaker` `VERSION` ganha `personal: readonly PersonalKind[]` | `builder/agents/{tools,agent,frozen}.ts` (passam `[]`), `copy-port.ts` (`[]`) | -| `packages/connectors/src/mercado-livre/` | apaga `omit`, `BUYER` e `without` (`answers.ts:78-126`, `shared.ts:47,56-61`) | o kit | -| `packages/connectors/src/sankhya/{door,read-only-sql}.ts` | a recusa de `TFP` | o kit | +| `packages/connectors/src/kernel/` | muda `personal.ts` (`PersonalField`, `findUndeclared`), `operation.ts`, `definition.ts` (`ReadDoor.personalColumns`), `conformance.ts`, `index.ts` | todo conector; o servidor | +| `packages/connectors/src/mercado-livre/` | apaga `omit`, `BUYER` de `vendas.ts`, `without` e o corte de `rows()` (`answers.ts:78-126`, `shared.ts:47,56-61`); declara `personal`; troca os `summary` que dizem que o comprador não vem (`envios.ts:12`, `faturamento.ts:55`) e a skill; planta a cidade da entrega no dublê | o kit | +| `packages/connectors/src/sankhya/` | declara `personal` em `operations/{buscas,cadastros}.ts` e `personalColumns` em `door.ts`; a recusa de `TFP` em `read-only-sql.ts` | o kit | +| `packages/connectors/package.json` | acrescenta `@brazilian-utils/brazilian-utils` (já no lock, pelo `hub-api`) | — | +| `apps/server/src/hub/connectors/` | cria `personal.ts`; muda `read.ts`, `rows.ts` (`Deps.identity`), `test-write.ts:113` | `app-calls.ts`, `builder-port.ts`, `assistant-port.ts`, `copy-port.ts`, `notes.ts` | +| `apps/server/src/hub/identity/may-use.ts` | `Speaker` `VERSION` ganha `personal: readonly PersonalKind[]` | passam `[]`: `builder/agents/tools.ts:184`, `agent.ts:207,242`, `frozen.ts:145,234` (escritas de agente), `copy-port.ts:145`, e os testes `role-doors.test.ts`, `code-tool.test.ts`, `publish.test.ts`, `registry-line.test.ts`; `app-calls.ts:105` muda na U3 | ### Os contratos que a unidade cria ```ts +// packages/connectors/src/kernel/personal.ts +/** Um caminho com ponto relativo a cada linha de `linhas`, ou ao valor quando ele não tem `linhas`. */ +export type PersonalField = Readonly<{ path: string; kind: PersonalKind }> +/** As formas conhecidas achadas num valor fora dos caminhos dados: caminho e tipo, nunca o valor. */ +export function findUndeclared(value: unknown, declared: readonly PersonalField[]): readonly Readonly<{ path: string; kind: 'documento' | 'contato' }>[] +// operation.ts, em Operation: personal?: readonly PersonalField[] +// definition.ts, em ReadDoor: personalColumns?: readonly Readonly<{ column: string; kind: PersonalKind }>[] // apps/server/src/hub/connectors/personal.ts export function shapePersonal(value: unknown, personal: readonly PersonalField[], allowed: ReadonlySet): Readonly<{ value: unknown; given: PersonalKind[]; withheld: PersonalKind[] }> export function shapeDoor(value: unknown, columns: readonly Readonly<{ column: string; kind: PersonalKind }>[], allowed: ReadonlySet): ReturnType @@ -407,36 +371,63 @@ export function shapeDoor(value: unknown, columns: readonly Readonly<{ column: s async function personalKindsOf(deps: Deps, a: ReadRequest): Promise> ``` -`shapePersonal` tira o caminho (o twenty tira, não mascara). Em `readThrough`, depois de `answerOf` -(`read.ts:181`) e só para `OK`. `by: 'assistant' | 'builder'` tira `documento`; `sensivel` nunca está -num papel. A porta: `shapeDoor` tira as colunas declaradas e as células que `findUndeclared` acha. +`defineOperation` confere cada `kind` com `z.enum(PERSONAL_KINDS)`. **O caminho mais específico +vence:** `buyer` contato e `buyer.billing_info` documento quer dizer que quem tem contato e não tem +documento recebe `buyer` sem `billing_info`; `shapePersonal` anda os caminhos do mais fundo ao mais +raso e tira cada um cujo tipo não está em `allowed` (o twenty tira, não mascara). Em `readThrough`, +depois de `answerOf` (`read.ts:181`) e só para `OK`. `by: 'assistant' | 'builder'` tira +`documento`; `sensivel` nunca está num papel. A porta: `shapeDoor` tira as colunas declaradas e as +células que `findUndeclared` acha. `findUndeclared` olha textos e inteiros (um CPF pode vir como +número JSON: o inteiro vira texto de 11 dígitos com zeros à esquerda antes de `isValidCpf`), e +`isValidPhone` só em texto de 10 a 13 dígitos, para não pegar ids. Modelo: `read.ts` cumpre C21 +(`ReadAnswer`), C23 (`ConnectorId.parse` na entrada) e C26 (um `readThrough`). ### As skills e a leitura -`stack.md` para `apps/server/src/hub`: `better-auth-best-practices`; o kit pela U1. +`stack.md` para `packages/connectors` (`zod`; `@brazilian-utils/brazilian-utils`, README) e para +`apps/server/src/hub` (`better-auth-best-practices`). ### A ordem do trabalho -1. `personal.ts` e os seus testes de unidade. 2. `Speaker` `VERSION` com `personal`; todo chamador -passa `[]` (um caminho que esquece não compila). 3. `readThrough`: `personalKindsOf`, -`shapePersonal` ou `shapeDoor`, os escalares em `CONNECTOR_CALLED`. 4. Apagar `omit` e o resto (C75, -apagar antes de somar no mesmo commit). 5. `read-only-sql.ts`: recusar `\bTFP\w*` no texto e -`carregar` de entidade de RH. Regras: S40, S41, C21, T5. +1. `personal.ts` do kit: `PersonalField` e `findUndeclared`. Regras: C21, C26. +2. `operation.ts` e `definition.ts`: `personal` e `personalColumns`. +3. `conformance.ts`: para cada probe com resposta, cada caminho declarado existe em ao menos uma + linha (`valueAt`, `operation.ts:103`), e `findUndeclared` volta vazio. +4. Mercado Livre, apagando antes de somar (C75): o `omit` sai de `rows()`; `pedidos` e `pedido` + declaram `buyer` contato e `buyer.billing_info` documento; `envio` declara `destination` endereço + e `destination.receiver_name`, `destination.receiver_phone` contato (assim `receiver_id` e + `comments`, que o `omit` de hoje escondia, ficam sob endereço); `perguntas` `from` contato; + `mensagensDoPacote` `from.email`, `from.name`, `to.email`, `to.name` contato; `notaFiscalDoPedido` + `recipient` documento; `conta` `email`, `phone`, `alternative_phone`, `first_name`, `last_name` e + `address` como contato e endereço e `identification` documento (são dados do vendedor, pessoa: + o dono os vê, o Membro não). As leituras sem `omit` (`pacote`, `mensagensNaoLidas`, + `reclamacoes`) passam pela conformidade: se o dublê não planta nada, nada se declara. Os `summary` + e a skill deixam de dizer que o comprador não vem. +5. Sankhya: `buscarParceiros` `documento` documento e `nome`, `razaoSocial` contato; `clientes` pelos + nomes que o dublê traz; `sankhyaDoor` com `personalColumns`. +6. Servidor: `personal.ts`; `Speaker` `VERSION` com `personal`, todo construtor passa `[]` (um + caminho que esquece não compila); `readThrough` com `personalKindsOf`, `shapePersonal` ou + `shapeDoor`, e os escalares `personal` e `withheld` em `CONNECTOR_CALLED`. +7. `read-only-sql.ts`: recusar `\bTFP\w*` no texto de `consultar`. Regras: S40, S41, C21, T5. ### Como se prova -Critérios 8 a 14, pelas portas públicas do Assistente, do Builder e da cópia, com o dublê que planta o -comprador (`mercado-livre/fake-data.ts:21-42`). Falha hoje: o membro e o dono recebem a mesma linha. +Critérios 1 a 4 com `node --test` nos arquivos do kit (o crit. 3 tira `buyer` de `pedidos` num teste +e espera a falha); critérios 8 a 14 pelas portas públicas do Assistente, do Builder e da cópia, com o +dublê que planta o comprador (`mercado-livre/fake-data.ts:21-29`). Falha hoje: `personal` não existe, +e o membro e o dono recebem a mesma linha. ### Parar se -Uma porta chama `readThrough` sem uma pessoa nem uma versão: parar. O teste do critério 10 do Sync -quebra por outro motivo que não o `omit`: parar e relatar. +O dublê de uma leitura que declara um caminho não o planta: plantar no dublê (T29), nunca tirar a +declaração. Uma forma pega um id que não é pessoal: estreitar a forma e dizer no relatório. Uma porta +chama `readThrough` sem uma pessoa nem uma versão: parar. O teste do critério 10 do Sync quebra por +outro motivo que não o `omit`: parar e relatar. -## U4 O app declara os tipos +## U3 O app declara os tipos -**Base e modelo.** U3, no mesmo pull request (sem U4, os apps do Sankhya publicados perderiam nomes). -Sonnet. +**Base e modelo.** U2, no mesmo pull request (sem ela, os apps do Sankhya publicados perderiam +nomes). Sonnet. ### As linhas da tabela @@ -447,6 +438,8 @@ O app declara os tipos. | Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | | --- | --- | --- | | `packages/app-contract/src/manifest.ts` | `personal` em cada `connectors[]` | o Builder, `hub-api` | +| `packages/hub-api/src/{apps,failures}.ts` | o motivo `PERSONAL_NOT_IN_ROLE` (`apps.ts:191`) e o seu texto | Publicar, a tela de Publicar | +| `apps/server/src/hub/apps/{handoff,versions}.ts` | `ConnectorCall` ganha `sentBy` (`handoff.ts:204-209`; a coluna é `tables.ts:155`) | `app-calls.ts` | | `apps/server/src/hub/connectors/{publish-check,app-calls}.ts` | a conferência e o `Speaker` | Publicar, o clique do app | | `packages/app/skills/conexus-data/` | a frase de quando declarar | o Builder | @@ -455,10 +448,13 @@ O app declara os tipos. ```ts // manifest.ts, em cada connectors[]: personal: z.array(z.string().regex(/^[a-z]{1,20}$/)).max(3).optional() // UngrantedBy ganha reason 'PERSONAL_NOT_IN_ROLE' com `kind` +// handoff.ts, ConnectorCall: sentBy: PersonId ``` -Uma versão sem o campo lê com os tipos do papel de quem a publicou, lidos a cada chamada (como -`builder/agents/tools.ts:221` trata o campo novo de uma versão antiga). +Uma versão sem o campo lê com os tipos do papel de quem a enviou (`sentBy`, a única pessoa que a +versão guarda), lidos a cada chamada; se essa pessoa saiu da empresa, sem tipo nenhum (como +`builder/agents/tools.ts:221` trata o campo novo de uma versão antiga). Esta regra está na página do +fundador. Uma versão com o campo lê com o que declara, conferido em Publicar contra quem publica. ### As skills e a leitura @@ -467,7 +463,8 @@ Uma versão sem o campo lê com os tipos do papel de quem a publicou, lidos a ca ### A ordem do trabalho 1. O manifesto. 2. `publish-check.ts`: depois das operações, os tipos contra `personalKinds` de quem -publica. 3. `app-calls.ts`: o `Speaker` com os tipos da versão. 4. A skill. Regras: C23, S10. +publica. 3. `handoff.ts`: `sentBy`. 4. `app-calls.ts`: o `Speaker` com os tipos da versão, ou os de +`sentBy`. 5. `hub-api`. 6. A skill. Regras: C23, S10. ### Como se prova @@ -475,11 +472,12 @@ Critérios 15 e 16, pela rota de Publicar e pelo clique do app. ### Parar se -A versão publicada não guarda quem a publicou: parar e relatar (a regra da versão antiga depende disso). +Quem publica e quem enviou divergem numa versão antiga e o teste mostra um caso em que isso abre um +tipo que quem publicou não tinha: parar e relatar. -## U5 A tela de Papéis +## U4 A tela de Papéis -**Base e modelo.** U2. Sonnet. +**Base e modelo.** U1. Sonnet. ### As linhas da tabela @@ -515,9 +513,9 @@ Critério 17; capturas a 1366 e 390 px, nos dois temas (conexus-qa). Um texto da tela muda o sentido da página: volta ao fundador. -## U6 A coluna pessoal na cópia +## U5 A coluna pessoal na cópia -**Base e modelo.** U3 e as U3 e U4 do Sync no `main`. Opus: banco da empresa, `GRANT` de coluna e +**Base e modelo.** U2 e as U3 e U4 do Sync no `main`. Opus: banco da empresa, `GRANT` de coluna e Desligar. ### As linhas da tabela @@ -558,4 +556,4 @@ Critérios 18 e 19, com o PostgreSQL de teste (o login do Projeto lê e recebe 4 ### Parar se -O motor do Sync no `main` não guarda o `path` de cada coluna: parar; a U6 volta ao cartão. +O motor do Sync no `main` não guarda o `path` de cada coluna: parar; a U5 volta ao cartão. From 9656e2addf8984e5e6d9cfa9c884965411c26645 Mon Sep 17 00:00:00 2001 From: developmentconexus Date: Wed, 11 Nov 2026 19:56:51 -0300 Subject: [PATCH 3/5] =?UTF-8?q?docs(conectores):=20as=20respostas=20F1,=20?= =?UTF-8?q?G=20e=20H1=20do=20fundador=20no=20cart=C3=A3o=20da=20pol=C3=ADt?= =?UTF-8?q?ica=20de=20dados=20pessoais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/approvals.md | 4 + docs/steps/a-politica-de-dados-pessoais.md | 117 +++++++++++---------- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/docs/approvals.md b/docs/approvals.md index 88396104..27ed3abf 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -943,6 +943,10 @@ Decisões do fundador no meio da etapa (L6), antes da aprovação da página. Em Decisão do fundador no meio da etapa (L6), depois da aprovação do cartão, em 2026-11-11, sobre a gravação da U2 (as quatro respostas reais do Mercado Livre só podem ser lidas pelo Hub no ar, e o Hub no ar ainda recusa as entradas `order.date_last_updated.from`, `search_type=scan` e `scroll_id`): à pergunta da divisão (A: a U1 entra sozinha, a U2a leva só as declarações das leituras novas, sem dublê nem resposta inventada, e a U2b grava as quatro respostas pelo Hub depois que a U2a estiver no ar e faz o dublê, a conformidade e a skill; B: gravar com uma execução avulsa; C: esperar): "A". O cartão não muda, e a U2 entra em dois pull requests: a U2a, empilhada na U1, e a U2b, depois. +## A política de dados pessoais dos conectores + +Decisões do fundador antes da aprovação da página (L6). Em 2026-11-11, às perguntas do estudo da política de dados pessoais (A, quem vê dados pessoais de clientes; B, o que vai ao modelo; C, a cópia; D, os sensíveis; E, o advogado): "A1, mas seria uma capabilidade para criar e selecionar POR EXEMPLO PARA DAR PARA OUTRO cargo"; "B1, okay, mas como vamos tratar isso Fácil a gente selecionar um conhecimento específico, tipo CPF, mas eu sempre quero pensar na lógica, porque a gente vai trabalhar com a criação de vários conectores, de uma plataforma muito robusta. Qual que seria a lógica para a gente conseguir fazer isso, entende? Vale a pena? É uma gambiarra? Referências fazem? Entende meu ponto?"; "`C1` porém mesma coisa de B"; "D1"; "E Só vai vir depois". No mesmo dia, às decisões F (num app, de quem é o "pode ver"), G (a porta genérica do Sankhya) e H (o lugar no roteiro) da página do cartão (`docs/steps/a-politica-de-dados-pessoais.md`) no commit 30e2b034ca27eebb0f9559e3052b942d1eb06fcd: "F1, PORÉM QUERO entender a parte tencia de código arquitetura que vamos implementar isso de forma funcional / G Não vou limitar nada disso / H1". A página as escreve como decididas: F1, com a seção "Como funciona no código"; G, a porta genérica do Sankhya fora da política, sem trava nova, e o RH só pela frase da skill, como hoje; H1, logo depois do Sync e antes da Capacidade 4. + ## Mudança de método: um pull request por grupo de unidades Em 2026-11-09, como mudança de método (L20), o fundador disse: "eu queria fazer é mudar a nossa regra de toda o 1C1PR, 1CI, né? Eu acho que seria no mínimo um conjunto de 3 etapas, tipo 1, o 2, o 3. Aí sim poderia subir um PR e aí rodar CI e tudo. Lógico que o nosso teste, né? A gente faz testes, né? Tipo, do 1, a gente teria fazer e testar e validar. Mas eu falo realmente subir e utilizar o CI, sabe? Para ser mais rápido, mais efetivo." E, aprovando o formato que o firstmate propôs (o cartão segue como pull request só de documentos, aprovado antes de qualquer código; o build vai num pull request por etapa, ou por grupo de unidades quando a etapa é grande; um commit por unidade, cada uma construída, testada e validada antes do seu commit; o revisor Opus lê commit a commit; o CI roda uma vez, no fim; o merge guarda os commits): "Sim, por exemplo U1, U2 e U3 seriam um PR por exemplo". Muda a L9, a L13 e as skills `conexus-build`, `conexus-review` e o cartão leve do padrão. diff --git a/docs/steps/a-politica-de-dados-pessoais.md b/docs/steps/a-politica-de-dados-pessoais.md index 77517826..52bda60c 100644 --- a/docs/steps/a-politica-de-dados-pessoais.md +++ b/docs/steps/a-politica-de-dados-pessoais.md @@ -65,7 +65,7 @@ Builder), mesmo que a pessoa tenha. **`C1`** é a mesma regra na cópia: a cópi pessoal de um tipo que quem a liga pode ver, e a apaga ao desligar. **Vale a pena?** Sim. O corte de hoje (`omit` no Mercado Livre) já é um mecanismo por caminho; a -política troca a lista fixa por "caminho + tipo" e põe a decisão no papel. São cerca de 1.150 linhas, +política troca a lista fixa por "caminho + tipo" e põe a decisão no papel. São cerca de 1.070 linhas, e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo (`sankhya/operations/buscas.ts:30-34`, `cadastros.ts:35`), entra na mesma regra. @@ -96,7 +96,7 @@ e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo | Ana pergunta ao Assistente "quem comprou 3 vezes?" | "não tenho o comprador" | "Maria Fictícia (3 pedidos)"; o CPF não vai ao modelo | | Beto pergunta o mesmo | o mesmo | pedidos sem nome | | App "Quem comprou", publicado pela Ana com contato | só "pedido 123, R$ 89,90" | "Maria Fictícia, Campinas/SP" para quem você deu acesso ao app | -| Clientes do Sankhya, perguntados pelo Beto | CPF e nome | sem CPF e sem nome | +| Clientes do Sankhya (`buscarParceiros`), perguntados pelo Beto | CPF e nome | sem CPF e sem nome; a porta SQL fica como hoje (G) | | O registro | só escalares | mais `personal: ['contato']`, nunca o valor | **O roteiro de uso.** Você é dono, com o Mercado Livre e o Sankhya ligados. @@ -114,7 +114,7 @@ e o Sankhya, que hoje entrega CPF e nome de clientes a todo papel e ao modelo 5. Volte o papel do segundo e-mail para **Membro** e repita a pergunta do passo 3: os pedidos vêm sem nome. -**O que você faz, e quando.** Antes: nada novo. No começo: aprova esta página e responde F, G e H. +**O que você faz, e quando.** Antes: nada novo. No começo: aprova esta página (F, G e H você já respondeu). No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. **Fica de fora.** @@ -122,50 +122,58 @@ No fim: faz o roteiro, diz o que viu e aprova os textos da tela de Papéis. - **E, o advogado** (contrato de operador, suboperadores, cláusulas para o modelo no exterior): vem depois, como você disse. Não bloqueia o código. - **Sensível (`D1`)**: o tipo existe no kit e na regra, mas nenhum papel o recebe e a tela não o - mostra; as tabelas de RH do Sankhya ficam recusadas pela porta genérica (G). + mostra; o RH do Sankhya segue fora só pela frase da skill (`D1`), como hoje. - **Agentes publicados**: a 24.6 está congelada; hoje um agente publicado só escreve pelos conectores, não lê. Quando os agentes voltarem, a versão deles declara tipos como a do app. - **Prazo de guarda escolhido pela empresa** (a parte opcional de `C1`) e **apagar um titular a pedido**: cartão próprio, com a decisão do prazo. - **Máscara** ("M***a F***a") no lugar de tirar o campo: o twenty e a Shopify tiram; nós também. -**As decisões suas nesta etapa.** O estudo não as trouxe; elas aparecem ao desenhar o app e a porta. - -- **F. Num app, de quem é o "pode ver"?** Hoje o app usa as leituras que a versão declara, conferidas - contra o papel de quem publica (sua decisão `C1` da etapa 20, `app-calls.ts:103-109`), e quem abre o - app é quem você deu acesso. - - **F1 (recomendada):** igual para os tipos. A versão declara "mostro contato e endereço"; publicar - confere que quem publica tem esses tipos; quem abre o app vê o que ele mostra. Você controla quem - vê pelo acesso ao app. Exemplo: o vendedor da Capacidade 4, que entra só no app e não tem papel - na empresa, vê os clientes dele com nome. É a forma do Retool, do Appsmith e da Shopify (a - liberação é do app); vale igual para a cópia lida pelo app, que não pode filtrar por pessoa (o - Projeto lê o banco com um login só). Veredito: nativo (estende a `C1`). - - **F2:** o tipo segue o papel de quem abre. O Beto não vê nome no app da Ana; o vendedor da - Capacidade 4, sem papel na empresa, nunca vê nome; e uma cópia com coluna pessoal não pode ser - liberada a Projeto nenhum. +**As decisões suas nesta etapa, já respondidas em 2026-11-11.** O estudo não as trouxe; elas +apareceram ao desenhar o app e a porta. + +- **F. Num app, de quem é o "pode ver"?** Você: "F1, PORÉM QUERO entender a parte tencia de código + arquitetura que vamos implementar isso de forma funcional". Fica F1: a versão do app declara os tipos + que mostra ("contato e endereço"); Publicar confere que quem publica tem esses tipos; quem abre o + app vê o que ele mostra, e você controla quem abre pelo acesso ao app. É como o app já usa as + leituras hoje (sua decisão `C1` da etapa 20, `app-calls.ts:103-109`), e vale igual para a cópia lida + pelo app. O vendedor da Capacidade 4, que entra só no app, vê os clientes dele com nome. A parte + técnica está logo abaixo, em "Como funciona no código". - **Os apps já publicados**, de antes desta etapa, não declaram tipos. Eles passam a ler com os tipos do papel de quem os enviou, lidos a cada clique: um app que você enviou continua mostrando os clientes do Sankhya, e passa a mostrar também o comprador do Mercado Livre. Se quem enviou sair da empresa, o - app deixa de mostrar dado pessoal até ser publicado de novo. Não é uma decisão nova: segue F1. Se - escolher F2, esta linha some. -- **G. A porta genérica do Sankhya** (`consultar`, SQL livre; `carregar`). Ela lê qualquer coluna de - qualquer tabela, com qualquer apelido, então não dá para declarar caminhos. - - **G1 (recomendada):** três travas: (1) o conector declara as colunas pessoais do dicionário do - Sankhya pelo nome (`CGC_CPF` documento, `NOMEPARC` contato e as de endereço e contato que o - construtor achar no dicionário); (2) para quem não tem o tipo, cada célula da porta que for um CPF - válido, um e-mail ou um telefone sai, mesmo com outro nome de coluna; (3) a consulta que nomeia - uma tabela de RH (`TFP...`) é recusada (`D1`). O que ainda passa, dito: um nome ou endereço com - apelido inventado; uma tabela de RH lida por uma visão ou por `carregar` (a entidade do - dicionário não tem o nome `TFP`); os campos `AD_` que cada empresa cria, que o conector não conhece. Veredito: remendo (nenhuma referência tem SQL livre com política de campo; o - nativo seria o `GRANT` de coluna do banco, que no Sankhya não é nosso). - - **G2:** a porta não entrega nada a quem não tem os três tipos: o Assistente do Membro perde a - porta. -- **H. Onde entra no roteiro.** Pelo método (delivery L5), esta etapa é da trilha principal e espera - o Sync fechar, num pull request rascunho. - - **H1 (recomendada):** logo depois do Sync e **antes da Capacidade 4**, porque a 4 traz pessoas - que só entram no app e veem clientes, e hoje o Sankhya entrega CPF a todo papel. O Sync não precisa - esperar: a cópia já nasce certa (veja "O Sync" abaixo). - - **H2:** depois da Capacidade 4. + app deixa de mostrar dado pessoal até ser publicado de novo. Segue de F1. +- **G. A porta genérica do Sankhya** (`consultar`, SQL livre; `carregar`). Você: "G Não vou limitar + nada disso". A porta fica fora desta política por decisão sua: nenhuma trava nova nela, e o RH segue + fora só pela frase da skill (`D1`), como hoje. Por ela, o Assistente ainda pode ler CPF e nome de + clientes do Sankhya com SQL. +- **H. Onde entra no roteiro.** Você: "H1". Logo depois do Sync e antes da Capacidade 4. Pelo método + (delivery L5), o cartão espera o Sync fechar num pull request rascunho; o Sync não precisa esperar, + porque a cópia já nasce sem dado pessoal (veja "O Sync" abaixo). + +**Como funciona no código** (a sua pergunta em F1). Uma leitura passa por cinco lugares, sempre os +mesmos, para qualquer conector: + +1. **No conector** (`packages/connectors/src//operations/`), cada leitura ganha uma linha: + `personal: [{ path: 'buyer', kind: 'contato' }, { path: 'buyer.billing_info', kind: 'documento' }]`. + É só dado; o conector não corta nada. +2. **No kit** (`packages/connectors/src/kernel/personal.ts`) moram os quatro tipos e a conferência + que roda no CI: se um caminho declarado não existe na resposta do dublê, ou se aparece nela um CPF, + e-mail ou telefone num caminho não declarado, o teste falha. +3. **Nos papéis** (`apps/server/src/hub/identity/roles.ts`), o better-auth ganha o recurso + `personal` com as ações `contato`, `endereco`, `documento`, `sensivel`, guardado no mesmo registro do + papel que já guarda "usa a leitura Pedidos". A tela de Papéis marca e desmarca essas ações. +4. **Na publicação do app** (`publish-check.ts`), o manifesto do app diz `personal: ['contato']` por + conector, e Publicar confere contra o papel de quem publica, como já confere as leituras. +5. **Na leitura comum** (`apps/server/src/hub/connectors/read.ts`, `readThrough`), por onde passam o + app, o Assistente, o Builder e a cópia: depois da resposta do conector, uma função pura, + `shapePersonal`, recebe a resposta, as declarações e os tipos de quem a resposta serve (o papel da + pessoa no Assistente e no Builder, menos documento; o que a versão declara no app; o que a cópia + declara na cópia) e tira cada caminho cujo tipo não está liberado. Em seguida o registro + `CONNECTOR_CALLED` anota quais tipos foram entregues e quais foram tirados, nunca o valor. + +Um conector novo, escrito pela equipe ou pelo Builder, só faz o passo 1; os passos 2 a 5 já valem para +ele. **O Sync.** O cartão do Sync supõe o corte: a linha 47 ("a leitura do conector já o corta") e o critério 10 ("o `omit` da leitura vale para a cópia"). Com esta política, a cópia lê pela mesma @@ -178,7 +186,7 @@ leitura comum como uma versão que não declara tipo nenhum, então continua sem | Unidade | O que entrega | Pastas | Guia além do de código | Depende de | | --- | --- | --- | --- | --- | | U1 Os tipos nos papéis | a lista `PERSONAL_KINDS` no kit; o recurso `personal` nos `Statements`; dono e administrador com três tipos; `RoleView.personal`, `RoleCreate.personal`, `RoleChange.personal`; `personalKinds` na identidade; o registro da troca | `packages/connectors/src/kernel/`, `apps/server/src/hub/identity/`, `apps/server/src/hub/connectors/role-kit.ts`, `apps/server/src/main.ts`, `packages/hub-api/src/{company-roles,failures}.ts` | [security](../guides/security.md) | esta página | -| U2 A declaração e a aplicação num ponto | `personal` na operação e `personalColumns` na porta; `findUndeclared`; a conformidade; as declarações do Mercado Livre e do Sankhya, com o `omit`, o `BUYER` e o `without` apagados; `shapePersonal` em `readThrough`; `Speaker` `VERSION` com `personal`; menos documento para `assistant` e `builder`; os escalares em `CONNECTOR_CALLED`; a porta do Sankhya (G1) | `packages/connectors/src/{kernel,mercado-livre,sankhya}/`, `packages/connectors/package.json`, `apps/server/src/hub/connectors/`, `apps/server/src/hub/identity/may-use.ts`, `apps/server/src/builder/agents/` | [security](../guides/security.md) | U1; a U2b do Sync no `main` (pasta do Mercado Livre, L1); entra junto com a U3 | +| U2 A declaração e a aplicação num ponto | `personal` na operação; `findUndeclared`; a conformidade; as declarações do Mercado Livre e do Sankhya, com o `omit`, o `BUYER` e o `without` apagados; `shapePersonal` em `readThrough`; `Speaker` `VERSION` com `personal`; menos documento para `assistant` e `builder`; os escalares em `CONNECTOR_CALLED` | `packages/connectors/src/{kernel,mercado-livre,sankhya}/`, `packages/connectors/package.json`, `apps/server/src/hub/connectors/`, `apps/server/src/hub/identity/may-use.ts`, `apps/server/src/builder/agents/` | [security](../guides/security.md) | U1; a U2b do Sync no `main` (pasta do Mercado Livre, L1); entra junto com a U3 | | U3 O app declara os tipos | `personal` no manifesto por conector; a conferência de Publicar; `sentBy` na chamada do app e a versão anterior ao campo; a skill `conexus-data` | `packages/app-contract/src/manifest.ts`, `packages/hub-api/src/{apps,failures}.ts`, `apps/server/src/hub/apps/`, `apps/server/src/hub/connectors/{publish-check,app-calls}.ts`, `packages/app/skills/conexus-data/` | [security](../guides/security.md) | U2 | | U4 A tela de Papéis | a seção **Dados pessoais de clientes** no editor, criar e abrir | `apps/web/src/hub/identity/` | [web](../guides/web.md) | U1 | | U5 A coluna pessoal na cópia | o tipo de cada coluna, derivado do caminho; Ligar confere o papel; o `GRANT SELECT (colunas)` ao Projeto; Desligar apaga as colunas pessoais | `apps/server/src/hub/copies/`, `apps/server/src/hub/connectors/copy-port.ts`, `apps/server/src/platform/copy-sql.ts` | [data](../guides/data.md) | U2; as U3 e U4 do Sync no `main` | @@ -186,8 +194,8 @@ leitura comum como uma versão que não declara tipo nenhum, então continua sem U4 corre ao lado de U2 e U3, porque não divide pasta com elas. U5 espera o Sync. Todas entram no mesmo pull request da etapa, então o `main` nunca vê a U2 sem a U3. -**Orçamento de código alterado.** U1: 200. U2: 430. U3: 160. U4: 150. U5: 200. Total: -cerca de 1.140 linhas, abaixo de delivery L2. +**Orçamento de código alterado.** U1: 200. U2: 360. U3: 160. U4: 150. U5: 200. Total: +cerca de 1.070 linhas, abaixo de delivery L2. ## Os critérios de aceite @@ -205,7 +213,7 @@ Cada um falha hoje: não há `personal` no kit, nem recurso `personal` nos papé | 8 | U2 | Pela porta do Assistente, a mesma leitura `pedidos` do dublê: o dono recebe `buyer` com o nome plantado e sem `billing_info`; o membro recebe a linha sem `buyer`; em `envio`, o membro não recebe `destination` (nem `receiver_id` nem `comments`) | `connectors/personal-read.test.ts`, "dois papéis, duas respostas" | must | | 9 | U2 | Pelo Assistente e pelo Builder, nem `pedidos` (`buyer.billing_info`) nem `notaFiscalDoPedido` (`recipient`) trazem o CPF plantado, nem para o dono (B1); pelo app de uma versão que declara documento, trazem | `connectors/personal-read.test.ts`, "documento não vai ao modelo" | must | | 10 | U2 | `buscarParceiros` pelo Assistente de um membro não traz `documento` nem `nome` | `connectors/personal-read.test.ts`, "Sankhya pelo membro" | must | -| 11 | U2 | `consultar` com `SELECT CGC_CPF AS X FROM TGFPAR` pelo Assistente de um membro não traz o CPF plantado; com `FROM TFPFUN` é recusado (`REFUSED`, `INPUT_INVALID`) antes da chamada | `connectors/personal-read.test.ts`, "porta com apelido", "RH recusado" | must | +| 11 | U2 | A porta genérica do Sankhya (`consultar`, `carregar`) responde como hoje: as mesmas linhas para o dono e para o membro (fora da política, G) | `connectors/personal-read.test.ts`, "a porta fica como hoje" | must | | 12 | U2 | Cada `CONNECTOR_CALLED` de uma leitura que declara tipos traz `personal` (os tipos entregues) e `withheld` (os tirados), e nenhuma linha do registro traz o valor plantado | `connectors/personal-read.test.ts`, "o registro sem o valor" | must | | 13 | U2 | Uma página de cópia (`by: 'copy'`) de `pedidos` não traz o nome plantado em célula nenhuma (o critério 10 do Sync, na forma nova) | `connectors/copy-port.test.ts`, "a cópia sem tipo" | must | | 14 | U2 | `grep -rn "omit" packages/connectors/src/mercado-livre` não acha nada | revisão | must | @@ -224,7 +232,6 @@ Cada um falha hoje: não há `personal` no kit, nem recurso `personal` nos papé | Campo de contrato | Quem escreve | Quem lê | | --- | --- | --- | | `Operation.personal` | o autor do conector | a conformidade (U2), `readThrough` (U2), a cópia (U5) | -| `ReadDoor.personalColumns` | o autor do conector (U2) | `shapeDoor` (U2) | | `ConnectorCall.sentBy` | `handoff.ts` (U3) | `app-calls.ts` (U3), só para a versão anterior ao campo | | `RoleView.personal` | `readRoles` (U1) | a tela de Papéis (U4) | | `RoleCreate.personal`, `RoleChange.personal` | a tela (U4) | `permissionOf` (U1) | @@ -255,14 +262,13 @@ As leituras não gravam nada novo; o escalar do registro não é gravação. | Os tipos como permissão do papel | U1 | `identity/roles.ts:68-82` (recursos e ações), `role-statements.ts`, `company-roles.ts:237-244` | better-auth 1.7.8 `createAccessControl`, `hasPermission`, `createOrgRole`/`updateOrgRole` (o editor de hoje); twenty `field-permission.entity.ts:65-66` | uma ação no papel, lida a cada chamada | nativo: recurso `personal` com as ações `contato`, `endereco`, `documento`, `sensivel` | — | A1 ("para dar para outro cargo") | — | | Aplicação num ponto | U2 | o corte dentro de `mercado-livre/answers.ts:101-126`, antes do papel | twenty `common-select-fields-builder.ts:46-52`; n8n `node-defined-field-redaction.strategy.ts:44-69` | um filtro único sobre o resultado | embrulhado: `shapePersonal`, função pura, chamada em `readThrough` depois de `answerOf` | crit. 8 | — | `apps/server/src/hub/connectors/personal.ts` | | Documento nunca ao modelo | U2 | `cut.ts` (só tamanho) | Mastra `PIIDetector` (`pii-detector.d.ts:97,105`), por modelo; LGPD art. 33 | detecção por LLM | embrulhado: o leitor `assistant`/`builder` perde `documento` | crit. 9 | B1 | — | -| A porta genérica do Sankhya | U2 | `sankhya/door.ts:69-145` (qualquer coluna); `sankhya/skill/SKILL.md:21` (RH só por texto) | n8n `unknownNodes` oculta tudo do que não conhece (`node-defined-field-redaction.strategy.ts:52-58`); Postgres `GRANT SELECT (colunas)` (PostgREST `test/spec/fixtures/privileges.sql:55`) | fechar o que não se conhece; ou o banco libera por coluna | remendo: colunas pelo nome, formas conhecidas nas células, `TFP` recusado | crit. 11, com `@brazilian-utils/brazilian-utils@2.5.0`; entrada em `docs/reference/boundaries.md` | G | — | | O registro sem o valor | U2 | `read.ts:189-204` (`CONNECTOR_CALLED`, escalares) | n8n `execution-redaction.service.ts:250` (o evento da revelação; as recusas em `:133,145,168`); Mercado Livre "registro de acesso" (documentado) | um evento por revelação | embrulhado: dois escalares na linha que já existe (security S41) | crit. 12 | — | — | | O app declara os tipos | U3 | `app-contract/src/manifest.ts:46-57`; `publish-check.ts:35-61` (`C1`) | Shopify (a liberação é do app); a `C1` da etapa 20 | o app pede o nível; a plataforma confere | nativo: estende o manifesto e a conferência de Publicar | — | F | — | | A tela de Papéis | U4 | `apps/web/src/hub/identity/role-editor.tsx:152`, `role-unattended.tsx` | a seção Agentes do mesmo editor | uma caixa por ação | nativo | — | A1 | `apps/web/src/hub/identity/role-personal.tsx` | | A cópia guarda só o liberado | U5 | o critério 10 do Sync; `copies/grants.ts` (`GRANT` da tabela) | Airbyte `docs/platform/using-airbyte/mappings.md:7` (por conexão); PostgREST `privileges.sql:55` | por conexão; coluna liberada por `GRANT` | nativo: `GRANT SELECT (colunas)` | — | `C1` | — | | Apagar ao desligar | U5 | o Desligar do Sync U3 (`copy_table_left`) | Mercado Livre (termos e segurança, documentado) | apagar as cópias e a PII | embrulhado: no mesmo Desligar | crit. 18 | `C1` | — | -**Ficam fora da tabela, com a razão.** RH e dados sensíveis (`SKILL.md:21`, LGPD art. 11): `D1`; a porta recusa `TFP` (G, U2). A detecção por modelo (o `PIIDetector` do Mastra): a declaração é determinística e mais barata; o detector fica como rede opcional para texto livre, num cartão futuro. +**Ficam fora da tabela, com a razão.** RH e dados sensíveis (`SKILL.md:21`, LGPD art. 11): `D1`, só pela frase da skill. A porta genérica do Sankhya (`sankhya/door.ts:69-145`): fora da política por decisão do fundador (G, 2026-11-11), sem trava nova. A detecção por modelo (o `PIIDetector` do Mastra): a declaração é determinística e mais barata; o detector fica como rede opcional para texto livre, num cartão futuro. **A medida do transplante.** Nada vem do repositório antigo. @@ -341,15 +347,15 @@ declarado não existe no valor; e tirar o `omit` sem a aplicação entregaria o ### As linhas da tabela A declaração; a conferência das formas não declaradas; a aplicação num ponto; documento nunca ao -modelo; a porta do Sankhya (G1); o registro. +modelo; o registro. ### O raio da mudança | Pasta, ou dono de um contrato | Cria, muda ou apaga | Quem importa ou chama | | --- | --- | --- | -| `packages/connectors/src/kernel/` | muda `personal.ts` (`PersonalField`, `findUndeclared`), `operation.ts`, `definition.ts` (`ReadDoor.personalColumns`), `conformance.ts`, `index.ts` | todo conector; o servidor | +| `packages/connectors/src/kernel/` | muda `personal.ts` (`PersonalField`, `findUndeclared`), `operation.ts`, `conformance.ts`, `index.ts` | todo conector; o servidor | | `packages/connectors/src/mercado-livre/` | apaga `omit`, `BUYER` de `vendas.ts`, `without` e o corte de `rows()` (`answers.ts:78-126`, `shared.ts:47,56-61`); declara `personal`; troca os `summary` que dizem que o comprador não vem (`envios.ts:12`, `faturamento.ts:55`) e a skill; planta a cidade da entrega no dublê | o kit | -| `packages/connectors/src/sankhya/` | declara `personal` em `operations/{buscas,cadastros}.ts` e `personalColumns` em `door.ts`; a recusa de `TFP` em `read-only-sql.ts` | o kit | +| `packages/connectors/src/sankhya/` | declara `personal` em `operations/{buscas,cadastros}.ts`; `door.ts` e `read-only-sql.ts` não mudam (G) | o kit | | `packages/connectors/package.json` | acrescenta `@brazilian-utils/brazilian-utils` (já no lock, pelo `hub-api`) | — | | `apps/server/src/hub/connectors/` | cria `personal.ts`; muda `read.ts`, `rows.ts` (`Deps.identity`), `test-write.ts:113` | `app-calls.ts`, `builder-port.ts`, `assistant-port.ts`, `copy-port.ts`, `notes.ts` | | `apps/server/src/hub/identity/may-use.ts` | `Speaker` `VERSION` ganha `personal: readonly PersonalKind[]` | passam `[]`: `builder/agents/tools.ts:184`, `agent.ts:207,242`, `frozen.ts:145,234` (escritas de agente), `copy-port.ts:145`, e os testes `role-doors.test.ts`, `code-tool.test.ts`, `publish.test.ts`, `registry-line.test.ts`; `app-calls.ts:105` muda na U3 | @@ -363,10 +369,8 @@ export type PersonalField = Readonly<{ path: string; kind: PersonalKind }> /** As formas conhecidas achadas num valor fora dos caminhos dados: caminho e tipo, nunca o valor. */ export function findUndeclared(value: unknown, declared: readonly PersonalField[]): readonly Readonly<{ path: string; kind: 'documento' | 'contato' }>[] // operation.ts, em Operation: personal?: readonly PersonalField[] -// definition.ts, em ReadDoor: personalColumns?: readonly Readonly<{ column: string; kind: PersonalKind }>[] // apps/server/src/hub/connectors/personal.ts export function shapePersonal(value: unknown, personal: readonly PersonalField[], allowed: ReadonlySet): Readonly<{ value: unknown; given: PersonalKind[]; withheld: PersonalKind[] }> -export function shapeDoor(value: unknown, columns: readonly Readonly<{ column: string; kind: PersonalKind }>[], allowed: ReadonlySet): ReturnType /** Os tipos de quem a resposta serve: o papel da pessoa, ou o que a versão declara; menos documento para um modelo. */ async function personalKindsOf(deps: Deps, a: ReadRequest): Promise> ``` @@ -376,8 +380,8 @@ vence:** `buyer` contato e `buyer.billing_info` documento quer dizer que quem te documento recebe `buyer` sem `billing_info`; `shapePersonal` anda os caminhos do mais fundo ao mais raso e tira cada um cujo tipo não está em `allowed` (o twenty tira, não mascara). Em `readThrough`, depois de `answerOf` (`read.ts:181`) e só para `OK`. `by: 'assistant' | 'builder'` tira -`documento`; `sensivel` nunca está num papel. A porta: `shapeDoor` tira as colunas declaradas e as -células que `findUndeclared` acha. `findUndeclared` olha textos e inteiros (um CPF pode vir como +`documento`; `sensivel` nunca está num papel. A porta genérica do Sankhya não declara nada e passa como hoje (G); a conferência das formas não +roda nas operações de `readDoor`, cuja resposta depende da consulta. `findUndeclared` olha textos e inteiros (um CPF pode vir como número JSON: o inteiro vira texto de 11 dígitos com zeros à esquerda antes de `isValidCpf`), e `isValidPhone` só em texto de 10 a 13 dígitos, para não pegar ids. Modelo: `read.ts` cumpre C21 (`ReadAnswer`), C23 (`ConnectorId.parse` na entrada) e C26 (um `readThrough`). @@ -390,7 +394,7 @@ número JSON: o inteiro vira texto de 11 dígitos com zeros à esquerda antes de ### A ordem do trabalho 1. `personal.ts` do kit: `PersonalField` e `findUndeclared`. Regras: C21, C26. -2. `operation.ts` e `definition.ts`: `personal` e `personalColumns`. +2. `operation.ts`: `personal`. 3. `conformance.ts`: para cada probe com resposta, cada caminho declarado existe em ao menos uma linha (`valueAt`, `operation.ts:103`), e `findUndeclared` volta vazio. 4. Mercado Livre, apagando antes de somar (C75): o `omit` sai de `rows()`; `pedidos` e `pedido` @@ -404,11 +408,10 @@ número JSON: o inteiro vira texto de 11 dígitos com zeros à esquerda antes de `reclamacoes`) passam pela conformidade: se o dublê não planta nada, nada se declara. Os `summary` e a skill deixam de dizer que o comprador não vem. 5. Sankhya: `buscarParceiros` `documento` documento e `nome`, `razaoSocial` contato; `clientes` pelos - nomes que o dublê traz; `sankhyaDoor` com `personalColumns`. + nomes que o dublê traz. A porta (`door.ts`) não muda. 6. Servidor: `personal.ts`; `Speaker` `VERSION` com `personal`, todo construtor passa `[]` (um - caminho que esquece não compila); `readThrough` com `personalKindsOf`, `shapePersonal` ou - `shapeDoor`, e os escalares `personal` e `withheld` em `CONNECTOR_CALLED`. -7. `read-only-sql.ts`: recusar `\bTFP\w*` no texto de `consultar`. Regras: S40, S41, C21, T5. + caminho que esquece não compila); `readThrough` com `personalKindsOf` e `shapePersonal`, e os escalares `personal` e `withheld` em `CONNECTOR_CALLED`. +Regras: S40, S41, C21, T5. ### Como se prova From fb76b03a1284e6c906f309fa0da3ddb7488bd59f Mon Sep 17 00:00:00 2001 From: developmentconexus Date: Wed, 11 Nov 2026 20:00:39 -0300 Subject: [PATCH 4/5] =?UTF-8?q?docs(conectores):=20a=20aprova=C3=A7=C3=A3o?= =?UTF-8?q?=20da=20p=C3=A1gina=20da=20pol=C3=ADtica=20de=20dados=20pessoai?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/approvals.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/approvals.md b/docs/approvals.md index 27ed3abf..ef5b21ed 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -945,6 +945,8 @@ Decisão do fundador no meio da etapa (L6), depois da aprovação do cartão, em ## A política de dados pessoais dos conectores +Cartão de A política de dados pessoais dos conectores (`docs/steps/a-politica-de-dados-pessoais.md`) aprovado em 2026-11-11 (commit 9656e2addf8984e5e6d9cfa9c884965411c26645), depois de ler o fluxo do cartão em quatro cenas (a dona pede o app ao Builder; outras pessoas abrem o app pela F1; um cargo sem tipos pede o mesmo app e o Builder explica e o Publicar para; o Assistente responde com nomes e sem CPF): "Gostei aprovado" + Decisões do fundador antes da aprovação da página (L6). Em 2026-11-11, às perguntas do estudo da política de dados pessoais (A, quem vê dados pessoais de clientes; B, o que vai ao modelo; C, a cópia; D, os sensíveis; E, o advogado): "A1, mas seria uma capabilidade para criar e selecionar POR EXEMPLO PARA DAR PARA OUTRO cargo"; "B1, okay, mas como vamos tratar isso Fácil a gente selecionar um conhecimento específico, tipo CPF, mas eu sempre quero pensar na lógica, porque a gente vai trabalhar com a criação de vários conectores, de uma plataforma muito robusta. Qual que seria a lógica para a gente conseguir fazer isso, entende? Vale a pena? É uma gambiarra? Referências fazem? Entende meu ponto?"; "`C1` porém mesma coisa de B"; "D1"; "E Só vai vir depois". No mesmo dia, às decisões F (num app, de quem é o "pode ver"), G (a porta genérica do Sankhya) e H (o lugar no roteiro) da página do cartão (`docs/steps/a-politica-de-dados-pessoais.md`) no commit 30e2b034ca27eebb0f9559e3052b942d1eb06fcd: "F1, PORÉM QUERO entender a parte tencia de código arquitetura que vamos implementar isso de forma funcional / G Não vou limitar nada disso / H1". A página as escreve como decididas: F1, com a seção "Como funciona no código"; G, a porta genérica do Sankhya fora da política, sem trava nova, e o RH só pela frase da skill, como hoje; H1, logo depois do Sync e antes da Capacidade 4. ## Mudança de método: um pull request por grupo de unidades From 05467671a04afc2263ee62046b9667c2d1ed0104 Mon Sep 17 00:00:00 2001 From: developmentconexus Date: Wed, 11 Nov 2026 20:01:00 -0300 Subject: [PATCH 5/5] =?UTF-8?q?docs(conectores):=20a=20linha=20de=20aprova?= =?UTF-8?q?=C3=A7=C3=A3o=20na=20forma=20que=20check-approval=20l=C3=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/approvals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/approvals.md b/docs/approvals.md index ef5b21ed..ab912d79 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -945,7 +945,7 @@ Decisão do fundador no meio da etapa (L6), depois da aprovação do cartão, em ## A política de dados pessoais dos conectores -Cartão de A política de dados pessoais dos conectores (`docs/steps/a-politica-de-dados-pessoais.md`) aprovado em 2026-11-11 (commit 9656e2addf8984e5e6d9cfa9c884965411c26645), depois de ler o fluxo do cartão em quatro cenas (a dona pede o app ao Builder; outras pessoas abrem o app pela F1; um cargo sem tipos pede o mesmo app e o Builder explica e o Publicar para; o Assistente responde com nomes e sem CPF): "Gostei aprovado" +Cartão curto de A política de dados pessoais dos conectores (`docs/steps/a-politica-de-dados-pessoais.md`) aprovado em 2026-11-11 (commit 9656e2addf8984e5e6d9cfa9c884965411c26645), depois de ler o fluxo do cartão em quatro cenas (a dona pede o app ao Builder; outras pessoas abrem o app pela F1; um cargo sem tipos pede o mesmo app e o Builder explica e o Publicar para; o Assistente responde com nomes e sem CPF): "Gostei aprovado" Decisões do fundador antes da aprovação da página (L6). Em 2026-11-11, às perguntas do estudo da política de dados pessoais (A, quem vê dados pessoais de clientes; B, o que vai ao modelo; C, a cópia; D, os sensíveis; E, o advogado): "A1, mas seria uma capabilidade para criar e selecionar POR EXEMPLO PARA DAR PARA OUTRO cargo"; "B1, okay, mas como vamos tratar isso Fácil a gente selecionar um conhecimento específico, tipo CPF, mas eu sempre quero pensar na lógica, porque a gente vai trabalhar com a criação de vários conectores, de uma plataforma muito robusta. Qual que seria a lógica para a gente conseguir fazer isso, entende? Vale a pena? É uma gambiarra? Referências fazem? Entende meu ponto?"; "`C1` porém mesma coisa de B"; "D1"; "E Só vai vir depois". No mesmo dia, às decisões F (num app, de quem é o "pode ver"), G (a porta genérica do Sankhya) e H (o lugar no roteiro) da página do cartão (`docs/steps/a-politica-de-dados-pessoais.md`) no commit 30e2b034ca27eebb0f9559e3052b942d1eb06fcd: "F1, PORÉM QUERO entender a parte tencia de código arquitetura que vamos implementar isso de forma funcional / G Não vou limitar nada disso / H1". A página as escreve como decididas: F1, com a seção "Como funciona no código"; G, a porta genérica do Sankhya fora da política, sem trava nova, e o RH só pela frase da skill, como hoje; H1, logo depois do Sync e antes da Capacidade 4.