feat(crm-ops): estrategia-marca 1.3.0 — SPEC v2.0, versão completa (36 capítulos), redação em paralelo, montagem e PDF A4; funde marketing:brand-strategy (marketing 2.1.0)

This commit is contained in:
AIkTop
2026-10-17 01:39:40 +01:00
parent bce12944ae
commit c18bb90ff6
16 changed files with 662 additions and 590 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Junta as partes de uma estratégia de marca num único markdown.
Uso:
montar.py --partes DIR --intro intro.md --briefing briefing-llm.md --config marca.json --out estrategia.md
[--glossario glossario.md] [--complementos complementos.md] [--historico historico.md]
- DIR contém parte-I.md ... parte-VIII.md (só os capítulos, `## N. Título`).
- intro.md: resumo executivo + "Sobre este documento" (títulos `# ...`).
- briefing-llm.md: a secção `## BRIEFING-LLM` completa (fica sempre em último).
- marca.json: chaves `frontmatter` (dict) e, opcional, `partes` ([[romano, nome], ...]).
Gera o anexo A (pontos a validar) a partir das marcas `[a validar: ...]`.
"""
import argparse, json, os, re, sys
PARTES = [["I", "Contexto"], ["II", "Fundamentos da marca"], ["III", "Públicos"],
["IV", "Identidade e expressão"], ["V", "Oferta e venda"], ["VI", "Comunicação e canais"],
["VII", "Crescimento"], ["VIII", "Medição e implementação"]]
def yaml_value(v):
if isinstance(v, list):
return "[" + ", ".join(str(x) for x in v) + "]"
s = str(v)
return '"' + s.replace('"', '\\"') + '"' if re.search(r"[:#®·\-]", s) else s
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--partes", required=True)
ap.add_argument("--intro", required=True)
ap.add_argument("--briefing", required=True)
ap.add_argument("--config", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--glossario")
ap.add_argument("--complementos")
ap.add_argument("--historico")
a = ap.parse_args()
cfg = json.load(open(a.config, encoding="utf-8"))
fm = "---\n" + "".join(f"{k}: {yaml_value(v)}\n" for k, v in cfg.get("frontmatter", {}).items()) + "---\n\n"
doc = [fm, open(a.intro, encoding="utf-8").read().rstrip() + "\n\n"]
for rom, nome in cfg.get("partes", PARTES):
p = os.path.join(a.partes, f"parte-{rom}.md")
if not os.path.exists(p):
sys.exit(f"Falta {p}")
t = open(p, encoding="utf-8").read().strip()
if re.search(r"^# ", t, re.M):
sys.exit(f"{p} tem títulos de nível 1; só o montar.py escreve as Partes")
doc.append(f"# Parte {rom}. {nome}\n\n{t}\n\n")
full = "".join(doc)
pend, cap, seen, rows = [], None, set(), []
for line in full.split("\n"):
m = re.match(r"^## (\d+)\. ", line)
if m:
cap = int(m.group(1))
for v in re.findall(r"\[a validar:\s*([^\]]+)\]", line):
pend.append((cap or 0, v.strip().rstrip(".")))
for c, v in pend:
if (c, v.lower()) in seen:
continue
seen.add((c, v.lower()))
rows.append(f"| {c or '—'} | {v[0].upper() + v[1:]} |")
anexos = ["# Anexos\n\n## A. Pontos a validar\n\n",
f"{len(rows)} pontos, por capítulo. Cada ponto fechado corrige-se no capítulo e sai desta lista.\n\n",
"| Cap. | O que falta confirmar |\n| --- | --- |\n", "\n".join(rows) or "| — | Nenhum |", "\n\n"]
extras = [(t, f) for t, f in [("Glossário", a.glossario), ("Complementos do sector", a.complementos),
("Histórico de alterações", a.historico)] if f]
for letra, (titulo, f) in zip("BCD", extras):
anexos.append(f"## {letra}. {titulo}\n\n" + open(f, encoding="utf-8").read().strip() + "\n\n")
full += "".join(anexos)
br = open(a.briefing, encoding="utf-8").read().strip()
if not br.startswith("## BRIEFING-LLM"):
sys.exit("briefing-llm.md tem de começar por '## BRIEFING-LLM'")
full += br + "\n"
open(a.out, "w", encoding="utf-8").write(full)
caps = len(re.findall(r"^## \d+\. ", full, re.M))
print(f"{a.out}: {len(full.split())} palavras, {caps} capítulos, {len(rows)} pontos a validar")
if __name__ == "__main__":
main()