mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
O pnpm archive:publish passa cada draft elegível pelo inspectPrivateMediaObject do C246 (download pelo caminho da rota pública + decode sharp) antes de qualquer payload.update: objeto missing/corrupt fica fora do lote, é nomeado no recibo como skippedBroken (stage + motivo) e o comando sai 1 — no plan (read-only) e no apply. A fila seleciona filename além de id; o lote vira o seam exportado runArchivePublishBatch (temp por foto removido no finally) com guard isDirectRun, e o puro scripts/lib/archivePublishPlan.mjs ganha skippedBroken/skippedBrokenCount no summary e as linhas do recibo. scripts/publish-archive-photos.mjs entra no SCRIPTS_SPEC_PINNED (high-risk exato: unit/int full + e2e curado). Guardas do publish intactas; sem S3 num alvo não-local o probe classifica tudo missing (fail-closed). Sem migration, sem UI. Runbook C242/C246 e changelog atualizados; unit do summarize/format, int do lote real (íntegro aprovado, corrompido decode, removido missing, plan sem escrita) e impl plan em docs/plans/acervo-publish-preflight-integridade-impl.md.
117 lines
4 KiB
JavaScript
117 lines
4 KiB
JavaScript
/**
|
|
* C242 — pure planning/reporting of `pnpm archive:publish`: the argv parser,
|
|
* the run summary and the human lines + JSON receipt. No I/O of its own, so the
|
|
* unit spec drives it with fakes. C249 — the summary/receipt also carry the
|
|
* integrity preflight verdicts (`skippedBroken`).
|
|
*/
|
|
|
|
const USAGE = 'uso: `pnpm archive:publish [--apply] [--limit <n>] [--out <dir>]`'
|
|
|
|
export const ARCHIVE_PUBLISH_DEFAULT_OUT_DIR = 'data/archive'
|
|
|
|
export const archivePublishReportStamp = (runAt) => String(runAt).replace(/[:.]/g, '-')
|
|
|
|
/**
|
|
* @param {string[]} [argv]
|
|
* @returns {{ apply: boolean, limit: number | null, out: string, help: boolean }}
|
|
*/
|
|
export const parseArchivePublishCliArgs = (argv = process.argv.slice(2)) => {
|
|
const options = { apply: false, limit: null, out: ARCHIVE_PUBLISH_DEFAULT_OUT_DIR, help: false }
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index]
|
|
const value = () => {
|
|
const next = argv[++index]
|
|
if (next === undefined || next.startsWith('--')) {
|
|
throw new Error(`faltou valor para ${arg} — ${USAGE}.`)
|
|
}
|
|
return next
|
|
}
|
|
|
|
if (arg === '--help' || arg === '-h') options.help = true
|
|
else if (arg === '--apply') options.apply = true
|
|
else if (arg === '--limit') options.limit = Number(value())
|
|
else if (arg === '--out') options.out = value()
|
|
else throw new Error(`argumento desconhecido: ${arg} — ${USAGE}.`)
|
|
}
|
|
|
|
if (options.help) return options
|
|
if (options.limit !== null && (!Number.isSafeInteger(options.limit) || options.limit < 1)) {
|
|
throw new Error('--limit deve ser >= 1.')
|
|
}
|
|
if (options.out.split(/[\\/]/).includes('..')) {
|
|
throw new Error('--out não pode escapar do diretório do repo (sem "..").')
|
|
}
|
|
return options
|
|
}
|
|
|
|
/**
|
|
* C249 — the preflight verdict never counts as an update failure: a draft whose
|
|
* object is missing/corrupt is `skippedBroken` (stage + reason), kept in its own
|
|
* list so the receipt names it without pretending an approval was attempted.
|
|
* `eligible` is the plan-mode census (inspected clean, no write) and is
|
|
* deliberately not a failure — plan and apply receive the same results, so the
|
|
* summary composes for both modes.
|
|
*
|
|
* @param {Array<{ photoId: number, status: string, stage?: string | null, reason?: string | null, error?: string | null }>} results
|
|
*/
|
|
export const summarizeArchivePublishResults = (results) => {
|
|
const failures = []
|
|
const skippedBroken = []
|
|
let approved = 0
|
|
let failed = 0
|
|
|
|
for (const result of results) {
|
|
if (result.status === 'approved') {
|
|
approved += 1
|
|
} else if (result.status === 'skippedBroken') {
|
|
skippedBroken.push({
|
|
photoId: result.photoId,
|
|
stage: result.stage ?? null,
|
|
reason: result.reason ?? null,
|
|
})
|
|
} else if (result.status === 'eligible') {
|
|
continue
|
|
} else {
|
|
failed += 1
|
|
failures.push({ photoId: result.photoId, error: result.error ?? null })
|
|
}
|
|
}
|
|
|
|
return {
|
|
approved,
|
|
failed,
|
|
failures,
|
|
skippedBroken,
|
|
skippedBrokenCount: skippedBroken.length,
|
|
}
|
|
}
|
|
|
|
export const formatArchivePublishReport = (report) => {
|
|
const lines = [
|
|
`[archive:publish] modo: ${report.mode} | alvo: ${report.target}`,
|
|
`[archive:publish] fila: ${report.queue.items} de ${report.queue.totalDrafts} draft(s)`,
|
|
]
|
|
|
|
if (report.mode === 'plan') {
|
|
lines.push(
|
|
`[archive:publish] drafts quebrados (fora do lote): ${report.summary?.skippedBrokenCount ?? 0}`,
|
|
)
|
|
} else {
|
|
lines.push(
|
|
`[archive:publish] aprovadas: ${report.summary?.approved ?? 0} | falharam: ${report.summary?.failed ?? 0} | puladas (quebradas): ${report.summary?.skippedBrokenCount ?? 0}`,
|
|
)
|
|
}
|
|
|
|
for (const skipped of report.summary?.skippedBroken ?? []) {
|
|
lines.push(
|
|
`[archive:publish] ! foto ${skipped.photoId} fora do lote (${skipped.stage ?? 'missing'}): ${skipped.reason ?? 'objeto ausente'}`,
|
|
)
|
|
}
|
|
|
|
for (const failure of report.summary?.failures ?? []) {
|
|
lines.push(`[archive:publish] ✗ foto ${failure.photoId}: ${failure.error}`)
|
|
}
|
|
|
|
return lines
|
|
}
|