From b8c3ab83ed5392fc13baf96fc03cdb141db40f35 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Wed, 16 Sep 2026 00:34:06 -0300 Subject: [PATCH] =?UTF-8?q?feat(OPS118):=20relat=C3=B3rio=20de=20cidade=20?= =?UTF-8?q?em=20lote=20+=20researcher=20por=20cidade=20(Closes=20#1065)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/relatorio-cidade/SKILL.md | 111 +++++++++-- .opencode/agent/relatorio-cidade.md | 32 +-- .opencode/commands/relatorio-cidade.md | 9 + docs/changelog/2026-09-16-ops118.md | 1 + ...cidade-lote-municipios-sub-agentes-impl.md | 184 ++++++++++++++++++ ...elatorio-cidade-pagina-1-pesquisa-cheia.md | 58 ++++++ tests/unit/cityReportBatchSkill.unit.spec.ts | 74 +++++++ tests/unit/opencodeCommands.unit.spec.ts | 8 +- 8 files changed, 447 insertions(+), 30 deletions(-) create mode 100644 .opencode/commands/relatorio-cidade.md create mode 100644 docs/changelog/2026-09-16-ops118.md create mode 100644 docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes-impl.md create mode 100644 docs/plans/ops120-relatorio-cidade-pagina-1-pesquisa-cheia.md create mode 100644 tests/unit/cityReportBatchSkill.unit.spec.ts diff --git a/.agents/skills/relatorio-cidade/SKILL.md b/.agents/skills/relatorio-cidade/SKILL.md index 959e2f7b..d771d7f8 100644 --- a/.agents/skills/relatorio-cidade/SKILL.md +++ b/.agents/skills/relatorio-cidade/SKILL.md @@ -1,6 +1,6 @@ --- name: relatorio-cidade -description: 'Gera o relatório de cidade pré-viagem (PDF A4 + .md) a partir da base Teqo read-only + pesquisa web datada + emendas oficiais.' +description: 'Gera o relatório de cidade pré-viagem (PDF A4 + .md) a partir da base Teqo read-only + pesquisa web datada + emendas oficiais; aceita um município ou um lote separado por vírgula.' --- # Relatório de cidade pré-viagem (C163) @@ -13,21 +13,56 @@ URL por item) e emendas lidas da fonte oficial **em tempo de geração**. Págin ## Quando usar -- O candidato/CG pede "o relatório de " antes de uma viagem. -- Quem executa é o agente (pesquisa web + escrita dos JSONs) + os dois scripts - (`extract` no homeserver, `build` local). O PDF sai para leitura de bolso. +- O candidato/CG pede "o relatório de " antes de uma viagem — ou de + várias numa invocação (`/relatorio-cidade Ilheus, Itacare, Una`). +- Quem executa são o **orquestrador** (agente principal) + um sub-agente + **researcher** por cidade + os dois scripts (`extract` no homeserver, `build` + local). O PDF sai para leitura de bolso; o detalhe das etapas está em + "Pipeline (etapas)". -## Pipeline (3 passos) +## Lote (várias cidades) -1. **Slug canônico.** O município é sempre um slug do catálogo - (`src/lib/municipalityCatalog.ts`, 435 unidades; Salvador = `salvador-ze-N`). +A invocação aceita **um** município (caso de sempre) ou **vários**, separados por +**vírgula** — a lista é o argumento, não um novo comando: + +```text +/relatorio-cidade Feira de Santana # N=1 — caminho atual, sem regressão +/relatorio-cidade Ilheus, Itacare, Una # lote +``` + +Parsing (orquestrador, **antes** de qualquer pesquisa): + +- separa por `,`; aplica `trim`; descarta vazio; +- cada token é aceito como **slug canônico** (`isMunicipalitySlug`) **ou** como + **nome** (fold acento-insensível `resolveMunicipalityName`; `null` = nome + desconhecido → token inválido) → `municipalityCatalogEntriesForCity`; +- **0 entradas** = token inválido; **>1 entrada** = ambíguo (ex.: `Salvador` + resolve para 19 zonas `salvador-ze-N` — peça o slug da zona explícito); +- **dedupe após a resolução**, preservando a ordem — duas grafias do mesmo + município (`Ilheus, ilheus`) geram **um** artefato, não dois; +- token inválido/ambíguo vira **falha isolada** com o motivo no summary final — + **nunca** aborta o lote nem inventa slug. + +Uma cidade = um par PDF+MD (`-`); **sem** índice/PDF agregado. + +## Pipeline (etapas) + +1. **Orquestrador (agente principal).** Parseia a lista (seção "Lote"), resolve os + slugs no catálogo (`src/lib/municipalityCatalog.ts`, 435 unidades; Salvador = + `salvador-ze-N`) e dispara **um researcher por cidade em paralelo** (Task). Não invente slug: confirme no catálogo (`pnpm exec tsx -e` ou a lista em - `/campanha/municipios`). -2. **Pesquisa web → `data/relatorios-cidade/.research.json`.** - Cada item do checklist com `sourceUrl` + `sourceDate`; sem fonte, **não - escreva o item** (ele vira lacuna). Notícias: janela ≤90 dias. -3. **Extração read-only no homeserver** (receita completa no runbook - `docs/ops/teqo-1313-deploy.md` §"C163 — relatório de cidade"): + `/campanha/municipios`). Nunca retém pesquisa web nem o corpo de um + `research.json` — só os recibos. +2. **Researcher (sub-agente, ×N, em paralelo).** `.opencode/agent/relatorio-cidade.md` + — o **único** passo pesado de contexto. Faz a pesquisa web datada e escreve + `data/relatorios-cidade/.research.json`: cada item do checklist com + `sourceUrl` + `sourceDate`; sem fonte, **não escreva o item** (ele vira + lacuna). Notícias: janela ≤90 dias. Devolve **apenas o recibo curto** (seção + "Recibo do researcher") — nunca o corpo. Não roda ssh nem build. +3. **Extração read-only no homeserver — serializada.** Um município por vez: o + checkout `~/teqo-report` é compartilhado e ssh concorrente colide. Receita + completa no runbook + `docs/ops/teqo-1313-deploy.md` §"C163 — relatório de cidade": ```bash ssh homeserver source ~/.nvm/nvm.sh @@ -41,9 +76,12 @@ URL por item) e emendas lidas da fonte oficial **em tempo de geração**. Págin --municipality= --out=data/relatorios-cidade/.snapshot.json ``` Depois `scp homeserver:~/teqo-report/data/relatorios-cidade/.snapshot.json data/relatorios-cidade/`. + Crie `data/relatorios-cidade/logs/` e redirecione a saída para + `logs/.extract.log 2>&1`; retenha só o status + o caminho — o log não + entra no contexto do orquestrador. O `~/teqo-report` é checkout de **scratch** — não use `~/teqo-deploy` (deploy em andamento no mesmo host). O SHA do extrator e do builder devem ser o mesmo. -4. **Render local:** +4. **Render local (por cidade):** ```bash NODE_OPTIONS="--no-deprecation --import=tsx/esm" node scripts/build-city-report.mjs \ --snapshot=data/relatorios-cidade/.snapshot.json \ @@ -52,11 +90,43 @@ URL por item) e emendas lidas da fonte oficial **em tempo de geração**. Págin ``` Saídas: `docs/research/relatorios-cidade/-.pdf` + `.md` (companion revisável em diff). Intermediários gitignored em - `data/relatorios-cidade/` (HTML, JSONs de emendas). + `data/relatorios-cidade/` (HTML, JSONs de emendas, `logs/`). Também + redirecione a saída do builder para `logs/.build.log`. Flags úteis: `--emendas=` (replay sem rede), `--author=` (default `JORGE SOLLA`), `--generated-at=` (reprodutibilidade), `CITY_REPORT_STRICT=1` (falha o run se houver lacuna de pesquisa — para conferência, não para entrega). +5. **Summary final.** Uma linha por entrada, na ordem da lista: `entrada · status + (ok|failed) · PDF/MD (quando ok) · motivo (quando falha)` — `entrada` é o slug + resolvido ou, quando o token falha antes de resolver, o próprio token cru. + Falha de uma cidade **não** cancela as demais (sucesso parcial explícito); a + cidade que falha é pulada nas etapas seguintes. + +## Recibo do researcher + +O sub-agente por cidade devolve **só** este recibo curto (≤ ~15 linhas) — **nunca** +o corpo do `research.json` (`items[].answer/details`, `news[].summary`, +`approach`, `leaders`, …): + +```jsonc +{ + "slug": "ilheus", + "status": "ok", // ou "failed" + "researchPath": "data/relatorios-cidade/ilheus.research.json", + "researchedAt": "2026-09-16T10:00:00.000Z", + "itemCount": 6, // itens do checklist com fonte + "gapCount": 3, // == gaps.length (checklist + blocos de pesquisa) + "gaps": ["prefeito", "vice", "disputa_local"], + "newsCount90d": 4, // notícias na janela ≤90 dias + "weakSourceCount": 1, // item cujo único apoio é fonte de região/polo + "failureReason": "…" // opcional (só quando status = failed) +} +``` + +`itemCount` + `gapCount` cobrem o checklist fixo; `gapCount` conta também os +blocos de pesquisa sem fonte. `status: "failed"` carrega o motivo e **não** escreve +corpo parcial. O orquestrador agrega os recibos no summary final — é o único dado +de pesquisa que cruza para ele. ## Contrato dos JSONs @@ -304,6 +374,10 @@ cacheado em `data/relatorios-cidade/.emendas.json` para replay. ataque pessoal. Prioriza o local (operadores na região) sobre o líder estadual e sinaliza temas de mão dupla; onde a marca do adversário domina, o insumo é para contraste, não para confronto frontal. +- **Falha isolada no lote**: cidade que falha (token inválido/ambíguo, pesquisa + sem fonte suficiente, snapshot×research mismatch) não cancela as demais; o + summary final diz explicitamente o que saiu e o que falhou (sucesso parcial + visível). - **Artefato gitignored**: o repo é público; o PDF/MD com dado interno nunca é commitado (só a skill/scripts/changelog). @@ -321,10 +395,17 @@ cacheado em `data/relatorios-cidade/.emendas.json` para replay. `pnpm exec playwright install chromium`. - **Snapshot de outro município**: o builder recusa (`municipalitySlug` ≠ snapshot) — regenere, nunca edite o JSON à mão para "casar". +- **Token inválido/ambíguo no lote**: vira linha `failed` no summary com o + motivo; `Salvador` é ambíguo (19 zonas) e exige o slug da zona + (`salvador-ze-N`) — nunca expanda nem adivinhe. +- **Extração ssh em paralelo**: não faça — o `~/teqo-report` é compartilhado + (`git fetch/checkout/pull` + `pnpm install`); serialize a extração por cidade. ## Referências - Intenção: `docs/plans/relatorio-cidade-viagem.md`; impl: `docs/plans/relatorio-cidade-viagem-impl.md`. +- Lote + sub-agentes: `docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes.md` + e `-impl.md`. - Precedente de PDF: `scripts/build-solla-ceuci-salvador-report.mjs` (C157). - Runbook: `docs/ops/teqo-1313-deploy.md` §C163. diff --git a/.opencode/agent/relatorio-cidade.md b/.opencode/agent/relatorio-cidade.md index 18157e00..a03fe03f 100644 --- a/.opencode/agent/relatorio-cidade.md +++ b/.opencode/agent/relatorio-cidade.md @@ -1,13 +1,15 @@ --- -description: Gera o relatório de cidade pré-viagem (PDF A4 + .md) — base Teqo read-only + pesquisa web datada + emendas oficiais +description: Researcher por município — pesquisa web datada e escreve o research.json do relatório de cidade pré-viagem; devolve só o recibo curto (etapa do lote /relatorio-cidade) mode: subagent --- -# Relatório de cidade pré-viagem — subagente +# Relatório de cidade pré-viagem — researcher (por município) -Você executa a skill `.agents/skills/relatorio-cidade/SKILL.md` (fonte canônica: -pipeline, contrato dos JSONs, guardrails, troubleshooting). Leia-a antes de -rodar qualquer comando. +Você é a etapa **researcher** da skill `.agents/skills/relatorio-cidade/SKILL.md` +(fonte canônica: contrato dos JSONs, guardrails, checklist, recibo). Leia-a antes +de rodar qualquer comando. Você é invocado **uma vez por cidade** pelo +orquestrador (agente principal), que já resolveu o slug e coordena as demais +cidades em paralelo. ## Papel @@ -15,17 +17,19 @@ rodar qualquer comando. 2. Fazer a **pesquisa web datada** e escrever `data/relatorios-cidade/.research.json` no contrato da skill — item sem `sourceUrl`/`sourceDate` não vai para o arquivo (vira lacuna explícita). -3. Conduzir a extração read-only no homeserver (`~/teqo-report`, `CITY_REPORT_CONFIRM=1`, - proxy `127.0.0.1:5433`) e trazer o snapshot. -4. Rodar o builder local e entregar os caminhos do PDF + `.md`. +3. Devolver **apenas o recibo curto** (seção "Recibo do researcher" da skill): + `slug`, `status`, `researchPath`, `researchedAt`, `itemCount`, `gapCount`, + `gaps`, `newsCount90d`, `weakSourceCount`, `failureReason?`. **Nunca** devolva + o corpo do `research.json` (`items`, `news`, `approach`, `leaders`, …). ## Limites -- **Nunca** escreva na base de produção: a extração é read-only por connection - options; não use `db:pull`/snapshot de banco. -- **Nunca** commite o PDF/MD/snapshot (dado interno; repo público — artefato - gitignored). +- **Não** conduza a extração nem o build — são etapas determinísticas do + orquestrador (`scripts/extract-city-report-snapshot.mjs` no homeserver, + `scripts/build-city-report.mjs` local). Não faça `ssh`, `scp` nem render. +- **Nunca** escreva na base de produção: não use `db:pull`/snapshot de banco. +- **Nunca** commite o `research.json`/PDF/MD/snapshot (dado interno; repo público + — artefato gitignored). - Não invente fato sem fonte; não trate empenho como pagamento; não inclua telefone/e-mail de liderança. -- Não edite os scripts nem o layout para "caber": se a página 1 estourar, corte - copy/caps do resumo. +- Não edite os scripts nem o layout do relatório para "caber". diff --git a/.opencode/commands/relatorio-cidade.md b/.opencode/commands/relatorio-cidade.md new file mode 100644 index 00000000..1af7f523 --- /dev/null +++ b/.opencode/commands/relatorio-cidade.md @@ -0,0 +1,9 @@ +--- +description: Gera o relatório de cidade pré-viagem (PDF A4 + .md) para um município ou um lote separado por vírgula +--- + +Carregue a skill `relatorio-cidade` (ferramenta de skills, nome exato) e siga o fluxo dela de ponta a ponta: o orquestrador parseia a lista (`$ARGUMENTS`, separada por vírgula), resolve os slugs canônicos, dispara um researcher por cidade, roda a extração read-only e o build por cidade e fecha o summary. A fonte canônica é `.agents/skills/relatorio-cidade/SKILL.md` — não a transcreva nem recrie o fluxo. + +Uso: /relatorio-cidade [, ...] — ex.: `/relatorio-cidade Ilhéus, Itacaré, Una`. Uma cidade = um par PDF+MD (`-`). + +$ARGUMENTS diff --git a/docs/changelog/2026-09-16-ops118.md b/docs/changelog/2026-09-16-ops118.md new file mode 100644 index 00000000..24c7c459 --- /dev/null +++ b/docs/changelog/2026-09-16-ops118.md @@ -0,0 +1 @@ +**OPS118 — relatório de cidade: lote de municípios + sub-agentes por etapa (2026-09-16):** a skill `/relatorio-cidade` deixa de ser single-city: a invocação aceita um município (caso de sempre, N=1 sem regressão) ou vários separados por vírgula (`/relatorio-cidade Ilheus, Itacare, Una`), cada token resolvido a slug canônico pelo catálogo existente (`isMunicipalitySlug` ou fold `resolveMunicipalityName` → `municipalityCatalogEntriesForCity`; `Salvador` é ambíguo — 19 zonas `salvador-ze-N` — e vira falha explícita, nunca expansão silenciosa). O pipeline vira etapas: o **orquestrador** (agente principal) parseia/deduplica a lista e coordena; um **sub-agente researcher por cidade** (`.opencode/agent/relatorio-cidade.md`, que perde os papéis de ssh/build) faz a pesquisa web — o único passo pesado de contexto — e devolve só um **recibo curto** (`slug`, `status`, `itemCount`, `gapCount`, `gaps`, `newsCount90d`, `weakSourceCount`, `failureReason`), nunca o corpo do `research.json`; a extração read-only no homeserver é **serializada** (checkout `~/teqo-report` compartilhado) e o build local é por cidade, ambos com log em `data/relatorios-cidade/logs/.log` fora do contexto do orquestrador. **Falha isolada:** cidade inválida/ambígua, mismatch snapshot×research (guard fail-closed inalterado) ou erro de build não cancela as demais — o summary final diz o que saiu e o que falhou. Sem PDF/índice agregado, sem mudar conteúdo/layout do relatório, sem tocar nos dois CLIs por cidade, sem command/`opencode.json`/`src/`, artefatos seguem gitignored. Entrega documental: `.agents/skills/relatorio-cidade/SKILL.md` (seções "Lote", "Pipeline (etapas)" e "Recibo do researcher") + subagente researcher + docs-guard `tests/unit/cityReportBatchSkill.unit.spec.ts`; prova local: 3 cidades → 3 pares PDF+MD, resolução (`Ilheus`/`Itacare`/`Una` ok, `salvador` ambíguo, token inválido isolado) e mismatch fail-closed, além de uma corrida real do researcher devolvendo só o recibo. Issue #1065. diff --git a/docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes-impl.md b/docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes-impl.md new file mode 100644 index 00000000..ac634cd7 --- /dev/null +++ b/docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes-impl.md @@ -0,0 +1,184 @@ +# Impl: OPS118 — relatorio-cidade: lote de municípios + sub-agentes por etapa + +Status: executado +Atualizado em: 2026-09-16 +Aprovado no gate em: 2026-09-16 (seguir com docs-guard; execução autorizada no gate) +Issue: #1065 +Intenção: docs/plans/ops118-relatorio-cidade-lote-municipios-sub-agentes.md +Appetite restante: herdado — ~1 dia eng. Cortes explícitos se estourar: (a) o teste docs-guard (`tests/unit/cityReportBatchSkill.unit.spec.ts`) cai e a prova fica só no tracer manual; (b) a corrida live N=1 é adiada para o primeiro uso real, mantendo o tracer local semeado. + +## Leitura da intenção + +- **Outcome:** uma invocação `/relatorio-cidade Salvador, Ilheus, Itacare` produz, por cidade, o par `docs/research/relatorios-cidade/-.pdf|.md` com o mesmo contrato do single-city; a pesquisa web (etapa cara de contexto) roda em **um sub-agente por cidade**; o orquestrador retém só um **recibo curto** por cidade; falha de uma cidade não cancela as outras (sucesso parcial explícito). +- **O que NÃO negociar:** + - Guardrails da skill intactos: sem fonte não publica; empenho ≠ pagamento; leitura relativa; PII mínima; indício regional ≠ emenda da cidade; artefato gitignored. + - Contrato de artefato inalterado: `.research.json`, `.snapshot.json`, `-.pdf|.md`; mismatch snapshot × research continua fail-closed (`scripts/build-city-report.mjs:94-98`). + - Single-city (`/relatorio-cidade Feira de Santana`) sem regressão — é o caso N=1 do mesmo caminho. + - Sem PDF/índice agregado; sem tocar conteúdo/seções/layout do relatório; sem escrita na base de produção; sem commit de artefato. + - Nada de novo em `src/` (schema/migration/Consent/superfície de app): `opencode.json` não muda, sem command novo (precedente C163, `relatorio-cidade-viagem-impl.md:152-155`). +- **O que reavaliar (hipóteses da intenção confrontadas na execução):** + - **Quem executa a extração/build.** A intenção já diz "fora do researcher"; a execução precisa decidir _quem_ os dispara sem trazer o log de ssh/Chromium para dentro do modelo. Resolvido em D2: o **orchestrator** dispara os comandos com saída **redirecionada para log em arquivo**, lendo só exit status + caminhos. + - **"Salvador" é ambíguo no lote.** Salvador = 19 zonas (`salvador-ze-N`, `src/lib/municipalityCatalog.ts:84-97`); um token `Salvador` mapeia para 19 slugs. A regra de resolução precisa tratar ambiguidade como **falha isolada**, não expandir em silêncio (D1). + - **Onde mora a regra de parse.** O resolver de nome já existe e é testado (`resolveMunicipalityName`, `src/lib/municipalityNameAliases.ts:53`; `isMunicipalitySlug`/catálogo, `municipalityCatalog.ts:126-133`); o único delta é _compor_ lista→slugs, que o agente executa. Decide-se docs-only + docs-guard (D1/D5), não um helper novo sem consumidor de app. + - **Prova sem homeserver.** O caminho é documental; a prova determinística pode rodar **local e semeada** (o extrator aceita qualquer `DATABASE_URL` e já foi exercitado localmente no C163), evitando depender de ssh para o aceite. + +## Abordagem recomendada + +```mermaid +flowchart TD + A["/relatorio-cidade Salvador, Ilheus, Itacare"] --> B["orchestrator (agente principal): split por vírgula, trim/dedupe, resolve slug no catálogo"] + B -->|tokens válidos N cidades| C["N sub-agentes researcher em paralelo (Task) — único passo pesado de contexto"] + B -->|token inválido ou ambíguo| X["linha failed isolada — nunca aborta o lote"] + C --> D["pesquisa web datada -> data/relatorios-cidade/.research.json"] + D --> R["recibo curto: slug, contagens, gaps, flags — nunca o corpo do research.json"] + R --> E["orchestrator: extração read-only SERIALIZADA (~/teqo-report) -> .snapshot.json (log em arquivo)"] + E --> F["build local por cidade (snapshot+research) -> PDF+MD datados"] + F --> S["summary final: saiu x falhou por cidade + caminhos + motivo"] + X --> S + E -. falha da cidade .-> S + F -. falha da cidade .-> S +``` + +**Opções consideradas:** A (recomendada) orchestrator = **agente principal** pela SKILL.md, researcher = `.opencode/agent/relatorio-cidade.md` redefinido, extract/build determinísticos disparados pelo orchestrator com log em arquivo | B um subagente-orchestrator novo (`mode: all`) chamado por command | C collector + analyst como **dois** sub-agentes por cidade. + +**Recomendação:** **A** — só o agente principal dispõe de Task para fan-out; a SKILL.md já é o contrato que ele carrega; e extract/build são comandos determinísticos que não precisam de um modelo com contexto — o que infla é a _saída_ (ssh/Chromium), resolvida por redirecionamento para `data/relatorios-cidade/logs/.log`, com só exit status + caminhos voltando ao contexto. Isso preserva o C163 (skill + subagente, sem command) e não cria superfície nova. + +**Rejeitadas:** + +- **B** porque um command/subagente-orchestrator é atalho redundante ao agente principal (o precedente C163 rejeitou o command por falta de consumidor) e um subagente `mode: all` extra adiciona um handoff sem ganho. +- **C** porque o analyst ingere o fact-set inteiro de qualquer forma; separar collector de analyst duplica o handoff (o `research.json` completo) e o ganho de contexto é marginal — só valeria se o fact-set de **uma** cidade ficasse gigante (gatilho de revisitação, ver Não escopo). + +### Componentes / mudanças + +- **`.agents/skills/relatorio-cidade/SKILL.md`** (editar, dono do contrato): + - Nova seção **"Lote (várias cidades)"**: invocação literal `/relatorio-cidade Salvador, Ilheus, Itacare`; **vírgula** como separador; cada token sofre `trim`, vazio descartado, **dedupe preservando ordem**; token aceito como **slug canônico** (`isMunicipalitySlug`) **ou** nome (fold `resolveMunicipalityName` → `municipalityCatalogEntriesForCity`); **0 entradas = inválido**, **>1 entrada = ambíguo** (ex.: `Salvador` → `salvador-ze-N`); inválido/ambíguo vira **linha failed isolada**, nunca aborta o lote. + - **"Pipeline (3 passos)" → fluxo por etapas**: (1) orchestrator parse/resolve; (2) researcher ×N em paralelo (`.opencode/agent/relatorio-cidade.md`); (3) extração read-only **serializada** (mesma receita de hoje, `~/teqo-report`, `CITY_REPORT_CONFIRM=1`, proxy `127.0.0.1:5433`), com `> data/relatorios-cidade/logs/.log 2>&1` e só o status no contexto; (4) build local por cidade (`scripts/build-city-report.mjs`), também com log em arquivo; (5) summary final. A receita **single-city permanece literal** como caso N=1. + - Nova seção **"Recibo do researcher"** (contrato D3) e nota de que o researcher **não** roda ssh nem build. + - `Quando usar` e `Referências` atualizados (impl OPS118). +- **`.opencode/agent/relatorio-cidade.md`** (editar): vira o **researcher/analyst por cidade** — confirma slug, faz a pesquisa web datada, escreve `.research.json`, devolve **apenas o recibo**. Remove os passos 3–4 (ssh/build) do papel; mantém os limites (nunca escreve na produção, nunca inventa fonte, não commita artefato). Frontmatter segue `description` + `mode: subagent` (`.opencode/agent/relatorio-cidade.md:1-4`). +- **`.opencode/commands/relatorio-cidade.md`** (novo, wrapper fino): a intenção usa a invocação literal `/relatorio-cidade ` e o orquestrador precisa receber a lista por `$ARGUMENTS` — sem o command o `/` seria ficção (achado alto da revisão). Segue o padrão dos outros wrappers (carrega a skill pelo nome exato, aponta para `.agents/skills/relatorio-cidade/SKILL.md`, sem lógica, sem `model:`); `tests/unit/opencodeCommands.unit.spec.ts` passa a cobri-lo (lista de commands). +- **Sem** `opencode.json` novo, **sem** permissão nova, **sem** alias `pnpm`. +- **`scripts/extract-city-report-snapshot.mjs` e `scripts/build-city-report.mjs` intocados** — já são CLIs por cidade (`--flag=value`, `scripts/lib/cli.mjs:34-48`); o lote é um loop no orquestrador, não um script novo. +- **`docs/changelog/2026-09-16-ops118.md`** (novo, uma entrada). +- **Teste docs-guard:** `tests/unit/cityReportBatchSkill.unit.spec.ts` (novo) — lê os arquivos e pina os literais (ver D5). Padrão `tests/unit/skillsAutoFlag.unit.spec.ts`/`opencodeCommands.unit.spec.ts`. +- **Runbook (`docs/ops/teqo-1313-deploy.md` §C163) intocado:** a skill é canônica e a receita de extração não muda. + +### Dados → forma + +- **Entrada (forma):** string de argumento com N tokens separados por vírgula → lista de slugs canônicos deduplicada + lista de tokens inválidos/ambíguos. N=1 é o caso atual (inalterado). +- **Intermediários (inalterados):** `data/relatorios-cidade/.research.json` (researcher), `.snapshot.json` (extrator), `docs/research/relatorios-cidade/-.pdf|.md` (builder). Logs novos em `data/relatorios-cidade/logs/.log` (gitignored, mesma raiz já ignorada). +- **Recibo (forma):** JSON curto por cidade — ver D3 — o único dado de pesquisa que cruza para o orquestrador. +- **Sem forma nova de produto:** nenhum índice/PDF agregado; o "sumário" é a resposta do orquestrador (tabela saiu × falhou), não um artefato. + +## Fases verificáveis + +1. **F1 — Contrato de lote (`SKILL.md`).** Separador vírgula, trim/vazio/dedupe, aceita slug ou nome via catálogo + `resolveMunicipalityName`, single-city = N=1 literal, inválido/ambíguo = falha isolada. Prova: leitura + asserções do docs-guard; `pnpm format:check`. +2. **F2 — Decomposição.** `.opencode/agent/relatorio-cidade.md` redefinido para researcher (só `research.json` + recibo); "Pipeline" da skill reescrito por etapas com extração serializada e logs em arquivo; contrato do recibo. Prova: docs-guard (o researcher não **executa** as etapas extract/build — sem `--municipality=`/`--snapshot=`; o recibo tem os campos; a skill mantém o exemplo single-city) + `opencodeCommands` (o command aponta para a skill e passa `$ARGUMENTS`). +3. **F3 — Prova + gates.** (a) **tracer local semeado**: `pnpm db:seed:minimal` na worktree → extração local read-only de 2–3 cidades → `research.json` mínimo por cidade → build → **um par PDF+MD por cidade**; depois injetar **1 token inválido** e **1 cidade com falha** (research ausente/`municipalitySlug` trocado) e conferir **sucesso parcial** + o summary do orquestrador; (b) **corrida live N=1** por `/relatorio-cidade Feira de Santana` para provar não-regressão e o shape do recibo; (c) `pnpm gate:fast` (lint+typecheck+unit, inclui o docs-guard), `pnpm gate:push` (knip/cycles), `pnpm push`; PR `Closes #1065`. E2E/int não são tocados (sem `src/`); o classifier do CI decide o escopo. + +## Rabbit holes / Não escopo (engenharia) + +- **Não** criar PDF/índice/sumário agregado do lote (produto novo: agregação/ranking/mapa). +- **Não** dividir em N sub-agentes por etapa (collector+analyst); gatilho de revisitação: fact-set de uma cidade grande o bastante para estourar um agente. +- **Não** paralelizar a extração ssh (colisão no `~/teqo-report` compartilhado: `git fetch/checkout/pull` + `pnpm install`, `SKILL.md:60-80`); serializar é obrigatório. +- **Não** paralelizar build/Chromium em massa (estampida de recurso local) — build por cidade, sequencial. +- **Não** mudar conteúdo/seções/layout do relatório nem o contrato dos JSONs. +- **Não** escrever na base de produção, nem `db:pull`/snapshot de banco; extração read-only fail-closed. +- **Não** criar script batch-runner, alias `pnpm`, helper em `src/lib` sem consumidor de app (knip), nem módulo em `src/utilities` (pin `tests/unit/codebaseConventions.unit.spec.ts:27-70`); **não** mexer em `scripts/lib/cli.mjs` (skeleton pinado, `codebaseConventions.unit.spec.ts:327-354`). +- **Não** commitar artefato (PDF/MD/snapshot/research/logs; gitignored). +- **Não** mexer em `opencode.json`, criar command ou registrar permissão. +- **Não** editar o runbook §C163 (a skill é a fonte canônica). + +## Riscos e mitigação + +- **Colisão do checkout compartilhado (`~/teqo-report`).** Mitigação: extração **serializada** por cidade no orquestrador (fila única, ordem da lista); nunca `ssh` em paralelo. +- **Mismatch snapshot × research.** O guard do builder (`build-city-report.mjs:94-98`) segue fail-closed; a cidade falha isolada e entra no summary — nunca casar JSON à mão. +- **Regressão do single-city.** N=1 é o mesmo caminho com um token; exemplo literal preservado e coberto pela corrida live (F3b) e pelo docs-guard. +- **Contexto vazando para o orquestrador.** Recibo curto **por contrato** (D3) + logs de extract/build em arquivo; docs-guard verifica que o recibo não pede corpo de `research.json` e que o researcher não descreve ssh/build. +- **Resolver errado / slug inventado.** Tokens passam por `isMunicipalitySlug` ou pelo fold existente; 0 entradas = inválido, >1 = ambíguo → falha isolada (não inventa, não expande). +- **Paralelismo indisponível no harness.** Se não houver Task para fan-out, o lote degrada para execução sequencial (N≥1) sem mudar o contrato; latência maior, correção igual. +- **Custo/latência de N pesquisas.** Aceito: a intenção pede fan-out; a falha isolada impede que uma cidade lenta derrube o lote. +- **Ambiguidade de Salvador.** Documentada como caso de falha explícita com o slug correto sugerido (`salvador-ze-N`), sem silêncio. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: lote com vírgula → um par PDF+MD por cidade `-`; single-city preservado; falha isolada e sucesso parcial explícitos. +- [ ] Decomposição: pesquisa web roda em sub-agente por cidade (Task), isolada do orquestrador; o orquestrador retém só o recibo; extract/build fora do researcher e com log em arquivo. +- [ ] Guardrails intactos: sem fonte não publica; empenho ≠ pagamento; leitura relativa; PII mínima; indício regional ≠ emenda; artefato gitignored. +- [ ] Invariantes AGENTS/engineering-standards: sem collection/migration/Consent; sem superfície `src/`; sem command/`opencode.json`/alias; `scripts/lib/cli.mjs` e os dois CLIs intocados; produção read-only. +- [ ] Prova: tracer local (2–3 cidades + inválida + falha) com saídas por cidade e summary parcial; corrida live N=1; docs-guard verde. +- [ ] Gates: `pnpm format:check`, `pnpm gate:fast`, `pnpm gate:push`, `pnpm push`; CI verde com PR `Closes #1065`. +- [ ] Docs: `SKILL.md` + `.opencode/agent/relatorio-cidade.md` atualizados; changelog `docs/changelog/2026-09-16-ops118.md`. + +## Decisões de engenharia + +1. **Contrato de lote e resolução de slugs.** + **Opções:** A docs-only — regra literal na `SKILL.md` (vírgula; trim/vazio/dedupe; slug ou nome via `isMunicipalitySlug`/`resolveMunicipalityName`+catálogo; N=1 = hoje; inválido/ambíguo = falha isolada), sem código novo | B helper puro novo (`src/lib/municipalityBatchInput.ts` ou `scripts/lib/…`) + unit test | C script batch-runner `scripts/city-report-batch.mjs`. + **Recomendação:** **A** — o único consumidor é o agente (a lista vem de linguagem natural), o resolver já existe e é testado (`municipalityNameAliases.unit.spec.ts`, `municipalityCatalog.unit.spec.ts`) e o delta é só composição; mantém o shape C163 (skill+subagente, sem `src/`) e evita knip/`server-only`. A prova documental fica no docs-guard (D5). + **Rejeitadas:** **B** porque export sem consumidor de app falha knip/convenções e duplica um fold já testado; **C** porque enterra a orquestração e a falha isolada em código e perde o summary legível, sem necessidade — os CLIs por cidade já existem. + +2. **Split de papéis.** + **Opções:** A orchestrator = agente principal (contrato na `SKILL.md`); researcher = `.opencode/agent/relatorio-cidade.md` (`mode: subagent`); extract/build disparados pelo orchestrator com log em arquivo | B subagente-orchestrator `mode: all` + command | C um runner subagent extra para extract/build. + **Recomendação:** **A** — só o agente principal faz fan-out de Task; o researcher carrega o contexto pesado e devolve recibo; extract/build são determinísticos e só a _saída_ infla, resolvida por redirecionamento `> logs/.log 2>&1` (só status + caminhos no contexto). + **Rejeitadas:** **B** pela redundância ao agente principal (precedente C163 rejeitou command); **C** porque o comando em si é minúsculo — um terceiro handoff não reduz contexto e adiciona latência/falha. + +3. **Recibo curto do researcher.** + **Opções:** A JSON fixo por cidade | B bullet livre do agente | C orquestrador lê o `research.json` e resume. + **Recomendação:** **A** — campos fixos: `{ slug, status: 'ok'|'failed', researchPath, researchedAt, itemCount, gapCount, gaps: [id…], newsCount90d, weakSourceCount, failureReason? }`, teto de ~15 linhas, **nunca** o corpo (`items[].answer/details`, `news[].summary`, `approach/leaders/…`); `weakSourceCount` sinaliza item sustentado só por fonte de região/polo (baixa confiança) e `status:'failed'` carrega o motivo sem escrever corpo parcial. + **Rejeitadas:** **B** porque formato livre deixa o corpo vazar; **C** porque obriga o orquestrador a carregar exatamente o que a intenção quer manter fora. + +4. **Falha isolada e ordem/concorrência.** + **Opções:** A research paralelo; extração **serializada**; build por cidade sequencial; falha em qualquer etapa marca a cidade e segue | B tudo paralelo | C tudo serial. + **Recomendação:** **A** — só a pesquisa é paralelizável com segurança; a extração colide no `~/teqo-report` e o build/Chromium é recurso local (sequencial); uma cidade que falha é pulada nas etapas seguintes e o **summary final** lista, na ordem da lista resolvida, `slug · status · PDF/MD (quando ok) · motivo (quando falha)`, explicitando saiu × falhou. + **Rejeitadas:** **B** pela colisão e pela estampida de Chromium; **C** porque serializa justamente a etapa cara (pesquisa) sem motivo. + +5. **Nível de prova/teste.** + **Opções:** A docs-only sem teste + tracer manual | B docs-guard unit novo (`tests/unit/cityReportBatchSkill.unit.spec.ts`) + tracer manual (local semeado, 2–3 cidades + 1 inválida + 1 falha) + corrida live N=1 | C testes em `src/`/int/e2e. + **Recomendação:** **B** — sem superfície `src/`, unit/int/e2e de app não se aplicam; o docs-guard (arquivos puros, padrão `skillsAutoFlag.unit.spec.ts`) pina o contrato no CI (vírgula, N=1 literal, campos do recibo, researcher sem ssh/build); o tracer local prova o ponta-a-ponta determinístico (o extrator aceita `DATABASE_URL` local e já rodou assim no C163 — **read-only**); a corrida live N=1 prova não-regressão e o shape do recibo. Se homeserver for usado, só a receita read-only documentada. + **Rejeitadas:** **A** porque deixa um deliverable documental sem sinal de CI; **C** porque é armadilha de cobertura — não há `src/` a testar e a pesquisa web não é determinística. + +6. **Docs / entry point.** + **Opções:** A editar `SKILL.md` + `.opencode/agent/relatorio-cidade.md` + changelog, **sem** command | B editar os mesmos + um **command-fino** `/relatorio-cidade` que passa `$ARGUMENTS` | C manter tudo e só descrever o lote no changelog. + **Recomendação:** **B** (revisada na execução) — a skill é o contrato canônico (lote + pipeline por etapas + recibo) e o agente vira researcher, mas a intenção fixa a invocação **literal** `/relatorio-cidade `; sem command o `/` é ficção (a skill carrega pela tool `skill`, não recebe argumento) e a lista não teria por onde entrar. O wrapper é o mesmo dos demais `/` do repo (carrega a skill pelo nome, aponta para a canônica, sem lógica/model). + **Alternativas rejeitadas:** **A** por deixar a invocação literal da intenção sem mecanismo (achado alto da revisão de simplify); **C** porque o lote precisa estar na skill (é o que o agente lê) e o agente precisa perder o papel de ssh/build. + +## Self-score + +- **Decisões caras com rejeitadas: 5/5** — D1–D6 com opções, recomendação e rejeitadas explícitas, incluindo a não-obviedade de Salvador ambíguo e a fronteira docs-only × helper × script. +- **Cabe no appetite: 5/5** — entrega documental (skill + agente + changelog + 1 docs-guard) em ~1 dia; cortes prontos (docs-guard, corrida live) sem ferir a intenção. +- **Rabbit holes nomeados: 4/5** — índice agregado, collector+analyst, ssh paralelo, build/Chromium paralelo, mudar layout/conteúdo, escrita em prod, twin de script/helper/command, artefato commitado — todos fora com gatilho. +- **Reusa shells/helpers: 5/5** — `resolveMunicipalityName`/`isMunicipalitySlug`/catálogo, dedupe no shape de `parseSlugsParam` (`municipalityUpdateListUrl.ts:63-72`), os dois CLIs por cidade, `scripts/lib/cli.mjs` (intocado), padrão docs-guard e a decomposição Task de `work-issue`/`plan-issue`/`testing-audit`. +- **Aceite de produto satisfeito: 5/5** — lote com vírgula, um artefato por cidade, falha isolada visível, single-city N=1, guardrails intactos; única dependência externa (homeserver) contornada com tracer local read-only. + +Média **4,8/5** — gate ≥4 atendido. + +## Simplificação e débitos (triage 2026-09-16) + +**Aplicado na sessão** (2 revisores + fixes): dedupe **após** a resolução de slugs +(`Ilheus, ilheus` → um artefato); o summary identifica o token cru quando ele falha +antes de resolver; logs distintos `logs/.extract.log` × `.build.log`; aritmética +do recibo coerente (`itemCount` + `gapCount`, `gapCount == gaps.length`) e +`weakSourceCount` definido; `failureReason` marcado opcional; "nunca retém"; "Quando +usar" enxugado. **Achado alto da revisão:** a invocação literal `/relatorio-cidade` não +existia como command — criado `.opencode/commands/relatorio-cidade.md` (wrapper fino com +`$ARGUMENTS`) e `opencodeCommands` estendido para cobri-lo (D6 revisada). O docs-guard +deixou de ser tautológico/frágil (pina `corpo do research.json` e a ausência de +`--municipality=`/`--snapshot=` no researcher). + +**Registrado (Issue nova):** **#1069 (OPS120, defect, P2)** — página 1 estoura com +pesquisa cheia (9 itens + notícias ≤90d) e o builder aborta; o tracer F3 reproduziu +(`data/relatorios-cidade/logs/itacare.build.log`). Pré-existente do C163/#1007, bloqueia +o produto do lote; plano curto em +`docs/plans/ops120-relatorio-cidade-pagina-1-pesquisa-cheia.md`. + +**Adiado com gatilho:** + +- **docs-guard acoplado a literais de prosa + derivação fina de `weakSourceCount`** + (mesma superfície de contrato). Gatilho: a primeira reescrita legítima da skill que + derrube o guard, ou o primeiro lote real comparando recibos. +- **Corrida live N=1 completa (pesquisa web real via orquestrador):** o tracer local + determinístico + o researcher de 1 cidade cobriram o contrato; a corrida full fica + para o primeiro uso real do lote. + +**Já resolvido / descartado:** redundância "Quando usar" (aplicado); os 6 médios acima +(aplicados); nenhum achado virou ruído puro de score ≤2 fora do defer. diff --git a/docs/plans/ops120-relatorio-cidade-pagina-1-pesquisa-cheia.md b/docs/plans/ops120-relatorio-cidade-pagina-1-pesquisa-cheia.md new file mode 100644 index 00000000..d7e12fa0 --- /dev/null +++ b/docs/plans/ops120-relatorio-cidade-pagina-1-pesquisa-cheia.md @@ -0,0 +1,58 @@ +# OPS120 — relatório de cidade: página 1 sob pesquisa cheia (C163) + +Status: rascunho +Atualizado em: 2026-09-16 +Issue: #1069 +Priority: P2 +Impeccable: A — ajuste de conteúdo/orçamento do PDF (sem UI de app) +Rascunho UI: N/A — sem UI +Appetite: ~1 dia eng +Responsável: — + +## Intenção + +No caminho normal do relatório (checklist de pesquisa cheio + notícias ≤90 dias), a +página 1 estoura o guard do builder — `A página 1 estourou (1110px > 1001px úteis)` +(`data/relatorios-cidade/logs/itacare.build.log`, tracer da OPS118) — e o PDF não é +gerado. O guard e a composição da página 1 vêm do C163 (#1007); a OPS118 (#1065) +expôs o caso ao rodar N cidades com pesquisa real (o troubleshooting atual manda +"corte copy/caps", mas a página 1 é montada de dados — não há knob de corte exposto). + +## Objetivo e aceite + +- Cidade com o checklist de pesquisa cheio (9 itens) + notícias ≤90 dias gera PDF+MD + sem estourar a página 1. +- O corte é por **teto/orçamento de conteúdo** na página 1 (não espremer layout): + notícias e "quem é quem" com cap explícito + `totalCount`, no padrão dos caps do + E16 — o aprofundamento continua nas seções 2+. +- Se ainda estourar, o erro segue fail-closed e acionável. +- Sem regressão no relatório de cidade pequena/sem pesquisa. + +## Fora de escopo + +- Mudar layout/identidade do PDF, criar seção nova ou agregar lote. +- A atribuição de emenda a município (#1025). + +## Fases (ordem de entrega) + +1. Medir o que compõe a página 1 com pesquisa cheia e definir os tetos (notícias / + quem é quem / resumo) com `totalCount`. +2. Aplicar os tetos na composição e no render, mantendo a guarda de página 1. +3. Prova local (Ilhéus/Itacaré/Una com pesquisa cheia) + gates. + +## Verificação + +- Rebuild local do tracer com pesquisa cheia → par PDF+MD, página 1 em 1 página. +- Cidade com base vazia continua gerando (sem seção inventada). + +## Riscos + +- Cortar notícia/liderança demais empobrece a página 1 — mostrar "Mostrando X de Y" e + preservar o detalhe nas seções 2+. + +## Referências + +- Issues #1007 (C163) · #1065 (OPS118). +- `scripts/build-city-report.mjs` (guarda de página 1, `PAGE_ONE_BUDGET_PX`). +- `scripts/lib/cityReportBlocks.mjs` (composição da página 1) — hipótese a confirmar. +- `data/relatorios-cidade/logs/itacare.build.log` (evidência). diff --git a/tests/unit/cityReportBatchSkill.unit.spec.ts b/tests/unit/cityReportBatchSkill.unit.spec.ts new file mode 100644 index 00000000..01fb8e47 --- /dev/null +++ b/tests/unit/cityReportBatchSkill.unit.spec.ts @@ -0,0 +1,74 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +// OPS118: pins the batch contract of the relatorio-cidade skill and the +// researcher-only role of its subagent. The batch entry is prompt text (a +// comma-separated argument), so the contract lives in the SKILL.md prose — this +// guard fails the build if the batch/resolver/receipt/serialization wording is +// dropped, or if the subagent regains the ssh/build pipeline. + +const repoRoot = process.cwd() + +const read = (path: string): string => readFileSync(resolve(repoRoot, path), 'utf8') + +const skill = read('.agents/skills/relatorio-cidade/SKILL.md') +const researcher = read('.opencode/agent/relatorio-cidade.md') + +describe('skill /relatorio-cidade documents the batch contract', () => { + it('keeps the comma-separated batch entry and the single-city case', () => { + expect(skill).toContain('## Lote (várias cidades)') + expect(skill, 'batch example must be literal').toContain( + '/relatorio-cidade Ilheus, Itacare, Una', + ) + expect(skill, 'single-city must remain valid').toContain('/relatorio-cidade Feira de Santana') + expect(skill, 'comma is the separator').toContain('vírgula') + expect(skill, 'N=1 is the preserved path').toContain('N=1') + }) + + it('pins the canonical slug resolution and the ambiguity policy', () => { + expect(skill).toContain('isMunicipalitySlug') + expect(skill).toContain('resolveMunicipalityName') + expect(skill, 'Salvador ambiguity must be explicit').toContain('salvador-ze-N') + expect(skill, 'ambiguous token is an isolated failure').toContain('ambíguo') + }) + + it('documents the staged pipeline with a serialized extraction', () => { + expect(skill).toContain('## Pipeline (etapas)') + expect(skill).toContain('serializada') + expect(skill, 'isolated failure must be visible as partial success').toContain( + 'sucesso parcial', + ) + expect(skill, 'summary lists status per entry').toContain('(ok|failed)') + }) +}) + +describe('finite researcher receipt (OPS118)', () => { + it('declares the short receipt and its fields', () => { + expect(skill).toContain('## Recibo do researcher') + for (const field of [ + 'itemCount', + 'gapCount', + 'newsCount90d', + 'weakSourceCount', + 'failureReason', + ]) { + expect(skill, `receipt must declare ${field}`).toContain(field) + } + expect(skill, 'receipt must never carry the body').toContain('corpo do `research.json`') + }) +}) + +describe('subagent .opencode/agent/relatorio-cidade.md is researcher-only', () => { + it('stays a subagent that writes research.json and returns the receipt', () => { + expect(researcher).toMatch(/^---\n[\s\S]*mode: subagent[\s\S]*\n---/) + expect(researcher).toContain('researcher') + expect(researcher).toContain('research.json') + expect(researcher).toContain('Recibo do researcher') + }) + + it('does not run the deterministic extract/build stages', () => { + expect(researcher, 'researcher must not run the extractor CLI').not.toContain('--municipality=') + expect(researcher, 'researcher must not run the builder CLI').not.toContain('--snapshot=') + }) +}) diff --git a/tests/unit/opencodeCommands.unit.spec.ts b/tests/unit/opencodeCommands.unit.spec.ts index a98b3890..6dbd1075 100644 --- a/tests/unit/opencodeCommands.unit.spec.ts +++ b/tests/unit/opencodeCommands.unit.spec.ts @@ -10,7 +10,13 @@ import { describe, expect, it } from 'vitest' const repoRoot = process.cwd() -const commands = ['work-issue', 'plan-issue', 'testing-audit', 'bug-fix'] as const +const commands = [ + 'work-issue', + 'plan-issue', + 'testing-audit', + 'bug-fix', + 'relatorio-cidade', +] as const describe('opencode commands couple to their skill by exact name', () => { it.each(commands)('command /%s references the existing skill of the same name', (name) => {