Adiciona a CLI pnpm archive:integrity: varredura read-only de todas as archivePhoto (download + decode pelo mesmo caminho da rota pública e do índice facial; sai 1 enquanto houver mídia quebrada approved) e --apply que re-ingere do Flickr pelo flickrId, substitui o objeto na mesma chave via payload.update filePath (sem tocar curadoria/status) e rebaixa a draft as irrecuperáveis (removed intocado). Dono da leitura ganha inspectPrivateMediaObject e isPrivateMediaMissingError (S3 NoSuchKey/404 + ENOENT local; checksum nunca é missing); archivePhotoIngest ganha repairArchivePhotoObject e withdrawArchivePhotoFromPublic; decisão do reparo em scripts/lib/archiveIntegrityRepair.mjs com deps injetadas e puro de CLI/recibo em scripts/lib/archiveIntegrityPlan.mjs. Sem migration, sem UI. Guardas: ARCHIVE_INTEGRITY_CONFIRM + TEQO_ENV exato + 4 S3_* (em qualquer modo no alvo não-local) + FLICKR_API_KEY. Runbook C246, changelog e unit/int/e2e afetado.
27 KiB
Impl: C246 — Integridade do acervo no Garage: achar e recuperar as fotos quebradas
Status: aprovado (modo --auto — apresentado no chat sem pausa)
Atualizado em: 2026-10-01
Issue: #1418
Intenção: docs/plans/acervo-integridade-garage.md
Appetite restante: herdado (~0,5–1 dia eng + ops) — cortes explícitos: sem migration, sem UI, sem varredura de media/vídeos, sem paralelismo de download do Flickr, varredura com concorrência limitada (--concurrency, default 3), reparo só do que a varredura acusar; a execução real em produção é ops (runbook no homeserver), não o agente.
Leitura da intenção
- Outcome: o álbum público nunca serve mídia quebrada — uma varredura read-only de TODAS as fotos acha objeto ausente/corrompido com motivo, a recuperação re-ingere do Flickr pelo
flickrIdsem duplicar linha nem tocar em curadoria, e o recibo diz o que foi varrido, achado, recuperado e falhou (irrecuperável sai do público com registro). - O que NÃO negociar: sem tocar em curadoria (campos curados nunca sobrescritos — régua C231/C232); sem duplicar linha (
flickrIdunique); irrecuperável não pode ficar quebrada no público (D3); fora de escopomedia/OPS52, vídeos e outras plataformas; read-only por padrão, escrita só com confirm explícito; produção via runbook do homeserver; nada de migration nem depush. - O que reavaliar: (a)
sourceUrlé a página do Flickr, não o arquivo (ArchivePhoto.ts:521-524"Página no Flickr";src/lib/archivePhoto.ts:107-116monta/photos/<alias>/<id>/) — a fonte real de recuperação égetLargestSizeporflickrId, o mesmo fallback documentado da ingestão; (b) "re-ingestão existente" não cura linha existente:ingestArchivePhotoretorna cedo emexistingsem tocar no objeto (archivePhotoIngest.ts:95-103) — o reparo é função nova no mesmo módulo (D1); (c) "volta ao público pelo fluxo normal" não exige re-approve: uma fotoapprovedcujo objeto é substituído na MESMA chave volta a servir sozinha, edraftsegue opnpm archive:publish(D5); (d) o guard de canal de remoção só cobre a entrada emapproved(ArchivePhoto.ts:248) — downgradeapproved → draftnunca é bloqueado (resolve a dúvida do D3); (e) semS3_*a varredura mede o disco local, não o bucket (D7).
Abordagem recomendada
flowchart LR
subgraph CLI["scripts/check-archive-integrity.mjs — varredura read-only por padrão"]
G["guardas: DB por TEQO_ENV, S3 completo no alvo não-local; ARCHIVE_INTEGRITY_CONFIRM só no --apply"] --> L["listArchivePhotos: todas as linhas (approved/draft/removed)"]
L --> S["por foto: inspectPrivateMediaObject — download (mesmo caminho do índice facial) + decode sharp failOn:'error'"]
S -- "ok" --> R["recibo: ok"]
S -- "ausente/corrompido" --> P{"--apply?"}
P -- "não (varredura)" --> R
P -- "sim" --> F["Flickr getSizes (flickrId) → downloadUrlToFile (UA, teto) → valida decode"]
F -- "fonte ok" --> U["repairArchivePhotoObject: payload.update com filePath na MESMA chave"]
F -- "fonte sumiu" --> D["approved → draft + registro (purge/revalidação pelos hooks)"]
U --> R
D --> R
end
S --> S3[("bucket Garage (objeto privado)")]
U --> S3
U --> DB[("linha archivePhoto")]
D --> DB
R --> J["data/archive/reports/archive-integrity-*.json + stdout"]
Opções consideradas: A) CLI nova de varredura + CLI de recuperação com PutObject direto (precedente recover-media.mjs) | B) estender ingestArchivePhoto para "curar" o existing | C) uma CLI nova (archive:integrity) cuja varredura reusa o caminho de leitura do índice facial e cujo reparo reusa o dono da escrita do upload (payload.update com filePath), com função nova no módulo de ingestão.
Recomendação: C — é o único desenho em que a varredura mede exatamente o que o índice facial e o proxy público medem (download real, com a validação de checksum do SDK e o decode do sharp), o reparo passa pelo dono da escrita de arquivo da collection (hooks de revalidação/purge preservados, metadados coerentes) e nada é duplicado entre media e archivePhoto; a rede do Flickr fica isolada no client já existente (flickrApi.mjs) e a escrita Payload fica testável em int.
Rejeitadas: A porque faria a CLI um segundo dono da escrita de objeto do archivePhoto (client S3 próprio) e pularia os hooks do ciclo de vida — o PutObject direto do recover-media existe porque lá o problema é DB-intocável, o que não é o caso; B porque mudaria o contrato de idempotência do existing (re-executar nunca re-baixa) e misturaria reparo com ingestão.
Decisões de engenharia
D1 — Onde vive a recuperação
- Opções: A) CLI dedicada com
PutObjectdireto (precedenterecover-media.mjs:235) | B) estenderingestArchivePhotopara curar oexisting| C)payload.update({ id, filePath, overwriteExistingFiles: true })orquestrado pela CLI, comrepairArchivePhotoObjectno módulo de ingestão. - Recomendação: C — o
payload.updateda Local API aceitafilePatheoverwriteExistingFiles; com o basename do temp fixado emrow.filename, o nome/chave não mudam (generateFileDatasó renomeia quando!overwriteExistingFiles) e o objeto é sobrescrito na mesma chave; os hooks seguem donos da revalidação (ArchivePhoto.ts:276) e do purge de descritores (:292);filesize/dimensões ficam coerentes com o objeto; nenhum segundo escritor de objeto paraarchivePhoto. A função entra emsrc/utilities/flickr/archivePhotoIngest.ts, irmã deupdateArchivePhotoExif(precedente de escrita de manutenção no mesmo módulo). - Rejeitadas: A porque duplicaria o dono da escrita de objeto e pularia os hooks (e
recover-mediaé DB-intocável por natureza, caso diferente); B porqueexistingé contrato de idempotência da ingestão ("reexecutar nunca re-baixa") e reparo não é ingestão — a extensão viraria um modo escondido do C231.
D2 — Varredura read-only
- Opções: A) baixar cada objeto pelo caminho do índice facial (
downloadPrivateMediaToFile) e validar a decodificação com sharpfailOn:'error'| B)headObject+ comparação dex-amz-checksum-crc32sem baixar | C) amostragem. - Recomendação: A — o GET real é a fonte da verdade: é exatamente o caminho em que as fotos 80/83 falham (o SDK valida o checksum na resposta) e o mesmo decode que a grade pública roda; um HEAD devolve o metadado armazenado (não o conteúdo recalculado) e não vê corrupção; a amostra de 20 já provou que o achado é raro e escapa. Implementar
inspectPrivateMediaObject({ media, staticDir })no donoprivateMediaResponse.ts: reusadownloadPrivateMediaToFilepara um temp (limpo por foto), força o decode (sharp({ failOn:'error' }).rotate().resize(...)), mede bytes e classificaok | missing | corruptcomstage: 'download' | 'decode'e motivo; reusa o classificador de ausência existente (exportar o atualisMissingObjectcomoisPrivateMediaMissingError, uso interno intacto). CLI com--concurrency(default 3) e--limit(canary). - Rejeitadas: B porque checksum armazenado não detecta corrupção de conteúdo (o mismatch só aparece no GET) e objeto truncado sem checksum passa; C porque o problema é raro e a amostragem falha no único caso que importa.
D3 — Fotos irrecuperáveis
- Opções: A)
draft+ registro no recibo | B)removed| C) ficar como está e só reportar. - Recomendação: A —
payload.update({ publicationStatus: 'draft' })comoverrideAccess: true(ator confiável sem sessão, precedente C231/C242); o guardrequireRemovalChannelForApprovalsó dispara na transição PARAapproved(ArchivePhoto.ts:248), então o downgrade é permitido mesmo sem canal configurado; os afterChange hooks purgam os descritores (:292-297) e revalidam a listagem (:276), tirando a foto do público e da busca por selfie. O recibo registra id, flickrId, motivo e status anterior/novo. Linhasremovednunca são reparadas nem rebaixadas (takedown continua takedown — C242): a varredura reporta e o reparo registraskippedRemoved. - Rejeitadas: B porque
removedé takedown de pessoa e um arquivo quebrado não é pedido de remoção — além de esconder a causa; C porque viola o aceite ("nunca ficam quebradas").
D4 — Recuperação da fonte
- Opções: A) re-
getSizes(maior size) peloflickrIdvia API | B) baixar osourceUrlpersistido | C) A com fallback B. - Recomendação: A —
sourceUrlé a página humana (fato acima), não o arquivo; ooriginalUrlnão é persistido;getLargestSize(scripts/lib/flickrApi.mjs:187-200) é o mesmo fallback documentado da ingestão (inclui "Original" quando disponível; quando o original é restrito, é exatamente o que foi ingerido). Só as fotos acusadas chamam a API (1 req/foto — rate limit irrelevante);FLICKR_API_KEYé exigida só no--apply, fail-fast antes de qualquer rede; o download usadownloadUrlToFilecomUser-Agentdo Flickr e teto de 512 MiB/2 min (constantes do C231). O recibo grava a URL-fonte, bytes e sha256 (proveniência). - Rejeitadas: B porque baixaria HTML (e scraping é anti-goal); C porque o fallback não existe de verdade — sem size usável, a foto é irrecuperável e segue o D3.
D5 — Reaprovação pós-recuperação
- Opções: A) a CLI não toca
publicationStatusno reparo (approved continua approved; draft segue draft) | B) re-aprovar automaticamente se estava approved antes | C) deixar sempre draft. - Recomendação: A — a linha nunca foi mutada; só o objeto foi substituído na mesma chave. O gate público é metadado (
approved+filenameinalterado,archivePhotoReads.ts:68) e a rota de mídia lê a linha fresca e faz stream do objeto (route.ts:33-72,force-dynamic+no-store), então a foto aprovada volta a servir sem nenhuma escrita de status;draftpermanecedrafte segue o fluxo normalpnpm archive:publish(que exige canal de remoção). O recibo lembra de rodarpnpm faces:index— foto cujo índice falhou fica stale (o marcador só é gravado no sucesso,faceDescriptorIndex.ts:180-252) e é reprocessada sozinha. - Rejeitadas: B porque reparo não é ato de curadoria e um re-approve re-dispararia o guard de canal (podendo transformar um reparo em falha); C porque despublicaria fotos boas (viola "voltar ao público pelo fluxo normal").
D6 — Recibo
- Opções: A) JSON datado em
data/archive/reports/archive-integrity-<stamp>.json(varredura) e...-repair.json(--apply) + resumo humano no stdout, viascripts/lib/archiveIntegrityPlan.mjspuro (parse/summarize/format) | B)data/flickr/reports/| C) só stdout. - Recomendação: A — segue o regime do C242 (
data/archive/reports/archive-publish-<stamp>.json) e o padrão plan-lib (archivePublishPlan.mjs), comwriteRepoFiledo esqueleto CLI. Campos:runAt,mode(verify|repair),target(sem segredo),scanned,ok,missing,corrupted[](id, flickrId, filename, stage, reason),recovered[],failed[],unrecoverable[](status anterior/novo),skippedRemoved,bytes,durationMs; no reparo,results[]por foto (status, stage, sourceUrl, sha256, bytes). Exit codes: varredura sai 1 semissing+corrupted > 0(espelhafaces:index --verify), 0 quando limpa — é o critério de convergência;--applysai 1 se restarfailedque não virouunrecoverabletratado. - Rejeitadas: B porque o domínio é o acervo, não o Flickr; C porque o aceite pede conferência posterior e a Issue pede recibo anexável.
- Nota de execução (triage pós-simplify, 2026-10-02): o shape final do recibo difere do previsto acima em três pontos, sem mudar a decisão: (a)
pendingconta só as quebradasapproved— é o exit 1 da varredura, e quebradasdraft/removed(draftBroken/removedBroken) são registradas sem bloquear a convergência pós---apply; (b)scanganhoudraftBroken,removedScannederemovedBroken(em vez do únicoskippedRemoved), e o rebaixamento de uma foto que já eradrafté registrado sempreviousStatus/newStatus; (c) o JSON é{ runAt, mode, target, scan, repair?, results[], repairResults?, durationMs }— os itens por foto ficam nas arrays e o formatador lê semprereport.scan.
D7 — Guardas de escrita
- Opções: A)
--applyexigeARCHIVE_INTEGRITY_CONFIRM=1+assertWriteConfirm+assertEnvironmentDatabaseTarget(TEQO_ENV casando o banco exato) + S3 completo (mirroredMediaRequired) +FLICKR_API_KEY; a varredura é read-only, mas exige S3 completo quando o alvo não é local | B) reusarARCHIVE_PUBLISH_CONFIRM| C)assertLocalDatabase+ALLOW_REMOTE_DB. - Recomendação: A — flag própria porque o write path é outro (reparo de objeto + eventual downgrade) e a intenção de operação é diferente da publicação em lote; mesmo stack do C231/C242 (
assertWriteConfirm+assertEnvironmentDatabaseTarget, que distingue staging/produção pelo nome do banco e recusaALLOW_REMOTE_DB); S3 completo obrigatório no alvo não-local viamirroredMediaRequired(sem S3 a varredura/reparo mediriam o disco errado);FLICKR_API_KEYfail-fast no--apply. Runbook no homeserver (compose maintenance, env do stack), comoflickr:import/archive:publish. - Rejeitadas: B porque publicar e reparar são intenções distintas (o operador pode ter armado só uma); C porque não separa staging de produção e admite o override remoto que o C195/C231 recusa.
Migration: sem migration — nenhum campo, índice ou collection novos. Stop-condition do modo autônomo: se aparecer necessidade de campo novo (ex.: marcador de integridade), parar e sinalizar em vez de criar schema.
Componentes / mudanças
inspectPrivateMediaObject(src/utilities/privateMedia/privateMediaResponse.ts, edição): novo export que baixa o objeto pelo caminho do índice facial (downloadPrivateMediaToFile), valida decode com sharpfailOn:'error'e classificaok | missing | corrupt(stage: 'download' | 'decode', motivo, bytes); exporta o classificador de ausência atual comoisPrivateMediaMissingError(renome doisMissingObjectprivado, uso interno intacto); temp limpo por chamada. Reusa o client S3 module-private e o fallback de disco local (dev/test).repairArchivePhotoObject(src/utilities/flickr/archivePhotoIngest.ts, edição):payload.update({ collection: ARCHIVE_PHOTO_SLUG, id, data: {}, filePath, overwriteExistingFiles: true, overrideAccess: true }); retorna{ status: 'repaired' | 'failed', filename, error? }; nunca envia campos curados nempublicationStatus; nome do temp =row.filename ?? archivePhotoStorageFilename(flickrId, originalUrl)(determinístico C231).archiveIntegrityPlan(scripts/lib/archiveIntegrityPlan.mjs, novo):parseArchiveIntegrityCliArgs(--apply,--limit,--only,--out,--help; modos exclusivos),archiveIntegrityReportStamp,summarizeArchiveIntegrity,formatArchiveIntegrityReport; entra emSCRIPTS_SPEC_PINNED(scripts/lib/test-affected-core.mjs).check-archive-integrity(scripts/check-archive-integrity.mjs, novo + script"archive:integrity"nopackage.json): varredura default read-only (--concurrency,--limit,--out) e--apply(varredura + reparo com guardas D7); por foto acusada:client.getLargestSize(flickrId)→downloadUrlToFile(UA/teto C231) → validação decode →repairArchivePhotoObject; fonte sumiu → downgrade a draft (D3);removedintocado; recibo D6; exit codes D6.docs/ops/teqo-1313-deploy.md(edição): seção C246 com canary, varredura completa, reparo e re-varredura, no serviçoteqo-<env>-migratecom volume de recibos; lembrete docurl /api/revalidate?tag=archivePhotosquando houver downgrade (o write do CLI não revalida o servidor Next vivo).docs/changelog/2026-10-01-c246.md+ este impl no mesmo PR.- Access / Consent: nenhuma mudança de access; as chamadas são de ator confiável sem sessão (
overrideAccess: truedocumentado, precedentearchivePhotoIngest.ts:43-44); semConsentnovo e semContact(não há opt-in nem PII de titular nesta fatia). Escritas são single-collection (payload.update), sem necessidade de transação multi-collection. - UI: Impeccable A — sem UI; nenhum shell/rota nova; a ficha admin é gerada pelo Payload.
Dados → forma
- Forma escolhida: recibo JSON operacional (
data/archive/reports/) + linhas humanas no stdout (contagens e falhas nomeadas), consumido por operador — não há superfície pública. - Rejeitadas: relatório markdown separado (duplicaria o JSON e criaria segunda fonte de verdade); CSV (ninguém consome); painel/admin (não pedido; fora do appetite).
Fases verificáveis
- Tracer / schema+server — varredura read-only (~40% do appetite):
inspectPrivateMediaObjectno dono +archiveIntegrityPlan.mjs(parser/summarize/format) + modo varredura da CLI + recibo +SCRIPTS_SPEC_PINNED. Testes: unit do plan-lib (args, summarize, format) e do classificador; int com fixture real (ARCHIVE_PHOTO_JPEG_BYTES): objeto íntegro →ok; arquivo corrompido no staticDir →corrupt(stage decode); arquivo removido →missing; checksum error classificado comocorrupt(unit do predicado, sem S3). Prova local: ingerir fixture, corromper o arquivo,pnpm archive:integrity --limit 20acusa com motivo e sai 1; varredura limpa sai 0. - Recuperação (~30%):
repairArchivePhotoObject+ modo--apply(FlickrgetSizes+ download + validação + update) + guardas D7. Testes: int — reparo substitui os bytes na MESMA chave,filenameinalterado,curatedFields/curadoria/status intactos,filesizeatualizado; unit —flickrApi.getLargestSizejá pinado,--only/canary e guardas da CLI por spawn (espelho dearchiveCatalogCli.unit.spec.ts: sem confirm, sem TEQO_ENV, banco ≠ ambiente, S3 incompleto). - Irrecuperáveis +
removed(~15%): downgradeapproved → draftcom registro; int — descritores purgados no downgrade, canal de remoção ausente NÃO bloqueia o downgrade,removednão é tocado pelo reparo (fica no recibo comoskippedRemoved); convergência — corromper → reparar →inspectvoltaok. - Gates + docs + ops (~15%):
pnpm gate:fast; runbook noteqo-1313-deploy.md; changelog; push viapnpm push. Ops (fora do merge): no homeserver, canário nas fotos 80/83 (--only 80,83), checar/fotos/80/midiae/fotos/83/midia200, varredura completa (read-only) e--apply; anexar os recibos à Issue #1418; re-varredura limpa é o aceite final.
Rabbit holes / Não escopo (engenharia)
- Não varrer
media(OPS52), vídeos, gravações ou outras plataformas — sóarchivePhoto. - Não re-ingerir o acervo inteiro (corte de produto): só o que a varredura acusar.
- Não apagar fotos nem objetos (corte de produto): recuperar primeiro; sair do público só quando a fonte não existir.
- Não adicionar campo/marcador de integridade (sem migration): reexecutar converge por varredura completa, que é a checagem honesta.
- Não mexer em curadoria, não re-aprovar, não publicar lote, não tocar em
removed. - Não paralelizar download do Flickr (poucas fotos; client C231 sequencial com retry já basta).
- Não editar migrações existentes nem usar
push; nenhuma UI/admin nova. - Não criar
Consent/Contact/cadastro paralelo (não se aplica).
Riscos e mitigação
- Checksum/ETag multipart: ETag de multipart não é MD5 — não usar como hash; a validação é o download real (checksum do SDK quando presente) + decode do sharp (cobre objeto truncado sem checksum). Falso negativo residual aceito e registrado no recibo.
- Flickr auth/rate-limit: só as fotos acusadas chamam
getSizes(1 req/foto);FLICKR_API_KEYfail-fast; retry/backoff do client C231; falha de fonte →unrecoverable→ D3. - Objeto curado não pode ser sobrescrito:
repairArchivePhotoObjectsó enviafilePath(+overwriteExistingFiles), nunca campos curados nem status;deriveArchivePhotoCatalogIndexnão marca curadoria em escrita semreq.user(ArchivePhoto.ts:175); int pin decuratedFields/status. - Drift de filename/chave: temp com basename =
row.filename+overwriteExistingFiles: true(só renomeia quandofalse); int pin do filename inalterado e do objeto na mesma chave; sem filename, nome determinístico C231. Canário de produção confere as fotos 80/83 antes do lote. - S3 config parcial / alvo errado:
resolveS3StorageEnvlança em config parcial (mediaStorage.ts:46);mirroredMediaRequiredexige as 4 envs no alvo não-local; sem S3 em dev/test a varredura mede o disco local (documentado; nunca é o caso de produção). - Fotos já
approvedsem canal de remoção: não bloqueiam nada — o guard só cobre a entrada emapprovede o reparo não re-aprova; se o operador quiser publicar um draft de volta, o fluxo normal exige o canal. - Custo de baixar todas as fotos: ~6.492 objetos na rede local + decode;
--concurrency(default 3) e--limitcanary; recibo com bytes/tempo; execução no homeserver evita egress externo. - Falsos positivos: a varredura não escreve nada — um falso positivo custa apenas um reparo redundante (idempotente, re-baixar da fonte é seguro) e fica no recibo com motivo.
- Cache do servidor vivo após downgrade: o hook de revalidação roda no processo de manutenção e não alcança o Next vivo (mesmo caveat do
archive:publish); o runbook manda bustartag=archivePhotosapós--applycom downgrade; fotos reparadas (approved, filename inalterado) voltam a servir sem bust. - Janela scan→reparo: a foto segue 500 até o reparo; curto e aceitável (o scan é o pre-flight do próprio
--apply). payload.updatecomfilePathvia plugin S3 em produção: caminho não coberto por int sem credenciais; mitigado pelo canário--only 80,83+ verificação pública 200 antes do lote completo.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto (varredura de todas as fotos, recuperação sem duplicar/curar, irrecuperável fora do público, reexecução converge, recibo honesto)
- Invariantes AGENTS/engineering-standards (overrideAccess documentado em CLI sem sessão; sem migration; sem
push; copy pt-BR / identificadores em inglês; produção só com confirm e via runbook) - Testes de domínio previstos: unit (plan-lib, classificador, guardas da CLI por spawn) e int (inspect ok/missing/corrupt; repair preserva filename/curadoria/status; downgrade purga descritores sem canal;
removedintocado; convergência corromper→reparar→ok) - Sem migration / sem campo novo (stop-condition respeitada)
inspectPrivateMediaObjectreusa o caminho do índice facial e classificaok | missing | corruptcom motivorepairArchivePhotoObjectmantém a chave/filename e não toca curadoria nempublicationStatus- Varredura read-only não escreve e sai 1 com pendência;
--applyexigeARCHIVE_INTEGRITY_CONFIRM,TEQO_ENVexato, S3 completo eFLICKR_API_KEY - Recibos em
data/archive/reports/+ resumo stdout; runbook C246 noteqo-1313-deploy.md; changelog no PR pnpm gate:fastverde; push viapnpm push
Débitos deferidos (triage pós-simplify, 2026-10-02)
- S1 — pipeline de decode com donos múltiplos:
inspectPrivateMediaObject(privateMediaResponse.ts:384-387) e a prova da fonte do reparo (archiveIntegrityRepair.mjs:105-108) duplicam hoje o mesmo proof (failOn:'error'+rotate+resize width 1024 withoutEnlargement); oprepareFaceImage(faceDescriptorIndex.ts:55-60) é transform vizinho (resize comheight/fite saídaraw()), não o mesmo contrato. Gatilho: um 4º consumidor do proof de decode ou a necessidade de mudar edge/transform/failOn— então extrair umassertImageDecodesno donoprivateMediaResponsee consumir de inspect/repair (não forçar o face index no mesmo helper). - S3 —
mapWithConcurrencyno plan-lib de domínio (archiveIntegrityPlan.mjs:104), 1 consumidor (a própria CLI). Gatilho: um 2º script precisar de mapa com concorrência limitada — mover para o esqueletoscripts/lib/cli.mjse importar nos dois. - S4 — contrato do recibo em JSDoc solto em 3 arquivos (
archiveIntegrityPlan.mjs,archiveIntegrityRepair.mjs,check-archive-integrity.mjs). Gatilho: o recibo ganhar um 2º consumidor (parsing por script/painel) ou uma 4ª superfície — então typedef único no plan-lib e referência nos demais. - S6 — constantes de download do Flickr duplicadas (
ingest-flickr-archive.mjs:84-85do C231 ×archiveIntegrityRepair.mjs:25-26). Gatilho: um 3º consumidor de download do Flickr ou mudança do teto/timeout — mover para o dono do client (scripts/lib/flickrApi.mjs) e importar nos dois. - S5 — registrado como C249 (depende do C246):
pnpm archive:publishaprova todo draft sem olhar o objeto e pode devolver ao público uma foto que o C246 rebaixou por estar quebrada; hoje só o aviso do runbook mitiga. Plano:docs/plans/acervo-publish-preflight-integridade.md.
Já resolvido no /simplify (não reabrir)
isFlickrSourceGonesó reconhece códigos 1/2 + HTTP 404 (100/105/401/403/429/5xx/network viramfailedretryável).pendingconta sóapproved;draftBroken/removedScanned/removedBrokenregistram sem bloquear; runbook alinhado.process.exit()movido para depois dofinally(temp/tmp/archive-integrity-*não vaza mais).skipped already-draftnão mente maisdraft → draft(sem previous/newStatus).- Formatador sempre usa
report.scan(fim do fallback com NaN). isPrivateMediaMissingErrornarrowado antes do cast.- Docblock de
archivePhotoIngestalinhado ao reparo/withdraw. - Typo
jorgessollacorrigido no int spec. - Fila de reparo deriva de
scan.missing/scan.corrupted(fim da regra duplicada deremoved).
Descartado
- S2 —
messageOflocal no script de reparo: one-liner cuja cópia local é a convenção dos scripts (25+ equivalentes inline) e cujo dono (src/utilities/media/ffmpeg.ts) éimport 'server-only', não importável do script; sem risco real de divergência.
Self-score decision-quality
5/5 — (1) todas as decisões caras foram deliberadas agora com opções/recomendação/rejeitadas (D1 write path, D2 método de detecção, D3 saída do público, D4 fonte, D5 status, D7 guardas); (2) depth check feito — reusa o dono de leitura de mídia privada (privateMediaResponse), o dono de escrita do upload (payload.update via archivePhotoIngest) e o esqueleto CLI, sem twin e sem pass-through raso; (3) a hipótese "sourceUrl é arquivo" foi corrigida contra o codebase e a intenção preservada no outcome; (4) rabbit holes de produto (re-ingerir tudo, apagar) e de engenharia (campo novo/migration, UI, varredura de media) explicitamente cortados; (5) riscos com mitigação verificável (canário 80/83, int pins, cache bust do downgrade) e aceite com checkboxes.