mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Generaliza aditivamente o pipeline C186 (default município; specs verdes) via o descritor scripts/lib/dossieUnit.mjs (municipality|institution), extrai o emit A4/fit-guard para scripts/lib/buildPdf.mjs e a receita read-only para scripts/lib/readOnlyExtract.mjs. - Catálogo fail-closed src/lib/institutionCatalog.ts + institutionNameAliases.ts (seed UFBA/Correios/Enfermagem; escape one-off --slug/--name; dedup). - Skill/command/subagente /dossie-solla-instituicao; eras A/B/C reusadas. - Abrangência instituição × setor × rede nunca somada; era sem fonte vira página de lacuna explícita; lista capada declara "e mais N" (regra C188 no dono). - Boletim modelo 1×A4 com fase junto ao valor, sem fontes, sem CTA; herda apenas fatos com fonte do dossiê (incl. falas do acervo read-only). - Sem schema/migration/DB write; artefatos gitignored.
145 lines
4.4 KiB
JavaScript
145 lines
4.4 KiB
JavaScript
/**
|
|
* Snapshot composer for the institutional dossiê (C187).
|
|
*
|
|
* Runs on the homeserver against production, read-only: the institutional
|
|
* recorte does not exist as a relationship on `speech`, so the bridge is
|
|
* theme→institution (`identity.topics`, from the catalog). Reuses the C163
|
|
* speech loaders (no parallel source of truth), projects out PII and returns
|
|
* plain JSON. The render step never touches the database again.
|
|
*
|
|
* Integration layer, not a pure lib: it imports Payload-coupled utilities and
|
|
* is exercised by `tests/int/institutionSnapshot.int.spec.ts`.
|
|
*/
|
|
|
|
import { normalizeForSearch } from '../src/lib/speechSearch.ts'
|
|
import { excerptOffsetSeconds } from '../src/lib/speechVod.ts'
|
|
import { buildSpeechListWhere } from '../src/utilities/speech/speechListFilters.ts'
|
|
import { loadSegmentsForSpeeches } from '../src/utilities/speech/speechPageData.ts'
|
|
|
|
const SPEECH_LIMIT = 15
|
|
|
|
const SPEECH_SELECT = {
|
|
speechAt: true,
|
|
type: true,
|
|
phase: true,
|
|
summary: true,
|
|
officialTextUrl: true,
|
|
youtubeUrl: true,
|
|
vodPlaybackUrl: true,
|
|
excerptTMs: true,
|
|
eventStartAt: true,
|
|
topics: true,
|
|
}
|
|
|
|
/**
|
|
* Projects one speech: the official summary says what it is; `mentionExcerpt`
|
|
* is the passage that literally names the institution (null when the acervo tag
|
|
* has no literal mention in the ASR segments).
|
|
*/
|
|
const projectSpeechRow = (doc, segments, matchNames) => {
|
|
const candidates = matchNames
|
|
.map((name) => ({ name, key: normalizeForSearch(name) }))
|
|
.filter((candidate) => candidate.key)
|
|
const matched = candidates.find(({ key }) =>
|
|
segments.some((segment) => normalizeForSearch(segment.text).includes(key)),
|
|
)
|
|
const mentionSegment = matched
|
|
? segments.find((segment) => normalizeForSearch(segment.text).includes(matched.key))
|
|
: undefined
|
|
return {
|
|
id: doc.id,
|
|
speechAt: doc.speechAt ?? null,
|
|
type: doc.type ?? null,
|
|
phase: doc.phase ?? null,
|
|
summary: doc.summary ?? null,
|
|
officialTextUrl: doc.officialTextUrl ?? null,
|
|
youtubeUrl: doc.youtubeUrl ?? null,
|
|
vodPlaybackUrl: doc.vodPlaybackUrl ?? null,
|
|
youtubeExcerptStartSeconds: excerptOffsetSeconds(doc.excerptTMs, doc.eventStartAt),
|
|
mentionExcerpt: mentionSegment?.text ?? null,
|
|
matchedName: matched?.name ?? null,
|
|
topics: Array.isArray(doc.topics) ? doc.topics : [],
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* payload: any,
|
|
* actor: any,
|
|
* identity: { slug: string, name: string, kind: string, kindLabel: string, sphere: string, sphereLabel: string, scope: string, scopeLabel: string, aliases: string[], topics: string[] },
|
|
* readAt: string,
|
|
* codeSha?: string | null,
|
|
* database?: string | null,
|
|
* }} params
|
|
* @returns {Promise<any>}
|
|
*/
|
|
export const composeInstitutionSnapshot = async ({
|
|
payload,
|
|
actor,
|
|
identity,
|
|
readAt,
|
|
codeSha = null,
|
|
database = null,
|
|
}) => {
|
|
const topics = Array.isArray(identity.topics) ? identity.topics : []
|
|
const matchNames = [identity.name, ...(identity.aliases ?? [])]
|
|
|
|
let docs = []
|
|
let totalCount = 0
|
|
const gaps = []
|
|
if (topics.length === 0) {
|
|
gaps.push({
|
|
id: 'acervo_sem_tema',
|
|
reason: 'A instituição não declara temas no catálogo — o recorte do acervo fica vazio.',
|
|
})
|
|
} else {
|
|
const result = await payload.find({
|
|
collection: 'speech',
|
|
where: buildSpeechListWhere({ page: 1, topics }),
|
|
depth: 0,
|
|
limit: SPEECH_LIMIT,
|
|
sort: '-speechAt',
|
|
select: SPEECH_SELECT,
|
|
user: actor,
|
|
overrideAccess: false,
|
|
})
|
|
docs = result.docs
|
|
totalCount = result.totalDocs
|
|
if (docs.length === 0) {
|
|
gaps.push({
|
|
id: 'acervo_sem_falas',
|
|
reason: `Nenhuma fala do acervo nos temas ${topics.join(', ')} — o acervo cobre 2011+.`,
|
|
})
|
|
}
|
|
}
|
|
|
|
const segmentsBySpeech = await loadSegmentsForSpeeches(
|
|
payload,
|
|
actor,
|
|
docs.map((doc) => doc.id),
|
|
)
|
|
|
|
return {
|
|
meta: { readAt, codeSha, database, kind: 'institution' },
|
|
institution: {
|
|
slug: identity.slug,
|
|
name: identity.name,
|
|
kind: identity.kind,
|
|
kindLabel: identity.kindLabel,
|
|
sphere: identity.sphere,
|
|
sphereLabel: identity.sphereLabel,
|
|
scope: identity.scope,
|
|
scopeLabel: identity.scopeLabel,
|
|
aliases: identity.aliases ?? [],
|
|
topics,
|
|
},
|
|
speeches: {
|
|
topics,
|
|
totalCount,
|
|
rows: docs.map((doc) =>
|
|
projectSpeechRow(doc, segmentsBySpeech.get(doc.id) ?? [], matchNames),
|
|
),
|
|
},
|
|
gaps,
|
|
}
|
|
}
|