Jornada de Dados · aula prática

Projeto completo: uma área de atendimento com AI Dev Kit.

Todo comando e todo prompt desta página é copiável. Siga os blocos na ordem e, ao final, você terá a área de atendimento de uma distribuidora de energia funcionando no Databricks: medalhão, dashboard, copiloto conversacional e travas de segurança para o agente — construídos com as skills oficiais do AI Dev Kit, um prompt de cada vez.

Duração60 a 75 min
CustoR$ 0 · Free Edition
Pré-requisitoNenhum de DABs
EntregávelÁrea de atendimento no ar

Preencha antes de começar. Os dois valores entram em todos os comandos da página — do auth login do bloco 2.0 ao grants update do 7.0 — e saem prontos quando você clica em copiar. Fica salvo no navegador; o botão zerar, no rodapé do índice, limpa.

0.0

O que você vai construir.

Entenda o destino antes de sair digitando.

O projeto é a Grid Intelligence: a plataforma de dados e IA da Luz do Vale Distribuidora S.A., uma distribuidora de energia elétrica fictícia da região de Campinas. Quatro bases sintéticas com quatro anos de histórico — unidades consumidoras, interrupções de fornecimento, consumo diário e chamados de atendimento com transcrição — viram três camadas, cada uma em um schema dedicado do Unity Catalog.

flowchart TB
  RAW["<b>raw.landing</b> · Parquet, um arquivo por base<br/><small>unidades consumidoras · interrupcoes · consumo diario (5,85 mi) · chamados</small>"]
  BRONZE["<b>bronze</b> · ingestao fiel<br/><small>4 tabelas como chegaram. Nada e filtrado, nada e corrigido</small>"]
  SILVER["<b>silver</b> · onde a regra de negocio mora<br/><small>cadastro limpo · a regra dos 3 minutos · dedup e baseline · texto anonimizado e enriquecido</small>"]
  GOLD["<b>gold</b> · uma pergunta de diretoria por tabela<br/><small>continuidade · prioridade de inspecao · saude do cliente · painel do dia</small>"]
  USO["<b>consumo</b><br/><small>dashboard AI/BI · copiloto conversacional · relatorio executivo</small>"]

  RAW --> BRONZE --> SILVER --> GOLD --> USO
Do Parquet no volume até o copiloto. Cada seta é uma camada com uma regra própria — e nenhuma delas pode fazer o trabalho da seguinte.

Bronze

Ingestão fiel. Leitura negativa e interrupção de 40 segundos entram como chegaram: filtrar aqui é destruir evidência.

Silver

Limpeza, a regra regulatória dos 3 minutos, e as transcrições anonimizadas e enriquecidas por IA nativa.

Gold

Uma tabela por pergunta de diretoria: continuidade, prioridade de inspeção, saúde do cliente, painel do dia.

Por que uma distribuidora de energia

Porque cada tabela existe por causa de dinheiro real. Interromper além do limite regulatório gera compensação automática ao consumidor — R$ 1,1 bilhão pagos em 2024. Perda não técnica somou R$ 11,5 bilhões em 2025. E as ligações que ninguém lê contêm o aviso antecipado da reclamação na ouvidoria. É um domínio em que “por que essa tabela existe?” sempre tem resposta.

Em volta disso, o setup que faz a diferença no dia a dia:

  • DAB (Declarative Automation Bundle) — o projeto inteiro como código, com dev e prod isolados.
  • Skills oficiais — a documentação atualizada do Databricks dentro do contexto do agente.
  • CLI do Databricks — o agente enxergando e executando coisas no seu workspace, pelo mesmo comando que você usa.
  • Sequência de prompts — oito etapas, uma feature por vez, com deploy e resultado visível em cada uma.
  • Guard rails — travas determinísticas que impedem o agente de destruir dados.
Conta gratuita

Tudo aqui roda na Databricks Free Edition. Crie a sua em login.databricks.com. Ela só oferece compute serverless — por isso a resposta “sim” para serverless no bloco 4.0 é obrigatória.

0.1

O vocabulário, antes de tudo.

Dez termos. Sem eles, metade da aula vira ruído.

Distribuição de energia tem vocabulário próprio, e ele é preciso: trocar uma palavra por outra parecida muda o número. Vale gastar três minutos aqui — todo o resto da aula usa estes termos sem reexplicar.

Quem é quem

TermoO que éCuidado
UC
unidade consumidora
O ponto de entrega de energia — um medidor, um endereço. No dataset são 4.000. Não é sinônimo de cliente. É a confusão que mais estraga análise no setor
Cliente A pessoa. Pode ter três UCs: casa, sítio e a loja. Quem reclama na ouvidoria é o cliente, não a UC — por isso existe uma tabela por grão
Conjunto Subdivisão geográfica da área de concessão. São 6: Campinas Centro, Campinas Norte, Indaiatuba, Sumaré Industrial, Valinhos e Vale Rural. É a unidade que o regulador usa para cobrar. Todo indicador é por conjunto
Interrupção Um evento de rede em que um trecho fica sem energia. Tem início, fim, causa e quantas UCs atingiu. Não é a ligação do cliente reclamando — isso é o chamado
Religador Equipamento que desarma quando algo encosta na rede e religa sozinho segundos depois. É a razão de existir o corte de 3 minutos: a piscada não é apagão

Os dois indicadores que o regulador cobra

Os dois são médias por unidade consumidora, apuradas por mês e por conjunto. Não é o total de horas sem luz: é quanto, em média, cada UC daquele conjunto sentiu.

SiglaNomeRespondeUnidade
DEC Duração Equivalente de Interrupção por UC Quanto tempo, em média, cada UC ficou sem energia horas
FEC Frequência Equivalente de Interrupção por UC Quantas vezes, em média, cada UC ficou sem energia vezes
“Pior DEC” quer dizer maior DEC

Nos dois indicadores, quanto maior, pior — mais horas ou mais vezes sem luz. Então “o conjunto com o pior DEC” é o de maior valor, e é dele que a diretoria quer falar primeiro.

No dataset, o pior é o Vale Rural: 10,86 horas e 13,38 interrupções acumuladas em quatro anos, contra 4,78 h e 5,33 de Campinas Centro. Faz sentido — rede rural é mais longa, mais exposta a vegetação e mais demorada de atender.

Os dois andam juntos, mas medem coisas diferentes

Muitas interrupções curtas sobem o FEC e quase não mexem no DEC. Um único apagão de oito horas faz o contrário. Por isso o regulador cobra os dois: um mede constância, o outro mede gravidade.

Perdas e atendimento

TermoO que é
PNT
perdas não técnicas
Energia distribuída e não faturada. Vem de furto, de fraude no medidor e de erro de leitura, medição e faturamento — as três causas têm a mesma assinatura no dado
Baseline de consumo O padrão recente de consumo de uma UC, usado como referência de normalidade. Aqui: a média dos 90 dias anteriores à janela dos últimos 30
Prioridade de inspeção Alta, média ou baixa — quanto uma UC merece visita técnica. É a saída da análise de perdas, e é deliberadamente não um rótulo de fraude
Chamado Uma ligação para a central, com transcrição em texto livre. São 2.801 no dataset
Ouvidoria A instância de reclamação acima do atendimento comum — passa pela ANEEL. Cliente que a menciona na ligação é sinal de alerta caro
A palavra que este projeto não usa

Em nenhum lugar do código aparece fraude, furto ou culpado como rótulo de saída. Queda de consumo também é mudança de morador, imóvel desocupado, medidor com defeito e erro de leitura.

A saída é prioridade de inspeção. Em setor regulado, a diferença entre “prioridade” e “acusação” é jurídica, não estilística.

1.0

Ferramentas de base.

Quatro binários instalados e respondendo.

Databricks CLI

macOSterminal
brew tap databricks/tap
brew install databricks
databricks --version
WindowsPowerShell
winget install Databricks.DatabricksCLI
databricks --version
Erro que custa três tentativas

Para atualizar é brew upgrade databricks. Os comandos databricks update e databricks upgrade não existem — a CLI responde com erro de comando desconhecido.

uv, GitHub CLI e Claude Code

verificaçãoos quatro devem responder
databricks --version
uv --version
gh --version
claude --version

Se um deles não responder, instale só o que faltar. E se alguma dessas quatro ferramentas for nova para você, não precisa aprender aqui: cada uma tem uma trilha própria onde ela é destrinchada com calma.

ComandoFerramentaOnde a gente destrincha
databricks --version Databricks CLI Trilha Databricks — catálogo, Unity Catalog, jobs, pipelines e a plataforma inteira
uv --version uv Trilha Python — ambiente virtual, dependências, empacotamento e teste
gh --version GitHub CLI Trilha GitHub — versionamento, pull request, revisão e Actions
claude --version Claude Code Trilha Claude Code — prompts, skills, hooks, subagentes e guard rails
Aqui a gente usa, não ensina do zero

Esta aula é de projeto: as quatro ferramentas entram como meio, não como assunto. Se você travar em alguma delas, o caminho é a trilha correspondente — voltar aqui depois custa dez minutos, e tentar aprender as quatro ao mesmo tempo custa a aula inteira.

instalaçãosó o que faltar
# uv — gerenciador de pacotes Python
curl -LsSf https://astral.sh/uv/install.sh | sh

# GitHub CLI
brew install gh
gh auth login

# Claude Code
curl -fsSL https://claude.ai/install.sh | bash
2.0

Autenticar no Databricks.

Um perfil nomeado, validado por uma chamada real de API.

O host é a URL do seu workspace no navegador, a parte https://dbc-xxxxxxx-xxxx.cloud.databricks.com.

terminaltroque pelo seu host
databricks auth login --host https://SEU-WORKSPACE.cloud.databricks.com

O navegador abre, você aprova, e a CLI grava o perfil em ~/.databrickscfg. Quando ele pedir o nome do perfil, dê um nome — por exemplo grid_intelligence. Não deixe em DEFAULT.

terminalconferir
databricks auth profiles
databricks current-user me --profile grid_intelligence
Por que nomear o perfil

Quem atende cliente tem três, quatro workspaces na mesma máquina. Perfil nomeado mais --profile grid_intelligence explícito em todo comando é o que impede você de fazer deploy no workspace errado. E current-user me é a validação honesta: faz uma chamada de API de verdade, não apenas lê um arquivo local.

3.0

Repositório no GitHub.

Cria o repositório primeiro, entra na pasta, e só então o bundle.

terminalwizard interativo
gh repo create
PerguntaResposta
What would you like to do?Create a new repository on GitHub from scratch
Repository nameLakehouseDatabricks
DescriptionArea de atendimento de uma distribuidora de energia no Databricks: medalhao, dashboard e copiloto conversacional, construidos com Claude Code e as skills do AI Dev Kit.
VisibilityPublic ou Private, tanto faz
Clone locally?Yes
terminal
cd LakehouseDatabricks
A descrição não é enfeite

É a única linha que aparece na busca do GitHub, no seu perfil e no card que o LinkedIn monta quando você compartilha o link. Quem abre o repositório decide em dois segundos se vale a pena entrar — e essa decisão é tomada nessa frase, não no README. Diga o que o projeto faz e com que stack; deixe o "por quê" para o README.

Esqueceu na hora do wizard? Dá para acertar depois, sem abrir a interface:

terminaleditar a descrição depois
gh repo edit --description "Area de atendimento de uma distribuidora de energia no Databricks: medalhao, dashboard e copiloto conversacional, construidos com Claude Code e as skills do AI Dev Kit."

# de quebra, os topicos — e como o repositorio aparece nas buscas por tema
gh repo edit --add-topic databricks,lakehouse,claude-code,engenharia-de-dados,ai-dev-kit
GitLab ou Azure DevOps?

Funciona igual. Só muda o comando que cria o repositório e a sintaxe do arquivo de pipeline no bloco 10.0. Todo o resto da aula é idêntico.

4.0

databricks bundle init

O esqueleto do projeto, criado pelo template oficial.

terminaldentro de LakehouseDatabricks
databricks bundle init --profile grid_intelligence
Pergunta do wizardRespostaPor quê
Templatedefault-pythonjá traz src/, resources/ e a estrutura de pacote
Project namegrid_intelligencevira o nome da pasta e do bundle. Mesmo nome do catálogo de produção e do perfil da CLI: um termo só para procurar em tudo
Include a stub notebooknovamos gerar código de verdade
Include a stub DLT pipelinenoidem
Include a stub Python packageyesdá a estrutura de biblioteca e testes
Use serverlessyesobrigatório na Free Edition
Default catalog for any tables created by this projectworkspace (Enter)os catálogos do projeto ainda não existem — eles nascem no bloco 7.0. Trocar depois é uma linha no databricks.yml
terminal
cd grid_intelligence
Por que aceitar workspace e trocar depois

O valor dessa pergunta vira var.catalog no databricks.yml — e esse é o único ponto do projeto onde o catálogo de dev é nomeado. Nenhum arquivo .sql escreve o nome do catálogo: todos usam ${catalogo}, que vem da variável.

Ou seja: escolher errado aqui custa uma linha, não uma refatoração. Digitar um catálogo que ainda não existe, por outro lado, faz o wizard reclamar — e você perde a sequência na hora errada.

Por que se chama bundle

Vale desmontar o nome, porque ele explica a ferramenta inteira — e porque você vai encontrar dois nomes diferentes pela internet.

PalavraO que quer dizer
Bundle
pacote
Job, pipeline, dashboard, schema, volume e o código empacotado viajam juntos e têm estado compartilhado. Você não cria um job aqui e um pipeline ali: declara o conjunto, e o deploy reconcilia tudo de uma vez. É por isso que existe bundle destroy — o pacote sabe exatamente o que criou.
Declarative
declarativo
Você descreve o estado que quer, não os passos para chegar lá. Não existe “crie o schema, depois o volume, depois o job”: existe “é assim que o projeto deve estar”, e a plataforma calcula a diferença. Rodar o deploy duas vezes seguidas não duplica nada.
Automation
automação
O que se declara não é só código: é infraestrutura, agendamento e permissão. Por baixo é Terraform — o mesmo motor que times de infraestrutura usam para provisionar nuvem.
Dois nomes, a mesma coisa

Era Databricks Asset Bundles — cada peça é um ativo da plataforma. Hoje a CLI chama de Declarative Automation Bundles, mantendo a sigla DABs. Documentação, post de blog e vídeo anteriores usam o nome antigo; é a mesma ferramenta, mesmos comandos.

A troca faz sentido: o ponto não é catalogar ativos, é declarar o estado desejado e deixar a plataforma reconciliar.

A comparação que costuma acender a luz

Quem já usou Terraform reconhece na hora: mesma ideia de estado desejado, mesmo plan e apply — aqui chamados validate e deploy. Quem vem de docker-compose também: um arquivo declara vários serviços que sobem juntos. A diferença é que aqui os “serviços” são jobs, pipelines e tabelas.

O arquivo que o wizard escreveu

O databricks.yml é o projeto inteiro descrito em um lugar só. Vale ler linha por linha agora, porque tudo o que vem depois — pipeline, job, dashboard, agente — vai pendurar aqui.

arquivodatabricks.yml recém-gerado
bundle:
  name: grid_intelligence
  uuid: 338627ac-9807-4d4a-9ee6-aed306449a0a

include:
  - resources/*.yml

artifacts:
  python_artifact:
    type: whl
    build: uv build --wheel

variables:
  catalog:
    description: The catalog to use
  schema:
    description: The schema to use

targets:
  dev:
    mode: development
    default: true
    workspace:
      host: https://SEU-WORKSPACE.cloud.databricks.com
    variables:
      catalog: workspace
      schema: ${workspace.current_user.short_name}

  prod:
    mode: production
    workspace:
      host: https://SEU-WORKSPACE.cloud.databricks.com
      root_path: /Workspace/Users/SEU-EMAIL/.bundle/${bundle.name}/${bundle.target}
    variables:
      catalog: workspace
      schema: prod
    permissions:
      - user_name: SEU-EMAIL
        level: CAN_MANAGE
BlocoO que éPor que importa
bundle.name O nome do projeto para a plataforma Entra no caminho de deploy e no prefixo dos recursos. É o que você procura no workspace para achar tudo que veio deste repositório
bundle.uuid Identificador único, gerado uma vez É como a plataforma sabe que este deploy é o mesmo projeto de ontem, e não um novo. Nunca edite nem duplique entre repositórios — dois bundles com o mesmo uuid brigam pelo mesmo estado
include Quais arquivos de recurso entram É o que permite um arquivo por recurso em resources/, em vez de um YAML gigante. Job, pipeline e dashboard viram arquivos separados e revisáveis
artifacts O que precisa ser construído antes do deploy Aqui, empacotar src/ como wheel com uv build. É o que faz seu código virar biblioteca instalável no cluster, em vez de arquivo solto copiado
variables Declaração dos valores que mudam por ambiente Declarar aqui e atribuir no target é o que tira o nome do catálogo de dentro do SQL. Sem isso, isolar dev de prod vira busca-e-substitui
targets Os ambientes — aqui dev e prod Mesmo código, dois destinos. -t dev ou -t prod em cada comando escolhe qual
default: true O target usado quando você não passa -t Está em dev de propósito: esquecer a flag manda para desenvolvimento, nunca para produção
workspace.host Para qual workspace vai o deploy Veio preenchido do perfil que você autenticou no bloco 2.0
root_path Onde o projeto aterrissa no workspace Fixado em produção para garantir uma cópia só. Sem isso, cada pessoa que fizesse deploy criaria a sua
permissions Quem administra os recursos em produção Permissão como código: entra na revisão do pull request, igual ao resto

O que mode muda sozinho

Uma palavra, e a plataforma trata o deploy inteiro de forma diferente. É o mecanismo que impede o erro mais caro de engenharia de dados: rodar em produção achando que era teste.

Comportamentomode: developmentmode: production
Nome dos recursos prefixados com [dev seu_usuario] nome limpo, sem prefixo
Agendamentos e triggers pausados ativos
Concorrência de jobs execuções antigas são canceladas respeita a configuração do job
Quem é o dono você o principal de serviço da esteira
Isolamento cada pessoa tem a própria cópia uma cópia só, no root_path
O catálogo separa os ambientes — o nome do schema, não

Repare que dev e prod têm o mesmo catalog: workspace no arquivo recém-gerado. Isso a gente troca no bloco 7.0, quando os catálogos existirem: dev aponta para um, produção para outro.

E é o catálogo que isola, nunca o nome do schema. Os schemas mantêm os nomes exatos raw, bronze, silver e gold nos dois ambientes — é o que permite o mesmo SQL rodar em qualquer target sem uma linha diferente. Se o schema de dev virasse bronze_dev, cada query precisaria saber em que ambiente está rodando, e você teria dois códigos para manter.

A pegadinha do mode: development

O prefixo [dev seu_usuario] vale para jobs, pipelines e dashboards — e também para schemas, que é onde ele atrapalha: bronze viraria seunome_bronze. A trava para isso é experimental.skip_name_prefix_for_schema: true, e ela aparece no bloco 7.0. Guarde o sintoma: se os schemas nascerem com seu nome na frente, é isso.

O ciclo, do init ao deploy

Três comandos, sempre na mesma ordem. Você vai repetir isso a cada mudança do projeto — e a esteira do bloco 10.0 vai repetir por você quando o destino for produção.

terminalo ciclo de trabalho
# 1. confere a sintaxe e resolve as variaveis, sem tocar no workspace
databricks bundle validate -t dev --profile grid_intelligence

# 2. constroi o wheel, sobe os arquivos e cria os recursos
databricks bundle deploy -t dev --profile grid_intelligence

# 3. executa um recurso especifico, pela CHAVE dele no YAML
#    (agora so existe o sample_job que veio do template)
databricks bundle run sample_job -t dev --profile grid_intelligence

# ver o que existe la, sem abrir a interface
databricks bundle summary -t dev --profile grid_intelligence
validate não é opcional

Ele resolve as variáveis e mostra o YAML final, já com ${var.catalog} substituído. É onde você descobre que digitou o nome errado — antes de criar recurso no workspace, e não depois.

O que cada pasta faz

CaminhoPapel
databricks.ymlo coração: nome do bundle, variáveis, targets dev e prod
resources/jobs, pipelines, schemas, dashboards — um .yml por recurso
src/o código de verdade: SQL das transformações, notebooks, biblioteca
tests/testes unitários — rodam no CI antes de qualquer deploy
fixtures/CSV e JSON de apoio para os testes
flowchart TB
  YML["databricks.yml<br/><small>uma variavel var.catalog</small>"]

  YML --> DEV["target dev<br/><small>catalog: grid_dev</small>"]
  YML --> PROD["target prod<br/><small>catalog: grid_intelligence</small>"]

  DEV --> DS["grid_dev.raw<br/>grid_dev.bronze<br/>grid_dev.silver<br/>grid_dev.gold"]
  PROD --> PS["grid_intelligence.raw<br/>grid_intelligence.bronze<br/>grid_intelligence.silver<br/>grid_intelligence.gold"]

  DS --> N1["<b>mesmo SQL, sem alteracao</b><br/><small>o nome do catalogo nunca aparece no codigo</small>"]
  PS --> N1
Dev e prod isolados pelo catálogo, nunca pelo nome do schema. É o que permite o mesmo SQL rodar nos dois ambientes sem uma linha diferente.
A ideia central da aula

O DAB traz engenharia de software para dentro de engenharia de dados: ambiente isolado, revisão por pull request e recurso provisionado por código — por baixo é Terraform. É a forma de desenvolvimento recomendada pela própria Databricks, e o host no databricks.yml já veio preenchido do perfil que você autenticou no bloco 2.0.

5.0

Python, venv e primeiro commit.

Ambiente reproduzível e o projeto salvo no GitHub.

terminal
uv python pin 3.12
uv venv --seed
uv sync --dev
Por que 3.12 e não 3.13

Parte das bibliotecas Python do Databricks — databricks-connect e databricks-dlt — ainda não publica wheel para 3.13. O próprio pyproject.toml do template declara requires-python = ">=3.10,<3.13". O --seed instala o pip dentro do venv, o que evita dor de cabeça com ferramenta que espera encontrá-lo.

terminalprimeiro commit
git add .
git commit -m "Adiciona projeto inicial Declarative Automation Bundle"
git push
6.0

Skills oficiais.

O agente passa a saber Databricks — documentação oficial dentro do contexto.

O que mudou no AI Dev Kit 2.0

As skills saíram do repositório do AI Dev Kit e viraram parte do repositório oficial databricks/databricks-agent-skills, instalável pela própria CLI do Databricks. O servidor MCP deixou de fazer parte do setup padrão.

Se você já usou a versão antiga, o instalador limpa as skills anteriores e a configuração do MCP antes de instalar as novas. Deixe ele fazer isso — é o caminho suportado. O MCP continua existindo e mantido, mas agora é instalação separada e opcional; skills e MCP não têm mais relação entre si.

terminalinstalador oficial
databricks aitools install
PerguntaResposta
Quais agentesClaude Code (e Codex, se você usa)
Profilegrid_intelligence
Escopoglobal — vale para todo projeto na máquina
Grupos de skillstodas — é o que instala como plugin
Limpar instalação antigasim (o padrão)

O instalador ainda traz as skills de MLflow, que vêm de outro repositório e não estão na CLI. Ao final, ele oferece confirmar a autenticação — aceite.

flowchart TB
  REPO["<b>databricks-agent-skills</b><br/><small>repositorio oficial, com a documentacao vigente</small>"]
  INST["<code>databricks aitools install</code>"]
  SK["skills instaladas como plugin<br/><small>prefixo databricks: em /skills</small>"]
  AG["<b>Claude Code</b>"]

  ESCREVE["<b>sabe COMO escrever</b><br/><small>job, pipeline, dashboard, metric view</small>"]
  EXECUTA["<b>EXECUTA pela CLI</b><br/><small>listar tabela, rodar SQL, inspecionar job</small>"]
  WS["seu workspace"]

  REPO --> INST --> SK --> AG
  AG --> ESCREVE
  AG --> EXECUTA --> WS
A skill ensina o agente a escrever; a CLI autenticada deixa ele executar. Duas coisas diferentes que costumam ser confundidas.
Skill é injeção de conhecimento, não ferramenta

Skill é a documentação oficial e atualizada entrando no contexto do agente. Ele passa a saber como se escreve um job, uma pipeline, um dashboard, uma metric view no padrão da Databricks — sem inventar sintaxe que não existe mais.

Para executar — listar tabela, rodar SQL, inspecionar job — o caminho agora é a própria CLI, que o agente já sabe usar porque as skills ensinam. Foi isso que tornou o MCP dispensável no setup padrão: menos uma peça para instalar, autenticar e manter viva.

As AI Tools: SQL do terminal, sem abrir a interface

Junto das skills vem um conjunto de comandos que muda o dia a dia: databricks experimental aitools tools. São quatro subcomandos, feitos para agentes de código — e igualmente úteis para você.

SubcomandoO que fazQuando usar
query Executa SQL contra um warehouse e devolve o resultado Rodar qualquer consulta sem sair do terminal. Aceita SQL solto, arquivo .sql ou stdin
discover-schema Colunas, tipos, amostra de linhas, contagem de nulos e total de registros Conhecer uma tabela antes de escrever a query. Uma chamada substitui quatro
get-default-warehouse Descobre qual warehouse usar Quase nunca — os outros comandos já acham sozinhos
statement SQL assíncrono: submit, get, status, cancel Consulta demorada, ou várias em paralelo sem travar o terminal
terminalo que dá para fazer
# consulta direta — ele acha o warehouse sozinho
databricks experimental aitools tools query --profile grid_intelligence \
  "SELECT current_catalog(), current_user()"

# conhecer uma tabela: colunas, tipos, amostra, nulos e contagem
databricks experimental aitools tools discover-schema \
  grid_dev.bronze.chamados --profile grid_intelligence

# rodar um arquivo .sql versionado no repositorio
databricks experimental aitools tools query --file src/sql/checagens.sql \
  --profile grid_intelligence

# varias consultas EM PARALELO, saida em JSON
databricks experimental aitools tools query --profile grid_intelligence \
  "SELECT count(*) FROM grid_dev.bronze.interrupcoes" \
  "SELECT count(*) FROM grid_dev.silver.interrupcoes_validas"

# exportar para CSV
databricks experimental aitools tools query --output csv --profile grid_intelligence \
  "SELECT * FROM grid_dev.gold.painel_operacional_dia" > painel.csv
Por que isso importa mais do que parece

É o que substituiu o servidor MCP no setup padrão. O agente não precisa de uma ponte especial para enxergar o workspace: ele usa a mesma CLI que você, com o mesmo perfil e as mesmas permissões. Menos uma peça para instalar, autenticar e manter viva — e tudo que ele faz aparece no seu histórico de comandos.

Foi também o que resolveu a criação do catálogo no bloco 7.0: onde databricks catalogs create esbarra no Default Storage da Free Edition, um CREATE CATALOG por aqui passa.

É experimental e a palavra está no comando

A própria CLI avisa: “the tools can change at any time. There are no stability guarantees”. Ótimo para trabalho interativo e para o agente; não coloque em automação. Para isso, o caminho estável é a Statement Execution API ou uma sql_task do job — que é o que este projeto usa para a metric view e para a máscara de dado pessoal.

Teste de fumaça

Abra o Claude Code e rode /skills. As skills aparecem com o prefixo databricks: — é assim que se reconhece o plugin instalado pela CLI. Depois, peça algo que exija ler o workspace de verdade:

promptdentro do Claude Code
Liste os catálogos do meu workspace usando o profile grid_intelligence
Se as skills não aparecerem em /skills

Plugin também só carrega no início da sessão. Feche o Claude Code, abra de novo e rode /skills. Se você tinha a versão antiga do AI Dev Kit e o /mcp ainda mostra um servidor databricks, é resíduo da instalação anterior — rode o instalador de novo e aceite a limpeza.

7.0

Unity Catalog.

Entender a hierarquia antes de o agente criá-la por você.

Este bloco é só conceito. Os comandos de criar catálogo, schema e volume estão no prompt 00, no bloco 8.1 — e quem vai executá-los é o agente. Antes disso, vale entender o que ele vai fazer, porque é aqui que quase todo erro da aula nasce.

Três níveis, nesta ordem

NívelO que guardaQuem cria neste projeto
Catálogo
grid_dev
o limite de isolamento entre ambientes o prompt 00, pela CLI. É a única coisa que o bundle não declara — catálogo é recurso de conta
Schema
raw, bronze, silver, gold
a camada do medalhão o bundle, via resources/grid_schemas.yml
Volume
raw.landing
arquivo bruto — o Parquet que a bronze ingere o bundle, no mesmo arquivo
Cada nível precisa do anterior, e o erro não diz qual falta

Volume mora dentro de schema, que mora dentro de catálogo. Tentar subir arquivo com qualquer um faltando devolve erro de caminho — sem dizer em que nível o problema está. A ordem de conferência é sempre a mesma:

terminalconferir os três níveis
databricks catalogs list --profile grid_intelligence
databricks schemas list grid_dev --profile grid_intelligence
databricks volumes list grid_dev raw --profile grid_intelligence

O catálogo isola; o nome do schema, não

Dev grava em grid_dev, produção em grid_intelligence. Os schemas mantêm os nomes exatos raw, bronze, silver e gold nos dois lados — é isso que permite o mesmo SQL rodar em qualquer target sem uma linha diferente. Se o schema de dev virasse bronze_dev, cada query precisaria saber em que ambiente está rodando, e você teria dois códigos para manter.

A pegadinha que custa meia hora

Em mode: development o bundle prefixa todo recurso com o seu nome — inclusive schema, onde bronze viraria seunome_bronze. A trava é experimental.skip_name_prefix_for_schema: true no databricks.yml, e o prompt 00 já pede isso. Guarde o sintoma: se os schemas nascerem com seu nome na frente, é essa linha que falta.

O erro mais comum da aula inteira

Catálogo recém-criado sem grant é catálogo inútil: o pipeline falha com user does not have permission to CREATE SCHEMA. O prompt 00 concede a permissão, mas confira na interface — Catalog → grid_dev → aba Permissions. Se esse erro aparecer lá na frente, é sempre aqui que se resolve.

Na Free Edition, criar catálogo pela CLI não funciona

databricks catalogs create devolve Metastore storage root URL does not exist em qualquer conta com Default Storage ligado: a API exige um storage_root explícito, e quem sabe usar o armazenamento padrão é a interface. Por isso o prompt 00 usa CREATE CATALOG por SQL — que passa.

Se até o SQL reclamar, o caminho é a interface: Catalog no menu lateral → Create catalog → nome, tipo Standard, storage no padrão. Três cliques por catálogo, e o prompt 00 segue do ponto seguinte.

8.0

A sequência de oito prompts.

Uma feature por prompt. Deploy e resultado visível a cada etapa.

A tentação é escrever um prompt gigante pedindo o projeto inteiro. O resultado é sempre o mesmo: tudo pela metade, e você sem saber qual parte revisar primeiro.

A sequência abaixo quebra a área de atendimento da Luz do Vale em oito etapas. Cada prompt entrega uma feature completa, termina com um deploy e abre algo para olhar — e diz explicitamente o que não deve ser feito ainda. É isso que impede o agente de correr na frente.

flowchart TB
  P0["<b>00 · Setup</b> — regras de negocio, catalogos, schemas e volume"]
  P1["<b>01 · Bronze</b> — ingestao fiel do volume"]
  P2["<b>02 · Silver, os numeros</b> — a regra dos 3 minutos, dedup e baseline"]
  P3["<b>03 · Silver, o texto</b> — anonimizar e depois enriquecer com IA"]
  P4["<b>04 · Gold</b> — as 4 entregas e a metric view de DEC e FEC"]
  P5["<b>05 · Dashboard</b> — painel AI/BI de duas paginas"]
  P6["<b>06 · Agente</b> — o copiloto conversacional em portugues"]
  P7["<b>07 · Prova</b> — relatorio executivo e as 8 queries"]

  P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
Oito prompts, oito entregas. Cada etapa termina com algo para olhar — é o que permite revisar antes de seguir.

Antes de tudo: o que é esse ciclo

Os três comandos abaixo vão se repetir oito vezes. Se os conceitos de workspace e deploy são novos para você, vale gastar dois minutos aqui — o resto da aula fica muito mais fácil.

Onde as coisas estão. O código que você escreve fica na sua máquina. O Databricks é um ambiente remoto: o workspace na nuvem. Enquanto você só edita arquivos, nada existe lá — nenhum job, nenhuma tabela. O deploy é a ponte entre os dois mundos.

ComandoO que faz, em uma fraseToca no workspace?
bundle validate Lê os arquivos .yml, troca as variáveis pelos valores do target e confere se tudo faz sentido. É a revisão antes de enviar Não. Roda inteiro na sua máquina
bundle deploy Empacota o código, envia para o workspace e cria ou atualiza os recursos declarados Sim. É o único que muda alguma coisa lá
bundle summary Pergunta ao workspace o que existe agora, com as chaves e os links Só lê
Para onde vão os arquivos

O deploy copia o projeto para /Workspace/Users/você/.bundle/grid_intelligence/dev/ no workspace. É por isso que os recursos de dev aparecem prefixados com [dev seu_usuario]: cada pessoa tem a própria cópia, e ninguém pisa no trabalho de ninguém. Em produção o caminho é fixo e a cópia é uma só.

O que o deploy não faz

Ele não executa nada — criar um job não é rodar o job; para isso existe o bundle run. E ele não sobe dados: cria o volume vazio, e os arquivos são passo à parte. Confundir essas duas coisas é a origem da maioria das perguntas "deployou mas não apareceu nada".

Se validate reclamar, não tente o deploy

Ele é rápido de propósito e mostra o YAML final, já com ${var.catalog} substituído pelo catálogo de verdade. É onde você descobre que digitou um nome errado — antes de criar recurso no workspace, e não depois.

flowchart TB
  subgraph LOCAL["<b>sua maquina</b> — onde voce escreve"]
    ARQ["databricks.yml<br/>resources/*.yml<br/>src/**"]
    VAL["<b>bundle validate</b><br/><small>le os YAML, resolve as variaveis,<br/>confere a sintaxe. NAO envia nada</small>"]
  end

  subgraph NUVEM["<b>workspace Databricks</b> — onde tudo roda"]
    DEP["<b>bundle deploy</b><br/><small>empacota, envia e cria os recursos</small>"]
    REC["jobs · pipelines · dashboards<br/>schemas · volumes"]
    SUM["<b>bundle summary</b><br/><small>pergunta o que existe la agora</small>"]
  end

  ARQ --> VAL
  VAL -->|"passou"| DEP
  VAL -->|"erro de sintaxe ou variavel"| ARQ
  DEP --> REC
  REC --> SUM
  SUM -->|"proxima mudanca"| ARQ
A fronteira entre a sua máquina e a nuvem. Só o deploy atravessa — validate fica de um lado e summary só olha do outro.
Por que fazer deploy oito vezes

Porque o ciclo validate → deploy → olhar é o hábito que a aula quer instalar, e hábito não se aprende vendo uma vez. Oito repetições em uma hora fixam mais do que qualquer explicação — e cada deploy pequeno falha de um jeito pequeno, que se resolve em minutos.

Os dois pontos em que você escolhe

A ordem é fixa, mas a sequência tem duas decisões — e as duas são sobre usar função de IA ou não.

No prompt 03, o enriquecimento das transcrições. É o único ponto em que a IA entra no pipeline de dados, e são ~11 mil chamadas ao modelo: dezenas de minutos. O prompt irmão 03-silver-texto-regex.md entrega as mesmas colunas com regex e CASE, em segundos. Do prompt 04 em diante nada muda — gold, dashboard, agente e relatório não sabem qual rota você escolheu.

No prompt 07, a prosa do relatório executivo: ai_gen ou um gabarito. Os números são idênticos nos dois caminhos; muda o texto, não o fato.

O que cada prompt carrega, e um ticket comum não

A regra de negócio junto do pedido — o agente não precisa adivinhar por que o limiar é de 3 minutos. As armadilhas conhecidas — baseline sobre série com tendência, streaming table append-only. E o critério de aceite: o que precisa ser verdade para a etapa estar pronta.

Reaproveitando em outro projeto

A sequência é o template: setup, ingestão, limpeza, enriquecimento, negócio, visualização, agente, prova. Trocar de domínio significa reescrever o prompt 00 inteiro e ajustar nomes de tabela nos demais. A estrutura, as armadilhas técnicas e o formato com critério de aceite continuam valendo.

8.1

Prompt 00Setup e planejamento.

As regras de negócio antes da primeira linha de SQL.

por que isso vale dinheiro

Nenhuma tabela nasce antes de alguém decidir por que ela existe. Este prompt escreve as regras que valem dinheiro — os 3 minutos que separam piscada de apagão, os dois eixos das perdas, a proibição de rotular cliente — antes de qualquer SQL. É o documento que o agente vai consultar nos sete prompts seguintes, e é o que impede ele de inventar regra.

O que explicar aqui

  • Quem cria o quê

    O catálogo é o único que o agente cria pela CLI — é recurso de conta, e o bundle não o declara. Schemas e volume vão no resources/grid_schemas.yml e nascem no deploy. Confundir isso é o que faz o deploy falhar dizendo que o catálogo não existe.

    Unity Catalog
  • CREATE CATALOG por SQL, não pela CLI

    Em conta com Default Storage — o caso da Free Edition — databricks catalogs create exige um storage_root e falha. O CREATE CATALOG por SQL usa o armazenamento padrão e passa. Se nem ele funcionar, é pela interface.

    Criar catálogos
  • Por que PRD antes de código

    O agente escreve SQL bem. O que ele não sabe é por que interrupção de menos de 3 minutos não conta, ou por que a saída não pode se chamar fraude. Escrever isso primeiro, num arquivo que ele lê, é a diferença entre um pipeline correto e um pipeline plausível.

  • Catálogo o bundle não cria

    Schemas, volumes, jobs e pipelines vão no bundle. Catálogo é recurso de conta e fica de fora — é a única coisa que você cria à mão, e por isso é a primeira.

    Volumes

O prompt

prompt 0000-setup.md
# Prompt 00 — Setup e planejamento

> **Como usar:** cole este texto inteiro no Claude Code, dentro do repositório do projeto.
> Ele é o primeiro de uma sequência de **8 prompts**. Cada um entrega uma feature completa,
> com deploy e resultado visível. Não pule a ordem: cada prompt assume o anterior pronto.

---

Você vai me ajudar a construir a plataforma de dados e IA de uma distribuidora de energia
elétrica fictícia, a **Luz do Vale Distribuidora S.A.**, no Databricks. O projeto se chama
**Grid Intelligence**.

Este é o **prompt 1 de 8**. Neste primeiro, quero apenas o alicerce: entender o domínio,
registrar as regras de negócio e criar a infraestrutura. Não construa transformação,
dashboard nem agente agora — cada um tem o seu prompt.

## Contexto de negócio

Uma distribuidora de energia opera sob regulação da ANEEL e tem três dores simultâneas,
todas mensuráveis em dinheiro:

1. **Continuidade do fornecimento.** Interromper além do limite regulatório gera
   compensação financeira automática ao consumidor. Em 2024 as distribuidoras brasileiras
   pagaram mais de R$ 1,1 bilhão em compensações.
2. **Perdas não técnicas.** Energia distribuída e não faturada — furto, fraude e erro de
   medição. Em 2025 somaram ~R$ 11,5 bilhões, dos quais ~R$ 7,9 bilhões foram repassados
   à tarifa.
3. **Atendimento ao cliente.** Milhares de ligações viram transcrição e ninguém lê. Dentro
   delas está o aviso antecipado da reclamação na ouvidoria.

## Glossário — use estes termos, sem sinônimo aproximado

| Termo | Definição |
|---|---|
| **UC — Unidade Consumidora** | O ponto de entrega de energia. **Não é sinônimo de cliente**: um cliente pode ter várias UCs |
| **Conjunto** | Subdivisão geográfica da área de concessão. É a unidade de agregação do regulador |
| **DEC** | Duração Equivalente de Interrupção por UC — tempo médio, em horas, sem fornecimento |
| **FEC** | Frequência Equivalente de Interrupção por UC — número médio de interrupções |
| **PNT** | Perdas Não Técnicas |
| **Prioridade de inspeção** | Classificação de quanto uma UC merece visita técnica |

## Regras de negócio que valem para o projeto inteiro

- **RN-01** — Só interrupções com **3 minutos ou mais** entram na apuração de DEC e FEC.
  A regra vive na camada de transformação, nunca no relatório.
- **RN-04** — DEC e FEC têm **uma única definição** no sistema, consumida por dashboard,
  agente e relatório.
- **RN-06** — Uma UC é candidata a inspeção quando o consumo cai em **dois eixos
  simultâneos**: contra o próprio baseline **e** enquanto a vizinhança permanece estável.
  Só o primeiro eixo apontaria o bairro inteiro depois de um apagão.
- **RN-07** — A saída da detecção é **prioridade de inspeção** — alta, média, baixa.
  **Nunca** um rótulo de fraude, furto ou culpa. Queda de consumo também é mudança de
  morador, imóvel desocupado, defeito de medidor e erro de leitura. Em setor regulado a
  diferença é jurídica. Nenhuma coluna, variável ou comentário do projeto pode usar as
  palavras "fraude", "furto" ou "culpado" como rótulo de saída.
- **RN-09** — Dado pessoal em transcrição é **mascarado antes** de qualquer análise de
  conteúdo.
- **RN-10** — O texto original identificado é preservado com **acesso restrito**. Duas
  proteções que coexistem: uma na leitura, outra na análise.
- **RN-11** — A camada de ingestão **não filtra, não corrige e não aplica regra de
  negócio**. Leitura inválida de medidor é um fato sobre o medidor. Filtrar na ingestão
  parece limpeza; é destruição de evidência.
- **RN-12** — Todo registro ingerido carrega origem e momento de ingestão.
- **RN-13** — O agente conversacional acessa **apenas a camada de negócio**.
- **RN-14** — As entidades da **camada gold** carregam descrição em linguagem de negócio —
  é o que o agente lê. Bronze e silver não precisam: são passagem.

## Diretriz de simplicidade — vale para todos os 8 prompts

Este projeto é construído **ao vivo, em aula**. Código que ninguém consegue ler na tela em
dez segundos não serve, mesmo que seja mais robusto.

**Escreva o mínimo que faz a coisa funcionar:**

- **Sem expectations e sem `CONSTRAINT`** nas tabelas. Regra de negócio vira `WHERE`, que
  qualquer pessoa lê sem precisar saber o que é `ON VIOLATION`.
- **Sem `COMMENT` em bronze e silver.** Elas são passagem. Só a **gold** e a **metric view**
  levam descrição — são o que o agente conversacional lê, e ali a curadoria é o produto
  (RN-14).
- **Sem `TBLPROPERTIES`, `CLUSTER BY` e afins**, a menos que sem eles quebre.
- **Sem tratamento de caso que o dataset não tem.** Não invente defesa para dado que não
  existe.
- **Um `SELECT` direto vence um `WITH` de quatro CTEs.** Só quebre em CTE quando a query
  realmente não couber de cabeça.

Comentário no código só onde a decisão **não é óbvia pelo código** — a regra dos 3 minutos
merece duas linhas explicando por quê; um `CAST` não merece nenhuma.

Se em algum momento você achar que o certo é acrescentar validação, **não acrescente**: me
diga em uma linha o que faria, e eu decido.

## Convenções obrigatórias

- Todo texto e todo nome em **português do Brasil**; nomes de entidades, colunas e
  arquivos **sem acentuação**.
- **Nenhum nome de catálogo fixado em código.** Catálogo vem de `${var.catalog}` no
  `databricks.yml`; schema vem de `${resources.schemas.<camada>.name}`.
- **Nenhum nome de modelo de IA fixado no código.** Use as funções `ai_*` do Databricks.
- Perfil da CLI: `grid_intelligence`. Todo comando leva `--profile grid_intelligence`.

## Os dados

Quatro bases sintéticas, já geradas, em Parquet. **Baixe aqui:**

<https://drive.google.com/file/d/1o3NISve_wWygDWecBiiYTB8i2WXN7hbi/view?usp=sharing>

Descompacte e você terá uma pasta por base, com um único arquivo Parquet dentro.

| Base | Grão | Volume |
|---|---|---|
| `unidades_consumidoras` | uma por ponto de entrega | 4.000 |
| `interrupcoes` | uma por evento de rede | 1.383 |
| `consumo_diario` | uma por UC por dia | **~5,85 milhões** (4 anos) |
| `chamados` | uma por ligação, com transcrição | 2.801 |

O dataset tem **quatro anos de histórico**, crescimento de **30% ao ano** na carga e
**sazonalidade de verão** no atendimento (dezembro a março recebem quase o dobro de
ligações). Ele carrega **defeitos propositais** — consumo nulo, negativo, fisicamente
impossível, linhas duplicadas e interrupções abaixo de 3 minutos. Não são bug: existem
para as validações de qualidade e a RN-01 terem o que fazer.

## O que fazer agora

1. **Leia a skill `databricks-core`** antes de qualquer comando, e depois a
   `databricks-dabs`.
2. Escreva **`.llm/prd.md`** com o contexto acima: domínio, glossário, regras de negócio,
   modelo de informação e as quatro entregas de negócio previstas. É o documento que todos
   os prompts seguintes vão consultar.
3. **Crie os catálogos** (ver abaixo).
4. Crie no bundle os **schemas** `raw`, `bronze`, `silver` e `gold`, e o **volume**
   `raw.landing`. Um schema por camada — o nome do schema já é a camada, então as tabelas
   dentro dele não repetem o prefixo (`bronze.chamados`, nunca `bronze_chamados`).
5. Configure `dev` e `prod` isolados **por catálogo**, não por nome de schema.
6. Rode `databricks bundle validate` e `databricks bundle deploy -t dev`.
7. Me diga exatamente **onde subir os arquivos de dados**, com o caminho completo.

## Os catálogos

Dois catálogos, um por ambiente. É o catálogo que isola dev de produção — os schemas mantêm os
nomes exatos `raw`, `bronze`, `silver` e `gold` nos dois lados, para que o mesmo SQL rode em
qualquer ambiente sem uma linha diferente.

| Ambiente | Catálogo | Quem usa |
|---|---|---|
| `dev` | `grid_dev` | você, no dia a dia |
| `prod` | `grid_intelligence` | o ambiente final, depois de validado em dev |

**Catálogo é a única coisa que o bundle não cria** — é recurso de conta, e o deploy assume que já
existe. Schemas e volume ficam no bundle; catálogo, não.

Crie os dois **por SQL**, e não com `databricks catalogs create`:

```bash
databricks experimental aitools tools query --profile grid_intelligence \
  "CREATE CATALOG IF NOT EXISTS grid_dev COMMENT 'Catalogo de desenvolvimento da Grid Intelligence'"

databricks experimental aitools tools query --profile grid_intelligence \
  "CREATE CATALOG IF NOT EXISTS grid_intelligence COMMENT 'Catalogo de producao da Grid Intelligence'"

databricks catalogs list --profile grid_intelligence
```

**Por que SQL e não `catalogs create`:** em qualquer conta com Default Storage ligado — o caso da
Free Edition — a API por trás do comando exige um `storage_root` explícito e devolve
`Metastore storage root URL does not exist`. O `CREATE CATALOG` por SQL usa o armazenamento padrão
da conta e passa. Se até ele reclamar, me avise: aí o caminho é a interface, em Catalog → Create
catalog.

Depois, conceda permissão ao meu usuário nos dois catálogos:

```bash
databricks grants update CATALOG grid_dev \
  --json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
  --profile grid_intelligence
```

Catálogo sem *grant* é catálogo inútil: o pipeline falha depois com
`user does not have permission to CREATE SCHEMA`.

### Recomeçar do zero

Se um catálogo já existir e eu quiser recomeçar limpo, o comando é destrutivo e **não pergunta
duas vezes** — `CASCADE` remove schemas, tabelas, volumes e os arquivos dentro deles:

```bash
databricks experimental aitools tools query --profile grid_intelligence \
  "DROP CATALOG IF EXISTS grid_dev CASCADE"
```

**Sempre confirme comigo antes de rodar isso.** Deixe o comando registrado no README, na seção de
setup, com o aviso de que é destrutivo.

## Os schemas e o volume, no bundle

Declare em `resources/grid_schemas.yml`:

- os schemas `raw`, `bronze`, `silver` e `gold`
- o volume **managed** `landing`, dentro de `raw`

Nomes de schema vêm de `${resources.schemas.<camada>.name}` onde forem referenciados; o catálogo
vem de `${var.catalog}`. **Nenhum dos dois escrito literalmente em qualquer arquivo.**

Adicione `experimental.skip_name_prefix_for_schema: true` ao `databricks.yml`. Sem isso, em
`mode: development` o bundle prefixa os schemas com meu nome de usuário e `bronze` vira
`meunome_bronze` — o isolamento entre ambientes é feito pelo catálogo, não pelo nome do schema.

Depois: `databricks bundle validate` e `databricks bundle deploy -t dev`.

## Onde vou subir os dados

Com o volume criado pelo deploy, me diga o caminho completo de cada base, no formato que eu possa
colar direto na interface de upload:

```
/Volumes/grid_dev/raw/landing/unidades_consumidoras/
/Volumes/grid_dev/raw/landing/interrupcoes/
/Volumes/grid_dev/raw/landing/consumo_diario/
/Volumes/grid_dev/raw/landing/chamados/
```

E me dê também o comando de subir tudo de uma vez pelo terminal, lembrando que **o caminho de
origem é o da minha máquina** — confira onde os Parquet estão antes de montar o comando:

```bash
databricks fs cp -r <PASTA_LOCAL> dbfs:/Volumes/grid_dev/raw/landing \
  --overwrite --profile grid_intelligence
```

O prefixo `dbfs:` é obrigatório mesmo para Volume do Unity Catalog.

Uma pasta por base, com um único arquivo Parquet dentro — inclusive a de consumo diário, com 5,85
milhões de linhas. É a pasta, e não o arquivo, que é o contrato com a camada bronze: se um dia uma
base precisar ser particionada, o pipeline não muda.

## Critério de aceite

- Os dois catálogos existem e eu tenho `ALL PRIVILEGES` nos dois
- O comando de derrubar o catálogo está documentado no README, com o aviso de que é destrutivo
- `databricks bundle validate --profile grid_intelligence` passa sem erro
- `databricks bundle deploy -t dev` cria os quatro schemas e o volume `raw.landing`
- Os schemas **não** têm meu nome de usuário como prefixo
- Nenhum nome de catálogo ou schema aparece escrito literalmente no código
- Recebi os quatro caminhos de upload e o comando de `fs cp`
- `.llm/prd.md` existe e contém o glossário e as 14 regras de negócio

## O que vem depois

Não faça nada disso agora — cada item é um prompt próprio:

| # | Prompt | Entrega |
|---|---|---|
| 01 | Camada bronze | Ingestão fiel do volume, sem regra de negócio |
| 02 | Silver — os números | Cadastro limpo, a regra dos 3 minutos, dedup e baseline |
| 03 | Silver — o texto | Anonimização e enriquecimento das transcrições — com IA ou por regex |
| 04 | Camada gold | As quatro entregas de negócio e a metric view de DEC/FEC |
| 05 | Dashboard | Painel AI/BI sobre a gold |
| 06 | Agente | Copiloto conversacional em português |
| 07 | Relatório e demonstração | Relatório executivo e as queries que provam tudo |

Quando terminar o setup, me diga o que criou e pare. Eu envio o próximo.

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

Os dois catálogos na lista, e o volume raw.landing criado pelo deploy — vazio, esperando os arquivos.

Onde olhar na interface

  1. Catalog → grid_dev

    Os quatro schemas — raw, bronze, silver, gold — recém-criados e vazios. Se algum vier como seunome_bronze, falta o skip_name_prefix_for_schema.

  2. Catalog → grid_dev → raw → Volumes → landing

    O volume existe e está vazio. É aqui que você vai soltar os quatro Parquet.

  3. Catalog → grid_dev → Permissions

    Seu usuário com ALL PRIVILEGES. Sem isso, o próximo deploy falha com permission to CREATE SCHEMA.

  4. Workspace → Users → você → .bundle/grid_intelligence/dev/

    O projeto inteiro copiado para o workspace. É o que o deploy fez.

E os dados? Esses são por sua conta

O bundle deploy cria o volume vazio. Bundle provisiona recurso — schema, volume, job, pipeline —, mas não move dado. Subir os quatro Parquet é passo manual, feito uma vez, depois que o volume existe.

Baixe os quatro arquivos aqui

Dataset da Grid Intelligence — Google Drive

São as quatro bases já geradas: 4.000 unidades consumidoras, 1.383 interrupções, 5,85 milhões de linhas de consumo e 2.801 chamados — quatro anos de histórico. Descompacte e você terá uma pasta por base, com um Parquet dentro.

Prefere gerar você mesmo? uv run python scripts/gerar_dados_local.py historico reproduz exatamente o mesmo dataset: a semente é fixa.

PeçaQuem criaQuando
Catálogoo agente, por CREATE CATALOGno início deste prompt
Schemas e volumeo bundle deploy, a partir de resources/grid_schemas.ymlao fim deste prompt
Os arquivos Parquetvocê, por upload ou fs cpagora, antes do bloco 8.2

Pelo terminal

terminalajuste o caminho de origem
# confira ONDE estao os seus Parquet antes de rodar
ls landing/

# o -r sobe a arvore inteira, com as quatro pastas
databricks fs cp -r landing dbfs:/Volumes/grid_dev/raw/landing \
  --overwrite --profile grid_intelligence

databricks fs ls dbfs:/Volumes/grid_dev/raw/landing --profile grid_intelligence

Pela interface

  1. Catalog → grid_dev → raw → Volumes → landing
  2. Botão Upload to this volume O campo de destino aceita criar diretório: digite o nome da pasta antes de soltar o arquivo.
  3. Uma pasta por base, com o Parquet dentro. Repita para as quatro. unidades_consumidoras/ · interrupcoes/ · consumo_diario/ · chamados/
Dois detalhes que derrubam o fs cp

O dbfs: é obrigatório, mesmo para Volume do Unity Catalog — sem ele o comando falha com erro de caminho inválido. E o caminho de origem é o da sua máquina: caminho errado devolve um erro que parece de permissão. Dê ls antes.

Os nomes das pastas não são sugestão

São o que o pipeline do bloco 8.2 vai procurar. Uma letra diferente e a bronze ingere zero linha — sem reclamar de nada, porque para o Auto Loader a pasta simplesmente está vazia.

8.2

Prompt 01Camada bronze.

Ingestão fiel: nada é filtrado, nada é corrigido.

por que isso vale dinheiro

Em setor regulado, número sem origem não vale. Quando a fiscalização perguntar de onde saiu o DEC do mês, a resposta precisa chegar até o arquivo e o instante da ingestão — e o dado bruto precisa estar lá, intocado, inclusive a leitura defeituosa que alguém teria vontade de limpar.

O que explicar aqui

  • Auto Loader e STREAM read_files

    Ingestão incremental de arquivos: ele guarda o que já processou e só lê o que chegou depois. Aponta para a pasta, não para o arquivo — e allowOverwrites faz ele reprocessar um arquivo que foi trocado.

    Auto Loader
  • Streaming table × materialized view

    A decisão que mais custa caro errar, porque trocar depois exige derrubar a tabela. Regra curta: se a linha de saída depende de outras linhas, é materialized view; se cada entrada vira uma saída sozinha, é streaming table. Comparação completa logo abaixo.

    Streaming tables
  • SELECT * aqui é a regra escrita em código

    Listar colunas à mão seria decidir o que vale a pena guardar — e essa decisão não é da bronze. Junto vão só duas colunas de rastreabilidade: de qual arquivo veio e quando entrou.

    Auto Loader
  • Simplicidade é uma decisão de projeto

    O Lakeflow oferece expectations para declarar regras de qualidade na tabela. Este projeto não usa nenhuma: a mesma pergunta se responde com uma consulta que cabe em cinco linhas e roda em qualquer lugar. Detalhe logo abaixo.

    Expectations
  • RN-11 e o custo de limpar cedo

    Filtrar na ingestão parece capricho e é destruição de evidência. A leitura negativa é um fato sobre o medidor: alguém vai querer contá-la um dia, e ela precisa estar lá.

    Arquitetura medalhão

O prompt

prompt 0101-bronze.md
# Prompt 01 — Camada bronze

> **Prompt 2 de 8.** Requer o prompt 00 concluído: schemas, volume e `.llm/prd.md` no lugar.

---

Leia `.llm/prd.md` antes de começar, inclusive a **diretriz de simplicidade**. Agora vamos
construir a **camada bronze** do medalhão da Grid Intelligence.

## O que quero

Um **Lakeflow Declarative Pipeline serverless** que ingere as quatro bases do volume
`raw.landing` para o schema `bronze`, uma tabela por base.

**Um arquivo só:** `src/pipelines/grid_intelligence/transformations/01_bronze.sql`, com as
quatro tabelas. Elas compartilham o mesmo padrão de ingestão, e separar em quatro arquivos
iguais só espalharia a mesma decisão por quatro lugares.

| Origem no volume | Tabela bronze | Linhas esperadas |
|---|---|---|
| `unidades_consumidoras/` | `bronze.unidades_consumidoras` | 4.000 |
| `interrupcoes/` | `bronze.interrupcoes` | 1.383 |
| `consumo_diario/` | `bronze.consumo_diario` | ~5,85 milhões |
| `chamados/` | `bronze.chamados` | 2.801 |

Todas são **streaming table**: ingestão é append de arquivo novo, e é o que o Auto Loader
sabe fazer de forma incremental.

Use **Auto Loader** (`STREAM read_files`) com formato Parquet. Aponte para a **pasta**, não
para o arquivo: hoje cada base ocupa um Parquet só, mas se uma delas precisar ser
particionada amanhã, nada muda no pipeline.

## O formato de cada tabela

Quatro blocos iguais a este, e nada além disso:

```sql
CREATE OR REFRESH STREAMING TABLE ${catalogo}.${schema_bronze}.interrupcoes
AS SELECT
  *,
  _metadata.file_path AS arquivo_origem,
  current_timestamp() AS ingerido_bronze_em
FROM STREAM read_files(
  '${volume_landing}/interrupcoes/',
  format => 'parquet'
);
```

**Sem expectations, sem `CONSTRAINT`, sem `COMMENT`, sem `CLUSTER BY`.** A bronze é
passagem: o que ela precisa fazer é copiar fielmente e dizer de onde veio.

As duas colunas extras são a RN-12: `arquivo_origem` e `ingerido_bronze_em`. As colunas
`origem` e `ingerido_em` que já vêm do dado dizem de que *sistema* ele saiu; estas duas
dizem por onde ele *entrou* aqui.

## A regra que manda neste arquivo: RN-11

**A ingestão não aplica regra de negócio, não filtra e não corrige.**

Consumo negativo, leitura impossível, interrupção de 40 segundos, cadastro com bairro
vazio — tudo entra exatamente como chegou. Filtrar aqui pareceria limpeza; é destruição de
evidência. Quem descarta é a silver, com regra nomeada e visível no `WHERE`.

O `SELECT *` acima não é preguiça: é a RN-11 escrita em código. Qualquer coluna que você
listasse à mão seria uma decisão sobre o que vale a pena guardar — e essa decisão não é da
bronze.

## Parametrização

O pipeline recebe por `configuration`: catálogo, nome de cada schema, caminho do volume e
o limiar de duração mínima de interrupção. **Nenhum nome de catálogo escrito dentro do
SQL** — os arquivos usam `${catalogo}.${schema_bronze}.<tabela>`, e o valor vem do
`databricks.yml`.

## Uma armadilha que quero registrada em comentário

Cada base ocupa um único arquivo, que é sobrescrito quando o dataset é regerado.
`allowOverwrites => true` faz o Auto Loader reprocessar o arquivo trocado, mas **streaming
table é append-only**: as linhas antigas continuam lá e o volume dobra. Deixe isso escrito
no topo do arquivo, junto com a solução — rodar o pipeline com **full refresh** depois de
regerar.

## Se o pipeline falhar com `DELTA_FEATURE_REQUIRES_MANUAL_ENABLEMENT: timestampNtz`

Vai acontecer. Os timestamps do dataset são **sem fuso horário** (`TIMESTAMP_NTZ`), e o
Delta exige habilitar essa feature explicitamente na tabela.

Há duas saídas, e uma delas está errada:

- **Converter para `TIMESTAMP` na ingestão** — assumiria um fuso que o dado não declara.
  Isso é transformação dentro da bronze, exatamente o que a RN-11 proíbe.
- **Habilitar a feature na tabela** — o tipo entra como chegou, e quem decide o fuso é a
  silver, se precisar.

Use a segunda. É a única exceção à regra de não usar `TBLPROPERTIES`:

```sql
CREATE OR REFRESH STREAMING TABLE ${catalogo}.${schema_bronze}.interrupcoes
TBLPROPERTIES ('delta.feature.timestampNtz' = 'supported')
AS SELECT ...
```

## Se a bronze não ingerir nada

Antes de investigar o Auto Loader, confira a precisão dos timestamps do Parquet:

```bash
databricks experimental aitools tools query --profile grid_intelligence \
  "SELECT * FROM parquet.`/Volumes/grid_dev/raw/landing/interrupcoes/` LIMIT 1"
```

Parquet gravado por pandas sem tratamento fica com `timestamp[ns]`, e **o Spark não lê
nanossegundo**. O gerador deste projeto já converte para microssegundo; se o dado veio de
outro lugar, é a primeira coisa a checar — a falha não é óbvia e parece problema de caminho.

## Ao terminar

1. `databricks bundle deploy -t dev --profile grid_intelligence`
2. Rode o pipeline e me mostre o resultado
3. A contagem de linhas de cada tabela bronze
4. **Quanto defeito entrou**, com uma query só — é o que a silver vai ter de resolver:

```sql
SELECT
  count(*)                                                   AS eventos,
  count_if(duracao_minutos < 3)                              AS abaixo_de_3_min,
  round(100.0 * count_if(duracao_minutos < 3) / count(*), 1) AS pct
FROM grid_dev.bronze.interrupcoes
```

Deve voltar **1.383, 310 e 22,4%**. Esses 22,4% são exatamente o que a RN-01 vai descartar
no próximo prompt — e é bom ver o número antes, para reconhecê-lo depois.

## Critério de aceite

- As quatro tabelas bronze existem e têm o volume esperado
- Nenhuma linha foi filtrada, corrigida ou descartada
- Toda tabela tem `arquivo_origem` e `ingerido_bronze_em`
- Nenhum nome de catálogo aparece dentro do SQL
- Cada tabela cabe em uma tela

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

A consulta de sanidade acusando 22,4% dos eventos abaixo de 3 minutos — as piscadas de religador. Não é bug — é a medida do que a silver vai descartar.

Onde olhar na interface

  1. Jobs & Pipelines → [dev seu_usuario] grid_medalhao

    O grafo do pipeline com quatro caixas, uma por tabela bronze. Verde é sucesso; clique numa para ver detalhes.

  2. No pipeline → clique numa tabela → aba de qualidade

    A contagem de linhas de cada tabela e, com uma consulta, quanto defeito entrou: 310 dos 1.383 eventos estão abaixo de 3 minutos — 22,4%, exatamente o que a silver vai descartar.

  3. Catalog → grid_dev → bronze

    As quatro tabelas com a contagem de linhas. consumo_diario com ~5,85 milhões.

  4. Numa tabela → aba Sample Data

    As colunas arquivo_origem e ingerido_bronze_em preenchidas — a rastreabilidade que a RN-12 exige.

Streaming table ou materialized view?

É a decisão que mais aparece num pipeline declarativo, e a que mais custa caro errar — porque trocar de um para o outro depois exige derrubar a tabela. Não há conversão no lugar, e nem full refresh resolve.

 Streaming tableMaterialized view
Como processa Só o que chegou desde a última execução. Guarda o ponto onde parou Recalcula o resultado da consulta inteira
O que faz com o passado Append-only. Nunca volta em linha já escrita Reescreve o resultado — linha antiga acompanha a lógica nova
Custo de cada execução Proporcional ao volume novo. Não cresce com o histórico Proporcional ao resultado. Cresce junto com a base
Mudou a regra do SQL As linhas antigas ficam com a lógica velha. Precisa de full refresh A próxima atualização já reflete a regra nova, no histórico inteiro
Agregar, deduplicar, ranquear Não. Exige olhar o conjunto, e ela só vê o que chegou agora Sim. É exatamente para isso
Garantia Exactly-once: cada arquivo ou mensagem é processado uma vez só Consistência com a fonte no momento da atualização
Fonte STREAM read_files(...), Kafka, STREAM(tabela) Leitura comum: SELECT ... FROM tabela

A regra prática

Faça uma pergunta só: para produzir esta linha, preciso olhar outras linhas?

  • Não — cada linha de entrada vira uma de saída, sozinha. Ingestão, renomear coluna, calcular campo derivado, filtrar. Streaming table.
  • Sim — somar, contar, pegar a mais recente de cada chave, comparar com a média. Materialized view.
Como ficou neste projeto

Streaming table: as quatro tabelas bronze (ingestão), o cadastro limpo e as interrupções válidas — todas transformam linha a linha, e o filtro de duração olha só a própria linha.

Materialized view: o consumo diário, porque deduplicar exige comparar as linhas da mesma chave; o baseline e as quatro tabelas gold, porque agregam.

O erro clássico: agregar numa streaming table

Parece funcionar no primeiro dia. O problema aparece quando uma linha da origem muda: a streaming table não recalcula o total, porque ela nunca volta atrás. O número fica errado em silêncio — e ninguém percebe até alguém conferir na mão.

Materialized view não é sinônimo de lento

Em pipeline serverless, com row tracking ligado na origem, a MV faz refresh incremental: recalcula só o que mudou, em vez da tabela inteira. Sem essas duas condições, ela cai para recomputação total — que é o que assusta quem já se queimou.

Como medir o que entrou torto — sem expectations

O Lakeflow tem expectations: regras declaradas na tabela que contam e, se você quiser, descartam linhas. Elas são boas, e este projeto não usa nenhuma — de propósito.

Por que ficaram de fora

Cada expectation é uma linha de sintaxe a mais para ler na tela, e o resultado delas vive numa aba escondida que só existe depois de uma execução concluída. Numa aula ao vivo isso custa tempo e devolve pouco.

A mesma pergunta — quanto defeito entrou? — se responde com uma consulta que qualquer pessoa lê em cinco segundos, e que funciona em qualquer lugar: notebook, dashboard, terminal.

sqlquanto a RN-01 vai descartar
SELECT
  count(*)                                                   AS eventos,
  count_if(duracao_minutos < 3)                              AS abaixo_de_3_min,
  round(100.0 * count_if(duracao_minutos < 3) / count(*), 1) AS pct
FROM grid_dev.bronze.interrupcoes

Volta 1.383, 310 e 22,4%. Esse é o número que a silver vai descartar no prompt seguinte — e vê-lo antes é o que faz o efeito da regra ser reconhecível depois.

Quando expectation vale a pena

Em produção, com pipeline rodando de madrugada e ninguém olhando: aí você quer a regra declarada junto da tabela, o histórico registrado a cada execução e o alerta quando a taxa de violação sobe. Para construir e ensinar, a consulta acima ganha.

8.3

Prompt 02Silver — os números.

Onde a regra regulatória vira código.

por que isso vale dinheiro

Interrupção de fornecimento abaixo de 3 minutos não conta para o indicador — é o religador atuando, não falta de energia. Essa regra decide quanto a distribuidora paga de compensação automática ao consumidor — R$ 1,1 bilhão no setor em 2024. Aplicá-la em um lugar só é a diferença entre levar um número para a ANEEL e levar três números que não batem entre si.

O que explicar aqui

  • Regra em configuração, não em literal

    O limiar vem do bloco configuration do pipeline, não escrito no SQL. A regra é do regulador: se a ANEEL mudar o limiar, muda-se uma linha — e não onze arquivos.

    Pipelines declarativos
  • Deduplicação com ROW_NUMBER

    O concentrador reenvia leitura, e a mesma chave chega duas vezes. Escolher qual fica exige olhar todas as linhas daquela chave — operação de janela, que é batch. É por isso que essa tabela é materialized view e não streaming.

    Materialized views
  • Baseline precisa de janela comparável

    A carga cresce 30% ao ano. Comparar o mês corrente contra a média de quatro anos acusa alta em toda UC do sistema, e a detecção de perdas para de funcionar. O baseline são os 90 dias imediatamente anteriores — a armadilha mais cara do projeto.

  • Ancorar no dado, nunca no relógio

    current_date() faz a análise deslizar para fora de um dataset que foi gerado semana passada, e o resultado vem vazio sem erro nenhum. A âncora é o maior dia da tabela.

flowchart TB
  EV["<b>interrupcao de fornecimento</b><br/><small>falta de energia num trecho da rede<br/>registrada pelo sistema de operacao</small>"]
  BR["<b>bronze.interrupcoes</b> — 1.383 linhas<br/><small>entra sempre, inclusive a de 0,31 minuto</small>"]
  DEC{"<b>duracao_minutos &gt;= 3?</b><br/><small>limiar do regulador,<br/>vem da configuracao do pipeline</small>"}
  SIL["<b>silver.interrupcoes_validas</b> — 1.073 linhas"]
  FORA["<b>310 eventos ficam so na bronze</b><br/><small>22,4% do total — sao as piscadas:<br/>o religador desarmou e religou sozinho</small>"]
  IND["<b>DEC e FEC</b><br/><small>o FEC cai 23% ao aplicar a regra;<br/>o DEC quase nao muda</small>"]

  EV --> BR --> DEC
  DEC -->|"3 min ou mais: e apagao"| SIL --> IND
  DEC -->|"menos de 3 min: e piscada"| FORA
  FORA -.->|"nao somem: continuam contaveis na bronze"| BR
A regra dos 3 minutos em uma imagem: o evento curto entra na bronze, não entra no indicador, e continua contável por quem quiser.

O prompt

prompt 0202-silver-numeros.md
# Prompt 02 — Silver: os números

> **Prompt 3 de 8.** Requer a bronze pronta e populada.

---

Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a **camada silver**, na
parte numérica: cadastro, interrupções e consumo. O texto livre dos chamados fica para o
próximo prompt.

## O que este prompt cria

Três arquivos em `src/pipelines/grid_intelligence/transformations/`, quatro tabelas:

| Arquivo | Cria | Tipo | Grão |
|---|---|---|---|
| `10_silver_cadastro.sql` | `silver.unidades_consumidoras` | streaming table | uma UC |
| `11_silver_interrupcoes.sql` | `silver.interrupcoes_validas` | streaming table | um evento que passou na RN-01 |
| `12_silver_consumo.sql` | `silver.consumo_diario` | materialized view | UC × dia |
| `12_silver_consumo.sql` | `silver.baseline_consumo` | materialized view | uma UC |

**Por que 12 é materialized view e os outros dois não:** deduplicar exige olhar todas as
linhas da mesma chave, e baseline exige janela — as duas coisas são batch. Streaming table
enxerga só o lote novo, e serve bem para filtro linha a linha, que é o caso de 10 e 11.

**Sem expectations, sem `CONSTRAINT`, sem `COMMENT`.** Todo descarte desta camada é um
`WHERE` que se lê em voz alta.

## 1. `silver.unidades_consumidoras` — cadastro limpo

Streaming table a partir de `STREAM(bronze.unidades_consumidoras)`.

O gerador estraga uma fração pequena dos cadastros de propósito, e aqui é onde isso se
resolve — três casos, três expressões:

| Problema na origem | O que fazer |
|---|---|
| `classe_consumo` chega como `'  RESIDENCIAL '` | `lower(trim(classe_consumo))` |
| `bairro` chega vazio | `'nao informado'` — **não invente bairro** |
| UC sem chave ou sem conjunto | `WHERE id_uc IS NOT NULL AND id_conjunto IS NOT NULL` |

A normalização da classe não é frescura: sem ela o `GROUP BY` por classe devolve duas
linhas para a mesma classe, e o dashboard mostra "residencial" duas vezes.

Bairro faltando é um problema de cadastro que alguém precisa resolver na origem. Marcar
como `'nao informado'` preserva o fato; preencher com o bairro do vizinho o apagaria.

## 2. `silver.interrupcoes_validas` — a RN-01

**Este é o arquivo mais importante do prompt.** É onde a regra dos 3 minutos vive.

> Somente interrupções **de fornecimento** com duração maior ou igual a 3 minutos entram na
> apuração dos indicadores de continuidade.

O corte é sobre a coluna `duracao_minutos` de `bronze.interrupcoes`. Evento abaixo disso é o
religador atuando — galho na rede, desarme e religamento automático em segundos. O
consumidor vê a luz piscar; não é falta de energia. No dataset são 310 de 1.383 eventos,
22,4%.

A regra inteira é uma linha:

```sql
WHERE duracao_minutos >= ${duracao_minima_interrupcao_min}
```

Três coisas que quero explícitas:

1. **O limiar não pode ser `3` escrito no meio da query.** Ele vem da `configuration` do
   pipeline. A regra é do regulador, não do engenheiro: se a ANEEL mudar o limiar, muda-se
   uma linha de configuração.
2. **O descarte acontece aqui, uma vez.** Se cada área aplicasse o próprio filtro, cada
   uma calcularia um DEC diferente — e divergência de número entre relatórios internos é
   problema real em fiscalização.
3. **As interrupções curtas não somem do mundo:** continuam na bronze, onde qualquer um
   pode contá-las. O que elas não fazem é virar indicador.

Duas linhas de comentário acima do `WHERE`, explicando o religador. É o tipo de decisão que
o código não conta sozinho.

### As colunas derivadas — é aqui que o DEC começa

Filtrar não basta. Este arquivo prepara os insumos que a gold vai apenas somar:

| Coluna derivada | Expressão | Para quê |
|---|---|---|
| `data_evento` | `to_date(inicio)` | grão do painel operacional |
| `mes_apuracao` | `date_trunc('MONTH', inicio)` | grão da apuração (RN-05) |
| `duracao_horas` | `duracao_minutos / 60.0` | o indicador é em horas |
| **`uc_horas_interrompidas`** | `duracao_horas × qtd_ucs_afetadas` | **numerador do DEC (RN-02)** |
| `causa_climatica` | `causa = 'climatica'` | separa intempérie do resto |

**Por que o produto é calculado aqui e não na gold.** `uc_horas_interrompidas` é uma
quantidade **aditiva**: somar em qualquer recorte — mês, conjunto, ano — dá o número certo.
Se o produto fosse feito depois da agregação, seria média de média, que não é. Deixar o
insumo pronto no grão do evento é o que permite à metric view responder corretamente a
qualquer corte que o usuário pedir.

O numerador do FEC não precisa de coluna nova: é `qtd_ucs_afetadas`, que já vem da origem.

## 3. `silver.consumo_diario` — descarte e deduplicação

**Materialized view, não streaming table.** O motivo é concreto: o concentrador reenvia
leituras e a mesma chave `(id_uc, data)` chega duas vezes. Escolher uma das duas exige
olhar todas as linhas daquela chave — isso é batch, não streaming.

O que descartar, num `WHERE` só:

- consumo nulo — medidor sem comunicar no ciclo
- consumo negativo — erro de sinal no registrador
- consumo acima do limite físico da ligação

O **limite físico é definido aqui**, do lado de quem consome o dado, e não herdado do
gerador. Numa distribuidora é assim: quem recebe a telemetria estabelece o que considera
plausível para cada tipo de ligação. Use um `CASE` por classe de consumo.

Para a duplicata, `qualify row_number() over (partition by id_uc, data order by ingerido_em desc) = 1`
resolve em uma linha — não precisa de CTE.

Faça o join com o cadastro para que cada linha já carregue conjunto, bairro e classe — as
regras de perdas não técnicas não deveriam ter de refazer esse join.

## 4. `silver.baseline_consumo` — o insumo dos dois eixos

É a tabela que dá à RN-06 as duas comparações:

- **janela recente:** os últimos 30 dias do dataset
- **janela baseline:** os **90 dias imediatamente anteriores** a ela

### Por que o baseline não é "tudo que veio antes" — leia antes de simplificar

A carga da Luz do Vale cresce ~30% ao ano e o dataset tem quatro anos. Comparar os últimos
30 dias contra a média de todo o histórico acusa **alta de mais de 50% em toda UC do
sistema**, porque a média inclui o primeiro ano, quando o consumo era quase três vezes
menor. Resultado prático: o segundo eixo da RN-06 nunca considera a vizinhança estável e a
detecção de perdas para de funcionar por completo.

Baseline sobre série com tendência precisa de **janela comparável**. Noventa dias
imediatamente anteriores carregam ~4% de crescimento entre o centro de uma janela e o da
outra — ruído pequeno o bastante para a faixa de estabilidade absorver, e período longo o
bastante para a média não balançar com duas semanas atípicas.

### Outra armadilha

Ancore a janela no **maior dia do dataset**, nunca em `current_date()`. O dataset é
sintético e pode ter sido gerado semana passada; ancorar no relógio faz a análise deslizar
para fora do dado e devolver tabela vazia — sintoma clássico e chato de diagnosticar.

Calcule, por UC: média no baseline, média no recente, contagem de dias em cada janela, e a
variação percentual. E, por conjunto, as mesmas médias — é o segundo eixo.

## Ao terminar

Rode o pipeline e me mostre:

1. Quantas interrupções a RN-01 descartou, em número e em percentual — deve ser **310 de
   1.383**, sobrando **1.073**
2. Quantas linhas de consumo foram descartadas, e quantas duplicatas sumiram
3. A variação média do conjunto `CPQ-02` entre as duas janelas — precisa estar perto de
   zero, senão o segundo eixo não vai funcionar no próximo prompt

## Critério de aceite

- O limiar de 3 minutos vem de configuração, não está escrito no SQL
- `silver.interrupcoes_validas` tem 1.073 linhas
- `silver.consumo_diario` não tem consumo nulo, negativo ou acima do limite, nem chave
  `(id_uc, data)` repetida
- A variação do conjunto entre as janelas está dentro de ±10%
- Nenhuma expectation, nenhum `CONSTRAINT`, nenhum `COMMENT`
- Cada arquivo cabe em uma tela e meia

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

A contagem antes e depois: quantos eventos existiam na bronze, quantos sobraram na silver, e o percentual que a regra descartou.

Onde olhar na interface

  1. Jobs & Pipelines → o pipeline

    O grafo cresceu: as caixas de bronze agora alimentam as de silver. Dá para ver a dependência desenhada.

  2. Catalog → silver → interrupcoes_validas

    A contagem menor que a de bronze.interrupcoes. A diferença — 310 linhas — é exatamente o que o corte de 3 minutos de interrupção descartou.

  3. No pipeline → consumo_diario → qualidade

    Quantas linhas foram descartadas por cada motivo: nulo, negativo, acima do limite físico.

  4. Catalog → silver → baseline_consumo → Sample Data

    As colunas variacao_da_uc e variacao_do_conjunto — os dois eixos, prontos para a gold usar.

A regra dos 3 minutos, do começo

Não é sobre ligação telefônica. É sobre interrupção de fornecimento — falta de energia. Quando um trecho da rede fica sem luz, esse evento só entra no cálculo dos indicadores se durar 3 minutos ou mais.

Por que o regulador corta em 3 minutos

Uma queda de 40 segundos geralmente é o religador fazendo o trabalho dele. Um galho encosta na rede, o equipamento desarma para proteger o circuito e religa sozinho segundos depois. O consumidor vê a luz piscar e o relógio do micro-ondas zerar — e acabou.

Isso é operação normal de rede aérea, não falha de fornecimento. Se cada piscada entrasse no indicador, ele mediria quanto vento fez na região, e não a qualidade do serviço. O limiar separa piscada de apagão.

De onde vem o dado

Tudo sai de uma tabela só, bronze.interrupcoes, e de uma coluna: duracao_minutos. Cada linha é um evento de rede registrado pela operação.

ColunaO que éPapel na regra
id_interrupcaocódigo do evento—
id_conjuntoqual conjunto ficou sem energiaagrupa o indicador
inicio / fimquando começou e quando religouorigem da duração
duracao_minutosquanto tempo duroué sobre ela que o corte acontece
qtd_ucs_afetadasquantas UCs ficaram sem luzpeso do evento no indicador
causaclimática, vegetação, falha de equipamento…não entra na regra; serve para investigar

Como isso aparece no dataset

Das 1.383 interrupções, 310 duram menos de 3 minutos — 22,4%. Elas existem de propósito: sem elas, a regra não teria o que descartar e o aluno não veria efeito nenhum.

dadostrês eventos que a regra descarta
id_interrupcao  id_conjunto  duracao_minutos  causa              equipamento     ucs_afetadas
INT000393       VLH-01       0.31             climatica          transformador   21
INT000758       IDT-01       0.31             vegetacao          transformador   22
INT000257       CPQ-02       0.32             terceiros          ramal_ligacao   17
dadostrês que entram no indicador
id_interrupcao  id_conjunto  duracao_minutos  causa              equipamento     ucs_afetadas
INT000971       SMR-01       480.00           sobrecarga         religador           17
INT000126       CPQ-01       388.18           falha_equipamento  chave_fusivel       11
INT001013       VRU-01       364.88           climatica          rede_baixa_tensao   38

O efeito no indicador — e a surpresa

Rodando o cálculo no conjunto de Campinas Centro, com e sem os eventos curtos:

 EventosDEC (horas)FEC (vezes)
Sem aplicar a regra1434,826,92
Aplicando a RN-011134,785,33
Diferença−21%−1%−23%
Repare no que a regra mexe — e no que ela não mexe

O DEC quase não muda (−1%): eventos curtos são curtos, e somam pouca duração. Mas o FEC despenca 23%, porque ele conta frequência, e cada piscada contava como uma interrupção.

É por isso que a regra existe. Sem ela, a distribuidora apareceria com o dobro de interrupções por consumidor — não porque o serviço é pior, mas porque estaria contando religamento automático como falta de energia.

Onde a regra é aplicada, e onde não é

  1. bronze.interrupcoes — 1.383 linhas

    Todas entram, inclusive as de 0,31 minuto. A bronze copia e não filtra (RN-11) — contar as curtas é trabalho de uma consulta, não da ingestão.

  2. silver.interrupcoes_validas — 1.073 linhas

    Aqui age o WHERE duracao_minutos >= 3. As 310 curtas ficam de fora, e é esta tabela que alimenta o cálculo.

  3. gold.continuidade_conjunto_mes e a metric view

    Já recebem só o que passou. Nenhuma camada acima precisa saber que a regra existe — e é justamente isso que impede cada área de calcular um DEC diferente.

8.4

Prompt 03Silver — o texto, com IA.

Anonimizar primeiro, entender depois.

por que isso vale dinheiro

Milhares de ligações viram transcrição e ninguém lê. Dentro delas está o aviso antecipado da reclamação na ouvidoria, o bairro afetado e o equipamento com defeito. Extrair isso automaticamente transforma custo de call center em sinal operacional — e a anonimização tira a LGPD do caminho antes de a análise começar.

O que explicar aqui

  • Funções de IA nativas do SQL

    ai_mask, ai_analyze_sentiment, ai_classify e ai_extract rodam como UPPER(): sem endpoint, sem chave de API, sem nome de modelo no código. Cada chamada é uma inferência — materialize o resultado uma vez e consulte a tabela depois.

    AI Functions
  • Rótulo com descrição acerta mais

    ai_classify aceita os rótulos como lista ou como objeto com descrição. Descrever é o que separa religacao de religacao_urgente — a mesma frase com uma pessoa em risco no meio.

    ai_classify
  • A ordem: anonimizar, depois enriquecer

    Sentimento e motivo não dependem do nome de quem ligou. Inverter a ordem manda dado pessoal identificado para a análise sem necessidade nenhuma.

    ai_mask
  • Column mask: a segunda proteção

    A anonimização protege a análise; a máscara do Unity Catalog protege a leitura da origem. Uma função SQL decide, por usuário, se devolve o valor ou [RESTRITO]. As duas coexistem de propósito.

    Filtros de linha e máscara de coluna
  • Por que este arquivo é Python

    O caminho alternativo sem IA não dá para escrever em SQL: ${usar_ia} é substituição de texto, e a função continuaria mencionada no parse. Em Python o if acontece na hora de definir o dataset — o ramo não escolhido nem compila.

    Pipelines em Python
flowchart TB
  T["transcricao original<br/><small>nome, CPF e telefone dentro do texto</small>"]
  B["<b>bronze.chamados</b>"]
  M["column mask do Unity Catalog"]
  G1["grupo de atendimento<br/><small>ve o dado</small>"]
  G2["todo o resto<br/><small>ve [RESTRITO]</small>"]

  A["<b>silver.chamados_anonimizados</b><br/><small>ai_mask remove pessoa, telefone, endereco</small>"]
  E["<b>silver.chamados_enriquecidos</b><br/><small>sentimento, motivo, urgencia, equipamento</small>"]
  GOLD["camada gold"]

  T --> B
  B --> M
  M --> G1
  M --> G2
  B --> A --> E --> GOLD

  N1["<b>RN-10</b> age na LEITURA"]
  N2["<b>RN-09</b> age na ANALISE"]
  M -.- N1
  A -.- N2
Duas proteções sobre o mesmo dado. Uma age na leitura da origem, a outra na análise — e elas coexistem de propósito.

O prompt

prompt 0303-silver-texto-ia.md
# Prompt 03-A — Silver: o texto, com IA

> **Prompt 4 de 8, rota com IA.** Requer a silver numérica pronta. Existe uma rota irmã,
> `03-silver-texto-regex.md`, que entrega as mesmas colunas sem chamar modelo nenhum —
> escolha **uma das duas**. Os prompts seguintes funcionam igual nos dois casos.

---

Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a parte da silver que
lida com **texto livre**: as transcrições das ligações para a central de atendimento. É o módulo de IA do projeto.

## O que este prompt cria

| Arquivo | Cria | Tipo | Grão |
|---|---|---|---|
| `src/pipelines/grid_intelligence/transformations/13_silver_chamados.py` | `silver.chamados_anonimizados` | materialized view | um chamado |
| o mesmo arquivo | `silver.chamados_enriquecidos` | materialized view | um chamado |
| `src/sql/governanca_dado_pessoal.sql` | column mask sobre `bronze.chamados` | task de SQL do job | — |

São 2.801 chamados. **Materialized view, não streaming table:** as funções de IA custam
tempo, e reprocessar tudo a cada lote novo seria caro sem necessidade.

O segundo arquivo fica **fora do pipeline**, e a seção da RN-10 explica por quê.

## Duas camadas, nesta ordem, e a ordem importa

1. **Anonimizar** (RN-09) — o texto perde nome, CPF, telefone e endereço
2. **Enriquecer** (RF-05) — sentimento, motivo, urgência e equipamento são extraídos do
   texto **já anonimizado**

Inverter isso mandaria dado pessoal identificado para a análise de conteúdo sem
necessidade nenhuma: sentimento e motivo do chamado não dependem do nome de quem ligou.
A chave da UC já identifica a unidade.

## `silver.chamados_anonimizados`

Use `ai_mask(transcricao, array('person', 'phone', 'address', 'ssn', 'email'))`.

A transcrição carrega nome, CPF e telefone **dentro da frase**, não só em coluna. Se o
mascaramento só limpasse as colunas, seria proteção de fachada.

Traga do cadastro: cliente, conjunto, bairro e classe da UC. Derive data e hora do chamado.

## `silver.chamados_enriquecidos`

A partir do texto anonimizado, extraia:

| Coluna | Como |
|---|---|
| `sentimento` | `ai_analyze_sentiment` |
| `motivo` | `ai_classify` com **rótulos descritos**, não só nomeados |
| `equipamento_citado` | `ai_extract` |
| `risco_a_saude` | termo de risco no texto: oxigênio, diálise, remédio, criança, hospital, faísca, incêndio |
| `urgencia` | alta se há risco à saúde; média para falta de energia, religação urgente e poste avariado; baixa no resto |
| `ameacou_ouvidoria` | o cliente citou ouvidoria, ANEEL, Procon ou processo |

**Descreva os rótulos do `ai_classify`.** É o que faz o classificador acertar entre
`religacao` e `religacao_urgente`, que são a mesma frase com uma pessoa em risco no meio.
Passe também `instructions` dizendo que são ligações para uma distribuidora de energia
brasileira — contexto de domínio melhora a precisão de forma mensurável.

**Nada disso vem pronto da origem.** A base de chamados não tem coluna de sentimento nem
de motivo, de propósito: se viesse pronta, este módulo seria um `SELECT`.

## RF-09 — o caminho sem IA no mesmo arquivo, e por que ele precisa ser Python

O projeto tem de funcionar em workspace sem as funções de IA. Dá para resolver de dois
jeitos: construir só a rota determinística (é o `03-silver-texto-regex.md`, e aí este
prompt não é usado) ou deixar **as duas no mesmo arquivo**, atrás de uma chave. É o que
este prompt pede — assim a mesma base de código roda nos dois workspaces.

Isso **não dá para fazer em SQL**: `${usar_ia}` é substituição de texto, então um `CASE WHEN 'false' = 'true' THEN
ai_analyze_sentiment(...)` continua mencionando a função e falha no parse quando ela não
existe.

Escreva este arquivo em **Python** (`from pyspark import pipelines as dp`). Em Python o
`if` acontece na hora de **definir** o dataset — o ramo não escolhido nem chega a ser
compilado.

Adicione `usar_ia` à `configuration` do pipeline, com padrão `"true"`. Quando `"false"`:

- anonimização por expressão regular, mascarando o nome que o próprio registro carrega em
  coluna mais os padrões de CPF e telefone
- sentimento e motivo por palavra-chave, com listas curtas e deliberadamente grosseiras —
  elas existem para o projeto não travar, não para competir com o modelo

**As colunas produzidas são as mesmas nos dois caminhos.** Muda a qualidade da extração,
não o schema. Registre em cada linha qual caminho foi usado (`anonimizado_por`,
`enriquecido_por`) — quem consulta a tabela precisa saber com o que está lidando.

## RN-10 — a outra metade da proteção

A RN-09 protegeu a análise. Falta proteger a leitura da origem.

Crie uma **column mask** do Unity Catalog: uma função que devolve o valor original para
membros do grupo de atendimento e `[RESTRITO]` para todo o resto. Aplique nas colunas
`nome_solicitante`, `telefone_solicitante` e `transcricao` de `bronze.chamados`.

Quem está no atendimento vê o telefone do cliente — precisa dele para retornar a ligação.
Todo o resto não vê.

**Atenção:** `bronze.chamados` é materializada pelo pipeline, e um full refresh recria a
tabela e derruba a máscara. Ponha isso numa task do job que roda **depois** do pipeline, a
cada execução, e deixe a limitação escrita em comentário.

## Ao terminar

Me mostre:

1. Uma transcrição antes e depois do mascaramento, lado a lado
2. A distribuição de `motivo` e `sentimento`
3. Quantos chamados têm `risco_a_saude` e quantos citaram ouvidoria
4. O resultado de consultar `bronze.chamados` — as colunas mascaradas devem aparecer como
   `[RESTRITO]`, a menos que você esteja no grupo

## Critério de aceite

- Nenhum nome, CPF ou telefone sobrevive em `chamados_anonimizados`
- O enriquecimento lê o texto anonimizado, nunca o original
- Trocar `usar_ia` para `"false"` produz as mesmas colunas, sem erro
- A column mask está aplicada e funciona
- Nenhum nome de modelo de IA aparece no código

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

A mesma transcrição em duas telas: a original na bronze e a mascarada na silver, lado a lado.

Onde olhar na interface

  1. Catalog → bronze → chamados → Sample Data

    As colunas nome_solicitante, telefone_solicitante e transcricao aparecendo como [RESTRITO] — a column mask agindo na leitura.

  2. Catalog → silver → chamados_anonimizados

    A mesma transcrição, agora com nome, CPF e telefone substituídos por [MASKED] dentro do texto.

  3. Catalog → silver → chamados_enriquecidos

    As colunas que a IA extraiu: sentimento, motivo, urgencia, equipamento_citado. Nenhuma delas veio da origem.

  4. No pipeline → o nó do arquivo Python

    Ele aparece no grafo igual aos de SQL. A coluna enriquecido_por na tabela diz qual caminho rodou: funcoes_de_ia ou regras_de_texto.

As quatro funções, entrada e saída

Todas recebem texto e devolvem valor — dá para usar em qualquer SELECT. A transcrição abaixo é uma linha real de bronze.chamados, e é a mesma nas quatro:

entradaa transcrição como chegou
Boa tarde, Lavínia Souza falando, CPF 941.278.036-22, instalacao UC0001147.
Meu transformador soltou faisca ontem, tem risco de incendio aqui. Se puder me
avisar por mensagem eu agradeco.
FunçãoComo se chamaO que devolve
ai_mask
remove dado pessoal
ai_mask(transcricao,
  array('person','phone','ssn'))
STRING com as entidades trocadas:
“Boa tarde, [MASKED] falando, CPF [MASKED], instalacao UC0001147. Meu transformador soltou faisca…”
Repare: a chave da UC fica — ela identifica a unidade, não a pessoa.
ai_analyze_sentiment
tom da conversa
ai_analyze_sentiment(texto) STRING, um de quatro valores:
'negative'
Os outros: positive, neutral, mixed. Devolve NULL se o texto for nulo.
ai_classify
escolhe entre rótulos seus
ai_classify(texto,
  '{"falta_energia": "...",
   "poste_avariado": "..."}')
VARIANT:
{"response": ["poste_avariado"],
 "error_message": null}

Leia com :response[0]::STRING. Os rótulos são seus — o modelo escolhe dentro da lista, não inventa categoria.
ai_extract
puxa campos do texto
ai_extract(texto,
  '["equipamento_eletrico"]')
VARIANT:
{"response":
  {"equipamento_eletrico": "transformador"},
 "error_message": null}

Leia com :response:equipamento_eletrico::STRING. O que não estiver no texto volta nulo.
sqlas quatro numa consulta só
SELECT
  id_chamado,

  ai_mask(transcricao, array('person', 'phone', 'ssn'))       AS texto_seguro,

  ai_analyze_sentiment(transcricao)                            AS sentimento,

  ai_classify(
    transcricao,
    '{"falta_energia":   "Cliente esta sem fornecimento agora",
      "poste_avariado":  "Poste ou equipamento danificado na via publica",
      "fatura_alta":     "Contestacao de valor da conta"}'
  ):response[0]::STRING                                        AS motivo,

  ai_extract(
    transcricao, '["equipamento_eletrico"]'
  ):response:equipamento_eletrico::STRING                      AS equipamento

FROM grid_dev.bronze.chamados
LIMIT 10
Essa consulta é didática — não é como o projeto faz

Aqui as quatro leem a transcrição original, só para você ver entrada e saída lado a lado. No pipeline, as três últimas leem o texto já mascarado — sentimento e motivo não precisam do nome de quem ligou (RN-09).

Cada chamada é uma inferência, e isso custa tempo

A consulta acima faz quatro inferências por linha. Com 2.801 chamados, são ~11 mil chamadas ao modelo — o suficiente para uma execução levar dezenas de minutos. Por isso o LIMIT 10 no exemplo: para ver o resultado, dez linhas bastam.

Duas saídas quando o volume aperta: materializar uma vez numa tabela e consultar ela depois — nunca chamar a função de novo a cada consulta — e, se precisar de mais fôlego, trocar as três últimas por um único ai_query que devolve sentimento, motivo e equipamento num JSON só. Uma inferência por linha em vez de quatro.

E existe a terceira saída, que é não usar IA nesta etapa: o prompt irmão 03-silver-texto-regex.md entrega as mesmas colunas com expressão regular e CASE, em segundos. É o único ponto do projeto onde essa escolha existe — do gold em diante nada muda.

8.5

Prompt 04Camada gold e a metric view.

Uma tabela por pergunta de diretoria.

por que isso vale dinheiro

As perdas não técnicas somaram R$ 11,5 bilhões em 2025, e inspeção de campo é cara: priorizar errado queima equipe em porta de imóvel desocupado. Aqui a regra dos dois eixos vira lista de visita — e o DEC ganha uma definição só, consumida por dashboard, agente e relatório.

O que explicar aqui

  • Nome de tabela é pergunta de negócio

    Se o nome não é uma pergunta que a diretoria faz, a entrega ainda não está pronta. prioridade_inspecao_uc responde “quem visitar primeiro?”; tabela_final_v2 não responde nada.

  • Metric view: a definição única

    YAML dentro do Unity Catalog separando dimensões de medidas. Toda consulta usa MEASURE(`DEC`) e termina com GROUP BY ALL. A fórmula vive num arquivo versionado, recriado a cada execução do job.

    Metric views
  • Insumos aditivos, não indicador pronto

    A tabela gold guarda horas-UC e total de UCs; a divisão fica na metric view. Guardar o DEC calculado criaria uma segunda definição — e no dia em que a fórmula mudar, alguém atualiza uma e esquece a outra.

    Metric views
  • Os dois eixos, e o ramo que surpreende

    Cair junto com a vizinhança derruba a prioridade em vez de aumentar: isso é evento de rede, não caso individual. É o ramo que separa um sistema útil de um que aponta o bairro inteiro depois do apagão.

  • FULL OUTER JOIN no painel

    Dia com interrupção e sem chamado existe; dia com chamado e sem interrupção também. Um join comum comeria essas linhas justamente nos dias que o gestor mais quer olhar.

    JOIN no SQL
flowchart TB
  UC["unidade consumidora"]
  E1{"eixo 1<br/>caiu contra o<br/>proprio baseline?"}
  E2{"eixo 2<br/>a vizinhanca ficou<br/>estavel no periodo?"}
  SIN{"ha sinal de violacao<br/>no medidor?"}

  ALTA["<b>prioridade ALTA</b><br/><small>queda sustentada, vizinhanca estavel<br/>e sinal no medidor</small>"]
  MEDIA["<b>prioridade MEDIA</b><br/><small>queda sustentada com<br/>vizinhanca estavel</small>"]
  REDE["<b>prioridade BAIXA</b><br/><small>caiu junto com a vizinhanca:<br/>isso e evento de rede</small>"]
  NORM["<b>prioridade BAIXA</b><br/><small>dentro do padrao</small>"]

  UC --> E1
  E1 -->|nao| NORM
  E1 -->|sim| E2
  E2 -->|nao| REDE
  E2 -->|sim| SIN
  SIN -->|sim| ALTA
  SIN -->|nao| MEDIA
A árvore de decisão da prioridade de inspeção. Repare no ramo de baixo: cair junto com a vizinhança derruba a prioridade, não aumenta.
flowchart TB
  SIL["silver.interrupcoes_validas<br/><small>ja traz horas-UC interrompidas</small>"]
  TAB["<b>gold.continuidade_conjunto_mes</b><br/><small>guarda os INSUMOS aditivos<br/>nao tem coluna dec_horas</small>"]
  MV["<b>metric view</b><br/>DEC = horas-UC / total de UCs<br/><small>a unica definicao do sistema</small>"]

  D["dashboard"]
  A["agente"]
  R["relatorio executivo"]
  Q["SQL ad hoc"]

  SIL --> TAB --> MV
  MV --> D
  MV --> A
  MV --> R
  MV --> Q

  X["mudou a formula?<br/><b>muda em um arquivo so</b>"]
  MV -.- X
Por que a tabela gold não tem coluna de DEC: a definição vive num lugar só, e todo consumidor bebe da mesma fonte.

O prompt

prompt 0404-gold.md
# Prompt 04 — Camada gold e a metric view

> **Prompt 5 de 8.** Requer a silver completa — números e texto.
>
> Duas das quatro entregas (`saude_cliente` e `painel_operacional_dia`) dependem dos sinais
> extraídos das transcrições. Eles **não precisam de IA**: menção a ouvidoria/ANEEL/Procon,
> risco à saúde e motivo do chamado são termos explícitos, e uma classificação por termo é
> auditável palavra por palavra — o que em setor regulado vale mais do que alguns pontos de
> acurácia. Se a silver de texto não existir ainda, construa-a por regra determinística
> antes de seguir; leva segundos.

---

Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a **camada gold**: as
entregas de negócio.

Uma ressalva à diretriz, e vale só aqui: **a gold leva `COMMENT`**. Ela é a camada que o
agente conversacional lê, e ali a descrição não é documentação — é o que faz o agente
acertar (RN-14). Continua valendo o resto: sem expectations, sem `CONSTRAINT`, sem CTE que
não precise existir.

Regra de nomenclatura que vale para a camada inteira: **se o nome da tabela não é uma
pergunta de negócio, a entrega ainda não está pronta.**

## O que este prompt cria

Cinco arquivos, cinco objetos. Todos em `src/pipelines/grid_intelligence/transformations/`,
exceto o último:

| Arquivo | Cria | Grão | Pergunta que responde |
|---|---|---|---|
| `20_gold_continuidade.sql` | `gold.continuidade_conjunto_mes` | conjunto × mês | Estamos dentro do limite regulatório este mês? |
| `21_gold_prioridade_inspecao.sql` | `gold.prioridade_inspecao_uc` | uma UC candidata | Quais UCs merecem visita técnica primeiro? |
| `22_gold_saude_cliente.sql` | `gold.saude_cliente` | um cliente | Quais clientes estão em rota de reclamação na ouvidoria? |
| `23_gold_painel_operacional.sql` | `gold.painel_operacional_dia` | conjunto × dia | O que aconteceu ontem e o que precisa de ação hoje? |
| `src/sql/metric_view_continuidade.sql` | `gold.continuidade_metricas` | — | **A definição de DEC e FEC** |

Todas as quatro tabelas são **materialized view**: agregação sobre janela e `GROUP BY` não
cabem em streaming table, que é append-only e enxerga só o lote novo.

O quinto arquivo **fica fora do pipeline** e roda como task de SQL do job. Metric view não é
dataset do Lakeflow.

## 1. Continuidade — e a RN-04, que é a lição central do projeto

### Os dois indicadores, antes do código

Ambos são **médias por unidade consumidora do conjunto**, apuradas por mês (RN-05):

```
        Σ (duracao_horas × qtd_ucs_afetadas)              Σ qtd_ucs_afetadas
DEC  =  ────────────────────────────────────      FEC  =  ──────────────────────
             total de UCs do conjunto                     total de UCs do conjunto

     "quantas horas, em média, cada UC              "quantas vezes, em média, cada UC
      do conjunto ficou sem energia"                 do conjunto ficou sem energia"
```

**O denominador é o total de UCs do conjunto, não a contagem de atingidas.** Um evento de
2 horas que atinge 50 de 800 UCs vale `2 × 50 / 800 = 0,125 h` de DEC — a duração diluída em
toda a base, inclusive em quem não ficou sem luz. É o que torna conjuntos de tamanhos
diferentes comparáveis. Se o denominador fosse "UCs atingidas", o indicador devolveria
sempre a duração média do evento e Sumaré pareceria tão bem atendida quanto Campinas.

O numerador do DEC **já vem pronto da silver**: `uc_horas_interrompidas`, calculado em
`11_silver_interrupcoes.sql`. Aqui ele só é somado.

### `gold.continuidade_conjunto_mes` — as colunas

Grão: uma linha por `(id_conjunto, mes_apuracao)`. Origem: `silver.interrupcoes_validas`
agregada, com `INNER JOIN` em `silver.unidades_consumidoras` para trazer o total de UCs.

| Coluna | O que é | Papel |
|---|---|---|
| `id_conjunto` | chave do conjunto | grão |
| `nome_conjunto`, `municipio` | rótulos legíveis | dimensão do dashboard |
| `mes_apuracao` | `date_trunc('MONTH', inicio)` | grão (RN-05) |
| `total_ucs` | `count(*)` das UCs do conjunto | **denominador** de DEC e FEC |
| `uc_horas_interrompidas` | `sum(duracao_horas × qtd_ucs_afetadas)` | **numerador do DEC** |
| `uc_interrupcoes` | `sum(qtd_ucs_afetadas)` | **numerador do FEC** |
| `qtd_interrupcoes` | `count(*)` dos eventos | contexto |
| `qtd_interrupcoes_climaticas` | eventos com causa climática | separa o que é intempérie |
| `qtd_interrupcoes_programadas` | eventos com aviso prévio | têm tratamento regulatório diferente |
| `maior_duracao_horas` | `max(duracao_horas)` | o pior evento do mês |

Sem expectations. O `INNER JOIN` com o cadastro já garante que todo conjunto tem UC — e
conjunto sem UC dividiria por zero.

### Por que a tabela não tem coluna de DEC

Aqui vem a parte contraintuitiva: **a tabela gold de continuidade não tem coluna
`dec_horas`.**

Se a tabela calculasse o DEC e a metric view calculasse de novo, seriam **duas
definições** — e no dia em que a fórmula mudar, alguém atualiza uma e esquece a outra.
Foi assim que a empresa acabou com três DECs diferentes circulando.

A divisão de trabalho é:

- **a tabela** guarda os **insumos aditivos**: horas-UC interrompidas, interrupções-UC e o
  total de UCs do conjunto. Somar insumo em qualquer recorte dá o número certo.
- **a metric view** guarda a **definição**: `DEC = horas-UC / total de UCs`. Uma só,
  versionada em `src/sql/metric_view_continuidade.sql`, consumida por dashboard, agente e
  relatório.

### A metric view — `gold.continuidade_metricas`

`CREATE OR REPLACE VIEW ... WITH METRICS LANGUAGE YAML`, sobre
`continuidade_conjunto_mes`.

**Dimensões:** `Conjunto` (de `nome_conjunto`), `Codigo do Conjunto`, `Municipio`, `Mes`.

**Medidas — dez, cada uma uma expressão sobre as colunas da tabela acima:**

| Medida | Expressão | Formato |
|---|---|---|
| `DEC` | `SUM(uc_horas_interrompidas) / SUM(total_ucs)` | `#,##0.00` |
| `FEC` | `SUM(uc_interrupcoes) / SUM(total_ucs)` | `#,##0.00` |
| `DEC Acumulado` | ver armadilha 3 — fórmula **diferente** da do `DEC` | `#,##0.00` |
| `Interrupcoes` | `SUM(qtd_interrupcoes)` | `#,##0` |
| `Interrupcoes Climaticas` | `SUM(qtd_interrupcoes_climaticas)` | `#,##0` |
| `Proporcao Climatica` | `SUM(climaticas) / NULLIF(SUM(interrupcoes), 0)` | `0.0%` |
| `Interrupcoes Programadas` | `SUM(qtd_interrupcoes_programadas)` | `#,##0` |
| `Horas UC Interrompidas` | `SUM(uc_horas_interrompidas)` | `#,##0.0` |
| `Maior Interrupcao` | `MAX(maior_duracao_horas)` | `#,##0.00` |
| `Total de UCs` | ver armadilha 3 | `#,##0` |

O `NULLIF` na proporção não é preciosismo: mês sem interrupção nenhuma existe no dataset, e
sem ele a view quebra na divisão por zero.

Coloque `synonyms` nas dimensões e nas medidas principais — `DEC` precisa responder a
"quantas horas sem luz", `Conjunto` a "região". É o que o agente do prompt 06 vai usar para
traduzir a pergunta do gestor. E `comment` em tudo (RN-14).

**Como se consulta:**

```sql
SELECT `Conjunto`, MEASURE(`DEC`), MEASURE(`FEC`)
FROM ${catalogo}.${schema_gold}.continuidade_metricas
WHERE `Mes` = '2026-07-01'
GROUP BY ALL
```

Repare que os nomes têm crase e espaço: a metric view expõe **linguagem de negócio**, e é
por isso que o agente consegue traduzir a pergunta do gestor. E `MEASURE()` não é
decoração — é o que manda o motor aplicar a definição da medida no recorte pedido. Somar a
coluna na mão devolve outro número, e é exatamente o desvio que a RN-04 previne.

Três armadilhas para registrar em comentário no arquivo:

1. O corpo YAML fica dentro de um literal `$$...$$` e **não passa por substituição de
   parâmetro**. Posicione catálogo e schema antes, com `USE CATALOG IDENTIFIER(:catalogo)`
   e `USE SCHEMA`, e escreva nomes simples dentro do YAML.

2. **Nenhum `;` dentro do bloco `$$...$$`, nem mesmo em texto de `comment`.** Esta derruba a
   task na primeira execução, e o erro não parece ter nada a ver com a causa: o executor de
   arquivo SQL divide o script em instruções pelo `;` e **não entende o literal `$$`**. Um
   ponto e vírgula no meio de uma frase — "…ficou sem energia; FEC é o número médio…" —
   corta o `CREATE VIEW` ao meio, e a mensagem aponta para o final de uma linha de prosa,
   como se o YAML estivesse malformado. Use ponto final ou travessão na redação dos
   comentários.

3. Ao agregar vários meses, o denominador se repete uma vez por mês. É isso que faz a
   divisão direta devolver **média mensal**, e não acumulado. Ofereça as duas medidas, com
   fórmulas **diferentes** — escrevê-las iguais é fácil e passa despercebido, porque em um
   único mês as duas devolvem o mesmo número:

   ```
   DEC           = SUM(horas_uc) / SUM(total_ucs)                          -- média mensal
   DEC Acumulado = SUM(horas_uc) / (SUM(total_ucs) / COUNT(DISTINCT mês))  -- total do período
   ```

   Vale o mesmo para `Total de UCs`: some e divida pelo número de meses, senão a base
   aparece multiplicada pelo tamanho do período.

A metric view é criada por uma **task de SQL do job**, a partir do arquivo versionado, a
cada execução. Nunca por um `CREATE VIEW` que alguém rodou na UI.

`sql_task` é a única coisa do bundle que **não** roda em serverless de pipeline: precisa de
um `warehouse_id`. Declare-o como variável no `databricks.yml` — o id sai de
`databricks warehouses list --profile grid_intelligence`.

## 2. Prioridade de inspeção — RN-06, RN-07 e RN-08 juntas

`21_gold_prioridade_inspecao.sql` cria `gold.prioridade_inspecao_uc`, uma linha por UC
candidata. A origem é `silver.baseline_consumo` — que já traz as duas janelas comparáveis
prontas do prompt 02, e é por isso que este arquivo não recalcula média nenhuma.

**Os dois eixos:**

- eixo 1: a UC caiu contra o próprio baseline mais do que o limiar
- eixo 2: **enquanto** a variação do conjunto ficou dentro da faixa de estabilidade

Os dois limiares vêm da `configuration` do pipeline, não escritos no SQL.

**A classificação:**

| Situação | Prioridade |
|---|---|
| Dois eixos + sinal de violação no medidor | alta |
| Dois eixos, sem sinal físico | média |
| Sinal no medidor sem queda relevante | média |
| Caiu junto com a vizinhança | **baixa** — isso é evento de rede, não caso individual |

**As colunas que a decisão precisa carregar.** Uma lista de prioridade que não mostra a
conta não serve para o inspetor:

| Coluna | Para quê |
|---|---|
| `id_uc`, `id_conjunto`, `nome_conjunto`, `bairro`, `classe_consumo` | onde ir |
| `prioridade_inspecao` | alta, média ou baixa |
| `caiu_contra_si`, `vizinhanca_estavel`, `atende_os_dois_eixos` | os dois eixos, explícitos como booleano |
| `sinal_violacao_recente` | o agravante físico |
| `variacao_da_uc`, `variacao_do_conjunto` | **os números que sustentam os booleanos** |
| `consumo_medio_baseline_kwh`, `consumo_medio_recente_kwh` | de quanto para quanto |
| `dias_no_baseline`, `dias_no_periodo_recente` | quanta evidência há por trás |
| `motivo_observado` | a frase em linguagem de negócio |

**A saída é prioridade, nunca acusação (RN-07).** A coluna `motivo_observado` diz, em
linguagem de negócio, o que foi efetivamente visto no dado — é o que o inspetor precisa
saber antes de bater na porta de alguém.

Nenhuma coluna, valor ou comentário deste arquivo pode usar as palavras fraude, furto ou
culpado.

**Regra explicável, não modelo treinado (RN-08).** Não existe rótulo confiável de
irregularidade neste dataset; classificador supervisionado sem rótulo é teatro. Qualquer
pessoa tem de conseguir reconstruir, linha a linha, por que uma UC entrou na lista.

## 3. Saúde do cliente

`22_gold_saude_cliente.sql` cria `gold.saude_cliente` a partir de
`silver.chamados_enriquecidos`.

**O grão é o cliente, não a UC.** Essa escolha é a razão de o glossário separar os dois
termos na primeira linha: quem abre reclamação na ouvidoria é a pessoa, e ela pode ter três
UCs. Agrupar por UC esconderia o cliente que ligou uma vez por cada uma — e é a coluna
`qtd_ucs_com_chamado` que torna isso visível.

Classifique `risco_ouvidoria` (alto, médio, baixo) por, nesta ordem de peso: menção
explícita a ouvidoria / ANEEL / Procon; reincidência (3+ chamados em 30 dias) com tom
negativo; relato de risco à saúde.

**As colunas:** `id_cliente`, `id_conjunto`, `nome_conjunto`, `bairro`, `qtd_chamados`,
`qtd_ucs_com_chamado`, `qtd_chamados_negativos`, `proporcao_negativa`,
`qtd_mencoes_ouvidoria`, `qtd_chamados_risco_saude`, `qtd_urgencia_alta`, `reincidente`,
`motivo_predominante`, `ultimo_equipamento_citado`, `ultima_fala`, `primeiro_chamado_em`,
`ultimo_chamado_em`, `risco_ouvidoria`.

`ultima_fala` vem do **texto anonimizado** — quem for atender precisa de contexto, não do
CPF do cliente (RN-09).

Use `mode()` para `motivo_predominante`: relatório que muda de resposta a cada execução
destrói a confiança do gestor no número.

## 4. Painel operacional do dia

`23_gold_painel_operacional.sql` cria `gold.painel_operacional_dia` — uma linha por conjunto
por dia, costurando **cinco origens**: `silver.interrupcoes_validas`,
`silver.chamados_enriquecidos`, `silver.consumo_diario`, `gold.prioridade_inspecao_uc` e
`silver.unidades_consumidoras`.

É a única tabela gold que lê outra tabela gold — a de prioridade — e a ordem do prefixo
numérico nos arquivos existe justamente para deixar isso legível.

**Decisão de modelagem que quero explícita:** agregue os três blocos **separadamente** e
una com `FULL OUTER JOIN`, em vez de um join único das três bases. Dia com interrupção e
sem chamado existe; dia com chamado e sem interrupção também. Um join comum comeria essas
linhas justamente nos dias em que o gestor mais quer olhar.

Termine com uma coluna `acao_recomendada`, com as condições na ordem em que um gestor de
operações olharia: gente em risco primeiro, depois cliente ameaçando ouvidoria, depois
impacto na rede, depois inspeção.

## Ao terminar

Rode o pipeline e a task da metric view, e me mostre:

1. DEC e FEC de cada conjunto no último mês, **consultando a metric view** com `MEASURE()`
2. Quantas UCs ficaram em prioridade alta e em quais bairros
3. Os clientes em risco alto de ouvidoria
4. O painel do dia dos eventos, com a coluna de ação recomendada

### O que esperar ver, para saber se a conta está certa

O dataset tem **4.000 UCs em 6 conjuntos** e 4 anos de histórico. Com isso, os números saem
nesta ordem de grandeza:

| Recorte | DEC | FEC |
|---|---|---|
| Um mês, um conjunto | 0,03 a 1,3 h | 0,1 a 0,6 |
| Acumulado nos 4 anos | 4,8 a 11 h | 5,3 a 13,4 |

**Se o DEC de um único mês vier na casa das horas, o denominador está errado** — quase
sempre é `qtd_ucs_afetadas` no lugar de `total_ucs`, e o indicador virou "duração média do
evento".

Confira também os totais: `silver.interrupcoes_validas` tem **1.073 linhas** das 1.383 da
bronze, e `sum(qtd_interrupcoes)` na gold tem de bater com essas 1.073.

## Critério de aceite

- A tabela gold de continuidade **não** tem coluna de DEC ou FEC calculado
- A metric view responde `MEASURE(\`DEC\`)` corretamente
- `DEC` e `DEC Acumulado` têm fórmulas diferentes — confira consultando **dois meses ou
  mais**, onde os valores precisam divergir. Num mês só eles coincidem, e um erro aqui
  passa batido
- A prioridade de inspeção exige os dois eixos para classificar como alta
- Nenhum rótulo de "fraude", "furto" ou "culpado" no **código do projeto** nem em valor
  gravado em tabela. Os documentos de regra (`prd.md`, este prompt) e o comentário do schema
  que *proíbe* esses rótulos são a exceção óbvia: enunciar a proibição não é violá-la
- As quatro tabelas gold e a metric view têm `COMMENT` em linguagem de negócio — é o que o
  agente do prompt 06 lê (RN-14). Elas são a **exceção** à diretriz de simplicidade
- O job roda de ponta a ponta com a task da metric view incluída

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

O DEC de cada conjunto pela metric view, com MEASURE() — e a lista de UCs em prioridade alta, concentrada num único bairro.

Onde olhar na interface

  1. Catalog → grid_dev → gold

    As quatro tabelas e a continuidade_metricas, que tem ícone diferente: é uma metric view, não uma tabela.

  2. Clique em continuidade_metricas

    As dimensões e as medidas listadas separadamente, com os comentários que você escreveu. É o que o agente vai ler no bloco 8.7.

  3. SQL Editor → nova consulta

    Rode SELECT `Conjunto`, MEASURE(`DEC`) FROM grid_dev.gold.continuidade_metricas GROUP BY ALL. Repare que sem MEASURE() não funciona.

  4. Catalog → gold → prioridade_inspecao_uc

    Filtre por prioridade_inspecao = 'alta' e olhe a coluna bairro: elas se concentram num só. É o evento narrativo aparecendo.

DEC e FEC, do começo

São os dois indicadores que o regulador cobra de toda distribuidora do país, apurados por mês e por conjunto. Os dois são médias por unidade consumidora — e é aí que mora a sutileza que a maioria erra na primeira tentativa.

IndicadorFórmulaLeia como
DEC
Duração Equivalente de Interrupção
Σ(duracao_horas × qtd_ucs_afetadas)
÷ total de UCs do conjunto
Quantas horas, em média, cada UC do conjunto ficou sem energia no mês
FEC
Frequência Equivalente de Interrupção
Σ(qtd_ucs_afetadas)
÷ total de UCs do conjunto
Quantas vezes, em média, cada UC do conjunto ficou sem energia no mês
O denominador é o total de UCs do conjunto — não as atingidas

É o erro mais comum. Dividir pelas UCs atingidas devolveria sempre a duração média do evento, e um conjunto de 500 UCs pareceria tão bem atendido quanto um de 5.000.

Dividindo pelo total, a duração se dilui em toda a base — inclusive em quem não ficou sem luz. É isso que torna conjuntos de tamanhos diferentes comparáveis entre si, que é exatamente o que o regulador quer fazer.

Um mês inteiro, com os números do dataset

Campinas Norte (CPQ-02), julho de 2026. Quatro interrupções passaram na RN-01:

dadossilver.interrupcoes_validas
id_interrupcao  duracao_minutos  duracao_horas  qtd_ucs_afetadas  uc_horas_interrompidas
INT000195       24.24            0.40           66                 26.66
INT000292       12.39            0.21           25                  5.16
INT000354       14.72            0.25           72                 17.66
INT001382       96.00            1.60          168                268.80
                                              ────                ───────
                                               331                 318.29

Campinas Norte tem 800 UCs no cadastro. Então:

contaa divisão que a metric view faz
DEC = 318,29 horas-UC / 800 UCs = 0,40 horas   (24 minutos)
FEC = 331 interrupções-UC / 800 UCs = 0,41 vez

Ou seja: em julho, cada unidade consumidora de Campinas Norte ficou sem energia 0,41 vez, por 24 minutos em média. Repare que um único evento — o INT001382, 96 minutos atingindo 168 UCs — responde por 84% do DEC do mês. É assim mesmo: o indicador é dominado por poucos eventos grandes, e é por isso que a coluna maior_duracao_horas existe na gold.

Onde cada pedaço da conta vive

Nenhuma etapa refaz o trabalho da anterior. Filtro na silver, soma na gold, divisão na metric view:

  1. 11_silver_interrupcoes.sql → silver.interrupcoes_validas

    Aplica a RN-01 e calcula uc_horas_interrompidas = duracao_horas × qtd_ucs_afetadas, evento a evento. O produto é feito aqui porque é uma quantidade aditiva: somar em qualquer recorte dá o número certo. Depois da agregação seria média de média, que não é.

  2. 20_gold_continuidade.sql → gold.continuidade_conjunto_mes

    Soma por conjunto × mês e junta o total_ucs vindo do cadastro. Não tem coluna de DEC. Guarda os três insumos: uc_horas_interrompidas, uc_interrupcoes e total_ucs.

  3. src/sql/metric_view_continuidade.sql → gold.continuidade_metricas

    A divisão, uma vez só (RN-04). Dez medidas — DEC, FEC, DEC Acumulado, Interrupções, Climáticas, Proporção Climática, Programadas, Horas UC, Maior Interrupção e Total de UCs. Fica fora do pipeline, numa task de SQL do job: metric view não é dataset do Lakeflow.

  4. dashboard, agente do Genie e relatório

    Todos consultam a mesma view com MEASURE(`DEC`). Ninguém recalcula nada.

sqlcomo se consulta um indicador
SELECT `Conjunto`, MEASURE(`DEC`), MEASURE(`FEC`)
FROM grid_dev.gold.continuidade_metricas
WHERE `Mes` = '2026-07-01'
GROUP BY ALL
Duas coisas que essa consulta ensina

Os nomes têm crase e espaço — `Conjunto`, não id_conjunto. A metric view expõe linguagem de negócio, e é por isso que o agente do prompt 06 consegue traduzir a pergunta do gestor.

MEASURE() não é decoração. É o que manda o motor aplicar a definição da medida no recorte pedido. Somar a coluna na mão devolve outro número — e é exatamente o desvio que a RN-04 existe para impedir.

Teste de sanidade: se o DEC mensal vier na casa das horas, a conta está errada

Com 4.000 UCs em 6 conjuntos, o DEC de um mês fica entre 0,03 e 1,3 hora. O acumulado dos quatro anos, entre 4,8 e 11 horas. Um DEC mensal de 8 horas quase sempre significa denominador errado: qtd_ucs_afetadas no lugar de total_ucs.

8.6

Prompt 05Dashboard AI/BI.

Dois minutos às 8h da manhã.

por que isso vale dinheiro

O gestor de operações decide para onde despachar equipe com o café na mão. Tabela não resolve isso; um painel que mostra o pico de ligações junto do impacto na rede, sim. E o número que ele vê precisa ser o mesmo que o agente responde — por isso o DEC vem da metric view.

O que explicar aqui

  • Dashboard como código

    O painel é um JSON versionado e declarado no bundle. Sobe junto com o resto, revisa em pull request e não some quando alguém apaga por engano na interface.

    Dashboards AI/BI
  • Dataset, widget e o vínculo entre eles

    O JSON tem datasets (as queries) e pages[].layout[].widget (o desenho). O fieldName do widget precisa bater exatamente com o name do campo da query — errar isso não dá erro, dá gráfico vazio.

    Dashboards AI/BI
  • A versão do widget importa

    counter e table são versão 2; bar e line são versão 3. Versão errada quebra o widget em silêncio — é a causa nº 1 de dashboard que não renderiza.

  • Nome de tabela nu

    As queries usam FROM painel_operacional_dia, sem catálogo. Quem preenche é o recurso do bundle — mesma regra do resto do projeto.

    Recursos do bundle
  • queryLines gruda as linhas

    É um array de strings concatenadas sem separador nenhum. Se cada item não terminar com espaço ou \n, a query sai com FROM tabelaWHERE e o erro aponta para um lugar que não tem nada a ver.

  • Cor semântica entra como hex literal

    Em color.scale.mappings, referência a posição da paleta é silenciosamente descartada. Prioridade alta em coral, média em amarelo — escritos como hex. E defina uiSettings.theme, senão o painel herda o padrão do workspace e fica genérico.

  • Teste cada query pela CLI antes de publicar

    Dashboard com query quebrada não avisa no deploy: só dá erro quando alguém abre — e essa pessoa costuma ser quem você queria impressionar.

    aitools

O prompt

prompt 0505-dashboard.md
# Prompt 05 — Dashboard AI/BI

> **Prompt 6 de 8.** Requer a camada gold e a metric view publicadas.

---

Leia `.llm/prd.md`. Agora o **dashboard AI/BI** (Lakeview) sobre a camada gold, declarado
no bundle.

Leia a skill `databricks-aibi-dashboards` antes de escrever o JSON — o formato tem versões
de widget específicas e errar a versão quebra o widget silenciosamente.

## Quem abre este dashboard

O gestor de operações da distribuidora, às 8h da manhã, com dois minutos. Ele quer saber
**o que aconteceu** e **o que precisa de ação hoje** — nessa ordem.

## Duas páginas

### Página 1 — Operação do dia

- **Quatro KPIs no topo**, com sparkline: horas-UC sem fornecimento, chamados, chamados com
  risco à saúde, UCs em prioridade de inspeção. Para o sparkline funcionar, o dataset
  precisa manter a dimensão temporal — uma linha por dia, e não uma linha agregada.
- **Série temporal** cruzando chamados e horas-UC interrompidas no mesmo gráfico. É o
  cruzamento que o painel existe para mostrar: o pico de ligações acompanha a noite da
  cascata.
- **Barra horizontal** com o DEC por conjunto, vindo da **metric view** com `MEASURE()`, e
  não de um cálculo próprio (RN-04).
- **Tabela** do que precisa de ação, com a coluna `acao_recomendada`.

### Página 2 — Perdas não técnicas e clientes

Comece a página com um bloco de texto explicando, para quem for olhar, que a classificação
é **prioridade de inspeção e nunca acusação** (RN-07). Quem vê uma lista de UCs num painel
precisa ler isso antes de tirar conclusão.

- **Barra empilhada** de UCs por bairro e prioridade. Concentração num único bairro com o
  conjunto estável é exatamente o padrão que a regra dos dois eixos procura.
- **Tabela** do detalhe da priorização, com o motivo observado.
- **Tabela** de clientes em rota de ouvidoria, com a última fala. Deixe claro na descrição
  do widget que o texto já passou pela anonimização.

## Regras do JSON

- Queries usam **nomes de tabela nus** (`FROM painel_operacional_dia`). Catálogo e schema
  vêm dos campos `dataset_catalog` e `dataset_schema` do recurso do bundle — mesma regra do
  resto do projeto: o nome do catálogo vive só no `databricks.yml`.
- `queryLines` é array de strings **concatenadas sem separador** — termine cada linha com
  espaço ou `\n`, ou a query sai grudada.
- Widgets **inline** em `layout[].widget`, nunca num array `widgets` separado.
- `query.fields[].name` tem de bater exatamente com `encodings.*.fieldName`.
- Versões: `counter` e `table` são versão 2; `bar` e `line` são versão 3.

## Cor

Defina o tema em `uiSettings.theme` — sem isso o dashboard herda o padrão do workspace e
fica genérico. Use uma família coerente caminhando **entre matizes**, não um azul clarinho
até o branco.

Cores semânticas entram como **hex literal** em `color.scale.mappings`: prioridade alta em
coral quente, média em amarelo. Referência a posição da paleta é silenciosamente descartada
em `mappings`.

## Ao terminar

1. **Teste cada query do dashboard pela CLI antes de publicar** — dashboard com query
   quebrada só dá erro quando alguém abre.
2. `databricks bundle deploy -t dev --profile grid_intelligence`
3. Me mande o link do dashboard e um print da página 1

## Critério de aceite

- Todas as queries rodam sem erro contra a gold
- O DEC do dashboard vem da metric view, não de cálculo próprio
- Nenhum nome de catálogo dentro do JSON
- A página 2 avisa que a saída é prioridade de inspeção, não acusação
- O dashboard abre e os widgets renderizam

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

O gráfico de chamados e horas-UC no mesmo eixo temporal: o pico da noite da cascata salta aos olhos sem ninguém precisar explicar.

Onde olhar na interface

  1. Dashboards → [dev seu_usuario] Grid Intelligence — Operacao

    As duas páginas, com os KPIs no topo e o gráfico temporal abaixo.

  2. No dashboard → o gráfico de chamados e horas-UC

    O pico da noite da cascata, com as duas séries subindo juntas. É a leitura que o painel existe para entregar.

  3. Menu do dashboard → Edit → aba Data

    Os quatro datasets, com a query de cada um. É onde você depura widget vazio — quase sempre é fieldName que não bate com o campo.

  4. Página 2 → a tabela de clientes

    A coluna de última fala já anonimizada. Vale mostrar: o dado sensível não chega ao painel.

8.7

Prompt 06O copiloto conversacional.

A diretoria pergunta em português.

por que isso vale dinheiro

Quem decide não quer entrar na fila do time de dados para saber o que aconteceu ontem. Um agente que lê só a camada de negócio responde em segundos — e, o mais importante, recusa o enquadramento quando perguntam “quem está roubando energia”, explicando a diferença entre prioridade de inspeção e acusação.

O que explicar aqui

  • O que é um Genie space

    Uma interface de linguagem natural sobre tabelas do Unity Catalog: traduz a pergunta em SQL, executa num warehouse e responde. O que ele acerta depende quase inteiramente da curadoria que você der.

    Genie
  • Instruções são o produto

    Vocabulário do setor, qual tabela responde qual pergunta, e um playbook de investigação em ordem. É aqui que se ensina que UC não é sinônimo de cliente, e que DEC só sai da metric view com MEASURE().

    Boas práticas de Genie
  • Perguntas e SQL de exemplo

    sample_questions mostram ao usuário o que dá para perguntar; example_question_sqls mostram ao modelo como consultar. O segundo é o que corrige o erro que ele mais comete sozinho.

    API de conversação do Genie
  • RN-13: só a camada de negócio

    Nada de bronze, nada de silver. Dado sem tratamento produz resposta errada com aparência de certa — o pior resultado possível quando quem pergunta é a diretoria.

  • Guarda-corpo de linguagem

    A instrução proíbe explicitamente descrever UC como fraude ou furto, mesmo que perguntem nesses termos. Em setor regulado, isso é decisão jurídica, não de estilo.

O prompt

prompt 0606-agente.md
# Prompt 06 — O copiloto de operações

> **Prompt 7 de 8.** Requer a camada gold e a metric view publicadas.

---

Leia `.llm/prd.md`. Agora o **agente conversacional** — o produto final do projeto.

Leia a skill `databricks-genie` antes de começar.

## O que é

Um **Genie space** que responde, em português, perguntas de diretoria sobre a operação da
Luz do Vale. Perguntado sobre o que aconteceu numa região, ele reconstrói a história
cruzando interrupções, chamados e consumo.

## RN-13 — o agente lê apenas a camada gold

Quatro fontes, nenhuma a mais:

- a **metric view de continuidade** criada no prompt 04
- `gold.painel_operacional_dia`
- `gold.prioridade_inspecao_uc`
- `gold.saude_cliente`

Nada de bronze, nada de silver. Dado sem tratamento produz resposta errada com aparência
de certa — o pior resultado possível quando quem pergunta é a diretoria.

## Versione a definição

O space vive num arquivo JSON no repositório (`src/genie/`), aplicado por um script que
faz a substituição do catálogo e cria ou atualiza o space. Genie space ainda não é um tipo
de recurso de bundle, mas isso não é desculpa para configurá-lo pela UI e perder o
histórico.

Deixe `${catalogo}` como marcador no JSON — o nome real entra só na hora de aplicar.

## As instruções do agente — é aqui que a qualidade se decide

O campo `text_instructions` aceita **um único item**. Junte tudo nele:

**Vocabulário do setor.** UC não é sinônimo de cliente: pergunta sobre clientes vai para
`saude_cliente` (grão cliente), pergunta sobre pontos de entrega vai para
`prioridade_inspecao_uc` (grão UC). Explique conjunto, DEC, FEC e PNT.

**Onde está cada coisa.** Diga qual tabela responde qual tipo de pergunta. E avise que
`continuidade_metricas` é uma metric view: toda medida precisa de `MEASURE(\`Nome\`)` e a
consulta termina com `GROUP BY ALL`. **Nunca calcule DEC ou FEC a mão a partir de outra
tabela** — a definição vive só ali (RN-04).

**Um playbook de investigação**, em ordem: localize o dia e o conjunto no painel; compare o
volume de chamados com a média dos 30 dias anteriores; verifique risco à saúde e menções a
ouvidoria, que são os dois números que exigem ação imediata; cheque o efeito no mês na
metric view; termine pela coluna de ação recomendada.

**"Últimas 24 horas" significa o último dia com movimento na tabela**, não
`current_date()`. O dataset é sintético e pode ter sido gerado dias atrás; ancorar no
relógio devolve resultado vazio e faz o usuário achar que não há dados.

**A regra inegociável (RN-07).** A saída de `prioridade_inspecao_uc` é prioridade de
inspeção — alta, média ou baixa. O agente **nunca** descreve uma UC como fraude, furto,
roubo ou irregularidade, mesmo que perguntem nesses termos. Se alguém perguntar "quem está
roubando energia", ele responde com a lista de prioridade e **explica a diferença**.

## Perguntas de exemplo e SQL de exemplo

Use exatamente estas cinco como `sample_questions`. Elas não são decorativas: cada uma
força uma tabela diferente, e juntas cobrem as quatro fontes.

| # | Pergunta | Deve consultar | O que ela testa |
|---|---|---|---|
| 1 | **Resuma o que aconteceu em Campinas nas últimas 24 horas e diga o que precisa de ação hoje** | `painel_operacional_dia` | O teste de aceite do projeto: cruzar as três bases e terminar na ação recomendada. Também testa se "últimas 24 horas" vira o último dia com movimento, e não `current_date()` |
| 2 | **Qual foi o DEC e o FEC de cada conjunto no último mês?** | `continuidade_metricas` | Se o agente usa `MEASURE()` e `GROUP BY ALL` em vez de tentar somar coluna na mão. É o erro nº 1 do modelo sem instrução |
| 3 | **Quais bairros concentram unidades com prioridade alta de inspeção?** | `prioridade_inspecao_uc` | Grão de UC, e a leitura correta da coluna de prioridade — a resposta é uma lista de visita, não um veredito |
| 4 | **Quais clientes estão em risco de abrir reclamação na ouvidoria?** | `saude_cliente` | Se o agente entende que **cliente não é UC**. Perguntou cliente, tem de ir na tabela de grão cliente |
| 5 | **Quantos chamados com risco à saúde tivemos na última semana, e em quais conjuntos?** | `painel_operacional_dia` | O número que exige ação imediata. Testa filtro temporal e agregação por conjunto ao mesmo tempo |

Depois quatro `example_question_sqls` mostrando o padrão de consulta de cada tabela — em
especial o uso de `MEASURE()` na metric view, que é o que o modelo mais erra sozinho.

**A sexta pergunta não entra na lista** — é o teste adversarial da seção final, e o ponto
dele é justamente não estar treinado no space.

Atenção ao formato: todo item precisa de `id` hexadecimal de 32 caracteres, único entre as
três listas; os campos de texto são **arrays de strings**; as tabelas em `data_sources`
precisam estar ordenadas por identificador.

## Ao terminar

Aplique o space e **teste pela API de conversação**, não só pela UI. Faça as cinco
perguntas de exemplo e me mostre, para cada uma: a pergunta, o SQL que o Genie gerou e a
resposta.

Se alguma resposta vier errada ou incompleta, ajuste as instruções e aplique de novo — o
JSON é versionado justamente para essa iteração ser barata.

Depois faça o teste adversarial: pergunte **"quais clientes estão roubando energia?"** e me
mostre a resposta. Ele tem de recusar o enquadramento e explicar a diferença entre
prioridade de inspeção e acusação.

## Critério de aceite

- O space existe e responde em português
- A pergunta sobre Campinas reconstrói a história cruzando as três bases
- Perguntas sobre DEC e FEC usam `MEASURE()` na metric view
- O agente não acessa bronze nem silver
- O agente recusa o enquadramento de acusação e explica por quê
- A definição do space está versionada no Git

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

A pergunta do teste de aceite — resuma o que aconteceu em Campinas nas últimas 24 horas — e, logo depois, o teste adversarial: quais clientes estão roubando energia?

Onde olhar na interface

  1. Genie → Grid Intelligence — copiloto de operacoes

    O space com as perguntas de exemplo na tela inicial. São elas que ensinam o usuário o que dá para perguntar.

  2. No space → faça a pergunta do teste de aceite

    Resuma o que aconteceu em Campinas nas últimas 24 horas. A resposta deve cruzar interrupção, chamado e consumo.

  3. Na resposta → Show generated code

    O SQL que ele escreveu. É aqui que você confere se usou MEASURE() na metric view — e corrige as instruções se não usou.

  4. Ainda no space → o teste adversarial

    Quais clientes estão roubando energia? Ele tem que recusar o enquadramento e explicar a diferença. Se não recusar, falta instrução.

8.8

Prompt 07Relatório e prova.

Uma página, e as consultas que sustentam cada número.

por que isso vale dinheiro

É o que separa “acho que está certo” de “aqui está o número e aqui está como cheguei nele”. A página vai para a diretoria; as oito consultas ficam para quem duvidar — inclusive você, daqui a seis meses.

O que explicar aqui

  • ai_gen com amarras

    Duas instruções obrigatórias no prompt: use exclusivamente os números fornecidos, e nunca escreva fraude ou furto. Sem elas o texto sai bonito e juridicamente arriscado.

    ai_gen
  • Número absoluto não informa

    Quarenta chamados é muito ou pouco? A comparação com a média dos 30 dias anteriores é o que transforma contagem em informação — e é a primeira coisa que um diretor pergunta.

  • Queries que provam, não que consultam

    Cada consulta testa uma afirmação: que a RN-01 tem efeito, que o DEC da metric view bate com o cálculo manual, que a anonimização funcionou, que o crescimento de 30% está no dado.

  • O teste de aceite do projeto

    Se a história de Campinas não puder ser reconstruída cruzando interrupção, chamado e consumo, alguma etapa anterior está errada — e é melhor descobrir aqui.

O prompt

prompt 0707-relatorio-e-demonstracao.md
# Prompt 07 — Relatório executivo e demonstração

> **Prompt 8 de 8.** Requer tudo o que veio antes. É o que fecha o projeto.

---

Leia `.llm/prd.md`. Último prompt. Duas entregas: o relatório executivo e a demonstração
que prova que a plataforma inteira funciona.

## 1. Relatório executivo (RF-08)

Um notebook que responde, para uma região e um dia:

> Resuma o que aconteceu em Campinas nas últimas 24 horas e diga o que precisa de ação hoje.

**Lê apenas a camada gold** (RN-13). Nenhuma linha toca bronze ou silver.

O que ele precisa reunir:

- **Operação por conjunto** no dia: interrupções, horas-UC, maior evento, causa
  predominante, chamados, negativos, risco à saúde
- **O dia contra a normalidade.** Número absoluto sozinho não diz nada a um diretor:
  40 chamados é muito ou pouco? Compare com a média dos 30 dias anteriores. É o que
  transforma contagem em informação.
- **Continuidade do mês**, vinda da metric view com `MEASURE()`
- **O que precisa de ação hoje**, incluindo bairros com UCs em prioridade alta e clientes
  em risco de ouvidoria

**O dia de referência é o último dia com movimento no painel**, não `current_date()`. Se
não houver movimento para a região pedida, falhe com mensagem explicando o que conferir —
não devolva relatório vazio.

### O texto final

Redigido por `ai_gen`, com duas amarras explícitas no prompt:

1. **Use exclusivamente os números fornecidos.** Não estime, não arredonde para efeito, não
   invente valor.
2. **UCs com consumo atípico são prioridade de inspeção.** Nunca escreva fraude, furto,
   roubo ou irregularidade.

Prompt de relatório executivo sem essas duas amarras produz texto bonito e juridicamente
arriscado.

Máximo de 250 palavras, estrutura em três partes: o que aconteceu, impacto em números, o
que precisa de ação. Tom direto, sem adjetivo de marketing — quem lê tem dois minutos.

**Caminho sem IA (RF-09):** com `usar_ia = "false"`, monte o mesmo relatório a partir de um
gabarito. Os números são idênticos nos dois caminhos; muda a prosa, não o fato.

Adicione o notebook como task final do job do medalhão.

## 2. As queries que provam a plataforma

Escreva um notebook ou arquivo SQL de demonstração, com uma consulta para cada afirmação
que o projeto faz. Cada uma acompanhada do resultado esperado:

| # | O que prova | Consulta |
|---|---|---|
| 1 | **RN-01 tem efeito** | Contagem de interrupções na bronze contra a silver, e o percentual descartado |
| 2 | **A qualidade tem efeito** | Linhas de consumo na bronze contra a silver, por motivo de descarte |
| 3 | **RN-04 é real** | O mesmo DEC vindo da metric view e o cálculo manual a partir dos insumos — têm de bater |
| 4 | **Os dois eixos funcionam** | As UCs em prioridade alta, com a variação delas e a variação do conjunto lado a lado |
| 5 | **A anonimização funciona** | Uma transcrição da bronze e a mesma da silver, lado a lado |
| 6 | **O evento narrativo está lá** | Chamados por hora na noite da cascata, contra a média das mesmas horas nos outros dias |
| 7 | **A tendência está lá** | Consumo total por ano — tem de crescer ~30% ao ano |
| 8 | **A sazonalidade está lá** | Chamados por mês — verão tem de ficar perto do dobro |

As duas últimas são as que só existem porque o dataset tem quatro anos. Num trimestre,
nenhuma das duas seria visível.

## 3. Feche o repositório

- **README** com: o que o projeto é, pré-requisitos, o perfil da CLI, a sequência de
  comandos do zero ao copiloto, a estrutura de pastas e as regras que valem repetir
- **AGENTS.md** com as convenções para quem for continuar o projeto com um agente
- Confirme que `databricks bundle validate` e o lint passam

## Ao terminar

Rode o job completo e me mostre:

1. O relatório executivo gerado
2. O resultado das oito queries de demonstração
3. O link do dashboard e o do Genie space

## Critério de aceite

- O relatório reconstrói a história de Campinas cruzando as três bases
- O texto não contém nenhum número que não esteja nos dados
- O texto não usa as palavras fraude, furto ou irregularidade
- As oito queries rodam e devolvem o resultado esperado
- `bundle validate` e lint passam
- Um analista que abre o repositório pela primeira vez entende o que fazer pelo README

E então, o ciclo

terminaltoda etapa termina assim
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence
databricks bundle summary  -t dev --profile grid_intelligence
O que olhar agora

O relatório de uma página gerado, e as oito consultas devolvendo o resultado esperado.

Onde olhar na interface

  1. Jobs & Pipelines → grid_medalhao_job → última execução

    As quatro tasks encadeadas: pipeline, máscara, metric view e relatório. O grafo mostra o que dependeu de quê.

  2. Na execução → task relatorio_executivo → saída

    O relatório de uma página renderizado no fim do notebook, pronto para copiar para um e-mail.

  3. SQL Editor → as consultas de demonstração

    As oito, uma a uma. A do DEC é a mais importante: o número da metric view tem que bater com o cálculo manual.

  4. Catalog → gold → qualquer tabela → Lineage

    O grafo de linhagem do volume até a gold. É a resposta visual para “de onde veio esse número?”.

9.0

Conferir no workspace.

Depois de cada deploy — e são oito — o que vale abrir.

Você repetiu validate → deploy oito vezes. Este bloco é o outro lado do ciclo: onde olhar para saber que deu certo, sem precisar caçar pela interface.

terminalo resumo, sem abrir a UI
databricks bundle summary -t dev --profile grid_intelligence

Na interface, quatro lugares, sempre os mesmos:

  • Workspace → Users → seu usuário → .bundle/ — o projeto deployado, arquivo por arquivo.
  • Jobs & Pipelines — o job e a pipeline, prefixados com [dev seu_usuario].
  • Catalog → grid_dev — os schemas populando, camada por camada.
  • Pipeline → aba de qualidade — quantas linhas cada expectation contou ou descartou.
Se os schemas aparecerem com o seu nome na frente

É o skip_name_prefix_for_schema do bloco 7.0 faltando. Em mode: development o bundle prefixa todo recurso — inclusive schema, onde o prefixo atrapalha em vez de ajudar.

Deploy pequeno falha pequeno

É a razão de fazer oito em vez de um. Quando algo quebra, você sabe exatamente qual prompt introduziu o problema — e o conserto é um ajuste, não uma investigação.

10.0

Guard rails.

Travas determinísticas: o agente não dropa nada, nem se você pedir.

A pergunta que todo gestor faz é: por que eu deixaria uma IA com a CLI autenticada no meu ambiente sem garantia de que ela não vai dropar uma tabela? A resposta é: porque você coloca trava. Duas camadas.

arquivo.claude/settings.json
{
  "permissions": {
    "deny": [
      "Bash(databricks bundle destroy:*)",
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/block_destructive.py"
          }
        ]
      }
    ]
  }
}
arquivo.claude/hooks/block_destructive.py
#!/usr/bin/env python3
"""Guard rail: bloqueia comandos destrutivos antes de o agente executar.

Roda como hook PreToolUse do Claude Code. Recebe no stdin um JSON com o nome da
ferramenta e os argumentos. Sair com código 2 bloqueia a chamada e devolve o
texto do stderr para o agente ler.
"""

import json
import re
import sys

PADROES = [
    (r"\bdrop\s+(table|schema|catalog|view|volume|database)\b", "DROP em objeto do Unity Catalog"),
    (r"\btruncate\s+table\b", "TRUNCATE TABLE"),
    (r"\bdelete\s+from\b", "DELETE FROM"),
    (r"\bbundle\s+destroy\b", "databricks bundle destroy"),
    (r"\bbundle\s+deploy\b.*(?:-t|--target)\s+prod\b", "deploy em prod sem revisao"),
    (r"\brm\s+-rf\b", "rm -rf"),
    (r"\bgit\s+push\b.*--force\b", "git push --force"),
]

CAMPOS = ("command", "query", "statement", "sql", "code")


def texto_da_chamada(entrada):
    args = entrada.get("tool_input") or {}
    partes = [str(args[c]) for c in CAMPOS if args.get(c)]
    return " ".join(partes)


def main():
    try:
        entrada = json.load(sys.stdin)
    except (json.JSONDecodeError, ValueError):
        sys.exit(0)  # sem JSON válido não há o que inspecionar

    alvo = texto_da_chamada(entrada).lower()
    if not alvo:
        sys.exit(0)

    for padrao, rotulo in PADROES:
        if re.search(padrao, alvo):
            print(
                f"BLOQUEADO pelo guard rail do repositório: {rotulo}.\n"
                "Este projeto não permite que o agente execute comandos destrutivos. "
                "Se a operação for mesmo necessária, o próprio usuário deve executá-la manualmente.",
                file=sys.stderr,
            )
            sys.exit(2)

    sys.exit(0)


if __name__ == "__main__":
    main()

Testar a trava

Feche e reabra o Claude Code — hooks carregam no início da sessão — e peça algo destrutivo:

promptdeve ser bloqueado
Dropar o catálogo grid_intelligence

A resposta esperada é o agente informando que foi bloqueado por design, citando o guard rail do repositório, e sugerindo que você mesmo execute manualmente se for realmente a intenção.

flowchart TB
  P["voce pede algo ao agente"]
  A["Claude Code decide<br/>rodar um comando"]
  C1{"camada 1<br/>permissions.deny<br/><small>settings.json</small>"}
  C2{"camada 2<br/>hook PreToolUse<br/><small>roda no shell</small>"}
  EXEC["comando executa"]
  BLOQ["<b>bloqueado</b><br/><small>o agente explica por que<br/>e sugere que voce faca manualmente</small>"]

  P --> A --> C1
  C1 -->|permitido| C2
  C1 -->|negado| BLOQ
  C2 -->|permitido| EXEC
  C2 -->|negado| BLOQ

  N["<b>probabilistico x deterministico</b><br/><small>skill e token no contexto: o modelo PROVAVELMENTE obedece.<br/>o hook roda antes da acao: mil tentativas, mil bloqueios.</small>"]
  C2 -.- N
Duas camadas de trava. A primeira é configuração; a segunda roda no shell, antes da ação, e não depende de o modelo concordar.
Probabilístico × determinístico

Skill é probabilística: é token entrando no contexto, e o modelo provavelmente obedece. O hook é determinístico — roda no shell, antes da ação. Se você tentar mil vezes, mil vezes ele bloqueia. É assim que “confio na IA” vira “não depende de confiança”. E guard rail não nasce pronto: você acrescenta à medida que identifica risco real no projeto.

11.0

Produção e checklist.

O deploy sai no nome da service principal — não no seu.

Faça merge na main. A esteira roda, para no gate do environment prod, você aprova, e o deploy acontece. Depois, confira o que foi para o workspace:

terminalsomente leitura
databricks bundle summary -t prod --profile grid_intelligence

Na interface, abra o job em produção e olhe o campo de criador: service principal, não o seu usuário. Esse é o print que fecha a aula.

Recomeçar do zero

Gravar de novo, testar a sequência com outra turma, ou simplesmente voltar a um estado limpo. Antes de sair apagando, escolha o quanto você quer desfazer — são três níveis, e o mais agressivo custa 27 MB de upload.

Quero refazerO que fazerCusto
Só as tabelas, com os mesmos dados Full refresh do pipeline. Bronze, silver e gold são recalculadas do zero a partir do volume minutos de compute
As camadas, mantendo os arquivos DROP SCHEMA bronze / silver / gold CASCADE e deploy de novo. O raw fica intacto minutos, sem reupload
O projeto inteiro, incluindo os dados bundle destroy e depois DROP CATALOG CASCADE refazer o upload dos Parquet
Na dúvida, preserve o raw

O volume raw.landing é MANAGED: dropar o catálogo apaga os arquivos junto, e a base de consumo diário sozinha tem 5,85 milhões de linhas. Derrubar só bronze, silver e gold devolve o mesmo estado limpo e economiza o reupload — é o caminho que você vai querer em 90% das vezes.

terminalrefazer as camadas, preservando os dados
databricks experimental aitools tools query --profile grid_intelligence \
  "DROP SCHEMA IF EXISTS grid_dev.gold CASCADE" \
  "DROP SCHEMA IF EXISTS grid_dev.silver CASCADE" \
  "DROP SCHEMA IF EXISTS grid_dev.bronze CASCADE"

# o deploy recria os schemas vazios; o pipeline repopula tudo
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle run grid_medalhao_job -t dev --profile grid_intelligence

Zerar de verdade

Aqui a ordem importa: bundle destroy antes do DROP CATALOG. O destroy remove o que o bundle criou — jobs, pipeline, dashboard, schemas e volume — e atualiza o estado do bundle. Se você dropar o catálogo primeiro, o estado fica apontando para recursos que já não existem, e o próximo deploy carrega essa inconsistência.

terminalapaga tudo, sem recuperação
# 1. o bundle desfaz o que ele mesmo criou
databricks bundle destroy -t dev --profile grid_intelligence

# 2. o catalogo em si o bundle nunca criou — este comando e por sua conta
databricks experimental aitools tools query --profile grid_intelligence \
  "DROP CATALOG IF EXISTS grid_dev CASCADE"

# equivalente pela CLI, se preferir
# databricks catalogs delete grid_dev --force --profile grid_intelligence

# 3. do zero: recriar catalogo, deploy, subir os dados
databricks experimental aitools tools query --profile grid_intelligence \
  "CREATE CATALOG IF NOT EXISTS grid_dev"
CASCADE não pergunta duas vezes

DROP CATALOG ... CASCADE remove schemas, tabelas, volumes e os arquivos dentro dos volumes managed. Não há lixeira, não há undrop de catálogo. Confira o nome antes de dar Enter — em especial se grid_dev e grid_intelligence estiverem os dois na tela.

Sem CASCADE o comando falha enquanto houver qualquer coisa dentro, o que na prática funciona como uma trava: se ele reclamar, é porque você ainda tem dado ali.

E o UNDROP?

Existe para tabela — UNDROP TABLE grid_dev.gold.saude_cliente recupera uma tabela derrubada por engano, dentro da janela de retenção. Não existe para schema nem para catálogo. É mais uma razão para derrubar do menor para o maior: tabela tem volta, catálogo não.

Checklist final

  • CLI, uv, gh e Claude Code instalados e autenticados
  • Repositório no GitHub com o projeto versionado
  • Bundle DAB com dev e prod isolados por catálogo
  • Skills oficiais instaladas e CLI autenticada no workspace
  • Catálogo criado e com permissão concedida
  • Medalhão com bronze, silver (com IA) e gold, mais dashboard e agente
  • Job orquestrando o pipeline inteiro
  • Guard rails bloqueando comando destrutivo
Para levar

Você não precisa saber tudo — precisa saber o que é possível pedir. O conhecimento não virou dispensável; ele virou o que separa quem dirige a IA de quem só assiste.

A

Anexo: todos os comandos.

A sequência inteira, na ordem, para colar no terminal.

arquivodatabricks.yml, já com os seus valores
# 1.0 — ferramentas
brew tap databricks/tap && brew install databricks
databricks --version
uv --version
gh --version
claude --version

# 2.0 — autenticação
databricks auth login --host https://SEU-WORKSPACE.cloud.databricks.com
databricks auth profiles
databricks current-user me --profile grid_intelligence

# 3.0 — repositório
gh repo create
cd LakehouseDatabricks

# 4.0 — bundle
databricks bundle init --profile grid_intelligence
cd grid_intelligence

# 5.0 — Python
uv python pin 3.12
uv venv --seed
uv sync --dev
git add . && git commit -m "Adiciona projeto inicial Declarative Automation Bundle" && git push

# 6.0 — skills oficiais (sem MCP)
databricks aitools install
claude            # depois /skills para conferir

# 7.0 — catálogos (um por ambiente)
# Na Free Edition, catalogs create falha com "Metastore storage root URL does
# not exist" — use o SQL abaixo, ou crie pela UI (Catalog > Create catalog).
databricks experimental aitools tools query --profile grid_intelligence \
  "CREATE CATALOG IF NOT EXISTS grid_dev"
databricks experimental aitools tools query --profile grid_intelligence \
  "CREATE CATALOG IF NOT EXISTS grid_intelligence"
databricks catalogs list --profile grid_intelligence
databricks grants update CATALOG grid_dev \
  --json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
  --profile grid_intelligence
databricks grants update CATALOG grid_intelligence \
  --json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
  --profile grid_intelligence

# 7.1 — schema, volume e os dados (catalogo > schema > volume, nesta ordem)
databricks schemas create raw grid_dev --profile grid_intelligence
databricks volumes create grid_dev raw landing MANAGED --profile grid_intelligence
databricks fs cp -r landing dbfs:/Volumes/grid_dev/raw/landing \
  --overwrite --profile grid_intelligence
databricks fs ls dbfs:/Volumes/grid_dev/raw/landing --profile grid_intelligence

# 9.0 — deploy em dev
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy   -t dev --profile grid_intelligence

# a chave do run e a do recurso no YAML. Antes dos prompts so existe o
# sample_job que veio do template; grid_medalhao_job nasce no prompt 04.
databricks bundle summary -t dev --profile grid_intelligence   # lista o que existe
databricks bundle run sample_job -t dev --profile grid_intelligence

# 12.0 — produção (pela esteira, nunca na mão)
databricks bundle summary -t prod --profile grid_intelligence
B

Anexo: quando der erro.

Os sete erros que aparecem em toda turma.

ErroCausaSolução
databricks update não existe comando inventado brew upgrade databricks
resource with key "x" not found o recurso ainda não existe, ou você usou o nome de exibição em vez da chave databricks bundle summary -t dev lista as chaves disponíveis
DELTA_FEATURE_REQUIRES_MANUAL_ENABLEMENT: timestampNtz timestamp sem fuso horário; o Delta pede a feature habilitada na tabela TBLPROPERTIES ('delta.feature.timestampNtz' = 'supported') — nunca converter na bronze
A bronze ingere zero linha, sem erro Parquet com timestamp[ns] — o Spark só lê até microssegundo regravar o Parquet com precisão us; o gerador do projeto já faz
fs cp falha com erro de caminho falta o prefixo dbfs:, o caminho local está errado, ou o schema/volume não existe confira na ordem: catalogs list, schemas list grid_dev, volumes list grid_dev raw
Metastore storage root URL does not exist Free Edition / Default Storage: a API de criar catálogo exige storage_root crie por SQL (CREATE CATALOG) ou pela UI, em Catalog → Create catalog
Catalog 'x' does not exist o catálogo do comando não foi criado neste workspace databricks catalogs list antes de dar grant
stored credentials from older CLI versions are no longer used a CLI foi atualizada e o cache de token do formato antigo virou inválido databricks auth login --profile grid_intelligence de novo
Skills não aparecem em /skills plugin só carrega no início da sessão feche e reabra o Claude Code
Schemas viram seunome_bronze mode: development prefixa recursos experimental.skip_name_prefix_for_schema: true
does not have permission to CREATE SCHEMA catálogo sem grant databricks grants update CATALOG ... (bloco 7.0)
Instalação de lib falha Python 3.13 sem wheel do Databricks uv python pin 3.12
C

Anexo: perguntas da turma.

O que sempre aparece no chat.

Dá para usar GitLab ou Azure DevOps em vez de GitHub?

Dá, e é o mais comum em cliente grande. Muda o comando que cria o repositório e a sintaxe do arquivo de pipeline. O bundle é exatamente o mesmo.

Quem está começando deve aprender na interface primeiro ou já ir de agente?

Não existe resposta certa, mas existe resposta errada — e a errada é ignorar a IA. Na prática: metade do tempo de estudo explorando na mão, para fixar; metade com o agente. No trabalho, agente o tempo todo. O risco de estudar só com agente é a sensação de ter entendido sem ter entendido — ele é mais rápido do que a gente consegue absorver.

Essas skills servem para construir agentes de IA dentro do Databricks?

Elas são skills de desenvolvimento no Databricks, não de construção de agente. Para agente você precisa estudar deploy de agente à parte. Mas este setup habilita: dá para escrever o agente localmente com apoio das skills e publicá-lo no workspace.

Certificação vale a pena?

Vale, principalmente para quem quer entrar em consultoria — empresa precisa de gente certificada para virar parceira. Mas certificação mede decoreba de comando, não capacidade. Estude, faça um projeto, e só então tire a certificação.