teqo-backup/scripts/lib/dossieUnit.mjs
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

449 lines
22 KiB
JavaScript
Raw 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.

/**
* Unit seam for the Solla dossiê pipeline (C187, owner: the C186 dossiê).
*
* The dossiê was born municipality-shaped (C186). C187 adds an INSTITUTION
* recorte and C190 a THEME/AREA recorte; neither may fork the pipeline, so the
* shared mechanics (research contract, print shell, PDF emit, bulletin ledger)
* take a `unit` descriptor and default to `MUNICIPALITY_UNIT` — the C186
* behaviour stays byte-identical when no unit is passed. Institution and theme
* share the same `subject` shape (three scope lists, per-era flowing sheets,
* honor handling, bulletin copy) and differ only in vocabulary/copy, which the
* descriptor carries.
*
* Vocabulary note: a unit's `spheres` are the item ABRANGÊNCIA values
* (`municipio|regiao|polo` × `instituicao|setor|rede` × `area|segmento|rede`).
* The catalog `sphere` (federal/estadual/…) is a different object and never
* enters an item row.
*/
/**
* Theme/area copy uses the preposition the canonical label asks for
* ("pela Educação", "pelo Esporte", "pelos Direitos Humanos…"), with an explicit
* map for the 18 `SPEECH_TOPICS` labels and a neutral fallback for any label
* outside the taxonomy — never a guessed gender.
*/
const THEME_SUBJECT_PHRASES = {
Saúde: 'pela Saúde',
Educação: 'pela Educação',
Cultura: 'pela Cultura',
Esporte: 'pelo Esporte',
'Segurança Pública': 'pela Segurança Pública',
'Meio Ambiente': 'pelo Meio Ambiente',
'Economia e Trabalho': 'pela Economia e Trabalho',
'Direitos Humanos e Assistência Social': 'pelos Direitos Humanos e Assistência Social',
'Infraestrutura e Transporte': 'pela Infraestrutura e Transporte',
'Ciência e Tecnologia': 'pela Ciência e Tecnologia',
'Política e Instituições': 'pela Política e Instituições',
'Agricultura e Agropecuária': 'pela Agricultura e Agropecuária',
'Habitação e Cidades': 'pela Habitação e Cidades',
'Comunicação e Mídia': 'pela Comunicação e Mídia',
'Igualdade Racial': 'pela Igualdade Racial',
'Mulheres e Gênero': 'pelas Mulheres e Gênero',
Juventude: 'pela Juventude',
'Pessoa com Deficiência': 'pela Pessoa com Deficiência',
}
/** @param {string} label @returns {string} */
export const themeSubjectPhrase = (label) => THEME_SUBJECT_PHRASES[label] ?? `pela área de ${label}`
export const MUNICIPALITY_UNIT = {
id: 'municipality',
/** Key that carries the resolved unit slug in research/snapshot files. */
slugField: 'municipalitySlug',
spheres: ['municipio', 'regiao', 'polo'],
defaultSphere: 'municipio',
sphereLabels: { municipio: 'município', regiao: 'região', polo: 'polo' },
sphereBadgeClass: { municipio: '', regiao: 'scope-region', polo: 'scope-region' },
/** Bulletin ranking: a direct item outranks a shared-scope one. */
breadthRank: { municipio: 0, regiao: 1, polo: 2 },
researchDir: 'data/dossie-solla-cidade',
series: 'Série municipal · BA',
title: 'Dossiê Solla por cidade',
bulletinTitle: 'Boletim informativo modelo',
/** "E mais" budget of the one-page boletim (long labels wrap, so units differ). */
bulletinMoreLimit: 12,
/** Print copy that changes with the recorte (shared renderers read these). */
sphereColumnLabel: 'Esfera',
notSummedInline: 'não somar à cidade',
sumGuardNote: 'região e polo não são somados ao município.',
acervoNote: 'recorte municipal (menção confirmada); região e polo não entram nesta conta.',
eraNumbersTitle: 'Recursos e entregas quantificáveis',
eraActionsTitle: 'O que fez — item, alcance e lastro',
honorsTitle: 'Títulos, honrarias e vínculos locais',
/** C186 literal kept verbatim (byte-parity); subject units override it. */
synthesisSumGuard: 'Setor e rede não são somados à instituição.',
}
export const INSTITUTION_UNIT = {
id: 'institution',
shape: 'subject',
/** Key that holds the recorte identity in research/snapshot files. */
slugField: 'institutionSlug',
snapshotField: 'institution',
spheres: ['instituicao', 'setor', 'rede'],
defaultSphere: 'instituicao',
sphereLabels: { instituicao: 'instituição', setor: 'setor', rede: 'rede' },
sphereBadgeClass: { instituicao: '', setor: 'scope-sector', rede: 'scope-network' },
breadthRank: { instituicao: 0, setor: 1, rede: 2 },
researchDir: 'data/dossie-solla-instituicao',
series: 'Série institucional · agenda com lastro',
title: 'Dossiê Solla por instituição',
bulletinTitle: 'Boletim informativo modelo',
bulletinMoreLimit: 10,
sphereColumnLabel: 'Abrangência',
notSummedInline: 'não somar à instituição',
sumGuardNote: 'setor e rede não são somados à instituição.',
acervoNote: 'recorte por tema, não nominal; não somar à instituição.',
eraNumbersTitle: 'Objeto, valor, fase e alcance',
eraActionsTitle: 'Papéis com evidência visível',
honorsTitle: 'Títulos, honrarias e vínculos institucionais',
synthesisSumGuard: 'Setor e rede não são somados à instituição.',
aria: {
synthesis: 'Síntese da atuação institucional',
charts: 'Gráficos consolidados da atuação institucional',
sources: 'Fontes e limites do dossiê institucional',
scope: 'Painel de abrangência institucional',
},
/** Subject-shape fields (shared with THEME_UNIT). */
nounPlural: 'instituições',
subjectNoun: 'instituição',
docLabel: 'Dossiê institucional',
contributionLabel: 'instituição',
scopeLabel: 'no recorte institucional',
summaryLinkEyebrow: '01 · vínculo com a instituição',
honorsSheet: true,
notSummedTo: 'à instituição',
scopeLists: [
{ key: 'institution', sphere: 'instituicao' },
{ key: 'sector', sphere: 'setor' },
{ key: 'network', sphere: 'rede' },
],
scopeListWords: { institution: 'instituicao', sector: 'setor', network: 'rede' },
scopeCardClass: { instituicao: '', setor: 'card-sector', rede: 'card-network' },
eraMethod: {
A: 'Registros anteriores a 2007 que citem nominalmente a instituição ou comprovem vínculo. Cargo geral não prova ação institucional.',
B: 'A gestão estadual é lida por equipamento, programa, convênio e obra; cada linha informa abrangência e fonte. Setor e rede não somam à instituição.',
C: 'Mandato, relatorias, parcerias e execução financeira têm datas e estágios distintos. Empenho não é pagamento; o valor sempre acompanha a fase.',
},
eraRecovery: {
A: 'biografias oficiais · atos e diários · acervos institucionais · busca web datada',
B: 'DOE-BA (DOOL) · notícias SESAB/instituição · Transparência Bahia · busca web datada',
C: 'API Câmara · Portal da Transparência · acervo interno read-only · busca web datada',
},
identityBadges: (identity) =>
[identity.kindLabel, identity.sphereLabel, identity.scope ? identity.scopeLabel : null].filter(
Boolean,
),
identityRows: (identity) =>
[
['Nome', identity.name],
['Tipo', identity.kindLabel],
['Esfera', identity.sphereLabel],
['Alcance', identity.scopeLabel],
['Alias', (identity.aliases ?? []).join(' · ') || '—'],
].filter(([, value]) => Boolean(value)),
coverRows: (report) => [{ label: 'Identificação', badges: report.meta.identityBadges }],
copy: {
coverSubtitle: () => 'O que Jorge Solla fez por, na e com a instituição ao longo da carreira',
coverHowToUse:
'Localize o vínculo ou a entrega, confira fonte, data, fase e abrangência. Transforme toda lacuna em tarefa de apuração. Este arquivo não é fala pronta nem peça pública.',
coverScope:
'Escopo: carreira técnica e gestão pública até 2006 · SESAB 2007–2014 · Câmara dos Deputados 2015–2027. Instituição, setor e rede separados; sem fonte, não entra como entrega.',
coverScopeWords: 'instituição, setor e rede separados',
coverAsset: ['selo da instituição', 'uso autorizado e origem'],
openingTitle: (subjectName) => `O que Jorge Solla fez pela e na ${subjectName}`,
reachRuleTitle: 'Setor/rede não é a instituição. Não some os recortes.',
reachRuleBody:
'Uma política para uma categoria ou uma articulação com entidades correlatas pode alcançar a instituição sem constituir entrega exclusiva para ela.',
synthesisGuard: 'setor e rede nunca somados à instituição.',
coverageNote: 'Instituição, setor e rede separados; sem fonte, não entra como entrega.',
timelineEmpty: 'Nenhum vínculo datado com fonte — ver lacunas explícitas.',
eraEmpty: {
body: 'A pesquisa não encontrou documento que sustenta uma afirmação institucional. Isso não prova que não houve vínculo ou atuação; prova apenas que o dossiê ainda não pode afirmá-los.',
work: [
'Busca nominal por instituição e aliases.',
'Consulta a fontes oficiais e acervos datados.',
'Triagem de resultados por vínculo e abrangência.',
'Descarte de menções sem lastro suficiente.',
],
todo: [
'Solicitar consulta ao arquivo físico.',
'Validar nome histórico e unidade da instituição.',
'Recuperar ato, ata ou documento contemporâneo.',
'Repetir busca com alias confirmado.',
],
rule: (eraLabel) =>
`Não preencher a seção por memória, cargo provável ou texto de outra era. Registrar “${eraLabel}: evidência institucional não localizada nas fontes consultadas”.`,
},
limits: {
coverage: [
'O acervo interno de falas cobre 2011+; períodos anteriores dependem de fontes externas.',
'Resultado de busca não prova ausência histórica.',
'Emenda de bancada ou relator só recebe autoria quando a fonte a confirma.',
'Menção a entidade correlata (setor/rede) não é atribuição à instituição.',
],
editorial: [
'Sem fonte, não publica.',
'URL e data acompanham cada afirmação não trivial.',
'Instituição, setor e rede permanecem separados.',
'Valor sempre informa a fase de execução.',
],
},
mdAcervoNote: 'Recorte por tema, não nominal.',
bulletin: {
aria: 'Modelo de boletim informativo institucional de uma página',
openingLabel: 'Uma trajetória de trabalho junto à instituição',
openingTitle: (subjectName) => `O que Jorge Solla fez pela e na ${subjectName}`,
lead: 'Ações, recursos e articulações explicados de forma direta — somente o que já foi documentado no dossiê institucional.',
leadSparse:
'Esta versão tem poucos registros confirmados. Em vez de preencher espaço, mostra só os fatos que o dossiê sustenta.',
asset: 'Solla na instituição ou na ação citada',
assetSparse: 'somente registro relacionado ao fato',
assetSelo: ['marca autorizada da instituição', 'arquivo vetorial'],
assetClipping: ['registro real da ação', 'origem e licença'],
moreEmpty:
'Conferir as lacunas explícitas no dossiê institucional antes de ampliar o boletim.',
defeso:
'fase acompanha cada valor · instituição, setor e rede não são somados · sem percentual estadual absoluto.',
orig: 'Conteúdo selecionado do dossiê institucional.',
sparseRule:
'Não repetir item, não ampliar efeito e não preencher com ação de setor ou rede como se fosse entrega exclusiva da instituição.',
title: 'Uma trajetória de compromisso com o serviço público',
titleSparse: 'contexto de carreira · não substitui fato institucional',
highlightsTitle: 'Resultados em leitura rápida',
highlightsTitleSparse: 'Poucos fatos, sem enchimento',
moreTitle: 'Outras ações confirmadas',
moreTitleSparse: 'Outros registros confirmados',
},
},
}
export const THEME_UNIT = {
id: 'theme',
shape: 'subject',
slugField: 'themeSlug',
snapshotField: 'theme',
spheres: ['area', 'segmento', 'rede'],
defaultSphere: 'area',
sphereLabels: { area: 'área', segmento: 'segmento', rede: 'rede' },
sphereBadgeClass: { area: '', segmento: 'scope-sector', rede: 'scope-network' },
breadthRank: { area: 0, segmento: 1, rede: 2 },
researchDir: 'data/dossie-solla-tema',
series: 'Série temática · por área',
title: 'Dossiê Solla por tema/área',
bulletinTitle: 'Boletim informativo modelo',
bulletinMoreLimit: 14,
bulletinKicker: 'Boletim informativo · série temática',
sphereColumnLabel: 'Abrangência',
notSummedInline: 'não somar à área',
sumGuardNote: 'segmento e rede não são somados à área.',
acervoNote: 'recorte pela tag do acervo (Speech.topics); segmento e rede não entram nesta conta.',
eraNumbersTitle: 'Objeto, valor, ano, fase e abrangência',
eraActionsTitle: 'Papéis e iniciativas com evidência visível',
honorsTitle: 'Títulos, honrarias e vínculos',
synthesisSumGuard: 'Segmento e rede não são somados à área.',
aria: {
synthesis: 'Síntese da atuação por área',
charts: 'Gráficos consolidados da atuação por área',
sources: 'Fontes e limites do dossiê temático',
scope: 'Painel de abrangência por área',
},
nounPlural: 'temas',
subjectNoun: 'área',
docLabel: 'Dossiê temático',
contributionLabel: 'área',
scopeLabel: 'no recorte da área',
summaryLinkEyebrow: '01 · vínculo com a área',
honorsSheet: false,
notSummedTo: 'à área',
scopeLists: [
{ key: 'area', sphere: 'area' },
{ key: 'segment', sphere: 'segmento' },
{ key: 'network', sphere: 'rede' },
],
scopeListWords: { area: 'area', segment: 'segmento', network: 'rede' },
scopeCardClass: { area: '', segmento: 'card-sector', rede: 'card-network' },
/** Theme-only scene overrides (the institution keeps the shared defaults). */
acervoScene: {
stats: [
{ key: 'universe', label: 'Universo recuperado' },
{ key: 'sample', label: 'Amostra exibida' },
],
columns: ['Data', 'Tema canônico', 'Trecho/contexto', 'Abrangência', 'Link'],
alert:
'O tema prova pertinência ao recorte do acervo; não prova por si só entrega, autoria material ou execução financeira.',
},
reachScene: {
columns: ['Item', 'Era / ano', 'Valor / fase', 'Evidência temática', 'Fonte'],
tableClass: 'document-table--reach',
},
gapsScene: {
columns: ['Era', 'Lacuna', 'Já consultado', 'Próxima busca', 'Status'],
alert:
'Portais que não filtram por área não autorizam atribuição. Sem fonte temática, a linha permanece lacuna — nunca zero.',
},
sourcesHierarchy: [
{
layer: 'Fontes primárias',
use: 'Atos, diários oficiais e documentos contemporâneos',
limit: 'Sem documento, não entra',
},
{
layer: 'Fontes institucionais',
use: 'Portais oficiais e de transparência',
limit: 'Não filtram por área',
},
{
layer: 'Acervo interno',
use: 'Falas classificadas por tema (2011+)',
limit: 'Prova pertinência, não entrega',
},
{
layer: 'Busca web datada',
use: 'Notícias e publicações com data',
limit: 'Resultado não prova ausência',
},
],
/** Síntese/rótulos temáticos (institution/município keep the shared literals). */
byAreaTitle: 'Tipos de atuação com mais registros',
byYearTitle: 'Pontos com fonte por ano',
fullYearAxis: true,
eraCoverageFromLedger: true,
moneyPhaseStyle: 'separate',
/**
* Theme bulletins read the year when a fact has no money value, and mark the
* card textual when neither exists (never a fake zero).
*/
bulletinNumberFallback: true,
eraMethod: {
A: 'Registros anteriores a 2007 que tratem diretamente da área ou comprovem atuação relacionada. Cargo geral não prova ação na política.',
B: 'A gestão estadual é lida por programa, projeto, convênio e obra ligados à área; cada linha informa abrangência e fonte. Segmento e rede não somam à área.',
C: 'Mandato, relatorias e execução financeira têm datas e estágios distintos. Empenho não é pagamento; o valor sempre acompanha a fase.',
},
eraRecovery: {
A: 'biografias oficiais · atos e diários · acervos de políticas públicas · busca web datada',
B: 'DOE-BA (DOOL) · notícias SESAB · Transparência Bahia · SIOPS/DATASUS · busca web datada',
C: 'API Câmara · Portal da Transparência · acervo interno read-only · busca web datada',
},
identityBadges: (identity) => [identity.label].filter(Boolean),
/** Theme bulletin pills: recorte label, canonical token (mono) and taxonomy origin. */
bulletinPills: (identity) => [
{ label: 'Área' },
{ label: identity.value, code: true },
{ label: identity.taxonomyNote ?? 'Taxonomia do acervo' },
],
identityRows: (identity) =>
[
['Área', identity.label],
['Token', identity.value],
['Origem', identity.taxonomyNote ?? 'Taxonomia do acervo'],
['Filtro', 'Speech.topics'],
].filter(([, value]) => Boolean(value)),
coverRows: (report) => [
{ label: 'Área', badges: report.meta.identityBadges },
{
label: 'Valor canônico',
code: report.meta.identity?.value,
note: 'taxonomia do acervo / Speech.topics',
},
],
copy: {
coverSubtitle: (subjectName) =>
`O que Jorge Solla fez ${themeSubjectPhrase(subjectName)} ao longo da carreira`,
coverHowToUse:
'Localize a entrega, confira fonte, data, fase e abrangência. A área vem da taxonomia canônica; segmento e rede não são somados a ela.',
coverScope:
'Escopo: carreira técnica e gestão pública até 2006 · SESAB 2007–2014 · Câmara 2015–2027. Área, segmento e rede separados; sem fonte, não entra como entrega.',
coverScopeWords: 'área, segmento e rede separados',
coverAsset: ['selo autorizado', 'e origem'],
openingTitle: (subjectName) => `O que Jorge Solla fez ${themeSubjectPhrase(subjectName)}`,
reachRuleTitle: 'Segmento/rede não é a área. Não some os recortes.',
reachRuleBody:
'Uma ação dirigida a um segmento ou a uma rede pode alcançar a política de área sem constituir entrega exclusiva dela.',
synthesisGuard: 'segmento e rede nunca somados à área.',
coverageNote: 'Área, segmento e rede separados; sem fonte, não entra como entrega.',
timelineEmpty: 'Nenhum vínculo datado com fonte — ver lacunas explícitas.',
eraEmpty: {
body: 'A pesquisa não encontrou documento que sustente uma afirmação sobre a área. Isso não prova ausência de atuação; prova apenas que o dossiê ainda não pode afirmá-la.',
work: [
'Busca pelo tema canônico e termos correlatos.',
'Consulta a fontes oficiais e acervos datados.',
'Triagem de resultados por atribuição à área.',
'Descarte de menções sem lastro suficiente.',
],
todo: [
'Solicitar consulta a acervo especializado.',
'Validar a taxonomia e o recorte do tema.',
'Recuperar ato, publicação ou documento contemporâneo.',
'Repetir busca com termos correlatos.',
],
rule: (eraLabel) =>
`Não preencher a seção por memória, cargo provável ou tema vizinho. Registrar “${eraLabel}: evidência da área não localizada nas fontes consultadas”.`,
},
limits: {
coverage: [
'O acervo interno de falas cobre 2011+; períodos anteriores dependem de fontes externas.',
'Resultado de busca não prova ausência histórica.',
'Emenda de bancada ou relator só recebe autoria quando a fonte a confirma.',
'Menção a segmento ou rede não é atribuição exclusiva da área.',
],
editorial: [
'Sem fonte, não publica.',
'URL e data acompanham cada afirmação não trivial.',
'Área, segmento e rede permanecem separados.',
'Valor sempre informa a fase de execução.',
'Atribuição temática a emenda ou proposição só entra com fonte; sem fonte, vira lacuna.',
],
},
mdAcervoNote: 'Recorte pela tag do acervo, não nominal.',
bulletin: {
aria: 'Modelo de boletim informativo temático de uma página',
openingLabel: 'Uma trajetória de trabalho pela área',
openingTitle: (subjectName) => `O que Jorge Solla fez ${themeSubjectPhrase(subjectName)}`,
lead: 'Ações, recursos e articulações explicados de forma direta — somente o que já foi documentado no dossiê da área.',
leadSparse:
'Esta versão tem poucos registros confirmados. Em vez de preencher espaço, mostra só os fatos que o dossiê sustenta.',
asset: 'Solla na ação citada',
assetSparse: 'somente registro relacionado ao fato',
assetSelo: ['selo autorizado', 'arquivo vetorial'],
assetClipping: ['registro real da ação', 'origem e licença'],
moreEmpty: 'Conferir as lacunas explícitas no dossiê da área antes de ampliar o boletim.',
defeso:
'fase acompanha cada valor · área, segmento e rede não são somados · sem percentual estadual absoluto.',
orig: 'Conteúdo selecionado do dossiê da área.',
sparseRule:
'Não repetir item, não ampliar efeito e não preencher com ação de segmento ou rede como se fosse entrega exclusiva da área.',
title: 'Uma trajetória de compromisso com o serviço público',
titleSparse: 'contexto de carreira · não substitui fato da área',
highlightsTitle: 'Resultados em leitura rápida',
highlightsTitleSparse: 'Poucos fatos, sem enchimento',
moreTitle: 'Outras ações confirmadas',
moreTitleSparse: 'Outros registros confirmados',
},
},
}
const DOSSIER_UNITS = {
municipality: MUNICIPALITY_UNIT,
institution: INSTITUTION_UNIT,
theme: THEME_UNIT,
}
/**
* Accepts an id string or a descriptor; absent → municipality (C186 default).
*
* @param {any} [unit]
* @returns {any}
*/
export const resolveDossierUnit = (unit) => {
if (!unit) return MUNICIPALITY_UNIT
if (typeof unit === 'string') {
const resolved = DOSSIER_UNITS[unit]
if (!resolved) throw new Error(`Unidade de dossiê desconhecida: "${unit}".`)
return resolved
}
return unit
}
/** Institution and theme share the `subject` shape (one builder/renderer). */
export const isSubjectUnit = (unit) => resolveDossierUnit(unit).shape === 'subject'