-- 0040 — skills: o fluxo pronto que o assistente sabe executar
--
-- O PROBLEMA
--
-- O assistente tem ferramentas genericas e nenhum fluxo. Medido nas 132
-- perguntas registradas ate 15/09/2026: `list_data_sources` roda em 73% das
-- respostas, `data_freshness` em 56% e `describe_data` em 53% -- MAIS que
-- `query_data`, que roda em 52%. Tres ferramentas de descoberta rodam mais que
-- a que responde. A distribuicao de voltas e bimodal (39 perguntas em 2-3
-- voltas, 53 em 7-8) e o custo e superlinear, porque cada volta reenvia o
-- contexto acumulado: oito voltas custam 9,5x o de duas.
--
-- E o modelo erra de formas que instrucao nao segura. Numa bateria de perguntas
-- de diretoria apareceram quatro numeros plausiveis e errados: um total apurado
-- como o maior componente, uma funcao de data sem o formato derrubando a
-- pergunta tres vezes, um estoque tratado como fluxo, um mes parcial ao lado de
-- meses cheios. Cinco redacoes de prompt no mesmo dia nao seguraram
-- comportamento; a definicao semantica segurou na primeira.
--
--
-- A SKILL GUARDA O METODO, NUNCA A CONSULTA
--
-- Isto e o que mais importa entender antes de mexer aqui.
--
-- Congelar SQL parece a solucao obvia e esta errado: SQL nomeia dataset e
-- coluna, portanto nao e portatil, e uma mudanca de schema o quebra em
-- silencio. Uma skill OFICIAL de DRE precisa rodar em qualquer cliente,
-- qualquer ERP de origem.
--
-- Entao a skill declara o que PRECISA, em `requires`:
--
--     grupos   o seletor grosso, do vocabulario de grupos de dataset do tenant
--     termos   o vinculo fino, da camada semantica (semantic_definitions)
--
-- e o servidor resolve isso para o tenant NA INVOCACAO: quais views, quais
-- colunas, quais significados. O modelo recebe o resultado pronto e vai direto
-- a consulta -- que continua sendo escrita na hora, passando por
-- `datasetAllowlist` e `SqlGuard` como qualquer outra.
--
-- A skill nao ganha caminho novo aos dados. O que muda e o modelo chegar a
-- porta de sempre sabendo o que procurar.
--
--
-- O RISCO DO VOCABULARIO DE GRUPO
--
-- Grupos de dataset sao por tenant e NAO sao padronizados. Medido em
-- 15/09/2026, dois tenants reais: um com quatro grupos, outro com dezoito,
-- coincidindo em dois nomes por convencao da skill de onboarding, nao por regra.
--
-- Uma skill oficial que exija um grupo que o cliente chamou de outra coisa
-- simplesmente nao roda. Por isso o requisito nao atendido NAO e erro em tempo
-- de execucao: a skill aparece INDISPONIVEL, com o motivo, e o catalogo vira a
-- lista de lacunas do onboarding.
--
--
-- TRES CAMADAS, E A DE CIMA E NOVA
--
-- `embed_prompts` tem duas camadas (tenant e projeto). Aqui ha tres, porque a
-- skill oficial e do PRODUTO e vale para todos os clientes:
--
--     tenant_id NULL                  OFICIAL   -- do produto, todos os tenants
--     tenant_id preenchido, project NULL        -- a skill daquele cliente
--     tenant_id e project preenchidos           -- o ajuste de um projeto
--
-- A mais especifica vence, pelo `slug`. Um cliente que queira a DRE dele
-- escreve uma skill com o mesmo slug no nivel dele: a oficial continua intacta
-- para os outros.

CREATE TABLE skills (
  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

  -- NULL = OFICIAL, do produto. Diferente de `embed_prompts`, onde tenant_id e
  -- NOT NULL: la nao existe camada acima do cliente, aqui existe.
  tenant_id      BIGINT UNSIGNED NULL,

  -- NULL = a camada do tenant (ou a oficial, quando tenant_id tambem e NULL).
  project_id     BIGINT UNSIGNED NULL,

  -- As duas existem so para a UNIQUE alcancar as camadas com NULL. Em InnoDB,
  -- UNIQUE com NULL nao restringe -- duas oficiais com o mesmo slug passariam.
  scope_tenant_id  BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(tenant_id, 0)) VIRTUAL,
  scope_project_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(project_id, 0)) VIRTUAL,

  -- O que se digita no chat: `/analisar-dre`. Minusculas, digitos e hifen.
  slug           VARCHAR(80) NOT NULL,

  name           VARCHAR(160) NOT NULL,
  description    VARCHAR(400) NOT NULL,

  -- QUANDO usar -- e o gatilho que o modelo le para escolher a skill sozinho,
  -- distinto da descricao, que e o que a pessoa le no catalogo.
  when_to_use    VARCHAR(400) NOT NULL,

  -- Agrupa no catalogo: "accounting-analysis", "fiscal", "operacional".
  category       VARCHAR(80) NOT NULL DEFAULT 'geral',

  -- {"grupos": ["financeiro"], "termos": ["receita", "despesa"]}
  -- Vazio e valido: uma skill que so organiza a resposta nao exige dado nenhum.
  requires       JSON NULL,

  -- O metodo. E o que entra no turno quando a skill e invocada.
  instructions   TEXT NOT NULL,

  -- A forma que a skill promete devolver, para o modelo nao escolher sozinho:
  -- metric | table | chart | report | texto
  output         VARCHAR(20) NOT NULL DEFAULT 'texto',

  -- A skill so LE, ou ela altera o contexto registrado (chama correct_context)?
  -- Separado de `approval` de proposito: a pergunta "isto muda alguma coisa?" e
  -- diferente de "alguem precisa autorizar?".
  writes         TINYINT(1) NOT NULL DEFAULT 0,

  -- none | required. Vale para o que escreve; uma skill de leitura publicada
  -- pelo proprio cliente nao precisa passar por ninguem.
  approval       VARCHAR(20) NOT NULL DEFAULT 'none',

  -- draft = existe, nao aparece no catalogo do usuario final. publicada = no ar.
  status         VARCHAR(20) NOT NULL DEFAULT 'draft',

  -- Sobe a cada alteracao publicada. Nao ha tabela de historico: o valor serve
  -- para quem administra saber que mudou, e a auditoria de uso registra qual.
  version        INT NOT NULL DEFAULT 1,

  -- Desliga a linha NA CAMADA. Para esconder uma HERDADA em um projeto so, o
  -- instrumento e skill_optouts.
  enabled        TINYINT(1) NOT NULL DEFAULT 1,

  -- Por que esta desligada. Visao administrativa; nunca sai no catalogo.
  note           VARCHAR(255) NULL,

  -- Quantas vezes foi invocada. Um contador e o suficiente: quem precisa de
  -- historico tem `query_audit`, e uma tabela de uso por skill cresceria sem
  -- ninguem ler.
  uses           BIGINT UNSIGNED NOT NULL DEFAULT 0,

  created_by     VARCHAR(120) NULL,
  created_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (id),

  -- As tres camadas protegidas pela mesma chave, via as colunas geradas.
  UNIQUE KEY uq_skills_camada (scope_tenant_id, scope_project_id, slug),

  -- As FKs precisam de indice comecando pela coluna, e a UNIQUE usa as geradas.
  KEY idx_skills_tenant (tenant_id),
  KEY idx_skills_project (project_id),

  -- Serve a leitura do catalogo: camada + publicadas e ligadas.
  KEY idx_skills_catalogo (scope_tenant_id, scope_project_id, status, enabled),

  CONSTRAINT fk_skills_tenant  FOREIGN KEY (tenant_id)  REFERENCES tenants(id),
  CONSTRAINT fk_skills_project FOREIGN KEY (project_id) REFERENCES projects(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Esconder uma skill HERDADA (oficial ou do tenant) em um projeto so.
--
-- Tabela e nao coluna pela mesma razao de `embed_prompt_optouts`: quem desliga
-- e o PAR (projeto, skill). A coluna `enabled` da skill desliga para todos que a
-- herdam, que e outra decisao.
CREATE TABLE skill_optouts (
  id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  project_id BIGINT UNSIGNED NOT NULL,
  skill_id   BIGINT UNSIGNED NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (id),
  UNIQUE KEY uq_skill_optouts (project_id, skill_id),
  KEY idx_skill_optouts_skill (skill_id),

  CONSTRAINT fk_skill_optouts_project FOREIGN KEY (project_id) REFERENCES projects(id),
  CONSTRAINT fk_skill_optouts_skill   FOREIGN KEY (skill_id)   REFERENCES skills(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------------
-- AS SKILLS OFICIAIS
-- ---------------------------------------------------------------------------
-- Vivem aqui, versionadas com o codigo, e nao num seed editavel: uma skill
-- oficial e comportamento do produto, e comportamento que muda sem passar por
-- revisao e o que produz duas instalacoes que respondem diferente.
--
-- Nenhuma delas nomeia dataset, coluna ou cliente -- so grupo e termo. Ha teste
-- que varre as oficiais procurando `ds_` e nome de coluna, e recusa.

INSERT INTO skills
  (tenant_id, project_id, slug, name, description, when_to_use, category,
   requires, instructions, output, writes, approval, status, created_by)
VALUES
(NULL, NULL, 'analisar-resultado',
 'Analisar resultado do periodo',
 'Compara entradas e saidas do periodo, aponta os meses fora da curva e diz o que puxou cada um.',
 'Use quando pedirem resultado, receita contra despesa, sobra de caixa, ou "como foi o periodo".',
 'accounting-analysis',
 '{"grupos":["financeiro"],"termos":[]}',
 'Apure entradas e saidas por mes no periodo pedido, em UMA consulta que ja agregue -- nunca somando linhas no texto.\n\nAntes de agrupar por qualquer classificacao de receita/despesa, confira a tabela de dominio que a define: se uma categoria de despesa aparecer classificada como receita, NAO use esse agrupamento, diga que a classificacao esta incorreta na origem e ofereca a abertura por categoria, cujo nome costuma estar certo.\n\nExclua transferencias entre contas proprias: elas inflam os dois lados sem nada ter entrado ou saido.\n\nSe a janela cortar um mes pela metade, diga que o periodo esta incompleto ou use meses inteiros.\n\nEntregue a serie mensal e aponte os dois meses mais fora da curva, dizendo o que pesou em cada um.',
 'table', 0, 'none', 'published', 'contextia'),

(NULL, NULL, 'analisar-inadimplencia',
 'Analisar inadimplencia',
 'Mede o valor vencido e ainda devido, e mostra onde ele esta concentrado.',
 'Use quando pedirem inadimplencia, atraso, vencidos, ou quem esta devendo.',
 'accounting-analysis',
 '{"grupos":["financeiro"],"termos":["inadimplencia"]}',
 'Use o criterio registrado na definicao de inadimplencia deste cliente -- ele manda sobre qualquer nocao sua do termo.\n\nO total sai de UMA consulta que ja o calculou. Uma consulta que devolve uma linha por empresa NAO e o total: some no SQL, nunca no texto.\n\nEntregue o valor consolidado e, junto, a concentracao: quem responde pela maior parte. Concentracao alta e a informacao que muda decisao, e ela some num numero unico.',
 'metric', 0, 'none', 'published', 'contextia'),

(NULL, NULL, 'analisar-a-pagar',
 'Analisar contas a pagar',
 'Mostra o que vence adiante, em que ritmo, e o que ja esta vencido.',
 'Use quando pedirem contas a pagar, o que vence, compromissos, ou folego de caixa.',
 'accounting-analysis',
 '{"grupos":["financeiro"],"termos":[]}',
 'Separe SEMPRE o vencido do a vencer: somados, escondem o problema. O vencido e passivo em atraso; o a vencer e agenda.\n\nPara o a vencer, use uma janela explicita (30 dias, salvo pedido diferente). Sem teto, um parcelamento longo arrasta anos e o numero perde sentido.\n\nO total sai da consulta, ja agregado. Entregue os dois numeros e, se pedirem detalhe, a abertura por credor.',
 'table', 0, 'none', 'published', 'contextia');
