edusquads-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/edusquads/SKILL.md +212 -0
- package/CLAUDE.md +40 -0
- package/README.md +92 -0
- package/_edusquads/evidencias/EVIDENCIA-MODELO.md +33 -0
- package/_edusquads/memoria/USUARIO-ATIVO.md +42 -0
- package/_edusquads/runs/RUN-MODELO.md +50 -0
- package/_edusquads/runs/RUNS-INDEX.md +25 -0
- package/base/catalogo-dos-160-agentes.md +175 -0
- package/base/comandos/edusquads-comandos.md +46 -0
- package/base/matriz-mestre-dos-16-squads.md +1147 -0
- package/base/matriz-mestre-dos-especialistas.md +579 -0
- package/base/modelos/modelo-de-agente.md +62 -0
- package/base/modelos/modelo-de-skill.md +55 -0
- package/base/paridade-opensquad.md +49 -0
- package/base/playbooks/investigacao/COMO-EXECUTAR.md +51 -0
- package/base/playbooks/investigacao/ESTRUTURACAO.md +28 -0
- package/base/playbooks/investigacao/PLAYBOOK-GERAL.md +34 -0
- package/base/playbooks/investigacao/instagram.md +23 -0
- package/base/playbooks/investigacao/linkedin.md +23 -0
- package/base/playbooks/investigacao/x-twitter.md +23 -0
- package/base/playbooks/investigacao/youtube.md +23 -0
- package/base/protocolo-memoria-usuario.md +50 -0
- package/base/protocolo-playwright-edusquads.md +48 -0
- package/base/scripts/edusquads_concluir_investigacao.py +356 -0
- package/base/scripts/edusquads_estruturar_coleta.py +237 -0
- package/base/scripts/edusquads_investigar.py +279 -0
- package/base/visao-geral.md +46 -0
- package/bin/edusquads.js +146 -0
- package/carrosseis.md +988 -0
- package/especialistas/branding/marty-neumeier.md +39 -0
- package/especialistas/copy/joanna-wiebe.md +41 -0
- package/especialistas/copy/russell-brunson.md +41 -0
- package/especialistas/mensagem/donald-miller.md +41 -0
- package/especialistas/posicionamento/april-dunford.md +39 -0
- package/especialistas/trafego-pago/pedro-sobral.md +41 -0
- package/package.json +31 -0
- package/pesquisa/web/april-dunford.md +37 -0
- package/pesquisa/web/claude-code-comandos.md +30 -0
- package/pesquisa/web/donald-miller.md +29 -0
- package/pesquisa/web/joanna-wiebe.md +29 -0
- package/pesquisa/web/marty-neumeier.md +37 -0
- package/pesquisa/web/opensquad.md +23 -0
- package/pesquisa/web/pedro-sobral.md +29 -0
- package/pesquisa/web/pendentes/biblioteca-pendente.md +20 -0
- package/pesquisa/web/russell-brunson.md +30 -0
- package/squads/01-estrategia/agentes/arquiteto-de-diferencial.md +62 -0
- package/squads/01-estrategia/agentes/auditor-de-coerencia-estrategica.md +62 -0
- package/squads/01-estrategia/agentes/especialista-em-posicionamento.md +61 -0
- package/squads/01-estrategia/agentes/estrategista-de-categoria.md +60 -0
- package/squads/01-estrategia/agentes/estrategista-de-mercado.md +61 -0
- package/squads/01-estrategia/agentes/planejador-de-tese.md +60 -0
- package/squads/01-estrategia/agentes/priorizador-estrategico.md +61 -0
- package/squads/01-estrategia/agentes/revisor-estrategico.md +65 -0
- package/squads/01-estrategia/agentes/sintetizador-estrategico.md +62 -0
- package/squads/01-estrategia/agentes/tradutor-estrategico-para-squads.md +62 -0
- package/squads/01-estrategia/squad.md +70 -0
- package/squads/02-pesquisa/agentes/analista-de-concorrencia.md +62 -0
- package/squads/02-pesquisa/agentes/analista-de-tendencias.md +60 -0
- package/squads/02-pesquisa/agentes/auditor-de-suficiencia-de-pesquisa.md +61 -0
- package/squads/02-pesquisa/agentes/bibliotecario-de-evidencias.md +62 -0
- package/squads/02-pesquisa/agentes/curador-de-fontes.md +61 -0
- package/squads/02-pesquisa/agentes/minerador-de-reviews.md +60 -0
- package/squads/02-pesquisa/agentes/organizador-de-insights.md +61 -0
- package/squads/02-pesquisa/agentes/pesquisador-de-mercado.md +61 -0
- package/squads/02-pesquisa/agentes/pesquisador-de-voz-do-cliente.md +63 -0
- package/squads/02-pesquisa/agentes/revisor-de-pesquisa.md +61 -0
- package/squads/02-pesquisa/squad.md +68 -0
- package/squads/03-copy/agentes/copywriter-de-anuncios.md +65 -0
- package/squads/03-copy/agentes/copywriter-de-email.md +65 -0
- package/squads/03-copy/agentes/copywriter-de-landing-page.md +66 -0
- package/squads/03-copy/agentes/critico-de-conversao.md +65 -0
- package/squads/03-copy/agentes/editor-de-copy-de-conversao.md +63 -0
- package/squads/03-copy/agentes/especialista-em-cta.md +65 -0
- package/squads/03-copy/agentes/especialista-em-headlines.md +63 -0
- package/squads/03-copy/agentes/pesquisador-de-mensagem.md +63 -0
- package/squads/03-copy/agentes/revisor-chefe-de-copy.md +65 -0
- package/squads/03-copy/agentes/roteirista-de-funil.md +63 -0
- package/squads/03-copy/skills/estruturar-hook-story-offer.md +61 -0
- package/squads/03-copy/squad.md +73 -0
- package/squads/04-conteudo/agentes/curador-de-temas.md +60 -0
- package/squads/04-conteudo/agentes/especialista-em-reaproveitamento.md +60 -0
- package/squads/04-conteudo/agentes/estrategista-de-conteudo.md +61 -0
- package/squads/04-conteudo/agentes/organizador-de-calendario.md +61 -0
- package/squads/04-conteudo/agentes/otimizador-editorial.md +62 -0
- package/squads/04-conteudo/agentes/planejador-editorial.md +61 -0
- package/squads/04-conteudo/agentes/redator-de-conteudo-seo.md +61 -0
- package/squads/04-conteudo/agentes/redator-social.md +61 -0
- package/squads/04-conteudo/agentes/revisor-de-conteudo.md +63 -0
- package/squads/04-conteudo/agentes/roteirista-de-conteudo.md +61 -0
- package/squads/04-conteudo/squad.md +70 -0
- package/squads/05-design/agentes/auditor-de-coerencia-visual.md +60 -0
- package/squads/05-design/agentes/designer-de-apresentacoes.md +62 -0
- package/squads/05-design/agentes/designer-de-criativos.md +90 -0
- package/squads/05-design/agentes/designer-de-landing-page.md +62 -0
- package/squads/05-design/agentes/designer-visual.md +72 -0
- package/squads/05-design/agentes/diretor-de-arte.md +71 -0
- package/squads/05-design/agentes/especialista-em-sistemas-visuais.md +60 -0
- package/squads/05-design/agentes/revisor-de-design.md +75 -0
- package/squads/05-design/agentes/revisor-de-hierarquia-visual.md +60 -0
- package/squads/05-design/agentes/tradutor-de-marca-para-design.md +62 -0
- package/squads/05-design/squad.md +67 -0
- package/squads/06-branding/agentes/arquiteto-de-diferenciacao.md +62 -0
- package/squads/06-branding/agentes/auditor-de-coerencia-de-marca.md +62 -0
- package/squads/06-branding/agentes/designer-de-narrativa-de-marca.md +62 -0
- package/squads/06-branding/agentes/estrategista-de-marca.md +62 -0
- package/squads/06-branding/agentes/guardiao-de-consistencia.md +62 -0
- package/squads/06-branding/agentes/guardiao-de-tom.md +62 -0
- package/squads/06-branding/agentes/planejador-de-identidade.md +60 -0
- package/squads/06-branding/agentes/revisor-de-distincao.md +60 -0
- package/squads/06-branding/agentes/revisor-de-marca.md +64 -0
- package/squads/06-branding/agentes/unificador-de-linguagem.md +60 -0
- package/squads/06-branding/squad.md +67 -0
- package/squads/07-ux-experiencia/agentes/analista-de-friccao.md +62 -0
- package/squads/07-ux-experiencia/agentes/arquiteto-de-informacao.md +60 -0
- package/squads/07-ux-experiencia/agentes/critico-de-jornada.md +62 -0
- package/squads/07-ux-experiencia/agentes/especialista-em-microcopy.md +63 -0
- package/squads/07-ux-experiencia/agentes/otimizador-de-formularios.md +62 -0
- package/squads/07-ux-experiencia/agentes/planejador-de-onboarding.md +62 -0
- package/squads/07-ux-experiencia/agentes/revisor-de-clareza-de-interface.md +60 -0
- package/squads/07-ux-experiencia/agentes/revisor-de-fluxo.md +60 -0
- package/squads/07-ux-experiencia/agentes/revisor-de-ux.md +62 -0
- package/squads/07-ux-experiencia/agentes/ux-writer.md +62 -0
- package/squads/07-ux-experiencia/squad.md +68 -0
- package/squads/08-growth/agentes/analista-de-alavancas.md +63 -0
- package/squads/08-growth/agentes/analista-de-gargalos.md +63 -0
- package/squads/08-growth/agentes/especialista-em-conversao.md +65 -0
- package/squads/08-growth/agentes/estrategista-de-growth.md +65 -0
- package/squads/08-growth/agentes/integrador-de-funil.md +64 -0
- package/squads/08-growth/agentes/leitor-de-performance-de-crescimento.md +63 -0
- package/squads/08-growth/agentes/planejador-de-testes.md +63 -0
- package/squads/08-growth/agentes/priorizador-de-experimentos.md +63 -0
- package/squads/08-growth/agentes/revisor-de-growth.md +64 -0
- package/squads/08-growth/agentes/revisor-de-hipoteses.md +64 -0
- package/squads/08-growth/squad.md +69 -0
- package/squads/09-seo/agentes/analista-de-intencao-de-busca.md +60 -0
- package/squads/09-seo/agentes/auditor-de-seo.md +60 -0
- package/squads/09-seo/agentes/criador-de-brief-seo.md +61 -0
- package/squads/09-seo/agentes/estrategista-de-seo.md +61 -0
- package/squads/09-seo/agentes/organizador-de-arquitetura-tematica.md +61 -0
- package/squads/09-seo/agentes/pesquisador-de-palavras-chave.md +60 -0
- package/squads/09-seo/agentes/planejador-de-clusters.md +61 -0
- package/squads/09-seo/agentes/planejador-de-links-internos.md +60 -0
- package/squads/09-seo/agentes/revisor-de-seo.md +61 -0
- package/squads/09-seo/agentes/revisor-de-serp-fit.md +60 -0
- package/squads/09-seo/squad.md +67 -0
- package/squads/10-comercial/agentes/arquiteto-de-narrativa-comercial.md +65 -0
- package/squads/10-comercial/agentes/copywriter-de-follow-up-comercial.md +63 -0
- package/squads/10-comercial/agentes/criador-de-soundbites-comerciais.md +62 -0
- package/squads/10-comercial/agentes/estrategista-comercial.md +65 -0
- package/squads/10-comercial/agentes/estruturador-de-proposta.md +63 -0
- package/squads/10-comercial/agentes/organizador-de-materiais-comerciais.md +62 -0
- package/squads/10-comercial/agentes/redator-de-pitch.md +63 -0
- package/squads/10-comercial/agentes/revisor-comercial.md +65 -0
- package/squads/10-comercial/agentes/revisor-de-objecoes.md +63 -0
- package/squads/10-comercial/agentes/tradutor-de-posicionamento-para-vendas.md +62 -0
- package/squads/10-comercial/squad.md +70 -0
- package/squads/11-oferta-monetizacao/agentes/analista-de-logica-de-monetizacao.md +60 -0
- package/squads/11-oferta-monetizacao/agentes/arquiteto-de-oferta.md +62 -0
- package/squads/11-oferta-monetizacao/agentes/designer-de-bonus.md +60 -0
- package/squads/11-oferta-monetizacao/agentes/especialista-em-garantia.md +61 -0
- package/squads/11-oferta-monetizacao/agentes/estrategista-de-monetizacao.md +62 -0
- package/squads/11-oferta-monetizacao/agentes/estruturador-de-pacotes.md +61 -0
- package/squads/11-oferta-monetizacao/agentes/planejador-de-escada-de-valor.md +61 -0
- package/squads/11-oferta-monetizacao/agentes/planejador-de-progressao.md +60 -0
- package/squads/11-oferta-monetizacao/agentes/revisor-de-oferta.md +61 -0
- package/squads/11-oferta-monetizacao/agentes/revisor-de-stack-de-valor.md +60 -0
- package/squads/11-oferta-monetizacao/squad.md +69 -0
- package/squads/12-operacoes/agentes/arquiteto-de-operacoes.md +61 -0
- package/squads/12-operacoes/agentes/auditor-de-processo.md +60 -0
- package/squads/12-operacoes/agentes/criador-de-checklist.md +60 -0
- package/squads/12-operacoes/agentes/documentador-operacional.md +62 -0
- package/squads/12-operacoes/agentes/estruturador-de-passagens-entre-squads.md +61 -0
- package/squads/12-operacoes/agentes/mapeador-de-fluxos.md +60 -0
- package/squads/12-operacoes/agentes/organizador-de-playbooks.md +60 -0
- package/squads/12-operacoes/agentes/redator-de-sop.md +61 -0
- package/squads/12-operacoes/agentes/revisor-operacional.md +64 -0
- package/squads/12-operacoes/agentes/verificador-de-governanca.md +61 -0
- package/squads/12-operacoes/squad.md +64 -0
- package/squads/13-qualidade/agentes/auditor-de-consistencia.md +62 -0
- package/squads/13-qualidade/agentes/auditor-de-friccao.md +62 -0
- package/squads/13-qualidade/agentes/critico-de-conversao.md +65 -0
- package/squads/13-qualidade/agentes/guardiao-de-aprovacao-final.md +62 -0
- package/squads/13-qualidade/agentes/identificador-de-risco-de-entrega.md +62 -0
- package/squads/13-qualidade/agentes/lider-de-qualidade.md +62 -0
- package/squads/13-qualidade/agentes/revisor-de-clareza.md +61 -0
- package/squads/13-qualidade/agentes/revisor-de-coerencia-de-mensagem.md +62 -0
- package/squads/13-qualidade/agentes/revisor-de-prontidao.md +62 -0
- package/squads/13-qualidade/agentes/verificador-de-logica.md +60 -0
- package/squads/13-qualidade/squad.md +63 -0
- package/squads/14-automacao-sistemas/agentes/arquiteto-de-sistema.md +61 -0
- package/squads/14-automacao-sistemas/agentes/auditor-de-coerencia-sistemica.md +62 -0
- package/squads/14-automacao-sistemas/agentes/curador-de-matrizes.md +60 -0
- package/squads/14-automacao-sistemas/agentes/designer-de-orquestracao.md +61 -0
- package/squads/14-automacao-sistemas/agentes/estruturador-de-skills.md +61 -0
- package/squads/14-automacao-sistemas/agentes/gestor-de-estado.md +61 -0
- package/squads/14-automacao-sistemas/agentes/guardiao-da-estrutura-do-framework.md +62 -0
- package/squads/14-automacao-sistemas/agentes/integrador-de-fluxos.md +61 -0
- package/squads/14-automacao-sistemas/agentes/planejador-de-automacao.md +61 -0
- package/squads/14-automacao-sistemas/agentes/revisor-de-sistema.md +62 -0
- package/squads/14-automacao-sistemas/squad.md +66 -0
- package/squads/15-executivo-pmo/agentes/coordenador-de-pmo.md +61 -0
- package/squads/15-executivo-pmo/agentes/definidor-de-escopo.md +62 -0
- package/squads/15-executivo-pmo/agentes/estrategista-executivo.md +62 -0
- package/squads/15-executivo-pmo/agentes/guardiao-de-escopo.md +61 -0
- package/squads/15-executivo-pmo/agentes/organizador-de-ativacao-de-squads.md +61 -0
- package/squads/15-executivo-pmo/agentes/planejador-de-prioridades.md +61 -0
- package/squads/15-executivo-pmo/agentes/registrador-de-decisoes.md +61 -0
- package/squads/15-executivo-pmo/agentes/revisor-de-dependencias.md +60 -0
- package/squads/15-executivo-pmo/agentes/revisor-executivo.md +63 -0
- package/squads/15-executivo-pmo/agentes/sequenciador-de-execucao.md +60 -0
- package/squads/15-executivo-pmo/squad.md +66 -0
- package/squads/16-trafego-pago/agentes/analista-de-metricas-e-otimizacao.md +62 -0
- package/squads/16-trafego-pago/agentes/analista-de-publicos.md +61 -0
- package/squads/16-trafego-pago/agentes/auditor-de-contas-e-campanhas.md +60 -0
- package/squads/16-trafego-pago/agentes/escalador-de-campanhas.md +61 -0
- package/squads/16-trafego-pago/agentes/especialista-em-criativos-de-performance.md +61 -0
- package/squads/16-trafego-pago/agentes/especialista-em-estrutura-de-funil-pago.md +63 -0
- package/squads/16-trafego-pago/agentes/estrategista-de-trafego-pago.md +67 -0
- package/squads/16-trafego-pago/agentes/gestor-de-remarketing.md +63 -0
- package/squads/16-trafego-pago/agentes/planejador-de-campanhas.md +61 -0
- package/squads/16-trafego-pago/agentes/revisor-de-performance.md +62 -0
- package/squads/16-trafego-pago/skills/calcular-faixa-de-investimento.md +61 -0
- package/squads/16-trafego-pago/squad.md +73 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fase 5/6 do /edusquads investigar.
|
|
4
|
+
|
|
5
|
+
Conclui uma investigação a partir do run_id:
|
|
6
|
+
- preenche síntese interpretativa com filtros por plataforma
|
|
7
|
+
- preenche extrações úteis por squad
|
|
8
|
+
- calcula score de confiança dos insights
|
|
9
|
+
- aproveita coleta estruturada (quando disponível)
|
|
10
|
+
- registra limites da evidência
|
|
11
|
+
- atualiza status do run para concluído
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import collections
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
23
|
+
EVID_DIR = ROOT / "_edusquads" / "evidencias"
|
|
24
|
+
RUNS_DIR = ROOT / "_edusquads" / "runs"
|
|
25
|
+
RUNS_INDEX = RUNS_DIR / "RUNS-INDEX.md"
|
|
26
|
+
|
|
27
|
+
BASE_STOPWORDS = {
|
|
28
|
+
"para", "com", "uma", "como", "mais", "sobre", "esta", "esse", "isso", "pela", "pelo",
|
|
29
|
+
"from", "meta", "instagram", "https", "www", "login", "account", "your", "you", "and", "the",
|
|
30
|
+
"that", "this", "from", "have", "will", "where", "which", "quando", "depois", "entre", "tambem",
|
|
31
|
+
"password", "email", "mobile", "create", "forgot", "terms", "privacy", "jobs", "help", "blog",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
PLATFORM_NOISE = {
|
|
35
|
+
"instagram": {"threads", "verified", "accounts", "facebook", "meta", "moments"},
|
|
36
|
+
"youtube": {"shorts", "subscribe", "watch", "playlist", "channel"},
|
|
37
|
+
"linkedin": {"connect", "message", "premium", "hiring", "follow"},
|
|
38
|
+
"x-twitter": {"retweet", "reply", "quote", "tweet", "tweets"},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
CTA_RE = re.compile(r"\b(comente|clique|saiba|inscreva|assine|dm|direct|link|cadastre|baixe|seguir|salvar|compartilhar|agende|mensagem)\b", re.I)
|
|
42
|
+
FORMAT_RE = re.compile(r"\b(carrossel|reel|reels|video|vídeo|thread|post|posts|live|stories|newsletter|artigo|webinar|aula|corte)\b", re.I)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clamp(v: int, lo: int = 0, hi: int = 100) -> int:
|
|
46
|
+
return max(lo, min(hi, v))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_evidence_file(run_id: str) -> Path:
|
|
50
|
+
matches = sorted(EVID_DIR.glob(f"EVIDENCIA-{run_id}-*.md"))
|
|
51
|
+
if not matches:
|
|
52
|
+
raise SystemExit(f"Nenhuma evidência encontrada para {run_id}")
|
|
53
|
+
return matches[0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def find_run_file(run_id: str) -> Path:
|
|
57
|
+
p = RUNS_DIR / f"{run_id}.md"
|
|
58
|
+
if not p.exists():
|
|
59
|
+
raise SystemExit(f"Run não encontrado: {p}")
|
|
60
|
+
return p
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_field(pattern: str, text: str, fallback: str = "") -> str:
|
|
64
|
+
m = re.search(pattern, text)
|
|
65
|
+
return m.group(1).strip() if m else fallback
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_top_terms(text: str, platform: str, n: int = 8) -> list[str]:
|
|
69
|
+
words = re.findall(r"[A-Za-zÀ-ÿ]{4,}", text.lower())
|
|
70
|
+
noise = PLATFORM_NOISE.get(platform, set())
|
|
71
|
+
words = [w for w in words if w not in BASE_STOPWORDS and w not in noise]
|
|
72
|
+
counter = collections.Counter(words)
|
|
73
|
+
return [w for w, _ in counter.most_common(n)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def extract_matches(pattern: re.Pattern[str], text: str, limit: int = 8) -> list[str]:
|
|
77
|
+
found = [m.group(1).lower() for m in pattern.finditer(text)]
|
|
78
|
+
uniq: list[str] = []
|
|
79
|
+
for x in found:
|
|
80
|
+
if x not in uniq:
|
|
81
|
+
uniq.append(x)
|
|
82
|
+
return uniq[:limit]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_structured_if_exists(run_id: str) -> dict | None:
|
|
86
|
+
p = EVID_DIR / f"ESTRUTURADA-{run_id}.json"
|
|
87
|
+
if not p.exists():
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def compute_confidence(
|
|
96
|
+
source_text: str,
|
|
97
|
+
terms: list[str],
|
|
98
|
+
formats: list[str],
|
|
99
|
+
ctas: list[str],
|
|
100
|
+
limitations: str,
|
|
101
|
+
structured: dict | None,
|
|
102
|
+
) -> dict[str, int]:
|
|
103
|
+
chars = len(source_text)
|
|
104
|
+
score_base = 20
|
|
105
|
+
|
|
106
|
+
if chars >= 3000:
|
|
107
|
+
score_base += 30
|
|
108
|
+
elif chars >= 1500:
|
|
109
|
+
score_base += 20
|
|
110
|
+
elif chars >= 800:
|
|
111
|
+
score_base += 10
|
|
112
|
+
else:
|
|
113
|
+
score_base += 2
|
|
114
|
+
|
|
115
|
+
score_base += min(len(terms) * 4, 20)
|
|
116
|
+
score_base += min(len(formats) * 6, 18)
|
|
117
|
+
score_base += min(len(ctas) * 6, 18)
|
|
118
|
+
|
|
119
|
+
low_access_markers = ["login", "sem autenticação", "acesso parcial", "bloqueado", "exigiu", "http 999", "999"]
|
|
120
|
+
if any(m in limitations.lower() for m in low_access_markers):
|
|
121
|
+
score_base -= 20
|
|
122
|
+
|
|
123
|
+
# Bônus da coleta estruturada (Fase 6)
|
|
124
|
+
if structured:
|
|
125
|
+
completeness = int(structured.get("quality", {}).get("completeness_score", 0))
|
|
126
|
+
score_base += min(completeness // 8, 10)
|
|
127
|
+
|
|
128
|
+
narrativa = clamp(score_base)
|
|
129
|
+
formato = clamp(score_base - (10 if not formats else 0))
|
|
130
|
+
cta = clamp(score_base - (12 if not ctas else 0))
|
|
131
|
+
geral = clamp(round((narrativa + formato + cta) / 3))
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"geral": geral,
|
|
135
|
+
"narrativa": narrativa,
|
|
136
|
+
"formato": formato,
|
|
137
|
+
"cta": cta,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def merge_with_structured(
|
|
142
|
+
terms: list[str],
|
|
143
|
+
formats: list[str],
|
|
144
|
+
ctas: list[str],
|
|
145
|
+
structured: dict | None,
|
|
146
|
+
) -> tuple[list[str], list[str], list[str], int]:
|
|
147
|
+
completeness = 0
|
|
148
|
+
if not structured:
|
|
149
|
+
return terms, formats, ctas, completeness
|
|
150
|
+
|
|
151
|
+
s = structured.get("signals", {})
|
|
152
|
+
s_terms = [str(x).lower() for x in s.get("topics", [])]
|
|
153
|
+
s_formats = [str(x).lower() for x in s.get("formats", [])]
|
|
154
|
+
s_ctas = [str(x).lower() for x in s.get("ctas", [])]
|
|
155
|
+
completeness = int(structured.get("quality", {}).get("completeness_score", 0))
|
|
156
|
+
|
|
157
|
+
def merge(a: list[str], b: list[str], limit: int = 10) -> list[str]:
|
|
158
|
+
out = list(a)
|
|
159
|
+
for item in b:
|
|
160
|
+
if item not in out:
|
|
161
|
+
out.append(item)
|
|
162
|
+
if len(out) >= limit:
|
|
163
|
+
break
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
return merge(terms, s_terms), merge(formats, s_formats), merge(ctas, s_ctas), completeness
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def build_synthesis(
|
|
170
|
+
run_id: str,
|
|
171
|
+
platform: str,
|
|
172
|
+
source_text: str,
|
|
173
|
+
target: str,
|
|
174
|
+
limitations: str,
|
|
175
|
+
) -> tuple[list[str], dict[str, list[str]], dict[str, int], int]:
|
|
176
|
+
terms = extract_top_terms(source_text, platform=platform)
|
|
177
|
+
ctas = extract_matches(CTA_RE, source_text)
|
|
178
|
+
formats = extract_matches(FORMAT_RE, source_text)
|
|
179
|
+
|
|
180
|
+
structured = load_structured_if_exists(run_id)
|
|
181
|
+
terms, formats, ctas, completeness = merge_with_structured(terms, formats, ctas, structured)
|
|
182
|
+
|
|
183
|
+
conf = compute_confidence(source_text, terms, formats, ctas, limitations, structured)
|
|
184
|
+
|
|
185
|
+
synthesis = [
|
|
186
|
+
f"Coleta processada para `{target}` com filtro de ruído para {platform}.",
|
|
187
|
+
f"Termos recorrentes úteis: {', '.join(terms) if terms else 'não identificado (fonte limitada)'}.",
|
|
188
|
+
f"Formatos detectados: {', '.join(formats) if formats else 'não detectado com confiabilidade'}.",
|
|
189
|
+
f"Sinais de CTA detectados: {', '.join(ctas) if ctas else 'não detectado com confiabilidade'}.",
|
|
190
|
+
f"Confiabilidade geral estimada: {conf['geral']}/100.",
|
|
191
|
+
]
|
|
192
|
+
if structured:
|
|
193
|
+
synthesis.append(f"Coleta estruturada aplicada com completude {completeness}/100.")
|
|
194
|
+
|
|
195
|
+
squads = {
|
|
196
|
+
"estrategia": [
|
|
197
|
+
f"Mapear narrativa central usando termos: {', '.join(terms[:4]) if terms else 'dados insuficientes'}.",
|
|
198
|
+
"Confrontar sinais observados com a tese/posicionamento atual da marca.",
|
|
199
|
+
],
|
|
200
|
+
"conteudo": [
|
|
201
|
+
f"Priorizar formatos observados: {', '.join(formats[:4]) if formats else 'testar carrossel/reel/post por hipótese'}.",
|
|
202
|
+
"Montar backlog de 3 temas derivados dos termos recorrentes.",
|
|
203
|
+
],
|
|
204
|
+
"copy": [
|
|
205
|
+
f"Testar variações de CTA com base em: {', '.join(ctas[:4]) if ctas else 'comente/salve/compartilhe'}.",
|
|
206
|
+
"Refinar hooks para promessa específica + ganho imediato.",
|
|
207
|
+
],
|
|
208
|
+
"design": [
|
|
209
|
+
"Traduzir formato dominante em padrão visual consistente por canal.",
|
|
210
|
+
"Aplicar contraste/hierarquia para leitura rápida no feed.",
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
return synthesis, squads, conf, completeness
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def update_evidence(
|
|
217
|
+
evidence_path: Path,
|
|
218
|
+
synthesis: list[str],
|
|
219
|
+
squads: dict[str, list[str]],
|
|
220
|
+
source_ref: str,
|
|
221
|
+
limitations: str,
|
|
222
|
+
confidence: dict[str, int],
|
|
223
|
+
completeness: int,
|
|
224
|
+
) -> None:
|
|
225
|
+
txt = evidence_path.read_text(encoding="utf-8")
|
|
226
|
+
|
|
227
|
+
txt = txt.replace(
|
|
228
|
+
"- (preencher após investigação de navegação)",
|
|
229
|
+
"\n".join(f"- {line}" for line in synthesis),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
extractions_block = (
|
|
233
|
+
"## Extrações úteis para squads\n"
|
|
234
|
+
"- Squad de Estratégia:\n"
|
|
235
|
+
+ "\n".join(f" - {b}" for b in squads["estrategia"]) + "\n"
|
|
236
|
+
+ "- Squad de Conteúdo:\n"
|
|
237
|
+
+ "\n".join(f" - {b}" for b in squads["conteudo"]) + "\n"
|
|
238
|
+
+ "- Squad de Copy:\n"
|
|
239
|
+
+ "\n".join(f" - {b}" for b in squads["copy"]) + "\n"
|
|
240
|
+
+ "- Squad de Design:\n"
|
|
241
|
+
+ "\n".join(f" - {b}" for b in squads["design"]) + "\n\n"
|
|
242
|
+
+ "## Confiabilidade dos insights\n"
|
|
243
|
+
+ f"- score_geral: {confidence['geral']}/100\n"
|
|
244
|
+
+ f"- score_narrativa: {confidence['narrativa']}/100\n"
|
|
245
|
+
+ f"- score_formato: {confidence['formato']}/100\n"
|
|
246
|
+
+ f"- score_cta: {confidence['cta']}/100\n"
|
|
247
|
+
+ f"- score_completude_coleta_estruturada: {completeness}/100\n\n"
|
|
248
|
+
+ "## Limites da evidência\n"
|
|
249
|
+
+ f"- {limitations}\n"
|
|
250
|
+
+ f"- Fonte usada na síntese: {source_ref}\n"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
txt = re.sub(
|
|
254
|
+
r"## Extrações úteis para squads\n(?:.|\n)*?## Segurança\n",
|
|
255
|
+
extractions_block + "\n## Segurança\n",
|
|
256
|
+
txt,
|
|
257
|
+
flags=re.M,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
evidence_path.write_text(txt, encoding="utf-8")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def update_run(run_path: Path, source_ref: str, confidence: dict[str, int], completeness: int) -> None:
|
|
264
|
+
txt = run_path.read_text(encoding="utf-8")
|
|
265
|
+
txt = txt.replace("- status: em-andamento", "- status: concluído")
|
|
266
|
+
|
|
267
|
+
txt = txt.replace(
|
|
268
|
+
"### Checkpoint de produção\n- resultado: em-andamento\n- observações: investigação web pendente de execução completa.",
|
|
269
|
+
"### Checkpoint de produção\n- resultado: aprovado\n- observações: investigação executada e evidência preenchida automaticamente.",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
txt = re.sub(
|
|
273
|
+
r"### Checkpoint de qualidade\n- resultado: .*\n- observações: .*",
|
|
274
|
+
"### Checkpoint de qualidade\n- resultado: aprovado\n"
|
|
275
|
+
f"- observações: síntese com score de confiança {confidence['geral']}/100 e completude estruturada {completeness}/100.",
|
|
276
|
+
txt,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
txt = txt.replace(
|
|
280
|
+
"- executar navegação e preencher síntese interpretativa da evidência.\n- atualizar status para concluído ao finalizar a investigação.",
|
|
281
|
+
"- none.",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
marker = "- validações realizadas:\n"
|
|
285
|
+
if marker in txt and source_ref not in txt:
|
|
286
|
+
txt = txt.replace(marker, marker + f" - síntese automática concluída com base em `{source_ref}`\n")
|
|
287
|
+
|
|
288
|
+
txt = txt.replace(str(ROOT).replace('\\', '/').rstrip('/'), "")
|
|
289
|
+
txt = txt.replace(str(ROOT).replace('/', '\\').rstrip('\\'), "")
|
|
290
|
+
txt = txt.replace("`/_edusquads/", "`_edusquads/")
|
|
291
|
+
|
|
292
|
+
run_path.write_text(txt, encoding="utf-8")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def update_runs_index_status(run_id: str, new_status: str = "concluído") -> None:
|
|
296
|
+
if not RUNS_INDEX.exists():
|
|
297
|
+
return
|
|
298
|
+
txt = RUNS_INDEX.read_text(encoding="utf-8")
|
|
299
|
+
pattern = rf"(\|\s*{re.escape(run_id)}\s*\|[^\n]*\|\s*)(planejado|em-andamento|bloqueado|concluído)(\s*\|[^\n]*\n)"
|
|
300
|
+
txt = re.sub(pattern, rf"\1{new_status}\3", txt)
|
|
301
|
+
RUNS_INDEX.write_text(txt, encoding="utf-8")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def main() -> int:
|
|
305
|
+
parser = argparse.ArgumentParser(description="Concluir investigação do /edusquads")
|
|
306
|
+
parser.add_argument("run_id", help="ID do run, ex: RUN-2026-04-11-005")
|
|
307
|
+
parser.add_argument("--fonte", required=True, help="Arquivo markdown fonte da coleta")
|
|
308
|
+
parser.add_argument("--limites", default="Coleta parcial ou condicionada por acesso/login da plataforma.")
|
|
309
|
+
args = parser.parse_args()
|
|
310
|
+
|
|
311
|
+
run_id = args.run_id.strip()
|
|
312
|
+
run_path = find_run_file(run_id)
|
|
313
|
+
evidence_path = find_evidence_file(run_id)
|
|
314
|
+
source_path = Path(args.fonte)
|
|
315
|
+
if not source_path.is_absolute():
|
|
316
|
+
source_path = (ROOT / source_path).resolve()
|
|
317
|
+
if not source_path.exists():
|
|
318
|
+
raise SystemExit(f"Fonte não encontrada: {source_path}")
|
|
319
|
+
|
|
320
|
+
source_text = source_path.read_text(encoding="utf-8", errors="ignore")
|
|
321
|
+
ev_text = evidence_path.read_text(encoding="utf-8")
|
|
322
|
+
target = extract_field(r"- url/perfil/query: (.+)", ev_text, "alvo não identificado")
|
|
323
|
+
platform = extract_field(r"- plataforma detectada: (.+)", ev_text, "instagram").lower()
|
|
324
|
+
|
|
325
|
+
synthesis, squads, confidence, completeness = build_synthesis(
|
|
326
|
+
run_id=run_id,
|
|
327
|
+
platform=platform,
|
|
328
|
+
source_text=source_text,
|
|
329
|
+
target=target,
|
|
330
|
+
limitations=args.limites,
|
|
331
|
+
)
|
|
332
|
+
source_ref = source_path.relative_to(ROOT).as_posix()
|
|
333
|
+
|
|
334
|
+
update_evidence(
|
|
335
|
+
evidence_path=evidence_path,
|
|
336
|
+
synthesis=synthesis,
|
|
337
|
+
squads=squads,
|
|
338
|
+
source_ref=source_ref,
|
|
339
|
+
limitations=args.limites,
|
|
340
|
+
confidence=confidence,
|
|
341
|
+
completeness=completeness,
|
|
342
|
+
)
|
|
343
|
+
update_run(run_path=run_path, source_ref=source_ref, confidence=confidence, completeness=completeness)
|
|
344
|
+
update_runs_index_status(run_id=run_id, new_status="concluído")
|
|
345
|
+
|
|
346
|
+
print(f"run_id={run_id}")
|
|
347
|
+
print(f"run_file={run_path.relative_to(ROOT).as_posix()}")
|
|
348
|
+
print(f"evidence_file={evidence_path.relative_to(ROOT).as_posix()}")
|
|
349
|
+
print(f"source_file={source_ref}")
|
|
350
|
+
print(f"confidence={confidence['geral']}/100")
|
|
351
|
+
print(f"structured_completeness={completeness}/100")
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fase 6 do /edusquads investigar.
|
|
4
|
+
|
|
5
|
+
Estrutura a coleta bruta em um artefato padronizado por plataforma:
|
|
6
|
+
- gera JSON estruturado por run
|
|
7
|
+
- gera resumo markdown para consumo humano
|
|
8
|
+
- calcula score de completude da coleta
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import collections
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
20
|
+
EVID_DIR = ROOT / "_edusquads" / "evidencias"
|
|
21
|
+
RUNS_DIR = ROOT / "_edusquads" / "runs"
|
|
22
|
+
|
|
23
|
+
STOPWORDS = {
|
|
24
|
+
"para", "com", "uma", "como", "mais", "sobre", "esta", "esse", "isso", "pela", "pelo",
|
|
25
|
+
"from", "meta", "instagram", "https", "www", "login", "account", "your", "you", "and", "the",
|
|
26
|
+
"that", "this", "from", "have", "will", "where", "which", "quando", "depois", "entre", "tambem",
|
|
27
|
+
"password", "email", "mobile", "create", "forgot", "terms", "privacy", "jobs", "help", "blog",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
FORMAT_RE = re.compile(r"\b(carrossel|reel|reels|video|vídeo|thread|post|posts|live|stories|newsletter|artigo|webinar|aula|corte)\b", re.I)
|
|
31
|
+
CTA_RE = re.compile(r"\b(comente|clique|saiba|inscreva|assine|dm|direct|link|cadastre|baixe|seguir|salvar|compartilhar|agende|mensagem|comprar|fale)\b", re.I)
|
|
32
|
+
HASHTAG_RE = re.compile(r"#([A-Za-z0-9_]{2,})")
|
|
33
|
+
URL_RE = re.compile(r"https?://[^\s)]+")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clamp(v: int, lo: int = 0, hi: int = 100) -> int:
|
|
37
|
+
return max(lo, min(hi, v))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_run(run_id: str) -> Path:
|
|
41
|
+
p = RUNS_DIR / f"{run_id}.md"
|
|
42
|
+
if not p.exists():
|
|
43
|
+
raise SystemExit(f"Run não encontrado: {p}")
|
|
44
|
+
return p
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_evidence(run_id: str) -> Path:
|
|
48
|
+
matches = sorted(EVID_DIR.glob(f"EVIDENCIA-{run_id}-*.md"))
|
|
49
|
+
if not matches:
|
|
50
|
+
raise SystemExit(f"Evidência não encontrada para {run_id}")
|
|
51
|
+
return matches[0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def detect_platform_from_evidence(evidence_text: str) -> str:
|
|
55
|
+
m = re.search(r"- plataforma detectada: (.+)", evidence_text)
|
|
56
|
+
return m.group(1).strip().lower() if m else "instagram"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def extract_target(evidence_text: str) -> str:
|
|
60
|
+
m = re.search(r"- url/perfil/query: (.+)", evidence_text)
|
|
61
|
+
return m.group(1).strip() if m else "não identificado"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def uniq(items: list[str], limit: int = 20) -> list[str]:
|
|
65
|
+
out: list[str] = []
|
|
66
|
+
for i in items:
|
|
67
|
+
s = i.strip()
|
|
68
|
+
if s and s not in out:
|
|
69
|
+
out.append(s)
|
|
70
|
+
if len(out) >= limit:
|
|
71
|
+
break
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_top_terms(text: str, limit: int = 12) -> list[str]:
|
|
76
|
+
words = re.findall(r"[A-Za-zÀ-ÿ]{4,}", text.lower())
|
|
77
|
+
words = [w for w in words if w not in STOPWORDS]
|
|
78
|
+
counter = collections.Counter(words)
|
|
79
|
+
return [w for w, _ in counter.most_common(limit)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def platform_fields(platform: str, text: str) -> dict:
|
|
83
|
+
t = text.lower()
|
|
84
|
+
|
|
85
|
+
if platform == "instagram":
|
|
86
|
+
return {
|
|
87
|
+
"bio_detectada": bool(re.search(r"\bbio\b|perfil|creator|digital|marketing", t)),
|
|
88
|
+
"destaques_detectados": bool(re.search(r"destaques|highlights", t)),
|
|
89
|
+
"sinal_reels": bool(re.search(r"\breels?\b", t)),
|
|
90
|
+
"sinal_carrossel": bool(re.search(r"\bcarrossel|slides?\b", t)),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if platform == "youtube":
|
|
94
|
+
return {
|
|
95
|
+
"sinal_titulo_video": bool(re.search(r"\bwatch\b|\bvídeo\b|\bvideo\b", t)),
|
|
96
|
+
"sinal_thumb": bool(re.search(r"\bthumb|thumbnail\b", t)),
|
|
97
|
+
"sinal_playlist": bool(re.search(r"\bplaylist\b", t)),
|
|
98
|
+
"sinal_descricao": bool(re.search(r"\bdescrição|description\b", t)),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if platform == "linkedin":
|
|
102
|
+
return {
|
|
103
|
+
"sinal_headline": bool(re.search(r"\bheadline\b|\btitle\b|\bcargo\b", t)),
|
|
104
|
+
"sinal_about": bool(re.search(r"\babout\b|\bsobre\b", t)),
|
|
105
|
+
"sinal_post": bool(re.search(r"\bpost|publicação|publicacao\b", t)),
|
|
106
|
+
"sinal_prova_social": bool(re.search(r"\bcomentários|comentarios|recomendações|recomendacoes\b", t)),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if platform == "x-twitter":
|
|
110
|
+
return {
|
|
111
|
+
"sinal_thread": bool(re.search(r"\bthread\b", t)),
|
|
112
|
+
"sinal_reply": bool(re.search(r"\breply|resposta\b", t)),
|
|
113
|
+
"sinal_retweet": bool(re.search(r"\bretweet\b", t)),
|
|
114
|
+
"sinal_link_externo": bool(re.search(r"http", t)),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {"sinal_generico": True}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def completeness_score(chars: int, formats: list[str], ctas: list[str], links: list[str], pf: dict) -> int:
|
|
121
|
+
score = 20
|
|
122
|
+
if chars > 2500:
|
|
123
|
+
score += 30
|
|
124
|
+
elif chars > 1200:
|
|
125
|
+
score += 20
|
|
126
|
+
elif chars > 600:
|
|
127
|
+
score += 10
|
|
128
|
+
|
|
129
|
+
score += min(len(formats) * 8, 20)
|
|
130
|
+
score += min(len(ctas) * 8, 20)
|
|
131
|
+
score += min(len(links) * 3, 10)
|
|
132
|
+
score += sum(1 for v in pf.values() if v) * 3
|
|
133
|
+
return clamp(score)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def write_outputs(run_id: str, data: dict) -> tuple[Path, Path]:
|
|
137
|
+
json_path = EVID_DIR / f"ESTRUTURADA-{run_id}.json"
|
|
138
|
+
md_path = EVID_DIR / f"ESTRUTURADA-{run_id}.md"
|
|
139
|
+
|
|
140
|
+
json_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
md = [
|
|
143
|
+
f"# Coleta Estruturada — {run_id}",
|
|
144
|
+
"",
|
|
145
|
+
"## Metadados",
|
|
146
|
+
f"- plataforma: {data['platform']}",
|
|
147
|
+
f"- alvo: {data['target']}",
|
|
148
|
+
f"- caracteres_fonte: {data['source']['chars']}",
|
|
149
|
+
f"- linhas_fonte: {data['source']['lines']}",
|
|
150
|
+
"",
|
|
151
|
+
"## Sinais extraídos",
|
|
152
|
+
f"- formatos: {', '.join(data['signals']['formats']) if data['signals']['formats'] else 'nenhum'}",
|
|
153
|
+
f"- ctas: {', '.join(data['signals']['ctas']) if data['signals']['ctas'] else 'nenhum'}",
|
|
154
|
+
f"- hashtags: {', '.join(data['signals']['hashtags']) if data['signals']['hashtags'] else 'nenhuma'}",
|
|
155
|
+
f"- links_detectados: {len(data['signals']['links'])}",
|
|
156
|
+
f"- topicos: {', '.join(data['signals']['topics']) if data['signals']['topics'] else 'nenhum'}",
|
|
157
|
+
"",
|
|
158
|
+
"## Campos por plataforma",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for k, v in data["platform_fields"].items():
|
|
162
|
+
md.append(f"- {k}: {'sim' if v else 'não'}")
|
|
163
|
+
|
|
164
|
+
md += [
|
|
165
|
+
"",
|
|
166
|
+
"## Qualidade da coleta",
|
|
167
|
+
f"- score_completude: {data['quality']['completeness_score']}/100",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
md_path.write_text("\n".join(md) + "\n", encoding="utf-8")
|
|
171
|
+
return json_path, md_path
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> int:
|
|
175
|
+
parser = argparse.ArgumentParser(description="Estruturar coleta por plataforma")
|
|
176
|
+
parser.add_argument("run_id", help="ID do run (ex.: RUN-2026-04-11-005)")
|
|
177
|
+
parser.add_argument("--fonte", required=True, help="Arquivo markdown da coleta bruta")
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
run_id = args.run_id.strip()
|
|
181
|
+
find_run(run_id)
|
|
182
|
+
|
|
183
|
+
evidence_path = find_evidence(run_id)
|
|
184
|
+
evidence_text = evidence_path.read_text(encoding="utf-8")
|
|
185
|
+
|
|
186
|
+
source_path = Path(args.fonte)
|
|
187
|
+
if not source_path.is_absolute():
|
|
188
|
+
source_path = (ROOT / source_path).resolve()
|
|
189
|
+
if not source_path.exists():
|
|
190
|
+
raise SystemExit(f"Fonte não encontrada: {source_path}")
|
|
191
|
+
|
|
192
|
+
source_text = source_path.read_text(encoding="utf-8", errors="ignore")
|
|
193
|
+
|
|
194
|
+
platform = detect_platform_from_evidence(evidence_text)
|
|
195
|
+
target = extract_target(evidence_text)
|
|
196
|
+
|
|
197
|
+
formats = uniq([m.group(1).lower() for m in FORMAT_RE.finditer(source_text)], limit=12)
|
|
198
|
+
ctas = uniq([m.group(1).lower() for m in CTA_RE.finditer(source_text)], limit=12)
|
|
199
|
+
hashtags = uniq([m.group(1).lower() for m in HASHTAG_RE.finditer(source_text)], limit=30)
|
|
200
|
+
links = uniq(URL_RE.findall(source_text), limit=100)
|
|
201
|
+
topics = extract_top_terms(source_text, limit=12)
|
|
202
|
+
pf = platform_fields(platform, source_text)
|
|
203
|
+
|
|
204
|
+
data = {
|
|
205
|
+
"run_id": run_id,
|
|
206
|
+
"platform": platform,
|
|
207
|
+
"target": target,
|
|
208
|
+
"source": {
|
|
209
|
+
"path": source_path.relative_to(ROOT).as_posix(),
|
|
210
|
+
"chars": len(source_text),
|
|
211
|
+
"lines": len(source_text.splitlines()),
|
|
212
|
+
},
|
|
213
|
+
"signals": {
|
|
214
|
+
"formats": formats,
|
|
215
|
+
"ctas": ctas,
|
|
216
|
+
"hashtags": hashtags,
|
|
217
|
+
"links": links,
|
|
218
|
+
"topics": topics,
|
|
219
|
+
},
|
|
220
|
+
"platform_fields": pf,
|
|
221
|
+
"quality": {
|
|
222
|
+
"completeness_score": completeness_score(len(source_text), formats, ctas, links, pf)
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
json_path, md_path = write_outputs(run_id, data)
|
|
227
|
+
|
|
228
|
+
print(f"run_id={run_id}")
|
|
229
|
+
print(f"platform={platform}")
|
|
230
|
+
print(f"structured_json={json_path.relative_to(ROOT).as_posix()}")
|
|
231
|
+
print(f"structured_md={md_path.relative_to(ROOT).as_posix()}")
|
|
232
|
+
print(f"completeness={data['quality']['completeness_score']}/100")
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
raise SystemExit(main())
|