Lê a janela recente (12 mídias) de @depjorgesolla pela Graph API oficial e cria Rascunho só para as novidades pelo pipeline C211/C220 (ou peça-link com motivo honesto), com progresso real e recibo novas/já estavam/falharam. Dedupe pela identidade do post (shortcode em qualquer grafia p/reel/tv), sem migration, compartilhado com o Adicionar por link; fail-closed de credencial nas duas pontas; nada é publicado. Closes #1362
26 KiB
Impl: C230 — Central de Conteúdos — importar publicações do perfil oficial (@depjorgesolla)
Status: aprovado Atualizado em: 2026-09-25 Issue: #1362 Intenção: docs/plans/central-conteudos-importar-perfil.md Appetite restante: herdado (~1–2 dias eng) — sem corte novo: o plano reusa o pipeline C211/C220, o cliente Graph API e a tela da Central; backfill, agendador/webhook e scraping seguem fora (intenção).
Modo
--auto(work-issue): plano nasce aprovado pelo agente. Design UI do gate:docs/plans/central-conteudos-importar-perfil-ui-design.html(cenas 1–7). Trigger de designer em UI: non-trigger com adaptações registradas — se a execução encontrar um estado não desenhado, o designer estende o artefato antes do markup (não improvisar estrutura visual).
Leitura da intenção
- Outcome: com a credencial configurada, "Importar do perfil" na lista da Central traz as mídias recentes de
@depjorgesolla: cada novidade vira Rascunho no pipeline existente (mídia baixada do perfil próprio, transcrita, catalogada) ou peça-link com motivo honesto; ao fim o operador lê o recibo novas / já estavam / falharam; nada duplica — inclusive contra peça que entrou por link manual em outra grafia — e nada é publicado. - O que NÃO negociar:
- Só o perfil próprio pela API oficial (
loadInstagramFeed,src/utilities/socialFeed/instagramFeed.ts:253-322); nunca scraping, terceiro ou stories; mídia de terceiro nunca é baixada (o match é dentro do feed da própria conta). - Tudo entra como Rascunho; nenhuma publicação automática;
curatedFieldsnunca sobrescrito. - Dedupe pela identidade do post, não pela grafia da URL; reexecutar não duplica.
- Sem credencial: fail-closed com a linguagem do board ("Instagram ainda não configurado"); token nunca em log/URL/wire.
- Board/feed da home intocado:
instagramFeed.tse o pipeline C220 (resolver/job/agendador) não mudam de comportamento; sem segundo resolvedor, sem estado naSocialFeedSettings. - LGPD: sem Consent novo (não há PII de eleitor; peça é material de campanha).
- Só o perfil próprio pela API oficial (
- O que reavaliar (hipóteses da intenção, confirmadas ou resolvidas no código):
- "identidade do post exige campo novo" — reavaliada e resolvida sem migration: o parse produz a URL canônica e o conjunto de variantes vivas é ≤3 (
p|reel|tv;reelscolapsa emreelemsrc/lib/contentPiece.ts:408-461), consultável porsourceUrl inno índice existente (src/collections/ContentPiece.ts:421-431) — D1. - "o dedupe exato do
addContentPieceByLinkForActorbasta" — confirmado insuficiente para "qualquer grafia" (contentPieces.ts:335-344comparacanonicalUrlexata;/p/ABC/≠/reel/ABC/): o predicado vira compartilhado — D1. - "a janela de 500 do C220 serve ao importador" — não: o recorte do aceite é "mídias recentes"; a janela vira decisão de implementação (12) e o histórico continua pelo link — D4.
- "o pipeline C220 re-resolve pelo feed por peça (N+1)" — confirmado e aceito: ~1 chamada de feed por post da janela, com early-stop (D3); o rate limit da edge (C212 §Q1) comporta 12.
- "falta credencial → só UI ou só server" — o fail-closed precisa das duas pontas: página (botão/banner) e action/rota (mensagem segura) — D5.
- "identidade do post exige campo novo" — reavaliada e resolvida sem migration: o parse produz a URL canônica e o conjunto de variantes vivas é ≤3 (
Abordagem recomendada
flowchart LR
P["Página Conteúdos<br/>lê o global → instagramConfigured (boolean)"] --> D["Dialog 'Importar do perfil'<br/>intro · running · recibo"]
D -->|"Fase A · POST .../conteudos/importar"| A["lista candidatos<br/>global + isInstagramFeedConfigured<br/>loadInstagramFeed maxResults 12"]
A -->|"sem credencial"| E1["erro seguro<br/>Instagram ainda não configurado"]
A -->|"feed lido"| DED["dedupe por identidade do post<br/>sourceUrl in variantes p · reel · tv"]
DED --> R["found · existingCount · candidates[]<br/>linkOnlyReason: carrossel · indisponivel · null"]
R -->|"loop no cliente · 1 POST por candidato"| B["Fase B · POST .../importar/criar"]
B --> C{"identidade já existe?"}
C -- "sim" --> EX["outcome existing → já estavam"]
C -- "não" --> CR["create no shape do link C220<br/>rascunho · processando/extraindo"]
CR --> J["after() → runContentPieceJob<br/>re-resolve por shortcode · baixa mídia · cataloga"]
J --> OK["pronto com arquivo<br/>ou peça-link com linkFailureReason honesto"]
CR --> REC["recibo: novas / já estavam / falharam<br/>+ peça-link por motivo"]
EX --> REC
REC --> L["Ver os rascunhos<br/>lista ?status=rascunho"]
Opções consideradas (geral): A) importador como orquestração cliente-servidor em 2 fases, reusando o pipeline C220, com dedupe por identidade do post via variantes de sourceUrl (sem migration) e fail-closed de credencial nas duas pontas; B) campo indexado novo (instagramShortcode/mediaId) com migration + action única server-side; C) fila/cron/webhook com estado próprio de varredura.
Recomendação: A — um dono por mecanismo (feed Graph API, pipeline da peça, lista), zero schema novo, progresso real do design (cena 3) e cabe no appetite.
Rejeitadas: B — migration é stop do modo --auto e cara de reverter, sem ganho sobre a query in num índice existente; C — fora de escopo (item irmão C212 §Q4) e contradiz a cadência sob demanda desta fatia.
D1 — Dedupe por identidade do post, sem migration
Opções: A) identidade = shortcode do Instagram → variantes canônicas de sourceUrl (https://www.instagram.com/{p|reel|tv}/{shortcode}/), consultadas com where: { sourceUrl: { in: variantes } }; para YouTube a identidade já é 1 URL canônica; predicado compartilhado com o owner C220; B) campo indexado novo instagramShortcode/mediaId (migration de schema) no dedupe; C) confiar só no unique exato de sourceUrl como hoje (contentPieces.ts:335-344).
Recomendação: A — o kind (p×reel×tv) é apresentação e o shortcode é a identidade (src/lib/contentPiece.ts:384-396,408-461); a query in usa o índice que sourceUrl já tem e o conjunto é ≤3 URLs por candidato numa janela de 12 — sem schema, migration ou backfill. O helper puro contentPiecePostIdentityUrls(link) é o único ponto com as variantes e findExistingContentPieceByPostIdentity o único predicado de dedupe; o addContentPieceByLinkForActor (owner C220) passa a usá-lo, então colar o link em outra grafia também responde CONTENT_PIECE_LINK_DUPLICATE_MESSAGE ("Esta peça já está na Central").
Rejeitadas: B — migration sem ganho real (teto de 3 variantes, reels normaliza no parse) e cara de reverter; C — deixa passar a gêmea /p/ABC/×/reel/ABC/, violando o aceite ("inclusive o que entrou por link manual, em qualquer grafia").
D2 — Fluxo em 2 fases orquestrado pelo cliente, com progresso real
Opções: A) Fase A (rota .../conteudos/importar) lê o feed e devolve { found, existingCount, candidates:[{ url, shortcode, linkOnlyReason }] } já deduplicado; Fase B (.../importar/criar) cria UMA peça por candidato, com loop sequencial no cliente ⇒ "Importando X de N…" real, falha de um não interrompe os outros e o recibo separa novas/já estavam/falhou; B) 1 action faz tudo server-side e devolve só o recibo final; C) fila/cron/webhook com estado próprio.
Recomendação: A — cumpre a cena 3 ("Importando 7 de 12…") sem progresso inventado, não trava uma request atrás de N jobs e isola cada falha (cena 4). Contrato: found = existingCount + candidates.length (só mídias com identidade reconhecível); o recibo fecha created + existingFinal + failed = found; corrida entre A e B (outro ator criou a peça) cai em existing no probe da Fase B. Fase B responde { status:'success', outcome:'created'|'existing' } — o loop só precisa do desfecho e a lista é refrescada no fim (sem devolver ContentPieceViewModel nem leitura extra do doc existente). Loop em série (1 request por vez) — progresso honesto e carga previsível; o upload usa 2 em voo porque o gargalo é o navegador, aqui é a API/DB.
Rejeitadas: B — contradiz a cena 3 (sem progresso real) e segura a request com N jobs; C — fora de escopo (C212 §Q4).
D3 — Reuso integral do pipeline C220 (nada de segundo resolvedor)
Opções: A) a Fase B cria a peça exatamente no shape do link — título contentPieceLinkTitle(link) (Instagram · SHORTCODE), type:'video', origin:'instagram', sourceUrl canônica, status:'rascunho', processingStatus:'processando', step:'extraindo' — e chama startContentPieceJobInBackground; o job C220 re-resolve pelo feed com early-stop, baixa só mediaUrl do perfil próprio, transcreve/cataloga e persiste linkFailureReason honesto; B) segundo resolvedor no importador (baixar mediaUrl na Fase A e passar adiante); C) persistir a classificação da Fase A como linkFailureReason sem o job re-resolver.
Recomendação: A — um dono por mecanismo (o job é dono do estado da peça; contentPieceLink.ts da extração), nenhuma semântica nova; o N+1 de ~1 chamada de feed por post da janela é aceitável no rate limit da edge e a reexecução segue idempotente. A classificação exibida no recibo vem só do metadado do feed (CAROUSEL_ALBUM → carrossel; mediaUrl == null → indisponivel; senão null) — sem valor novo no vocabulário fechado (src/lib/contentPiece.ts:360-379; adicionar valor seria migration).
Rejeitadas: B — duplicaria download/limite/erro tipado e criaria segunda semântica de peça-link; C — motivo persistido viraria palpite da Fase A, não o resultado do caminho oficial (que é quem tenta o download).
D4 — Janela = 12 mídias recentes
Opções: A) maxResults: 12 numa constante nomeada no módulo do importador (CONTENT_PIECE_PROFILE_IMPORT_INSTAGRAM_WINDOW), documentada como decisão revisável; B) 50/uma página cheia ou backfill do histórico; C) 3 (mesmo recorte do board).
Recomendação: A — o mock do design é "7 de 12" e o aceite fala "mídias recentes"; uma única página de feed (≤50) cobre a janela e o post antigo específico continua entrando pelo link C220 (janela 500 com early-stop). A constante fica ao lado do maxResults para revisão explícita.
Rejeitadas: B — vira backfill/rabbit hole do plano de intenção; C — pequeno demais para o uso (perde posts do fim de semana).
D5 — Fail-closed de credencial nas duas pontas
Opções: A) a página lê o global (payload.findGlobal({ slug: 'social-feed-settings', depth: 0 }), mesmo precedente de contentPieceLink.ts:228) e passa instagramConfigured — boolean derivado de isInstagramFeedConfigured, nunca o token ⇒ botão desabilitado + legenda "Instagram ainda não configurado" + banner âmbar da cena 5; actions/rotas recusam sem credencial com mensagem segura; B) só UI; C) só server.
Recomendação: A — fail-closed nas duas pontas: a UI não oferece e o servidor recusa mesmo chamado direto (rota HTTP). O global continua dono da credencial (src/globals/SocialFeedSettings.ts:63-66); se o feed estiver desligado (enabled:false/instagramEnabled:false) o mesmo caminho vale — a via oficial está indisponível. O token nunca cruza para o cliente, log ou wire.
Rejeitadas: B — servidor confiaria no cliente (falha aberta); C — erro só no meio da ação (contradiz cena 5).
Componentes / mudanças
src/lib/contentPiece.ts(puro; dono do vocabulário):contentPiecePostIdentityUrls(link)(3 variantes IGp|reel|tv; 1 canônica para YouTube) econtentPieceProfileCandidateFromPost({ permalink, mediaType, mediaUrl })→ContentPieceProfileCandidate | null({ url: canonicalUrl, shortcode, linkOnlyReason }; null defensivo quando o parse falha); o tipo nasce aqui (client-safe). Sem importarutilities/.src/utilities/content/contentPieceLink.ts(dono do link C220; só ganha exports, resolução intocada):contentPieceExistsForPostIdentity({ payload, actor, link })(o predicado único de dedupe,user+overrideAccess:false, compartilhado com a colagem C220) eisContentPieceSourceUrlDuplicateError(error)(a corrida contra o índice unique viraexisting/duplicata nos dois paths).src/utilities/content/contentPieceProfileImport.ts(novo,server-only):CONTENT_PIECE_PROFILE_IMPORT_INSTAGRAM_WINDOW = 12+ timeout do feed (30 s, espelho do C220);readContentPieceProfileImportAvailability(payload)(boolean);listContentPieceProfileImportCandidates({ payload, actor, loadFeed?, fetchImpl? })(global + fail-closed +loadInstagramFeed(maxResults:12)+ persist best-effort do token refrescado + classificação + dedupe em UMA querysourceUrl invariantes);createContentPieceFromProfilePost({ payload, actor, url, startJob? })(parse IG, probe →existing, create no shape do link, unique violation →existing, job em background).src/lib/schemas/contentPiece.ts:CONTENT_PIECE_PROFILE_IMPORT_UNAVAILABLE_MESSAGE = 'Instagram ainda não configurado.';CONTENT_PIECE_PROFILE_IMPORT_FEED_ERROR_MESSAGE = 'Não foi possível falar com o Instagram agora. Tente novamente.';contentPieceProfileImportRequestSchema = z.object({})(Fase A; a Fase B reusacontentPieceLinkRequestSchema);CONTENT_PIECE_PROFILE_IMPORT_SAFE_MESSAGES = [...CONTENT_PIECE_SAFE_MESSAGES, UNAVAILABLE, FEED_ERROR].src/lib/campaignPaths.ts:CAMPAIGN_CONTENT_PIECE_PROFILE_IMPORT_HREFe..._CREATE_HREF(pt-BR na URL, comolink/status/enviar—:77-83).src/app/(campaign)/campanha/actions/contentPieces.ts:listContentPieceProfileImportCandidatesForActor()ecreateContentPieceFromProfilePostForActor({ url })— finas, gate frescocanReadCommunicationCatalog;addContentPieceByLinkForActortroca o probe exato (:335-344) pelo predicado compartilhado (o catch de unique segue:367-377).- Rotas
.../conteudos/importar/route.tse.../conteudos/importar/criar/route.ts:campaignJsonMutationRoute(campaignJsonMutationRoute.ts:98-119) +dynamic='force-dynamic', mesmo envelope dalink/route.ts. .../conteudos/types.ts:ContentPieceProfileImportCandidatesResponseeContentPieceProfileImportCreateResponse(importam o tipo do candidato de@/lib/contentPiece).src/components/campaign/content/ImportContentPieceProfileDialog.tsx(novo, client): fasesintro | running | done(+error), port classe-a-classe das cenas 2/3/4 (bullets e caixa "Sem scraping" da cena 2; spinner + passo concluído + "Importando X de N…" da cena 3; contadores + linhas de peça-link/falha + "Ver os rascunhos" da cena 4); loop sequencial;router.refresh()ao concluir/fechar; bloqueia fechar enquanto roda (onInteractOutside/onEscapeKeyDown, precedenteContentPieceUploadDialog.tsx:217-225)..../comunicacao/conteudos/page.tsx: lêreadContentPieceProfileImportAvailability(payload)e passainstagramConfigured; toolbar da cena 1 e empty state da cena 7 com a 3ª saída; mobile em grid da cena 6 ("Enviar peças" full width + par link/importar); banner âmbar da cena 5 quando não configurado.scripts/lib/e2e-affected-manifest.mjs: prefixosrc/lib/schemas/contentPiece.tsna entry de campanha (:460-515) — mapeamento honesto paracampaignSpeechAcervoem diffs futuros do schema (o prefixo genéricosrc/lib/schemasjá mapeia o arquivo, então não há fail-closed). Como o próprio manifesto está emHIGH_RISK_EXACT(scripts/lib/test-affected-core.mjs:115-157), este PR roda o conjunto curado (que incluicampaignSpeechAcervo) — custo aceito.- Changelog:
docs/changelog/2026-09-25-c230.md.
Migration: sem migration — nenhuma collection/global/field muda; push:false intacto; contentPiece.sourceUrl já é unique+index (src/collections/ContentPiece.ts:421-431); pnpm generate:types desnecessário (os tipos novos são de aplicação).
Access / Consent: gate fresco canReadCommunicationCatalog(actor.role) (src/lib/campaignRoles.ts:27-28) nas duas actions, como todas as irmãs da vertical; access da collection é a barreira final; probe/create com user: actor + overrideAccess:false. Leitura do global via Local API sem user (bypass admin documentado, precedente contentPieceLink.ts:228) só para derivar o boolean; token nunca cruza. Rotas sob campaignJsonMutationRoute (same-origin estrutural). Sem Consent novo (sem PII de eleitor) e nenhuma chave hardcoded.
UI: Impeccable C (encaixe na tela existente). Reusa Dialog/Button/Alert/Spinner, postCampaignJson, buildContentPieceListHref (client-safe, contentPieceListUrl.ts:89-90) e os shells de lista sem mudança. Trigger de designer non-trigger com adaptações registradas (mesma estrutura, só indicador/copy): (i) passo 1 da cena 3 começa com spinner + "Buscando as publicações recentes de @depjorgesolla…" e vira o check desenhado quando a Fase A responde; (ii) recibo com zero candidatos usa a cena 4 com subtítulo honesto ("Nenhuma novidade na janela recente — a Central já estava em dia."); (iii) falha da Fase A ganha Alert destructive no lugar da lista + "Tentar de novo"; (iv) a legenda do botão fica sob o trigger no desktop e some no grid mobile (o banner cobre o motivo). Qualquer outro estado não desenhado → o designer estende o artefato antes do markup.
Dados → forma (se aplicável)
N/A — operação/curadoria, não analytics: o recibo é contagem operacional (novas/já estavam/falhas) e o detalhe é textual (motivo da peça-link), sem agregado, taxa, série, engajamento ou alcance. A intenção fixou "sem números do Instagram nesta tela"; métricas de post são C213 (fora).
Fases verificáveis
- Tracer / server — quota ~50%: lib pura (
contentPiecePostIdentityUrls+ classificação) com unit;contentPieceProfileImport.ts(Fase A e Fase B) com deps injetáveis; actions/rotas/schemas/paths; predicado compartilhado aplicado aoaddContentPieceByLinkForActor; entry do manifesto. Tracer bullet cedo: int comloadFeedfake devolve candidatos já deduplicados (uma peça existente por variante de kind) e a Fase B cria rascunhoprocessando/extraindocomsourceUrlcanônica; reexecutar →existing. Só então a UI. - UI — quota ~30%:
ImportContentPieceProfileDialog(intro/running/done + adaptações), toolbar/empty/banner e mobile dapage.tsx; refresh da lista ao concluir. - Gates — quota ~20%:
pnpm gate:fast(lint/format/typecheck/knip/cycles/unit);pnpm test:int; e2e curado (o PR é high-risk por editar o manifesto — rodar o conjunto curado não-zero);pnpm pushvia GitHub.
Testes previstos
tests/unit/contentPiece.unit.spec.ts:contentPiecePostIdentityUrls—/p/ABC/e/reel/ABC/produzem as mesmas 3 variantes (p/reel/tv),reelscolapsado, YouTube 1 canônica;contentPieceProfileCandidateFromPost—REEL/VIDEO/IMAGEcommediaUrl→linkOnlyReason: null;CAROUSEL_ALBUM→'carrossel'mesmo commediaUrl;IMAGEcommediaUrl: null→'indisponivel'; permalink fora do parse →null.tests/int/contentPiece.int.spec.ts— novo describe C230 (deps injetadas, sem rede): Fase A comloadFeedfake (reel extraível + carrossel + semmediaUrl+ um já existente no DB por variante/p/enquanto o feed traz/reel/) →found,existingCounte candidatos comlinkOnlyReason;receivedMaxResults === 12; sem credencial (setInstagramSettings(false)) → erro seguro eloadFeednunca chamado; feed lançando →CONTENT_PIECE_PROFILE_IMPORT_FEED_ERROR_MESSAGE. Fase B: shape exato do link,startJobinjetado recebe o id; repetir URL →'existing'sem segunda linha;/reel/depois de/p/→'existing'(sem gêmea); advisor/leader →CONTENT_PIECE_FORBIDDEN_MESSAGE. Owner C220: peça existente em/p/A/, colar/reel/A/→CONTENT_PIECE_LINK_DUPLICATE_MESSAGE(pina o predicado único).- e2e — não entra. O int cobre a fronteira (candidatos/existing/link-only, fail-closed de credencial e create sem duplicar) com
loadFeedinjetado; a suíte não tem credencial real e osocial-feed-settingsé estado compartilhado entre projetos; um novo consumidor seria vetor de flake sem cobrir o que o int cobre. A página continua exercitada porcampaignSpeechAcervo(HTTP), que o diff acorda. Gatilho: se a fronteira HTTP das rotas regredir sem o int pegar (envelope/status), um e2e HTTP curto com o stub (tests/e2e/instagram-stub.mjs) semeia o global e restaura emfinally. - Pins: invariantes do
e2eAffectedManifest.unit.spec.tsseguem verdes (prefixo coberto, nenhuma spec nova);pnpm gate:fast/int.
Rabbit holes / Não escopo (engenharia)
- Backfill/"já que varre, importa as 10K" — janela 12; post antigo pelo link C220 (com early-stop na janela 500); painel de cursor é o item irmão C212 §Q4.
- Fila/cron/webhook/estado de varredura — nada é agendado; a fatia nasce sob demanda e preparada para cadência futura (
after(), precedente C199). - Scraping/oEmbed/terceiro/yt-dlp/sessão — permanentes; o resultado é peça-link com motivo + "Anexar arquivo original".
- Álbum de carrossel (baixar filhos, montar capa) — não;
carrosselcomo peça-link. - Segundo resolvedor/segunda fila/segunda identidade — proibido; uma peça por post, pipeline C220.
- Respeitar
excludedItemsdo board — as exclusões são do board ("NÃO aparecer no board",SocialFeedSettings.ts:175-186); o importador traz a janela recente e a assessoria apaga o que não quer na Central. Gatilho: se o produto pedir o mesmo filtro aqui, decisão curta + design. - Auto-publicar recentes / analytics/engajamento / contador na home — fora (C213, C211).
- Nova tela de credencial / tocar snapshot-cache-kill switch-lock do board / alterar
instagramFeed.ts— o global é o dono; o diff do board é zero.
Riscos e mitigação
- N+1 de ~1 chamada de feed por post (job por peça): janela 12 e early-stop mantêm o custo ≤12 chamadas de página por execução, bem dentro do teto da edge; reexecução idempotente. Se o rate limit reclamar, o caminho é o item de varredura com cursor próprio (fora).
- Dedupe por variantes sem campo novo: as 3 grafias canônicas cobrem tudo que o parse gera (query/hash são descartados;
reelsnormaliza); o unique continua a última barreira e a corrida viraexisting. - Permalink irreconhecível (kind novo na API): defensivo e impossível nos kinds vivos — o post não vira candidato e não entra em
found; gatilho: kind novo → atualizarINSTAGRAM_KINDS+ variantes (item curto) e, se acontecer, o post não aparece na janela (não há perda silenciosa de algo já ingerido). - Jobs ainda
processandono fim do recibo: o recibo conta criações, não conclusão; a lista mostra o processamento e oContentPieceStatusRefresher(:16-62) acompanha; falha do job aparece na peça (falhou) e o motivo na ficha — nunca como silêncio. - Credencial cai entre as fases: Fase A recusa; o job grava
sem-credencial/indisponivelna peça (C220); nenhuma peça some e o retry/anexo seguem disponíveis. - Estado compartilhado do global na página: leitura read-only de um boolean; sem escrita, sem cache novo, sem tocar hooks/snapshot.
- Manifesto → curated: editar
e2e-affected-manifest.mjsclassifica o PR como high-risk e roda o conjunto curado (mais cobertura, custo previsível); a entry é o mapeamento durável para diffs futuros do schema. - Token:
loadInstagramFeedjá ignorapaging.next(token na URL); o módulo novo não loga settings nem URLs comaccess_token; o wire carrega sócanonicalUrl/shortcode; a página só recebe o boolean. - Appetite: o maior custo é o dialog; a ordem server→UI deixa o aceite coberto mesmo se o polish atrasar.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto: com credencial, importar traz as recentes; cada novidade é Rascunho no pipeline C220 ou peça-link com motivo; recibo honesto (novas/já estavam/falharam + detalhe de peça-link); nada duplica inclusive link manual em outra grafia; nada é publicado;
curatedFieldspreservado; só perfil próprio/caminho oficial; sem scraping/terceiro/stories; sem credencial → "Instagram ainda não configurado" nas duas pontas; token nunca em log; board/feed intocado. - Invariantes AGENTS/engineering-standards: gate fresco
canReadCommunicationCatalog;user+overrideAccess:false(bypass só com comentário já justificado no pipeline/leitura derivada do global); sem migration/collection/Consent novo;lib/não importautilities/; copy pt-BR e identificadores em inglês; um dono por mecanismo (sem segundo resolvedor/fila/identidade);instagramFeed.tse job/scheduler intocados (ocontentPieceLink.tssó ganha o probe de identidade e o predicado de corrida — nenhuma mudança na resolução). - Testes de domínio previstos (unit/int) verdes e pins atualizados; entry do manifesto adicionada com os invariantes do manifest verdes.
pnpm gate:fastverde;pnpm test:intverde; e2e curado verde (manifesto high-risk);pnpm pushvia GitHub.
Self-score
Self-score decision-quality: 4/5. (1) Decisões caras com Opções/Recomendação/Rejeitadas explícitas (dedupe/identidade sem migration, fluxo em 2 fases, reuso do pipeline, janela, fail-closed); (2) cabe no appetite herdado — reusa pipeline C211/C220, cliente do feed, rotas/actions/envelope, dialog precedente e shells da lista; (3) rabbit holes nomeados (backfill, scraping, álbum, segunda fila, auto-publicar, exclusões do board, painel de varredura); (4) depth check: variantes e classificação no dono puro, dedupe num predicado compartilhado (owner C220 incluso), orquestração nova só onde há volatilidade real, sem pass-through; (5) aceite de produto intacto. Não 5/5 por duas incertezas assumidas honestamente: os estados não desenhados do dialog (buscando/zero/erro) resolvidos por adaptações registradas sob non-trigger, e o contrato de progresso N (found = existing + candidatos) inferido do mock, que pode ser revisado na execução se o design pedir outra leitura.