Dono único de orçamento de texto em reportText.mjs (capList, excerptDeepDive, joinWithRemainder, summaryText/summarySurfaceText, summaryListText), reusado por C163 e C186. Contrato summary por item nos dois arquivos de pesquisa; superfície de resumo nunca corta com "…" e aponta o aprofundamento; toda lista capada mostra contador; builders com 2 passadas guard-gated (integral → ponteiro → die fail-closed). Closes #1136
17 KiB
Impl: Relatórios e dossiês sem truncamento mecânico — reformular para caber ou ampliar o campo
Status: aprovado Atualizado em: 2026-09-17 Issue: #1136 Intenção: docs/plans/relatorios-sem-truncamento.md Appetite restante: herdado (~1–2 dias eng) Design UI: N/A — superfície inalterada; alvo é o hi-fi já aprovado do C186 e do C163 (Impeccable B)
Leitura da intenção
- Outcome: gerar relatório/dossiê/boletim com texto longo sem nenhum "…" em superfície de resumo, sem item sumindo em silêncio (contador em toda lista truncada), com o integral preservado no aprofundamento e a guarda de fit verde (fail-closed).
- O que NÃO negociar: proibido corte mecânico no resumo; contador em toda lista truncada; integral no aprofundamento; guarda de fit fail-closed; sem redesenho do hi-fi aprovado C163/C186; o
summaryé redigido pelo researcher — o builder não redige; sem fonte não publica; empenho ≠ pagamento; esfera/abrangência nunca somadas; PII mínima; artefato gitignored; defeso. - O que reavaliar: (a) a "Direção" sugere dono único substituindo
excerpt()— a hipótese de onde ele vive é decisão de engenharia (D1); (b) a mecânica de "ampliar orçamento enquanto a guarda permitir" precisa de um mecanismo concreto (D5); (c) C157 pode virar sucessor (D-assumida).
Natureza explícita: mudança de scripts/ + skills. Sem Payload schema, sem migration, sem contrato de URL pública, sem Consent/LGPD; push:false intocado; nenhuma collection/global/field tocado.
Abordagem recomendada
flowchart LR
R["research.json: answer/details integral + summary opcional"] --> B["build CityReport / DossierReport"]
B -->|"item.summary existe"| S["superfície de resumo usa summary"]
B -->|"sem summary"| A["superfície usa answer integral"]
S --> G{"guarda de fit: scrollHeight > orçamento?"}
A --> G
G -->|"cabe"| P["render PDF"]
G -->|"estoura"| F["2ª passada textFallback=pointer:<br/>linha aponta o aprofundamento, SEM ..."]
F --> G2{"guarda de fit"}
G2 -->|"cabe"| P
G2 -->|"estoura"| D["die — fail-closed"]
B --> C["capList: items + total + remaining"]
C --> RC["renderer imprime 'e mais N' / 'Mostrando X de Y'"]
R --> DD["aprofundamento: answer/details integrais"]
Opções consideradas: A | B | C
Recomendação: A — dono único estendido em reportText.mjs, summary opcional por item, contador {items,total,remaining}, fallback de ponteiro sem "…", e ampliação decidida pela guarda em 2 passadas.
Rejeitadas: ver D1–D5.
Decisões de engenharia
D1 — Onde vive o dono único de orçamento/composição de texto (substitui o excerpt() do C163)?
Opções: A) estender scripts/lib/reportText.mjs (contrato compartilhado de texto impresso, já "one owner, two consumers" para C163+C186) | B) módulo novo scripts/lib/reportBudget.mjs | C) manter em cityReportBlocks.mjs e importar no C186.
Recomendação: A — reportText.mjs já é o dono declarado do texto impresso consumido pelos dois relatórios; adicionar capList/excerptDeepDive/joinWithRemainder/summaryText ali estende o dono em vez de gemar, e mantém o módulo puro (não importa modelo de blocos).
Rejeitadas: B porque cria um segundo lugar para o mesmo conceito ("texto compartilhado"), exatamente o twin que o AGENTS proíbe; C porque faria o C186 (dossiê) depender do modelo de blocos do C163 — inversão de dependência e grafo pesado.
D2 — Forma e fallback do summary.
Opções: A) summary opcional em items[] dos dois contratos de pesquisa, sem fonte própria (herda a do item); builder prefere summary, senão answer | B) summary global por arquivo | C) *Summary em todo campo livre (approach, leaders, leaderAgenda, emendasIndicators[].purpose…).
Recomendação: A — resolve na origem e dá ao researcher o controle editorial; é o menor contrato que cobre as superfícies de resumo; herda a fonte do item (não é fato novo, não passa por "sem fonte não publica" de novo).
Rejeitadas: B porque o próprio plano de intenção rejeita global ("summary por item, auto-contido por superfície"); C porque explode contrato/validador/testes por ganho marginal — purpose de indício segue sem corte, coberto pelo fallback de linha.
D3 — Forma do contador (toda lista truncada).
Opções: A) capList(list, max) devolve { items, total, remaining }; renderizadores imprimem a copy que já existe no repo ("e mais N" em listas; "Mostrando X de Y" em tabelas/painéis) | B) só remaining | C) só total/shown.
Recomendação: A — reusa as três formas já impressas (indicatorList, table note, region scope), não perde o total e cobre corte por desenho e por lista.
Rejeitadas: B porque não sustenta "Mostrando X de Y" onde o corte é por desenho; C porque perde o "e mais N" das listas.
D4 — O que fazer quando nem summary nem o answer cabem.
Opções: A) linha-ponteiro sem reticências ("Texto integral no aprofundamento — <seção>") | B) cortar com "…" | C) deixar a guarda abortar.
Recomendação: A — literal do plano de intenção ("a linha aponta o aprofundamento sem reticências"); o integral já vive no aprofundamento.
Rejeitadas: B porque viola o outcome; C porque a guarda fail-closed é a última linha de defesa, não a primeira.
D5 — Mecânica de "ampliar o orçamento enquanto a guarda permitir".
Opções: A) 2 passadas guard-gated: (1) summary ?? answer integral; se a guarda estourar, (2) rebuild com textFallback:'pointer' (só campos sem summary viram ponteiro; o summary do researcher é preservado); ainda estourou → die | B) descida em 3 degraus (integral → caps de referência → ponteiro) | C) manter caps fixos atuais e só trocar "…" por ponteiro.
Recomendação: A — a guarda é o teto real ("amplia enquanto permitir"); a 2ª passada cobre o caso comum (researcher sem summary) sem corte mecânico; determinístico, testável (o modelo aceita a flag, o builder orquestra as 2 medições). O die final permanece e, quando dispara com summary presente, aponta o arquivo de pesquisa a encurtar.
Rejeitadas: B porque custa mais uma medição/estado para ganho marginal; C porque mantém o corte mecânico e não honra "prefere answer".
Componentes / mudanças
scripts/lib/reportText.mjs(dono único; atualiza o header para declarar orçamento/composição): ganhacapList(list, max) → {items,total,remaining},excerptDeepDive(text, max)(corte explícito com "…", só aprofundamento),joinWithRemainder(values, limit)(junção por contagem, sempre "e mais N", sem corte de caractere),summaryText(item) → {text, fromSummary}eSUMMARY_POINTER_COPY. Puro, sem modelo de blocos. Substitui oexcerpt()do C163.scripts/lib/cityReportResearch.mjsescripts/lib/dossieResearch.mjs:items[]passa a aceitarsummary(isNonEmptyString → trim, senãonull; não gera lacuna — é fallback de renderização).news[].summaryfica como está (não tocado).scripts/lib/cityReportBlocks.mjs: removeexcerpt(); todos os call-sites de página 1 (:158,198,232,236,248,255,288,310/311,322,350,368,382) usamsummaryText(item);joinLimited→joinWithRemainder; slices silenciosos do aprofundamento (:870sinal,:1097menção,:1113sumário de fala) →excerptDeepDive;buildCityReport({ textFallback = 'full' })— em'pointer', campos semsummaryimprimem o ponteiro. Integral permanece embuildResearchAnswersTable.scripts/build-city-report.mjs: loop de 2 passadas (build → render → medir[data-page="summary"]; se> PAGE_ONE_BUDGET_PX, rebuild comtextFallback:'pointer'; se ainda estourar →die). Tolerância/budget atuais intocados; mensagem dedieatualizada.scripts/lib/dossieBlocks.mjs:limit()→capList;deliveries/hooks/pending/era.numbers/era.actions/regionItemscarregam{items,total,remaining}(fim do descarte silencioso);buildDossierReport({ textFallback = 'full' })para os títulos/sumários de página 1.scripts/lib/dossieRender.mjs:renderDeliveries/renderHooksAndPending/renderEra/região imprimem o contador reusando.meta/.muted small("Mostrando X de Y — o restante está nas eras"). Região já temtotal(:308/:317).scripts/lib/dossieBulletin.mjs+dossieBulletinRender.mjs: boletim passa a exporremaining/total; renderer imprime "e mais N item(ns) no dossiê" na seção "E mais".scripts/lib/dossieCamara.mjs:(sumario ?? transcricao).slice(0,280)→excerptDeepDive(aprofundamento, "…" explícito); nota de limite de coleta para o teto de 20 da API (sem inventar total).scripts/build-dossie-solla-cidade.mjs: mesmo loop de 2 passadas para dossiê e boletim; mensagens dedieatualizadas.- Skills:
.agents/skills/relatorio-cidade/SKILL.md(§Contrato dos JSONs :131-158 + copy de página 1 :305-322) e.agents/skills/dossie-solla-cidade/SKILL.md(§Contrato dos JSONs :141-170 + item/notes :117-139) documentamsummarypor item ("curto, completo e auto-contido, redigido para caber no orçamento da superfície de resumo") e a regra "integral no aprofundamento; semsummary, o builder prefereanswer; nunca há corte no resumo". - Migration: sem migration (nenhum schema Payload tocado).
- Access / Consent: N/A — sem write path, sem PII, sem auth; nada de
Consent/overrideAccess. - UI: N/A — Impeccable B; encaixe no hi-fi aprovado; sem componente/estado/layout novo. Se algo exigir mudança material de layout, o
designerentra e volta ao humano no PR.
Dados → forma (se aplicável)
- Forma: contador textual reusando padrões existentes — "e mais N" (listas/bullets) e "Mostrando X de Y" (tabelas/painéis/notas) — e ponteiro textual para o aprofundamento no limite. É a menor mudança sobre o hi-fi aprovado e não introduz linguagem visual nova.
- Rejeitadas: ícone/
badgede "há mais" (redesenho e novo vocabulário visual); reticências como marcador de resto (é o defeito a eliminar); paginação/expansão (não cabe em PDF A4 de uma página, é redesign).
Fases verificáveis
- Dono único (tracer) —
reportText.mjs:capList,excerptDeepDive,joinWithRemainder,summaryText+SUMMARY_POINTER_COPY. Novotests/unit/reportText.unit.spec.ts:capListdevolveitems/total/remaining;excerptDeepDivemarca "…" uma única vez e respeitamax;summaryTextpreferesummarye cai paraanswer; nenhum primitivo de superfície de resumo emite "…". Quota ~2–3h. - Contrato
summary—cityReportResearch.mjs+dossieResearch.mjs; casos novos nos specs respectivos (summary trimado, vazio→null, sem lacuna, item segue publicado só com a fonte doanswer). Skills atualizadas (contrato + copy de página 1). Quota ~2–3h. - C163 — remove
excerpt, call-sites de página 1 viasummaryText,joinWithRemainder, slices do aprofundamento viaexcerptDeepDive; flagtextFallback; 2 passadas nobuild-city-report.mjs. Atualizatests/unit/cityReportBlocks.unit.spec.ts:702,752,780(o :752 deixa de esperarendsWith('…')) etests/unit/cityReportRender.unit.spec.ts:127,166; novo teste "página 1 sem '…' e integral presente". Quota ~4–5h. - C186 + boletim —
capListe contadores em deliveries/hooks/pending/era/region; boletimremaining/total;dossieCamaracom corte explícito de aprofundamento; 2 passadas nobuild-dossie-solla-cidade.mjs. Atualizatests/unit/dossieBlocks.unit.spec.ts:155etests/unit/dossieBulletin.unit.spec.ts:27,60; novos testes de contador (nenhum item some;highlights+moreItems+remaining === total). Quota ~4–5h. - Gates —
pnpm lint,pnpm typecheck,pnpm test:unit(oupnpm gate:fast);pnpm knip; checagem de ciclos do CI; push viapnpm push. Quota ~1–2h.
Rabbit holes / Não escopo (engenharia)
- Varredura de todo
slicedo repo — só a família C163/C186. - C157 (
build-solla-ceuci-salvador-report.mjs, self-contained, "…" próprios em :392/:446 e slices :133/:142/:601) — sucessor, não este item; gatilho de revisitação: se o dono novo provar drop-in de ~1h no C157, incluir; senão abrir Issue sucessora. - Builder redigir/encurtar texto — proibido; sem
summary, o texto é integral ou ponteiro; nunca reescrito. summaryglobal — rejeitado (D2).- Ampliação como licença para crescer — a guarda fail-closed é o teto; nada de espremer layout.
- Confundir "e mais N" (lista) com "…" (frase) — remediações distintas, ambas tratadas.
news[].summaryvalidado e nunca renderizado — não mexer aqui (não é superfície do problema; bullet-in separado se houver demanda).- Redesenho do hi-fi C163/C186 — fora; se material, aciona
designer. - Payload schema/migration/URL pública/Consent/LGPD — nada.
Riscos e mitigação
- Página 1 estoura com a pesquisa real → 2 passadas guard-gated (D5); guarda fail-closed preservada; teste com
answerlongo semsummarye comsummary. - Pesquisas existentes sem
summary→ fallbackanswerintegral;summaryé opt-in, sem regressão e sem lacuna nova. - Linha de contador empurra o layout aprovado do C186 → nota curta reusando
.meta/.muted small; se estourar, a guarda aborta e a copy do contador é encurtada antes de qualquer mudança visual; se exigir mudança material →designer. - Corte explícito com "…" no aprofundamento confundido com resumo →
excerptDeepDivesó é usado em seções de aprofundamento (sinais, acervo de falas, ações da Câmara); teste de renderer garante ausência no[data-page="summary"]. - Duplicação C163↔C186 → dono único em
reportText.mjs;knip/ciclos no CI. - Testes que hoje afirmam "…" → atualização deliberada e listada (Fase 3/4), com o novo aceite "sem '…' no resumo".
- Câmara sem total confiável da API → contar o que o artefato imprime (era/deliveries) e rotular o limite de coleta; nunca inventar total.
Decisões assumidas
Registradas do §Questões em aberto da intenção (fluxo --auto grava os literais assumidos):
- Campo
summaryvs só ampliar orçamento → A)summarypor item + ampliação como fallback (assumido). - "…" sempre proibido ou só no aprofundamento → A) proibir nas superfícies de resumo, manter no aprofundamento (assumido).
- Cobrir C157 agora ou sucessor → B) sucessor (assumido; gatilho de revisitação em Não escopo).
- Prioridade P1 vs P2 → B) P2 (assumido; reavaliar para P1 se o C187 entrar em produção imediatamente).
- Hipóteses de engenharia assumidas: dono em
reportText.mjs(D1), contador{items,total,remaining}(D3), 2 passadas guard-gated (D5),summaryherda a fonte doanswer(D2). - Sem Payload schema/migration/URL pública/Consent/LGPD;
push:falseintocado.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto: nenhum "…" em superfície de resumo; toda lista truncada com contador; integral no aprofundamento; guarda verde fail-closed; contrato
summarydocumentado nas duas skills. - Invariantes AGENTS/engineering-standards: "edite o dono, não gema um irmão" (dono único em
reportText.mjs, reusado por C163+C186); sem Payload/migration/URL/Consent; scripts puros. - Testes de domínio previstos (unit) onde os caps/contrato mudam:
cityReportBlocks,cityReportRender,dossieBlocks,dossieBulletin,cityReportResearch/dossieResearche o novoreportText. pnpm gate:fast(lint + typecheck + unit) epnpm knipverdes; push viapnpm push.- Nenhuma migration criada;
push:falseinalterado.
Desvios na execução
- Boletim sem 2ª passada de ponteiro. O plano previa o "mesmo loop de 2
passadas para dossiê e boletim". Na execução, o defeito do boletim era o
descarte silencioso (teto 20 sem contador), não corte de texto; o texto por
fato já é capado por desenho e a guarda é fail-closed. Então: contador
factsRemaining("e mais N fatos com fonte no dossiê") +summary ?? answercomo headline, sem 2ª passada. O dossiê e o C163 mantêm a 2ª passada. - Indício de emenda (
emendasIndicators[].purpose) nunca vira ponteiro. Não há cópia dopurposeno aprofundamento; então ele entra integral (sem "…") e a guarda fail-closed decide — o researcher encurta se estourar. Coerente com a decisão D2 ("purpose segue sem corte"). - Nota do limite de coleta da Câmara no método da Era C, não num campo novo
de dados (não inventa total): o texto do
eraMethod.Cdeclara que a coleta é limitada às páginas consultadas. region.items/era.numbers/era.actionsganham contador, sem paginação. O integral dessas listas não vive em outra página; o contador torna o descarte declarado (fim do silêncio), sem redesenho — conforme o appetite.
Self-score decision-quality
Score: 5/5 — (1) decisões caras (dono único, contrato summary, contador, fallback, mecânica de ampliação) têm rejeitadas explícitas em D1–D5; (2) cabe no appetite herdado (5 fases, ~14–18h); (3) rabbit holes nomeados e cortados (C157 sucessor, varredura de slices, builder-redator, news[].summary); (4) depth check: reusa o dono existente reportText.mjs e as três formas de contador já impressas, sem módulo/helper especulativo; (5) o outcome de produto permanece intacto — a engenharia não o reescreveu (nada de "…", integral preservado, guarda fail-closed).