mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:35:21 +00:00
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
106 lines
2.8 KiB
JavaScript
106 lines
2.8 KiB
JavaScript
/**
|
|
* Snapshot composer for the theme/area dossiê (C190).
|
|
*
|
|
* Runs on the homeserver against production, read-only: the recorte IS the
|
|
* acervo tag (`Speech.topics`), so there is no institution-style bridge and no
|
|
* nominal match — the theme itself is the filter. 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/themeSnapshot.int.spec.ts`.
|
|
*/
|
|
|
|
import { excerptOffsetSeconds } from '../src/lib/speechVod.ts'
|
|
import { buildSpeechListWhere } from '../src/utilities/speech/speechListFilters.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. There is no
|
|
* `mentionExcerpt` here — the acervo tag is the recorte, not a literal mention.
|
|
*/
|
|
const projectSpeechRow = (doc) => ({
|
|
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),
|
|
topics: Array.isArray(doc.topics) ? doc.topics : [],
|
|
})
|
|
|
|
/**
|
|
* @param {{
|
|
* payload: any,
|
|
* actor: any,
|
|
* identity: { slug: string, value: string, label: string, taxonomyNote?: string | null },
|
|
* readAt: string,
|
|
* codeSha?: string | null,
|
|
* database?: string | null,
|
|
* }} params
|
|
* @returns {Promise<any>}
|
|
*/
|
|
export const composeThemeSnapshot = async ({
|
|
payload,
|
|
actor,
|
|
identity,
|
|
readAt,
|
|
codeSha = null,
|
|
database = null,
|
|
}) => {
|
|
const topics = [identity.value]
|
|
const gaps = []
|
|
|
|
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,
|
|
})
|
|
const docs = result.docs
|
|
const totalCount = result.totalDocs
|
|
if (docs.length === 0) {
|
|
gaps.push({
|
|
id: 'acervo_sem_falas',
|
|
reason: `Nenhuma fala do acervo no tema ${identity.value} — o acervo cobre 2011+.`,
|
|
})
|
|
}
|
|
|
|
return {
|
|
meta: { readAt, codeSha, database, kind: 'theme' },
|
|
theme: {
|
|
slug: identity.slug,
|
|
value: identity.value,
|
|
label: identity.label,
|
|
taxonomyNote: identity.taxonomyNote ?? 'Taxonomia do acervo',
|
|
topics,
|
|
},
|
|
speeches: {
|
|
topics,
|
|
totalCount,
|
|
rows: docs.map((doc) => projectSpeechRow(doc)),
|
|
},
|
|
gaps,
|
|
}
|
|
}
|