teqo-backup/docs/plans/dossie-solla-tema-impl.md
Francisco Solla e5e1ae7ee8 feat(C190): dossiê Solla por tema/área + boletim modelo (skill + builder)
Terceira variante da família de dossiês: recorte pela área canônica do
acervo (SPEECH_TOPICS), com o pipeline C186/C187 generalizado para a forma
subject via THEME_UNIT (generalização aditiva; C186/C187 byte-idênticos).

- resolveSpeechTopic no dono speechFacets (fail-closed, sem slug inventado)
- themeSnapshot + extract-theme-snapshot (THEME_REPORT_CONFIRM=1, read-only)
- build-dossie-solla-tema (pack medido, PDF A4 + .md + boletim 1 página)
- skill/comando/2 subagentes (researcher por era + redator) + specs
- cenas theme-only no descritor (acervo universo/amostra, abrangência,
  lacunas, hierarquia de fontes, pills do boletim) certificadas pelo designer

Closes #1143
2026-09-18 17:02:56 -03:00

171 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Impl: Dossiê Solla por tema/área + Boletim modelo (skill → dossiê + boletim)
Status: aprovado
Atualizado em: 2026-09-18
Issue: #1143
Intenção: docs/plans/dossie-solla-tema.md
Appetite restante: ~2–3 dias eng (herdado)
> Aprovado no modo `--auto` (`/work-issue --issue 1143 --auto`): a pausa do GATE virou apresentação-no-chat. Literais e cortes do plano de intenção foram assumidos; **sem schema/migration/DB write**, sem Consent, sem URL pública e sem alterar `/dossie-solla-cidade` nem `/dossie-solla-instituicao` (generalização aditiva do dono, com as specs C186/C187 verdes).
## Leitura da intenção
- **Outcome:** `/dossie-solla-tema Educação` entrega, numa invocação, um par **PDF A4 datado + companion `.md`** (identificação da área — label `Educação` + valor canônico `educacao` + nota da taxonomia do acervo; seções por era A/B/C; tabelas **Objeto/Valor/Ano/Fase/Abrangência/Fonte**; painel de abrangência `área × segmento × rede`; **evidência direta do acervo por `Speech.topics`**; lacunas; fontes; defeso) **e** o **Boletim modelo** (1 página A4, linguagem de eleitor, ≤6 destaques + ≤14 "E mais", sem fontes, sem CTA, rótulo "Modelo — insumo interno"). Lote de 1+N; falha isolada não cancela o lote.
- **O que NÃO negociar:** token **na taxonomia fechada** dos 18 `SPEECH_TOPICS` (`src/lib/speechFacets.ts:14`), **fail-closed** — nunca slug inventado; eras A/B/C **reusadas** do C186 (sem eras novas); abrangência `area | segmento | rede` **nunca somada**; fases `autorizado|empenhado|liquidado|pago|restos` (default `nao_informado`, empenho ≠ pago); "sem fonte, não publica" + lacuna explícita (emenda/proposição por área **só com fonte**); acervo cobre **2011+** (pré-2011 = lacuna); PII mínima; leitura relativa (nunca % estadual absoluto); artefato **gitignored**; **sem schema/migration/DB write**; **sem truncamento mecânico/descarte silencioso** (regra C188 — lista capada declara "e mais N"); boletim herda **apenas** `bulletinFacts` (fato com fonte).
- **O que reavaliar (hipóteses da "Direção no codebase"):**
- "só o render é binário" — verdade a confirmar: o **dispatch** é binário em 4 módulos (`dossieBlocks.mjs:222`, `dossieRender.mjs:813-827/1329-1330/2119-2127`, `dossieBulletinRender.mjs:372-376`); `isInstitutionUnit` tem 11 usos e o boletim (`dossieBulletin.mjs`) já é unit-aware.
- "o ramo instituição é quase todo reutilizável" — verdade: a variante tema é **estruturalmente a mesma família** (abrangência 3-listas, eras correntes sem caps, carta/síntese/gráficos, acervo), então o certo é **generalizar a forma `subject`** em vez de geminar um terceiro branch (o gatilho do 3º recorte já estava registrado no simplify do C187).
- "catálogo novo" — falso: o "catálogo" do tema já existe (`SPEECH_TOPICS`); falta **exportar** um resolver fail-closed do dono (`topicByKey` é privado, `speechFacets.ts:64`).
- "guard de extração igual ao institucional" — a **receita** (`readOnlyExtract.mjs`) reusa, mas o **alvo/guard** é próprio (`THEME_REPORT_CONFIRM=1`); o compositor institucional é _institution-shaped_ (`matchNames`/`mentionExcerpt`, `institutionSnapshot.mjs:84-113`).
- **Design (fonte de verdade do port — portar classe a classe):** dossiê `docs/plans/dossie-solla-tema-ui-design.html`, **14 cenas** (01 Capa e identificação da área; 02 A contribuição; 03 Resumo; 04 Síntese; 05 Gráficos; 06/07/08 Eras A/B/C; 09 Folha corrente/continuação; 10 Abrangência — lista completa; 11 Lacunas; 12 Notícias; 13 Acervo interno por tema; 14 Fontes/limites/defeso). Boletim `docs/plans/dossie-solla-tema-boletim-ui-design.html`, **2 cenas** (completo + variação de poucos fatos). O implementer **porta**, não redesenha.
## Abordagem recomendada
```mermaid
flowchart LR
A["/dossie-solla-tema Educação, Saúde"] --> B["orquestrador: resolve token<br/>resolveSpeechTopic (18 canônicos)<br/>fail-closed + dedup"]
B --> C["N researchers por era (A|B|C × tema)<br/>paralelo · MAX_RESEARCHERS_IN_FLIGHT=6"]
C --> R["<slug>.<era>.research.json<br/>+ recibo curto (status/paths/gaps)"]
B --> E["extração read-only, 1 tema por vez<br/>extract-theme-snapshot.mjs · THEME_REPORT_CONFIRM=1<br/>composeThemeSnapshot (Speech.topics direto)"]
E --> S["<slug>.theme.snapshot.json"]
R --> G["build-dossie-solla-tema.mjs"]
S --> G
G --> I["buildDossierReport(unit=THEME):<br/>capa+resumo+eras+abrangência+acervo+fontes<br/>→ bulletinFacts ledger (só fato com fonte)"]
I --> J["renderDossierHtml/Md → PDF A4 multi-folha + .md<br/>(buildPdf.mjs + dossiePack.mjs, sem caps)"]
I --> K["buildBulletin(unit=THEME) → renderBulletinHtml"]
K --> L["PDF 1×A4 boletim modelo"]
J --> M["docs/research/dossie-solla-tema/<slug>-<YYYY-MM-DD>-*.pdf/.md"]
L --> M
```
**Opções consideradas:** A) **descritor de unidade data-driven** + generalizar a forma `subject` (instituição e tema no mesmo dono) + registro por `unit.id`; B) terceiro branch `isThemeUnit` ao lado de `isInstitutionUnit` (relatório/render tema irmãos); C) o descritor carregar `buildReport`/`render` (função no dado).
**Recomendação:** **A** — a variante tema é a mesma forma da instituição (abrangência 3-listas, eras correntes, carta/síntese/gráficos/acervo); um **registro por id** substitui a cadeia de ternários e o descritor só ganha o vocabulário/copy que muda. Mantém **byte-idêntico** o caminho C186/C187 (mesmas funções, agora resolvidas por tabela) e escala para N recortes sem terceiro fork.
**Rejeitadas:** **B** — gemina o maior branch do pipeline (a drift que o repo proíbe; C187 já registrou a duplicação da mecânica com gatilho "3º unit/recorte"); **C** — função no descritor cria import circular (descritor em `dossieUnit.mjs`, builders em `dossieBlocks.mjs`) e transforma o dado em service locator.
### Componentes / mudanças
**Seam + resolução canônica (donos editados)**
- **`scripts/lib/dossieUnit.mjs`** — `THEME_UNIT` (`id:'theme'`, `slugField:'themeSlug'`, `shape:'subject'`, `spheres:['area','segmento','rede']`, `defaultSphere:'area'`, `sphereLabels`, `sphereBadgeClass` (`segmento→scope-sector`, `rede→scope-network`, `area→''`), `breadthRank`, `researchDir:'data/dossie-solla-tema'`, `series`, `title`, `bulletinTitle`, `bulletinMoreLimit`, `nounPlural:'temas'`, `subjectLabel:'Área'`); registro `DOSSIER_UNITS.theme` (`:63`); predicado `isSubjectUnit(unit)` (`shape === 'subject'`) — `isInstitutionUnit` permanece como alias fino (`id === 'institution'`) para não quebrar consumidores; `resolveDossierUnit` já fail-closed (`:74-82`).
- **`scripts/lib/dossieUnit.mjs` (descritor `subject`)** — campos que a forma genérica lê: `subjectName(snapshot)`, `identityBadges(snapshot)`, `scopeCopy`, `eraMethod`, `eraRecovery`, `reachCopy{ruleTitle,ruleBody}`, `coverageExtra`, `editorialSpheresLine`, `openingTitle(subjectName)`, `coverSubtitle`, `coverHowToUse`, `acervoNote`, `bulletinCopy`. `INSTITUTION_UNIT` recebe exatamente os literais de hoje (garante paridade); `THEME_UNIT` recebe os da família temática.
- **`src/lib/speechFacets.ts`** (editar o dono) — exportar `resolveSpeechTopic(token): SpeechTopic | null` (fecha sobre o `topicByKey`/`facetKey` já existentes), `speechTopicValues()` e `speechTopicLabel(value)`. Sem segundo mapa; o mesmo fold acento/caixa-insensível do parser do LLM.
**Pipeline generalizado (aditivo; C186/C187 verdes)**
- **`scripts/lib/dossieResearch.mjs`** — `THEME_RESEARCH_CHECKLIST` + `CHECKLIST_BY_UNIT.theme` (`:219`); nominal `era_a_formacao|era_a_sesab|era_a_consultor_ms|era_a_conquista|era_a_sas_ms|era_a_vinculo`; `era_b_sesab|era_b_politicas|era_b_investimentos|era_b_convenios|era_b_articulacao`; `era_c_discursos|era_c_proposicoes|era_c_relatorias|era_c_emendas|era_c_programas|era_c_audiencias|era_c_titulos`. Trocar o noun fixo do erro de merge (`:494`) por `unit.nounPlural` (`municípios|instituições|temas`).
- **`scripts/lib/dossieBlocks.mjs`** — generalizar `buildInstitutionReport` → `buildSubjectReport` (unit-driven): `directItems`/`scopeItem`/`speechFactsFromRows` usam `unit.defaultSphere`/`unit.spheres`; identidade/badges/`scopeCopy`/`eraMethod`/`reach`/`limits.coverage` vêm do descritor; `buildDossierReport` vira registro `{ municipality: buildMunicipalityReport, institution: buildSubjectReport, theme: buildSubjectReport }` (`:222`). `speechFactsFromRows(snapshot, sphere = default)` deixa de ter default `instituicao`.
- **`scripts/lib/dossieRender.mjs`** — generalizar `renderInstitutionDossierHtml/Md` → `renderSubjectDossierHtml/Md`; `unitHeader/unitFooter` (`:813-827`) passam a `isSubjectUnit(report.unit)`; `dossierPackAnchors` vira registro (`:1329`) com `THEME_PACK_ANCHORS` (`abrangencia-area|segmento|rede` → `scope:area|scope:segmento|scope:rede`); `INSTITUTION_SCOPE_WORDS` (`:1332`) sai do key do descritor. Reusa as classes já portadas (`.identity-badge`, `.scope-sector`, `.scope-network`, `.scope-cards`, `.more-note`, `.document-table`, etc.); o port das 14 cenas temáticas não introduz CSS novo além de copy.
- **`scripts/lib/dossieBulletin.mjs`** — sem mudança estrutural (já unit-aware: `breadthRank`/`bulletinMoreLimit`, `:51-68`).
- **`scripts/lib/dossieBulletinRender.mjs`** — generalizar `renderInstitutionBulletinHtml` → `renderSubjectBulletinHtml` (copy do descritor) e registrar `theme` (`:372-376`); mantém `defeso-band`, `identity-pill`, `lacuna-panel`, variação de poucos fatos.
**Extração read-only + builder (novos, reusando plumbing)**
- **`scripts/themeSnapshot.mjs`** (novo) — `composeThemeSnapshot({ payload, actor, value, label, taxonomyNote, readAt, codeSha, database })` → `{ meta:{kind:'theme'}, theme:{slug,value,label,taxonomyNote,topics:[value]}, speeches:{topics:[value],totalCount,rows}, gaps }`; reusa `buildSpeechListWhere({ page:1, topics:[value] })` (`speechListFilters.ts:38`) + `loadSegmentsForSpeeches`; projeta `summary`/links/ano/temas e **não** computa `matchNames`/`mentionExcerpt` (o recorte é a tag do acervo).
- **`scripts/extract-theme-snapshot.mjs`** (novo) — CLI `--theme=<valor|label> --out=<json>`; resolve com `resolveSpeechTopic` (fail-closed: desconhecido → lista os 18 canônicos, nunca slug inventado); `enterReadOnlySession({ confirmEnv:'THEME_REPORT_CONFIRM', … })`; serializado 1 tema por vez; escreve `data/dossie-solla-tema/<slug>.theme.snapshot.json`.
- **`scripts/build-dossie-solla-tema.mjs`** (novo) — CLI `--snapshot= --research-dir= --out-dir= [--generated-at=] --[--theme=]`; exige `snapshot.theme.slug`; lê `<slug>.{a,b,c}.research.json` (recusa slug divergente) e `<slug>.narrative.json` (opcional); **não** chama `fetchAuthorEmendas`/`fetchCamaraActivity`/`fetchHealthData`; `buildDossierReport({ …, unit: THEME_UNIT })` → dossiê + `buildBulletin({ facts, identity, unit: THEME_UNIT })`; `dossierPackAnchors(THEME_UNIT)`; HTML cache em `data/dossie-solla-tema/`; `.md` + dois PDFs via `buildPdf.mjs`/`dossiePack.mjs`; `DOSSIER_STRICT=1` falha em lacuna.
**Skill / command / subagentes**
- **`.agents/skills/dossie-solla-tema/SKILL.md`** (novo) — fonte canônica: `Lote`, `Áreas canônicas (taxonomia do acervo)`, `Pipeline (etapas)`, `Briefing por era (A/B/C)`, `Recibo do researcher`, `Recibo do redator`, `Contrato dos JSONs` (`themeSlug`, `sphere: area|segmento|rede`), `Conteúdo`, `Guardrails`, `Troubleshooting`, `Referências` (aponta os dois HTMLs hi-fi).
- **`.opencode/commands/dossie-solla-tema.md`** (novo) — frontmatter só `description:`; corpo carrega a skill pelo nome exato e termina em `$ARGUMENTS`.
- **`.opencode/agent/dossie-solla-tema.md`** + **`.opencode/agent/dossie-solla-tema-redacao.md`** (novos) — researcher por era / redator, espelhando C187; sem `ssh`/build/`--snapshot=`/`--theme=`/commit.
**Testes / CI / docs**
- Novos: `tests/unit/dossieTheme.unit.spec.ts`, `dossieThemeResearch.unit.spec.ts`, `dossieThemeBatchSkill.unit.spec.ts`, `tests/int/themeSnapshot.int.spec.ts`; spec do resolver em `tests/unit/speechFacets.unit.spec.ts` (estender).
- Editar: `tests/unit/opencodeCommands.unit.spec.ts` (`:13-21`, incluir `'dossie-solla-tema'`); `scripts/lib/test-affected-core.mjs` (`SCRIPTS_SPEC_PINNED` += `scripts/themeSnapshot.mjs`; `ciSkipInvariants` recalcula o closure até casar); `.gitignore` (`/data/dossie-solla-tema/`, `/docs/research/dossie-solla-tema/`); `docs/changelog/2026-09-18-c190.md`.
- **Manter verdes:** `dossie{Research,Blocks,Render,Bulletin}.unit.spec.ts`, `dossieInstitution*.unit.spec.ts`, `institutionCatalog.unit.spec.ts`, `speechFacets/speechListFilters/speechListUrl.unit.spec.ts` e o int institucional — provando o default aditivo.
### Dados → forma (pergunta 3 — data-presentation)
- **Dossiê — forma escolhida:** **tabela/lista com fonte por linha + badge de abrangência + badge de fase** (Objeto/Valor/Ano/Fase/Abrangência/Fonte; painel de abrangência com **3 listas e nenhum total combinado**; gráficos consolidados como no dono; **acervo por tema com universos/amostra declarados** — `Speech.topics = educacao` (2011+)). É a forma mais pobre que ainda desbloqueia a decisão (ângulo/entrega + o que falta). **Rejeitadas:** dashboard de área (perde a fonte item a item e convida a somar); gráfico de série entre áreas (não há série comparável); % estadual absoluto (anti-goal); painel territorial por município (território é o recorte do C186).
- **Boletim — forma escolhida:** **número destacado + rótulo curto** (≤6 `.highlight-card`) + **lista densa ✓** ("E mais", `bulletinMoreLimit`) + timeline; densidade > destaque grande. **Rejeitadas:** tabela/planilha (documento de gestor); parágrafo longo (estoura 1 página); fontes no boletim (vivem só no dossiê); CTA (defeso).
## Decisões de engenharia (A–G)
**A. Vocabulário de abrangência.** Opções: A) `area | segmento | rede` (default `area`); B) reusar `instituicao | setor | rede` do C187; C) só `area`. **Recomendação: A** — o render/blocks já resolvem esfera por descritor (`dossierSphereLabel/BadgeClass`, `scopeItem` vira `unit.spheres`); `area` é a política inteira, `segmento` o recorte interno (básica/superior/EJA), `rede` as entidades/escolas; preserva o guardrail "nunca somar" com vocabulário que não confunde com ente. **Rejeitadas:** B (uma "instituição" dentro do dossiê de área sugere atribuição de entidade — exatamente a confusão que o recorte evita; o hi-fi usa "Abrangência · área"); C (perde segmento/rede, que existem no material e no hi-fi).
**B. Generalização do dispatch e da forma.** Opções: A) registro por `unit.id` + generalizar a forma `subject` (instituição e tema no mesmo builder/renderer); B) terceiro branch tema irmão; C) descritor com funções `buildReport`/`render`. **Recomendação: A** — byte-parity C186/C187 (mesmas funções, agora resolvidas por tabela), sem terceiro fork do maior branch, honrando o gatilho do 3º recorte registrado no simplify do C187. **Rejeitadas:** B (twin da mecânica de `dossieBlocks`/`dossieRender`, drift garantido no próximo fix de pack/C188); C (import circular/TDZ e service locator).
**C. Onde vive a resolução do token.** Opções: A) exportar resolver do dono `src/lib/speechFacets.ts`; B) lib nova `src/lib/speechTopicCatalog.ts`; C) resolver na prosa da skill/script. **Recomendação: A** — `SPEECH_TOPICS`, `facetKey` e o `topicByKey` já vivem lá; exportar `resolveSpeechTopic` reusa o **mesmo** fold do parser do LLM (uma fonte de verdade para label↔valor). **Rejeitadas:** B (catálogo paralelo ao dono → drift); C (lógica de resolução não testável e duplicada em prompt).
**D. Composer e guard da extração read-only.** Opções: A) `scripts/themeSnapshot.mjs` + `extract-theme-snapshot.mjs` com `THEME_REPORT_CONFIRM=1`, reusando `readOnlyExtract.mjs`; B) parametrizar `composeInstitutionSnapshot`; C) reusar o extrator institucional com `--topics=`. **Recomendação: A** — a projeção do tema é diferente (sem `matchNames`/`mentionExcerpt`); a parte profunda (sessão read-only/ator/loaders) é reusada do dono, não geminada; guard próprio é o contrato de intenção explícita (o comando não adivinha o alvo). **Rejeitadas:** B (branch num compositor int-pinado por uma projeção que não compartilha o `institution` shape); C (o extrator institucional é institution-shaped e arrastaria `mentionExcerpt`/identidade de instituição para o dossiê temático).
**E. Escopo do recorte de dados em build.** Opções: A) sem APIs oficiais em build; checklist por era orientado a área; acervo por tema como **evidência de primeira classe**; emenda/proposição/relatoria só de item de pesquisa com fonte, senão lacuna; B) buscar Câmara por palavra-chave na ementa; C) reusar Portal/Câmara do C186. **Recomendação: A** — Portal não filtra por área e `fetchAuthorEmendas` exige município; match nominal/por tema é falso-positivo/omissão. O acervo (`Speech.topics`) é o recorte nativo e alimenta a seção de evidência e o ledger `bulletinFacts` (só fala com link). **Rejeitadas:** B/C (atribuição errada e "zero silencioso").
**F. Escape one-off.** Opções: A) **sem escape** — a taxonomia dos 18 temas é fechada; token desconhecido falha fechado com a lista canônica; B) espelhar `--slug=<x> --name="<Nome>"` do C187. **Recomendação: A** — o anti-goal é "inventar área/slug fora da taxonomia"; ao contrário do catálogo aberto de instituições, não existe "área desconhecida porém válida" (os labels são canônicos). **Rejeitada:** B (deixaria um slug inventado virar dossiê — contradiz outcome e recorte).
**G. Boletim por área.** Opções: A) generalizar `renderInstitutionBulletinHtml` → `renderSubjectBulletinHtml` (copy do descritor) e registrar `theme`; B) renderer tema separado. **Recomendação: A** — o delta é copy (pill `Área`/`educacao`, "pela <área>", rodapé "segmento e rede não são somados à área"); herança continua estrutural (`buildBulletin` só recebe `bulletinFacts`; ≤6 + `bulletinMoreLimit` "e mais"; sem fontes/CTA; `sparse`). **Rejeitada:** B (twin de ~130 linhas com delta de texto).
## Fases verificáveis (tracer bullet first)
1. **Tracer (seam + forma `subject` + resolução do token) — quota de meio dia.** `THEME_UNIT`/registro/`isSubjectUnit`; export do resolver em `speechFacets`; generalização de `dossieBlocks`/`dossieRender` para a forma `subject`; checklist tema em `dossieResearch`; um smoke de tema com snapshot/ pesquisa mínimos provando `buildDossierReport(unit=THEME)` → `renderDossierHtml/Md` e o `.md`. **Prova o encadeamento mais arriscado cedo** (paridade C187 + primeira composição temática) com as specs C186/C187 verdes.
2. **Pipeline agêntico + taxonomia.** `SKILL.md` + command + 2 subagentes; `dossieThemeBatchSkill.unit.spec.ts` (lote/resolução fail-closed/dedup/fan-out/sem escape/guarda); `opencodeCommands.unit.spec.ts` atualizado; `test-affected-core.mjs` atualizado; spec do resolver.
3. **Extração + builder.** `themeSnapshot.mjs`, `extract-theme-snapshot.mjs` (`THEME_REPORT_CONFIRM=1`), `build-dossie-solla-tema.mjs`; `tests/int/themeSnapshot.int.spec.ts`; decisão E exercida (sem emendas/Câmara/saúde em build; acervo alimenta evidência + ledger).
4. **Boletim + port visual + docs.** `renderSubjectBulletinHtml` tema (completo + variação de poucos fatos); port classe a classe das **14 cenas** do dossiê e das **2** do boletim; `.gitignore`; changelog `2026-09-18-c190.md`.
5. **Gates.** `pnpm gate:fast` (lint 0 warnings, `tsc --noEmit`, unit) → build local de **Educação** e de uma área de rótulo longo (ex.: Direitos Humanos) conferindo PDF/MD/boletim (aceite manual) → `pnpm push` (PR CI roda o resto). Sem e2e (não há runtime no app); `tests/int` só o snapshot novo.
## Rabbit holes / Não escopo (engenharia)
- Terceiro fork do pipeline (research/render/ledger/boletim) — twin proibido; tudo passa pelo dono + descritor registro.
- `INSTITUTION_SCOPE`/`institutionEraMethod`/copy hardcoded vazando para o tema; `sphere='instituicao'` default em `speechFactsFromRows`; noun fixo do `mergeDossierResearch`.
- Puxar emendas/Câmara/IBGE em build para "completar" a área; match por palavra-chave na ementa; tratar ausência como zero; somar segmento/rede à área.
- Catálogo/coleção Payload, schema/migration/DB write/segundo cadastro; UI in-app no `/campanha`; índice/PDF agregado de lote; fan-out das 18 áreas.
- Reabrir `/dossie-solla-cidade`/`/relatorio-cidade`/`/dossie-solla-instituicao` além da generalização aditiva; migrar o builder C163 para `buildPdf.mjs` agora.
- Reconstruir a carreira pré-2011 fora do acervo — vira **lacuna**, não invenção.
- Reabrir o design: os hi-fi estão prontos — só port; se faltar superfície, aciona-se o `designer` (não se redesenha na implementação).
## Riscos e mitigação
- **Regressão C186/C187 pela generalização:** mesmas funções resolvidas por tabela + descritor com os literais atuais; specs `dossie*`/`dossieInstitution*` no `gate:fast` são o oráculo; se a paridade falhar, a contingência é um `buildThemeReport` fino compondo os helpers já compartilhados (não geminar o branch inteiro).
- **`SCRIPTS_SPEC_PINNED` fora de sincronia:** `ciSkipInvariants` recalcula o closure e falha fechado; atualizar o mapa na mesma fase que cria `themeSnapshot.mjs`.
- **Recorte por tema impreciso (acervo):** o recorte é a tag `Speech.topics` (revisável na base); limite de amostra + "e mais N"; a skill declara "acervo cobre 2011+".
- **Token ambíguo/desconhecido:** valores e labels são únicos; o resolver do dono retorna `null` e o CLI/skill falham fechados com a lista canônica.
- **Fit/overflow de folha:** orçamentos explícitos + pack medido (`dossiePack` grow/shrink) + guarda A4 fail-closed; se estourar, aperta teto/copy — nunca espreme o layout nem corta texto.
- **PII/gitignore/fan-out:** projeção sem contato; artefatos gitignored; subagente proibido de commitar; `MAX_RESEARCHERS_IN_FLIGHT = 6`; extração serializada.
- **Appetite:** cortes embutidos — sem APIs em build (E), sem escape (F), sem agregado, sem casos especiais por área além do descritor.
## Aceite de engenharia
- [ ] Aceite de produto da intenção coberto: dossiê PDF A4 + `.md` (identificação da área label+valor canônico, eras A/B/C, abrangência `área×segmento×rede`, acervo por tema, lacunas, fontes, defeso) **e** boletim modelo 1×A4 (≤6 destaques + "E mais", sem fontes, sem CTA, rótulo "Modelo — insumo interno" + defeso, `NEEDS ASSET`).
- [ ] Taxonomia **fechada** fail-closed (18 canônicos) + dedup após resolução; **sem escape inventado**; eras A/B/C reusadas; abrangência `area|segmento|rede` **nunca somada**; fases por valor (empenho ≠ pago); "sem fonte, não publica"; acervo 2011+; PII mínima; artefato gitignored.
- [ ] Boletim herda **apenas** `bulletinFacts` (fato com fonte do dossiê); sem linha de fontes; sem CTA.
- [ ] Sem truncamento mecânico/descarte silencioso: lista capada declara "e mais N"; reformulação no `summary`.
- [ ] Generalização **aditiva** do dono; specs C186/C187 e int institucional verdes; `/dossie-solla-cidade` e `/dossie-solla-instituicao` sem mudança de comportamento; `buildPdf.mjs`/`dossiePack.mjs` seguem donos únicos do emit/paginação.
- [ ] **Sem schema, migration, Consent ou DB write**; extração read-only com `THEME_REPORT_CONFIRM=1`.
- [ ] `test-affected-core.mjs` sincronizado (closure exato) e `opencodeCommands.unit.spec.ts` com `dossie-solla-tema`; command/subagentes acoplados pelo nome exato.
- [ ] Unit novos verdes: `dossieTheme`, `dossieThemeResearch`, `dossieThemeBatchSkill` (+ resolver em `speechFacets`); int `themeSnapshot`; `.gitignore` + changelog; `pnpm gate:fast` verde; build local aceito em Educação + área de rótulo longo; `pnpm push`.
---
## Self-score decision-quality (gate ≥4/5)
1. **Decisões caras têm rejeitadas?** Sim — A–G com Opções/Recomendação/Rejeitadas explícitas (vocabulário, dispatch/forma, resolução do token, composer/guard, escopo de dados, escape, boletim). — **5/5**
2. **Abordagem cabe no appetite (2–3 dias)?** Sim — reuso máximo do dono C186/C187, sem schema/DB, cortes declarados (sem APIs em build, sem escape, sem agregado, sem e2e). — **4/5** (a generalização da forma `subject` é o trecho que mais consome quota; por isso o tracer cedo e o oráculo C187).
3. **Rabbit holes nomeados?** Sim — terceiro fork, copy hardcoded, APIs de área, match por palavra-chave, zero silencioso, catálogo/coleção, UI in-app, agregado, pré-2011 inventado, redesign. — **5/5**
4. **Depth check: reusa shells/helpers existentes?** Sim — `dossieUnit/Research/Blocks/Render/Bulletin/BulletinRender/Pack`, `buildPdf`, `readOnlyExtract`, `cityReportFormat`, `reportText`, `cli`, `buildSpeechListWhere`/`loadSegmentsForSpeeches`, `SPEECH_TOPICS` como catálogo; sem helper pass-through novo. — **4/5** (a generalização toca módulos em uso; mitigada por descritor com literais atuais + specs).
5. **Intenção (aceite de produto) permanece satisfeita?** Sim — os dois artefatos, a taxonomia fail-closed, os literais (1 página, ≤6/≤14, sem fontes, rótulo de modelo, abrangência não somável), o acervo como evidência de primeira classe e a regra C188 estão no aceite; a engenharia não reescreveu o outcome. — **5/5**
**Total 23/25 (média 4,6/5) — passa o gate ≥4/5.**
---
## Débitos capturados (triage pós-execute — C190)
Triage do simplify (2 revisores) + crítica do `designer`, via regras de
`capture-review-debts`. Nada aqui bloqueia a entrega certificada.
| # | Achado | Score | Tipo | Destino |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `scripts/build-dossie-solla-tema.mjs` é ~85% clone do builder institucional (3º clone do CLI); `extract-theme-snapshot.mjs` idem | 3 | cheap_polish | **defer** — lote `escala-dry-pos-c190` (runner único parametrizado por unit); gatilho: 4º recorte ou próxima edição simultânea dos builders |
| 2 | `SPEECH_SELECT`/limite/projeção base triplicados entre `themeSnapshot`/`institutionSnapshot`/`cityReportSnapshot` (drift real: typo `excerptTMS` corrigido nesta entrega) | 3 | cheap_polish | **defer** — mesmo lote (helper base + delta por unit) |
| 3 | Copy institucional vaza para o município (`MUNICIPALITY_UNIT.synthesisSumGuard`, arias "institucional" nas páginas compartilhadas) | 3 | cheap_polish | **descartar** do escopo C190 (pré-existente C186 sob byte-parity congelada); vira bug separado se o produto pedir |
| 4 | Pureza no seam: `scopeListWords` redundante com `scopeLists[].sphere`; regex de era duplicado; condição dupla no ramo de dinheiro | 2 | cheap_polish | **absorver** no lote acima (F3) |
| 5 | `dossieThemeBatchSkill.unit.spec.ts` é clone do spec institucional | 2 | cheap_polish | **absorver** (`describe.each`) no lote |
| 6 | Assert vacuoso herdado em `dossieTheme.unit.spec.ts` (`omitted >= 0`) | 2 | cheap_polish | **absorver** no lote |
| 7 | Sincronização dos artefatos hi-fi do C190 (tabelas 10,5pt; cenas medidas de continuidade 15+16+10 e 16+16+9) | 2 | cheap_polish | **absorver** — passo do artefato (designer), sem Issue; confirmação humana registrada no PR |
| 8 | Sem e2e (scripts/artefatos sem runtime no app) | 1 | defer_trigger | **descartar** — já declarado no impl; reavaliar se o pipeline ganhar rota/UI |
**Registro:** o lote `escala-dry-pos-c190` (itens 1, 2, 4, 5, 6) fica como
trabalho deferido deste plano, com gatilho explícito — sem Issue nova nesta
entrega para não editar/duplicar o tracker `in-progress`.