primocode 10.19.0 → 11.0.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.
Files changed (116) hide show
  1. package/LICENSES/opencode-MIT.txt +24 -0
  2. package/LICENSES/pptxgenjs-MIT.txt +31 -0
  3. package/README.md +16 -3
  4. package/agentes/acessibilidade.md +10 -0
  5. package/agentes/ambiente.md +10 -0
  6. package/agentes/api.md +10 -0
  7. package/agentes/backend.md +10 -0
  8. package/agentes/copy.md +10 -0
  9. package/agentes/css-layout.md +10 -0
  10. package/agentes/dados.md +10 -0
  11. package/agentes/depuracao.md +10 -0
  12. package/agentes/design.md +10 -0
  13. package/agentes/docs.md +10 -0
  14. package/agentes/frontend.md +10 -0
  15. package/agentes/geral/capacidades/arquivos.json +132 -0
  16. package/agentes/geral/capacidades/codigo.json +132 -0
  17. package/agentes/geral/capacidades/dados.json +126 -0
  18. package/agentes/geral/capacidades/depuracao.json +126 -0
  19. package/agentes/geral/capacidades/escrita.json +126 -0
  20. package/agentes/geral/capacidades/git.json +126 -0
  21. package/agentes/geral/capacidades/pesquisa.json +126 -0
  22. package/agentes/geral/capacidades/seguranca.json +132 -0
  23. package/agentes/geral/capacidades/shell.json +132 -0
  24. package/agentes/geral/capacidades/web.json +126 -0
  25. package/agentes/git-pr.md +10 -0
  26. package/agentes/indice.md +86 -0
  27. package/agentes/nextjs.md +10 -0
  28. package/agentes/node.md +10 -0
  29. package/agentes/performance.md +10 -0
  30. package/agentes/pesquisa.md +10 -0
  31. package/agentes/python.md +10 -0
  32. package/agentes/react.md +10 -0
  33. package/agentes/refatoracao.md +10 -0
  34. package/agentes/revisor.md +10 -0
  35. package/agentes/seguranca.md +10 -0
  36. package/agentes/sql.md +10 -0
  37. package/agentes/testes.md +10 -0
  38. package/agentes/typescript.md +10 -0
  39. package/agentes/ux.md +10 -0
  40. package/bin/primocode.js +65 -8
  41. package/editor/helpers/camera_viva.py +28 -2
  42. package/lib/act.js +79 -10
  43. package/lib/agentes/index.js +146 -0
  44. package/lib/agentes/nucleo.js +18 -0
  45. package/lib/catalogo.js +12 -2
  46. package/lib/cli/review.js +64 -0
  47. package/lib/cli/subcomandos.js +34 -0
  48. package/lib/cli/tentativas.js +107 -0
  49. package/lib/cli/util.js +51 -0
  50. package/lib/comandos-extra.js +4 -2
  51. package/lib/editor-pro-maquina.js +165 -0
  52. package/lib/editor-pro.js +10 -7
  53. package/lib/entregaveis/cli.js +26 -0
  54. package/lib/entregaveis/especialistas.js +50 -0
  55. package/lib/entregaveis/nucleo.js +111 -0
  56. package/lib/entregaveis/slides.js +221 -0
  57. package/lib/estudio/materiais.js +172 -0
  58. package/lib/fundo.js +34 -1
  59. package/lib/kits.js +41 -2
  60. package/lib/nomes.js +6 -0
  61. package/lib/placar.js +173 -0
  62. package/lib/plugin/autoteste.js +99 -0
  63. package/lib/plugin/edicao-visual.js +133 -0
  64. package/lib/plugin/pesquisar.js +92 -0
  65. package/lib/plugin/preview-cli.js +38 -0
  66. package/lib/plugin/preview.js +89 -0
  67. package/lib/plugin/tunel.js +27 -0
  68. package/lib/review.js +134 -0
  69. package/lib/session/compactacao.js +124 -0
  70. package/lib/statusline.js +52 -0
  71. package/lib/tool/diagnostico.js +100 -0
  72. package/lib/tool/perguntar.js +60 -0
  73. package/lib/tool/reparar-entrada.js +66 -0
  74. package/lib/tool/web.js +102 -0
  75. package/lib/tools.js +20 -1
  76. package/lib/ui.js +1 -1
  77. package/materiais/saas-linear/base.ts +84 -0
  78. package/materiais/saas-linear/camera/Camera.tsx +38 -0
  79. package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
  80. package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
  81. package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
  82. package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
  83. package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
  84. package/materiais/saas-linear/formas/Agenda.tsx +46 -0
  85. package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
  86. package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
  87. package/materiais/saas-linear/formas/Cursor.tsx +40 -0
  88. package/materiais/saas-linear/formas/Menu.tsx +46 -0
  89. package/materiais/saas-linear/formas/Monitor.tsx +28 -0
  90. package/materiais/saas-linear/paleta.json +18 -0
  91. package/materiais/saas-linear/pecas.json +590 -0
  92. package/materiais/saas-linear/regras.md +40 -0
  93. package/materiais/saas-linear/sons/Som.tsx +39 -0
  94. package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
  95. package/materiais/saas-linear/sons/clique.mp3 +0 -0
  96. package/materiais/saas-linear/sons/impacto.mp3 +0 -0
  97. package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
  98. package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
  99. package/materiais/saas-linear/sons/subida.mp3 +0 -0
  100. package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
  101. package/materiais/saas-linear/sons/trilha.mp3 +0 -0
  102. package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
  103. package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
  104. package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
  105. package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
  106. package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
  107. package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
  108. package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
  109. package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
  110. package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
  111. package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
  112. package/package.json +3 -1
  113. package/skills/deck-conferido/SKILL.md +29 -0
  114. package/vendor/pptxgenjs/carregar.js +12 -0
  115. package/vendor/pptxgenjs/jszip.min.js +13 -0
  116. package/vendor/pptxgenjs/pptxgen.cjs.js +7370 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * review.js — `primocode review --pr <n> [--postar]` (ideia do Bugbot): lê o diff do PR com `gh`, pede ao modelo
3
+ * uma revisão e devolve comentários em markdown. Postar no PR só com --postar (o padrão só mostra e grava em
4
+ * .primocode/review/pr-<n>.md). Nada de aprovar nem pedir mudanças no PR: só um comentário.
5
+ *
6
+ * NÃO é o /review de dentro da sessão (esse é da branch rebuild/paridade-cc). Este roda sem sessão, em CI.
7
+ * `gh` e o modelo são injetáveis. O workflow do GitHub: .github/workflows/primocode-review.yml.
8
+ */
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { sh, perguntarPadrao } = require('./util.js');
14
+
15
+ const MAX_DIFF = 60000;
16
+ const SISTEMA = 'Você é um revisor de código. Leia o diff e aponte SÓ problemas reais: bugs, falhas de segurança, casos de borda sem tratamento, testes que faltam. '
17
+ + 'Formato markdown: uma seção "### arquivo:linha — título curto" por problema, com 1-3 frases do porquê e a correção sugerida. '
18
+ + 'Ordene do mais grave ao menos. Sem elogio, sem resumir o diff. Se não achou nada relevante, escreva exatamente: "Nada relevante encontrado."';
19
+
20
+ function analisar(args) {
21
+ const r = { pr: null, postar: false, erro: null };
22
+ for (let i = 0; i < args.length; i++) {
23
+ if (args[i] === '--pr') r.pr = args[++i];
24
+ else if (args[i].startsWith('--pr=')) r.pr = args[i].slice(5);
25
+ else if (args[i] === '--postar') r.postar = true;
26
+ }
27
+ if (!r.pr || !/^\d+$/.test(String(r.pr))) r.erro = 'Uso: primocode review --pr <número> [--postar]';
28
+ return r;
29
+ }
30
+
31
+ function cortar(diff) {
32
+ if (diff.length <= MAX_DIFF) return { diff, cortado: false };
33
+ return { diff: diff.slice(0, MAX_DIFF), cortado: true };
34
+ }
35
+
36
+ async function revisar({ pr, dir = process.cwd(), postar = false, gh = (a) => sh('gh', a, { cwd: dir }), perguntar = perguntarPadrao }) {
37
+ const d = gh(['pr', 'diff', String(pr)]);
38
+ if (!d.ok) return { ok: false, erro: `não consegui ler o diff do PR #${pr} (${(d.err || 'gh falhou').trim().split('\n')[0]})` };
39
+ if (!d.out.trim()) return { ok: false, erro: `o PR #${pr} não tem diff` };
40
+ const { diff, cortado } = cortar(d.out);
41
+ const corpo = String(await perguntar(null, SISTEMA, `Diff do PR #${pr}:\n\n${diff}`)).trim();
42
+ const md = `## Revisão do PrimoCode — PR #${pr}\n\n${corpo}\n` + (cortado ? `\n_O diff passou de ${MAX_DIFF} caracteres: revisei só o começo._\n` : '')
43
+ + '\n<sub>Revisão automática: pode errar. Quem decide é a pessoa.</sub>\n';
44
+ const arquivo = path.join(dir, '.primocode', 'review', `pr-${pr}.md`);
45
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true }); fs.writeFileSync(arquivo, md);
46
+ let postado = false, erroPost = null;
47
+ if (postar) {
48
+ const r = gh(['pr', 'comment', String(pr), '--body-file', arquivo]);
49
+ postado = r.ok; if (!r.ok) erroPost = (r.err || '').trim().split('\n')[0];
50
+ }
51
+ return { ok: true, md, arquivo, postado, erroPost };
52
+ }
53
+
54
+ async function main(args, deps = {}) {
55
+ const a = analisar(args);
56
+ if (a.erro) { console.error(a.erro); return 1; }
57
+ const r = await revisar({ pr: a.pr, postar: a.postar, ...deps });
58
+ if (!r.ok) { console.error(' ' + r.erro); return 1; }
59
+ console.log(r.md);
60
+ console.log(a.postar ? (r.postado ? ` Comentário postado no PR #${a.pr}.` : ` Não consegui postar: ${r.erroPost}`) : ` Não postei nada (use --postar). Salvo em ${r.arquivo}`);
61
+ return a.postar && !r.postado ? 1 : 0;
62
+ }
63
+
64
+ module.exports = { analisar, revisar, main, SISTEMA, MAX_DIFF };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * subcomandos.js — as entradas `primocode <nome> ...` que saem ANTES da sessão (sem tela):
3
+ * autoteste · preview · review · pesquisar
4
+ * Cada uma chama o módulo de lib/plugin/ ou lib/cli/. O bin só faz `if (NOMES.includes(argv[2]))`.
5
+ */
6
+ 'use strict';
7
+
8
+ const path = require('path');
9
+ const { sh } = require('./util.js');
10
+
11
+ const NOMES = ['autoteste', 'preview', 'review', 'pesquisar'];
12
+
13
+ async function autoteste(args) {
14
+ const A = require('../plugin/autoteste.js');
15
+ const dir = path.resolve(args.find((a) => !a.startsWith('-')) || process.cwd());
16
+ const bin = path.join(__dirname, '..', '..', 'bin', 'primocode.js');
17
+ const r = await A.testar({
18
+ dir, log: (l) => console.log(' ' + l),
19
+ consertar: async (erros, prints) => { console.log(' mandando as falhas pro agente consertar…'); sh(process.execPath, [bin, '-p', A.pedidoDeConserto(erros, prints)], { cwd: dir }); },
20
+ });
21
+ if (r.ok) console.log(` Passou${r.rodadas ? ` depois de ${r.rodadas} conserto(s)` : ''}.`);
22
+ else { console.log(' ' + r.erro); (r.relatorio.slice(-1)[0] || { erros: [] }).erros.forEach((e) => console.log(' - ' + e)); }
23
+ return r.ok ? 0 : 1;
24
+ }
25
+
26
+ async function main(nome, args) {
27
+ if (nome === 'autoteste') return autoteste(args);
28
+ if (nome === 'preview') return require('../plugin/preview-cli.js').main(args);
29
+ if (nome === 'review') return require('./review.js').main(args);
30
+ if (nome === 'pesquisar') return require('../plugin/pesquisar.js').main(args);
31
+ return 1;
32
+ }
33
+
34
+ module.exports = { NOMES, main };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * tentativas.js — `primocode --tentativas N "pedido"` (ideia do Codex, "best of N"): roda a tarefa N vezes, cada
3
+ * uma num git worktree separado, roda o check do projeto em cada uma, e fica com a que passou e mexeu menos.
4
+ *
5
+ * N: padrão 3, máximo 4. Check: `npm test` se o package.json tiver script "test" (ou --check "<comando>").
6
+ * Uma linha por tentativa: "#2 passou · +12 −3 · 2 arquivos".
7
+ *
8
+ * `executar(dirDoWorktree, pedido)` e `checar(dir)` são injetáveis (os testes usam falsos; o padrão roda
9
+ * `primocode -p`). O vencedor vira um patch aplicado na pasta principal; os worktrees são apagados no fim.
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { sh } = require('./util.js');
17
+
18
+ const PADRAO = 3, MAX = 4;
19
+ const limitar = (n) => { const v = Number(n); return Number.isFinite(v) && v >= 1 ? Math.min(MAX, Math.floor(v)) : PADRAO; };
20
+
21
+ /** Tira `--tentativas N` / `--tentativas=N` / `--check cmd` dos argumentos. null = a flag não veio. */
22
+ function analisar(argv) {
23
+ const r = { tentativas: null, check: null, resto: [] };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--tentativas') { r.tentativas = limitar(argv[i + 1]); i++; }
27
+ else if (a.startsWith('--tentativas=')) r.tentativas = limitar(a.split('=')[1]);
28
+ else if (a === '--check') { r.check = argv[++i]; }
29
+ else r.resto.push(a);
30
+ }
31
+ return r;
32
+ }
33
+
34
+ /** O comando de check do projeto, ou null. */
35
+ function comandoDeCheck(dir, forcado) {
36
+ if (forcado) return ['sh', ['-c', forcado]];
37
+ try {
38
+ const p = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
39
+ const t = p.scripts && p.scripts.test;
40
+ if (t && !/no test specified/.test(t)) return ['npm', ['test', '--silent']];
41
+ } catch { /* sem package.json */ }
42
+ return null;
43
+ }
44
+
45
+ /** Tamanho do diff do worktree contra o HEAD, contando arquivos novos. { mais, menos, arquivos, patch }. */
46
+ function medirDiff(dir) {
47
+ sh('git', ['add', '-A', '-N'], { cwd: dir });
48
+ const ns = sh('git', ['diff', '--numstat', 'HEAD'], { cwd: dir }).out.trim().split('\n').filter(Boolean);
49
+ let mais = 0, menos = 0;
50
+ for (const l of ns) { const [a, b] = l.split('\t'); mais += Number(a) || 0; menos += Number(b) || 0; }
51
+ return { mais, menos, arquivos: ns.length, patch: sh('git', ['diff', '--binary', 'HEAD'], { cwd: dir }).out };
52
+ }
53
+
54
+ const linha = (t) => `#${t.n} ${t.erro ? 'falhou (' + t.erro + ')' : t.passou ? 'passou' : 'reprovou no check'}`
55
+ + (t.erro ? '' : ` · +${t.diff.mais} −${t.diff.menos} · ${t.diff.arquivos} arquivo${t.diff.arquivos === 1 ? '' : 's'}`);
56
+
57
+ /** A melhor: passou, e com o menor diff (linhas mexidas, depois arquivos). null se nenhuma passou. */
58
+ function escolher(ts) {
59
+ const ok = ts.filter((t) => t.passou && !t.erro);
60
+ ok.sort((a, b) => (a.diff.mais + a.diff.menos) - (b.diff.mais + b.diff.menos) || a.diff.arquivos - b.diff.arquivos || a.n - b.n);
61
+ return ok[0] || null;
62
+ }
63
+
64
+ async function rodar({ dir, pedido, n = PADRAO, check, executar, checar, log = () => {}, aplicar = true }) {
65
+ n = limitar(n);
66
+ if (!sh('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir }).ok) return { ok: false, erro: 'precisa de um repositório git (as tentativas usam git worktree)' };
67
+ const cmd = comandoDeCheck(dir, check);
68
+ executar = executar || ((d, p) => sh(process.execPath, [path.join(__dirname, '..', '..', 'bin', 'primocode.js'), '-p', p], { cwd: d }));
69
+ checar = checar || ((d) => (cmd ? sh(cmd[0], cmd[1], { cwd: d }).ok : true));
70
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'primocode-tentativas-'));
71
+ const ts = [];
72
+ for (let i = 1; i <= n; i++) {
73
+ const wt = path.join(base, `t${i}`);
74
+ const t = { n: i, dir: wt };
75
+ const add = sh('git', ['worktree', 'add', '--detach', wt, 'HEAD'], { cwd: dir });
76
+ if (!add.ok) { t.erro = 'worktree: ' + add.err.trim().split('\n')[0]; t.diff = { mais: 0, menos: 0, arquivos: 0, patch: '' }; ts.push(t); log(linha(t)); continue; }
77
+ try {
78
+ await executar(wt, pedido);
79
+ t.diff = medirDiff(wt);
80
+ t.passou = t.diff.arquivos > 0 && !!(await checar(wt));
81
+ } catch (e) { t.erro = String(e.message || e).slice(0, 60); t.diff = t.diff || { mais: 0, menos: 0, arquivos: 0, patch: '' }; }
82
+ ts.push(t); log(linha(t));
83
+ }
84
+ const melhor = escolher(ts);
85
+ let aplicado = false, patchPath = null;
86
+ if (melhor) {
87
+ patchPath = path.join(dir, '.primocode', 'tentativas', `${Date.now()}-melhor.patch`);
88
+ fs.mkdirSync(path.dirname(patchPath), { recursive: true }); fs.writeFileSync(patchPath, melhor.diff.patch);
89
+ if (aplicar) aplicado = sh('git', ['apply', patchPath], { cwd: dir }).ok;
90
+ }
91
+ for (const t of ts) { sh('git', ['worktree', 'remove', '--force', t.dir], { cwd: dir }); }
92
+ try { fs.rmSync(base, { recursive: true, force: true }); } catch { /* */ }
93
+ return { ok: !!melhor, melhor: melhor && melhor.n, resumo: ts.map(linha), aplicado, patchPath,
94
+ erro: melhor ? null : 'nenhuma tentativa passou no check — nada foi aplicado' };
95
+ }
96
+
97
+ async function main(argv, deps = {}) {
98
+ const a = analisar(argv);
99
+ const pedido = a.resto.filter((x) => !x.startsWith('-')).join(' ').trim();
100
+ if (!pedido) { console.error('Uso: primocode --tentativas 3 "pedido" [--check "npm test"]'); return 1; }
101
+ const r = await rodar({ dir: process.cwd(), pedido, n: a.tentativas, check: a.check, log: (l) => console.log(' ' + l), ...deps });
102
+ if (r.ok) console.log(` Fiquei com a #${r.melhor}. ` + (r.aplicado ? 'Aplicada na pasta.' : `Não consegui aplicar sozinho; o patch está em ${r.patchPath}`));
103
+ else console.log(' ' + r.erro);
104
+ return r.ok ? 0 : 1;
105
+ }
106
+
107
+ module.exports = { analisar, limitar, comandoDeCheck, medirDiff, escolher, rodar, main, PADRAO, MAX };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * util.js — peças pequenas que os subcomandos `primocode issue|review|headless|...` dividem.
3
+ * Tudo que fala com o mundo (shell, modelo) é injetável: os testes trocam por falsos.
4
+ */
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+
9
+ /** Roda um comando sem shell. Devolve { ok, status, out, err }. */
10
+ function sh(cmd, args = [], { cwd, input, timeout = 10 * 60 * 1000, env } = {}) {
11
+ const r = spawnSync(cmd, args, { cwd, input, encoding: 'utf8', timeout, env: env || process.env, maxBuffer: 64 * 1024 * 1024 });
12
+ return { ok: r.status === 0, status: r.status, out: r.stdout || '', err: (r.stderr || '') + (r.error ? String(r.error.message) : '') };
13
+ }
14
+
15
+ const temComando = (nome) => sh(process.platform === 'win32' ? 'where' : 'which', [nome]).ok;
16
+
17
+ /**
18
+ * O modelo, de fora da sessão: pergunta única, sem ferramentas.
19
+ * `modelo` vazio = o modelo da conta. Falha com mensagem clara (nunca inventa resposta).
20
+ */
21
+ async function perguntarPadrao(modelo, sistema, usuario) {
22
+ const cfg = require('../config.js');
23
+ const api = require('../api.js');
24
+ const conf = cfg.loadConfig();
25
+ let resposta = '';
26
+ await api.requestChat(conf.server, {
27
+ prompt: sistema ? `${sistema}\n\n${usuario}` : usuario, history: [], model: modelo || conf.model, token: conf.token || cfg.tokenDaConta(),
28
+ }, (evt) => { if (evt.delta) resposta += evt.delta; });
29
+ return resposta;
30
+ }
31
+
32
+ /** Tira cerca de código (```json ... ```) e acha o primeiro JSON do texto. */
33
+ function acharJson(texto) {
34
+ const t = String(texto || '');
35
+ const cerca = t.match(/```(?:json)?\s*([\s\S]*?)```/);
36
+ const candidatos = [cerca && cerca[1], t];
37
+ for (const c of candidatos) {
38
+ if (!c) continue;
39
+ const i = c.search(/[[{]/);
40
+ if (i < 0) continue;
41
+ for (let fim = c.length; fim > i; fim = c.lastIndexOf(c[i] === '{' ? '}' : ']', fim - 1) + 1) {
42
+ if (fim <= 0) break;
43
+ try { return JSON.parse(c.slice(i, fim)); } catch { /* tenta mais curto */ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ const slug = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'tarefa';
50
+
51
+ module.exports = { sh, temComando, perguntarPadrao, acharJson, slug };
@@ -379,6 +379,7 @@ function medirEvento(b, evt) {
379
379
  b.tokEnviados += Number(u.prompt_tokens || u.input_tokens || 0);
380
380
  b.tokRecebidos += Number(u.completion_tokens || u.output_tokens || 0);
381
381
  b.exatos++;
382
+ b.tokEmCache = (b.tokEmCache || 0) + Number(u.cached_tokens || (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cache_read_input_tokens || 0);
382
383
  }
383
384
  }
384
385
 
@@ -415,15 +416,16 @@ function instalarContador(api, cont, quemAtende) {
415
416
  /** As linhas do /custo: por provedor, e o total. */
416
417
  function resumoCusto(cont) {
417
418
  const linhas = [];
418
- let total = { pedidos: 0, enviados: 0, recebidos: 0 };
419
+ let total = { pedidos: 0, enviados: 0, recebidos: 0, emCache: 0 };
419
420
  for (const [provedor, b] of Object.entries(cont.porProvedor)) {
420
421
  // Com `usage` em todos os pedidos, vale o número do provedor; senão a
421
422
  // estimativa inteira, para não somar exato com chute.
422
423
  const exato = b.exatos > 0 && b.exatos === b.pedidos;
423
424
  const enviados = exato ? b.tokEnviados : Math.ceil(b.charsEnviados / 4);
424
425
  const recebidos = exato ? b.tokRecebidos : Math.ceil(b.charsRecebidos / 4);
425
- linhas.push({ provedor, pedidos: b.pedidos, enviados, recebidos, exato });
426
+ linhas.push({ provedor, pedidos: b.pedidos, enviados, recebidos, exato, emCache: exato ? (b.tokEmCache || 0) : 0 });
426
427
  total.pedidos += b.pedidos; total.enviados += enviados; total.recebidos += recebidos;
428
+ total.emCache += exato ? (b.tokEmCache || 0) : 0;
427
429
  }
428
430
  return { linhas, total };
429
431
  }
@@ -0,0 +1,165 @@
1
+ /* A máquina de vídeo do editor: entende cada frase, escolhe gancho e estilo pelo CONTEÚDO (e pela variante),
2
+ e confere o plano com uma régua numérica (criticar). Sem rede, sem Python: só texto e números.
3
+ Ver docs/rebuild/editor-maquina-arquitetura.md. */
4
+ 'use strict';
5
+
6
+ const FRACAS = new Set('a o e é de da do das dos em no na nos nas um uma uns umas que para pra por com como eu você ele ela isso esse essa este esta hoje vou vai ou se mas'.split(' '));
7
+ const NEGACAO = /\b(nunca|n[aã]o|ningu[eé]m|nada|jamais|zero|sem)\b/i;
8
+ const NUMERO = /\d|\b(dobro|metade|triplo|mil|milh[aã]o|dez|cem|vinte|trinta)\b/i;
9
+ const PROMESSA = /\b(como|jeito|segredo|passo|m[eé]todo|f[aá]cil|r[aá]pido|grátis|gr[aá]tis|consegui|descobri)\b/i;
10
+ const URGENCIA = /\b(agora|hoje|[uú]ltim[ao]s?|acaba|corre|r[aá]pido|j[aá])\b/i;
11
+ const EMOCAO_FORTE = /\b(erro|medo|perd[ei]|caro|problema|pior|absurdo|chocante|incr[ií]vel|louco|nunca|dobro|segredo)\b/i;
12
+
13
+ const limpar = (l) => String(l || '').replace(/^[\d.]+-[\d.]+\s+/, '').trim();
14
+ const tempos = (l) => { const m = /^([\d.]+)-([\d.]+)/.exec(String(l || '')); return m ? [Number(m[1]), Number(m[2])] : [null, null]; };
15
+
16
+ /** O que cada frase quer: intenção, emoção, palavra-chave e energia (0 a 1). Tudo medido no texto. */
17
+ function entenderFrases(fala) {
18
+ return (fala || []).map((linha, i) => {
19
+ const texto = limpar(linha);
20
+ const [t0, t1] = tempos(linha);
21
+ const palavras = texto.replace(/[.,!?;:]/g, '').split(/\s+/).filter(Boolean);
22
+ const fortes = palavras.filter((p) => !FRACAS.has(p.toLowerCase()) && (p.length > 2 || /\d/.test(p) || /^[A-ZÀ-Ú]{2}$/.test(p)));
23
+ const chave = fortes.slice().sort((a, b) => (NUMERO.test(b) - NUMERO.test(a)) || (b.length - a.length))[0] || null;
24
+ let intencao = 'afirma';
25
+ if (/\?\s*$/.test(texto)) intencao = 'pergunta';
26
+ else if (NUMERO.test(texto)) intencao = 'prova';
27
+ else if (NEGACAO.test(texto)) intencao = 'nega';
28
+ else if (PROMESSA.test(texto)) intencao = 'promete';
29
+ else if (URGENCIA.test(texto)) intencao = 'urge';
30
+ const energia = Math.min(1, 0.25 + (/!/.test(texto) ? 0.25 : 0) + (EMOCAO_FORTE.test(texto) ? 0.25 : 0) + (NUMERO.test(texto) ? 0.15 : 0) + (NEGACAO.test(texto) ? 0.1 : 0));
31
+ return { i, texto, t0, t1, palavras, fortes, chave, intencao, energia, emocao: energia >= 0.6 ? 'forte' : energia >= 0.4 ? 'media' : 'calma' };
32
+ }).filter((f) => f.texto);
33
+ }
34
+
35
+ const marcar = (ps, ult) => (ps.length ? ps.join(' ') + ' ' : '') + `*${ult}*`;
36
+
37
+ /** Candidatos a gancho, de tipos DIFERENTES, tirados das primeiras frases (o gancho pode nascer de qualquer uma). */
38
+ function ganchosCandidatos(fala) {
39
+ const frases = entenderFrases(fala);
40
+ const out = [];
41
+ const jaTem = new Set();
42
+ const por = (tipo, f, nota, fn) => {
43
+ if (!f || !f.fortes.length) return;
44
+ const texto = fn(f);
45
+ if (!texto || jaTem.has(texto.toLowerCase())) return;
46
+ jaTem.add(texto.toLowerCase());
47
+ out.push({ tipo, texto, de: f.i, nota });
48
+ };
49
+ for (const f of frases.slice(0, 6)) {
50
+ if (f.intencao === 'prova') por('numero', f, 3 + f.energia, (x) => { const c = x.fortes.find((p) => NUMERO.test(p)) || x.chave; const ix = x.fortes.indexOf(c); return marcar(x.fortes.slice(Math.max(0, ix - 2), ix), c); });
51
+ if (f.intencao === 'nega') por('negacao', f, 2.5 + f.energia, (x) => marcar(x.fortes.slice(-3, -1), x.fortes[x.fortes.length - 1]));
52
+ if (f.intencao === 'pergunta') por('pergunta', f, 2 + f.energia, (x) => marcar(x.fortes.slice(0, 3).slice(0, -1), x.fortes.slice(0, 3).slice(-1)[0]));
53
+ if (f.intencao === 'promete') por('promessa', f, 2 + f.energia, (x) => marcar(x.fortes.slice(0, 3), x.chave || x.fortes[x.fortes.length - 1]).replace(/\*\*/g, '*'));
54
+ if (f.intencao === 'urge') por('urgencia', f, 1.5 + f.energia, (x) => marcar(x.fortes.slice(-3, -1), x.fortes[x.fortes.length - 1]));
55
+ }
56
+ if (frases[0]) por('palavra-forte', frases[0], 1, (x) => { const fs = x.fortes.slice(-4); const u = fs.pop(); return marcar(fs, u); });
57
+ return out.sort((a, b) => b.nota - a.nota);
58
+ }
59
+
60
+ /** O gancho da variante `variante` (0 = o de maior nota; 1 = o próximo TIPO; e assim por diante, girando). */
61
+ function escolherGancho(fala, variante = 0) {
62
+ const c = ganchosCandidatos(fala);
63
+ if (!c.length) return null;
64
+ return c[((variante % c.length) + c.length) % c.length].texto;
65
+ }
66
+
67
+ const hash = (s) => { let h = 2166136261; for (const ch of String(s)) { h ^= ch.charCodeAt(0); h = Math.imul(h, 16777619); } return h >>> 0; };
68
+
69
+ /** O estilo vem da energia do que é dito, não de um padrão fixo: conteúdo forte pede escuro/laranja; calmo, limpo.
70
+ A variante gira entre os estilos que servem, e o mesmo roteiro sempre dá a mesma resposta (sem sorteio). */
71
+ function escolherEstilo(fala, variante = 0) {
72
+ const fr = entenderFrases(fala);
73
+ const e = fr.length ? fr.reduce((s, f) => s + f.energia, 0) / fr.length : 0.3;
74
+ const ordem = e >= 0.55 ? ['laranja', 'noite', 'papelaria', 'limpo'] : e >= 0.38 ? ['noite', 'laranja', 'limpo', 'papelaria'] : ['limpo', 'papelaria', 'noite', 'laranja'];
75
+ const sal = hash(fr.map((f) => f.texto).join('|')) % 2;
76
+ return ordem[(variante + sal) % ordem.length];
77
+ }
78
+
79
+ // ── a régua (autocrítica) ────────────────────────────────────────────────
80
+
81
+ const REGUA = {
82
+ parado_max_s: 3.0, // nenhum trecho maior que isso sem mudança visual
83
+ tratamentos_min_por_min: 12, // eventos visuais (peça, bloco, zoom) por minuto
84
+ tipos_distintos_min: 4, // famílias de tratamento diferentes no vídeo
85
+ sfx_no_corte_s: 0.15, // todo corte de bloco tem som a até 150 ms
86
+ sfx_min_por_min: 10, // camadas de som por minuto
87
+ gancho_ate_s: 1.0, // o gancho aparece no primeiro segundo
88
+ zooms_distintos_min: 3, // escalas de enquadramento diferentes
89
+ legenda_sobre_rosto: 0, // trechos com texto sobre o rosto
90
+ };
91
+ const POS_ROSTO = new Set(['centro', 'rosto', 'meio-rosto']);
92
+ const num = (x) => (typeof x === 'number' && isFinite(x) ? x : null);
93
+
94
+ /** Pontos do tempo em que a imagem muda: começo e fim de peça, bloco e zoom. */
95
+ function mudancasVisuais(cfg, duracao) {
96
+ const pts = [0, duracao];
97
+ const tipos = new Set();
98
+ let eventos = 0;
99
+ for (const a of cfg.animacoes || []) {
100
+ const e = num(a.em) ?? num(a.depois_de); const s = num(a.sai);
101
+ if (e != null) { pts.push(e); eventos++; } if (s != null) pts.push(s);
102
+ tipos.add('anim:' + (a.tipo || '?'));
103
+ }
104
+ for (const b of cfg.blocos || []) {
105
+ const d = num(b.de); const t = num(b.ate);
106
+ if (d != null) { pts.push(d); eventos++; } if (t != null) pts.push(t);
107
+ tipos.add('bloco:' + (b.cena || '?'));
108
+ for (const z of b.zooms || []) { if (num(z.em) != null) { pts.push(z.em); eventos++; } if (num(z.ate) != null) pts.push(z.ate); tipos.add('punch'); }
109
+ }
110
+ return { pts: pts.filter((p) => p >= 0 && p <= duracao).sort((a, b) => a - b), tipos, eventos };
111
+ }
112
+
113
+ /** Dá nota (0 a 100) a um plano montado (o projeto.json) e devolve as falhas, cada uma com o número e o que fazer. */
114
+ function criticar(cfg, { duracao } = {}, regua = REGUA) {
115
+ const dur = duracao || Math.max(0, ...(cfg.blocos || []).map((b) => num(b.ate) || 0), ...(cfg.animacoes || []).map((a) => num(a.sai) || 0));
116
+ const falhas = [];
117
+ const m = {};
118
+ if (!dur) return { nota: 0, falhas: [{ regra: 'duracao', valor: 0, limite: '>0', conserto: 'sem duração não há o que conferir' }], metricas: m };
119
+ const { pts, tipos, eventos } = mudancasVisuais(cfg, dur);
120
+ let parado = 0; let onde = 0;
121
+ for (let k = 1; k < pts.length; k++) if (pts[k] - pts[k - 1] > parado) { parado = pts[k] - pts[k - 1]; onde = pts[k - 1]; }
122
+ m.parado_max_s = Math.round(parado * 100) / 100;
123
+ if (parado > regua.parado_max_s) falhas.push({ regra: 'parado_max_s', valor: m.parado_max_s, limite: regua.parado_max_s, em: onde, conserto: `ponha peça, zoom ou b-roll a partir de ${onde.toFixed(1)} s` });
124
+ m.tratamentos_por_min = Math.round((eventos / dur) * 60 * 10) / 10;
125
+ if (m.tratamentos_por_min < regua.tratamentos_min_por_min) falhas.push({ regra: 'tratamentos_por_min', valor: m.tratamentos_por_min, limite: regua.tratamentos_min_por_min, conserto: 'mais mudanças de imagem ligadas ao que se diz' });
126
+ m.tipos_distintos = tipos.size;
127
+ if (tipos.size < regua.tipos_distintos_min) falhas.push({ regra: 'tipos_distintos', valor: tipos.size, limite: regua.tipos_distintos_min, conserto: 'misture famílias: lettering, moldura, contador, b-roll, punch' });
128
+ const sons = (cfg.sons || []).map((s) => num(s.em)).filter((x) => x != null);
129
+ m.sfx_por_min = Math.round((sons.length / dur) * 60 * 10) / 10;
130
+ if (m.sfx_por_min < regua.sfx_min_por_min) falhas.push({ regra: 'sfx_por_min', valor: m.sfx_por_min, limite: regua.sfx_min_por_min, conserto: 'rode o plano_sfx com --variar e mais camadas' });
131
+ const cortes = (cfg.blocos || []).map((b) => num(b.de)).filter((x) => x != null && x > 0.05);
132
+ const semSom = cortes.filter((c) => !sons.some((s) => Math.abs(s - c) <= regua.sfx_no_corte_s));
133
+ m.cortes_sem_som = semSom.length;
134
+ if (semSom.length) falhas.push({ regra: 'sfx_no_corte', valor: semSom.length, limite: 0, em: semSom[0], conserto: `som nos cortes: ${semSom.slice(0, 4).join(', ')} s` });
135
+ const g = (cfg.animacoes || []).find((a) => a.id === 'gancho' || a.tipo === 'lettering');
136
+ m.gancho_em_s = g ? num(g.em) : null;
137
+ if (!g || (num(g.em) ?? 99) > regua.gancho_ate_s) falhas.push({ regra: 'gancho_ate_s', valor: m.gancho_em_s, limite: regua.gancho_ate_s, conserto: 'o gancho entra no primeiro segundo' });
138
+ const escalas = new Set();
139
+ for (const b of cfg.blocos || []) { if (num(b.zoom) != null) escalas.add(b.zoom); for (const z of b.zooms || []) if (num(z.escala) != null) escalas.add(z.escala); }
140
+ m.zooms_distintos = escalas.size;
141
+ if (escalas.size < regua.zooms_distintos_min) falhas.push({ regra: 'zooms_distintos', valor: escalas.size, limite: regua.zooms_distintos_min, conserto: 'varie o enquadramento (mais de duas escalas)' });
142
+ const sobreRosto = (cfg.animacoes || []).filter((a) => POS_ROSTO.has(a.posicao) || a.cobre_rosto === true);
143
+ m.texto_sobre_rosto = sobreRosto.length;
144
+ if (sobreRosto.length > regua.legenda_sobre_rosto) falhas.push({ regra: 'texto_sobre_rosto', valor: sobreRosto.length, limite: regua.legenda_sobre_rosto, conserto: 'mova o texto para baixo do rosto ou ponha o rosto em moldura' });
145
+ const nota = Math.max(0, Math.round(100 - falhas.length * (100 / 8)));
146
+ return { nota, passou: falhas.length === 0, falhas, metricas: m };
147
+ }
148
+
149
+ /** Duas variantes não podem abrir igual nem ter o mesmo jeito (gancho, estilo e ritmo). */
150
+ function compararVariantes(cfgs) {
151
+ const falhas = [];
152
+ const sig = (c) => {
153
+ const g = (c.animacoes || []).find((a) => a.id === 'gancho');
154
+ const t = (c.blocos || []).length;
155
+ return { gancho: g ? JSON.stringify(g.principal || g.texto || g.titulo || '').toLowerCase() : '', estilo: c.estilo || '', ritmo: Math.round(t / 3) };
156
+ };
157
+ const s = cfgs.map(sig);
158
+ for (let a = 0; a < s.length; a++) for (let b = a + 1; b < s.length; b++) {
159
+ if (s[a].gancho && s[a].gancho === s[b].gancho) falhas.push({ regra: 'mesmo_gancho', entre: [a, b], valor: s[a].gancho });
160
+ if (s[a].estilo === s[b].estilo && s[a].ritmo === s[b].ritmo) falhas.push({ regra: 'mesmo_estilo_e_ritmo', entre: [a, b], valor: `${s[a].estilo}/${s[a].ritmo}` });
161
+ }
162
+ return { passou: falhas.length === 0, falhas };
163
+ }
164
+
165
+ module.exports = { REGUA, entenderFrases, ganchosCandidatos, escolherGancho, escolherEstilo, criticar, compararVariantes, mudancasVisuais };
package/lib/editor-pro.js CHANGED
@@ -45,6 +45,7 @@ const crypto = require('crypto');
45
45
  const { spawn, spawnSync } = require('child_process');
46
46
 
47
47
  const ORIGEM = path.join(__dirname, '..', 'editor');
48
+ const MAQ = require('./editor-pro-maquina.js');
48
49
  const casa = () => process.env.PRIMOCODE_EDITOR_CASA || path.join(os.homedir(), '.primocode', 'editor');
49
50
  const venvDir = () => process.env.PRIMOCODE_EDITOR_VENV || path.join(os.homedir(), '.primocode', 'editor-venv');
50
51
  const WIN = process.platform === 'win32';
@@ -641,7 +642,7 @@ function aplicarPlano(cfg, plano, { fala, duracao, broll = [], marca = null }) {
641
642
  cfg.titulo = plano.titulo || cfg.titulo;
642
643
  cfg.formato = plano.formato || cfg.formato || '9:16';
643
644
  // limpo (a régua: chão claro, cartão branco, sem brilho) é o padrão; papelaria, noite e laranja continuam
644
- cfg.estilo = plano.estilo || 'limpo';
645
+ cfg.estilo = plano.estilo || MAQ.escolherEstilo(fala, plano.variante || 0);
645
646
  // a legenda cinética é o padrão (Inter 800, palavra por palavra, a palavra-chave no degradê); padrao e discreta continuam
646
647
  cfg.legenda = Object.assign({ ligada: true, estilo: 'cinetica', max_palavras: 3, na_moldura: false }, cfg.legenda || {}, typeof plano.legenda === 'string' ? { estilo: plano.legenda } : (plano.legenda || {}));
647
648
  const pedida = typeof plano.legenda === 'string' ? plano.legenda : (plano.legenda && plano.legenda.estilo);
@@ -653,7 +654,7 @@ function aplicarPlano(cfg, plano, { fala, duracao, broll = [], marca = null }) {
653
654
  if (Array.isArray(grad) && grad.length >= 2) cfg.gradiente = grad.slice(0, 2);
654
655
  if (plano.correcoes) cfg.correcoes = Object.assign({}, cfg.correcoes || {}, plano.correcoes);
655
656
  const animacoes = [];
656
- const gancho = plano.gancho === false ? null : (plano.gancho || ganchoAutomatico(fala));
657
+ const gancho = plano.gancho === false ? null : (plano.gancho || MAQ.escolherGancho(fala, plano.variante || 0) || ganchoAutomatico(fala));
657
658
  if (gancho) {
658
659
  const prim = fala[0] ? Number(fala[0].split('-')[0]) : 0;
659
660
  const fimFrase = fala[0] ? Number(fala[0].split(' ')[0].split('-')[1]) : 3;
@@ -817,11 +818,13 @@ async function montar(opts = {}, ctx = {}) {
817
818
  }
818
819
  await passo('montando a edição…', () => build(E, P.proj, ['completo']));
819
820
  if (plano.sons !== false) {
820
- await passo('escolhendo os efeitos sonoros casados com cada animação…', () => helper(E, 'plano_sfx', ['--projeto', P.proj]));
821
+ await passo('escolhendo os efeitos sonoros casados com cada animação…', () => helper(E, 'plano_sfx', ['--projeto', P.proj, '--variar']));
821
822
  await passo('montando a edição com os sons…', () => build(E, P.proj, ['completo']));
822
823
  }
824
+ const critica = MAQ.criticar(lerJson(cfgPath, {}), { duracao: tl.duration || 0 });
825
+ for (const f of critica.falhas) avisosPlano.push(`régua: ${f.regra} = ${f.valor} (limite ${f.limite}): ${f.conserto}`);
823
826
  const lint = await build(E, P.proj, ['hf', 'lint']);
824
- if (opts.render === false) return { ok: true, pasta: P.pasta, projeto: P.proj, lint: cauda(lint.saida, 6), broll, camera, avisos: avisosPlano };
827
+ if (opts.render === false) return { ok: true, pasta: P.pasta, projeto: P.proj, lint: cauda(lint.saida, 6), broll, camera, critica, avisos: avisosPlano };
825
828
  await passo('renderizando o vídeo (leva alguns minutos)…', () => build(E, P.proj, ['hf', 'render', '--quality', 'delivery', '--output', 'renders/final.mp4'], { timeout: 5400000 }));
826
829
  let final = path.join(P.proj, 'renders', 'final.mp4');
827
830
  // trilha: arquivo da pessoa, ou a busca do PrimoCode (energia alta), ou nenhuma
@@ -856,7 +859,7 @@ async function montar(opts = {}, ctx = {}) {
856
859
  ok: !!(rel && rel.passou), passou: !!(rel && rel.passou), video: saida, quadros: fotos,
857
860
  audio, portao: rel ? { passou: rel.passou, falhas: rel.falhas, modos: rel.modos_do_volume, clipadas: rel.amostras_clipadas, folha: rel.folha, leitor: rel.leitor_de_texto } : cauda(portao.saida),
858
861
  trilha: trilha ? { arquivo: trilha.arquivo, titulo: trilha.titulo, creditos: trilha.creditos } : null,
859
- broll, avisos: avisosPlano, camera, projeto: P.proj, pasta: P.pasta,
862
+ broll, avisos: avisosPlano, camera, critica, projeto: P.proj, pasta: P.pasta,
860
863
  ...(rel && !rel.passou ? { error: 'o portão REPROVOU: ' + (rel.falhas || []).join(' | ') + ' — conserte no plano e chame montar de novo' } : {}),
861
864
  };
862
865
  } catch (e) {
@@ -882,7 +885,7 @@ const AJUDA = `editor_pro — edição completa de VÍDEO FALADO (motor EDIT IA
882
885
  acao "montar" {pasta, plano?, trilha?} gancho, peças, b-roll, câmera viva, sons, render, trilha com ducking, portão
883
886
  acao "preparar" baixa os arquivos e cria o Python do editor (sem editar nada)
884
887
  acao "estado" {pasta} onde a edição parou
885
- plano: {gancho:"Texto *destaque*", pecas:[peças do catálogo], broll:[{busca,de,ate,rotulo?}], moldura?, comparar?, blocos?, estilo?, legenda?, correcoes?, trilha?}
888
+ plano: {gancho:"Texto *destaque*", pecas:[peças do catálogo], broll:[{busca,de,ate,rotulo?}], moldura?, comparar?, blocos?, estilo?, variante? (0,1,2…: outro gancho e outro estilo, pelo conteúdo), legenda?, correcoes?, trilha?}
886
889
  estilo: "limpo" (padrão: chão claro, cartão branco, sem brilho) | "laranja" | "noite" | "papelaria"
887
890
  moldura: [{de, ate, fundo?:"papel"|"desfocado"}] a pessoa num quadro arredondado que flutua devagar sobre o chão
888
891
  comparar: [{de, ate, arranjo:"lado"|"empilhado"|"auto", a:{arquivo|"corte", inicio, rotulo}, b:{...}}] dois vídeos na mesma escala, rótulo literal
@@ -923,5 +926,5 @@ module.exports = {
923
926
  ORIGEM, AJUDA, casa, venvDir, venvPython, impressao, instalarArquivos, garantirArquivo, garantirVenv, gerarSintetizados,
924
927
  dependencias, acharUv, acharPython, garantir, helper, cortar, montar, editar, ferramenta, main,
925
928
  limparTrechos, trechosDoModelo, ganchoAutomatico, pecasDasSugestoes, aplicarPlano, prepararPasta, falaCompacta, urlDoEspelho,
926
- palavrasDoCorte, naOnda, limiaresDaOnda, resumoCamera, palavrasChave, gradienteDaMarca,
929
+ palavrasDoCorte, naOnda, limiaresDaOnda, resumoCamera, palavrasChave, gradienteDaMarca, maquina: MAQ,
927
930
  };
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli.js — a linha de comando que a skill e os especialistas chamam.
4
+ * node lib/entregaveis/cli.js conferir plano.json
5
+ * node lib/entregaveis/cli.js exportar plano.json [pasta] [--forcar] [--sem-pdf]
6
+ * Sai com 1 se a conferência reprova (ou se não exportou).
7
+ */
8
+ 'use strict';
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const slides = require('./slides');
12
+
13
+ async function main(argv) {
14
+ const [cmd, arq, pasta] = argv.filter((a) => !a.startsWith('--'));
15
+ if (!['conferir', 'exportar'].includes(cmd) || !arq) { console.error('uso: cli.js conferir|exportar plano.json [pasta]'); return 2; }
16
+ let plano;
17
+ try { plano = JSON.parse(fs.readFileSync(arq, 'utf8')); } catch (e) { console.error('plano ilegível: ' + e.message); return 2; }
18
+ if (cmd === 'conferir') { const r = slides.conferirDeck(plano); console.log(slides.relatorio(r)); return r.passou ? 0 : 1; }
19
+ const r = await slides.exportarDeck(plano, path.resolve(pasta || path.dirname(arq)), { forcar: argv.includes('--forcar'), pdf: !argv.includes('--sem-pdf') });
20
+ console.log(slides.relatorio(r.conferencia));
21
+ if (!r.exportado) { console.log('NÃO exportou: conserte as falhas (ou --forcar).'); return 1; }
22
+ r.arquivos.forEach((a) => console.log('arquivo: ' + a));
23
+ if (r.erroPdf) console.log('sem PDF: ' + r.erroPdf);
24
+ return 0;
25
+ }
26
+ main(process.argv.slice(2)).then((c) => process.exit(c));
@@ -0,0 +1,50 @@
1
+ /**
2
+ * especialistas.js (entregáveis) — quem faz cada parte do slide, do cartaz e do documento.
3
+ *
4
+ * Mesmo formato de lib/especialistas.js (faz + briefing curto que vai na frente da tarefa).
5
+ * Ficam separados de propósito: os nomes de lib/especialistas.js são um acordo com o enum
6
+ * do spawn_agent do servidor (test/especialistas.test.js), e aqui os papéis são de ofício
7
+ * de entregável. O briefing termina sempre em como PROVAR que acabou.
8
+ */
9
+ 'use strict';
10
+
11
+ const NUCLEO = 'Conferência é código: rode `node lib/entregaveis/cli.js conferir plano.json` e só diga "pronto" com nota >= 80 e sem falha de bloqueio.';
12
+
13
+ const ESPECIALISTAS_ENTREGAVEIS = {
14
+ roteirista: {
15
+ tipos: ['slides', 'documento', 'cartaz'],
16
+ faz: 'transforma o brief em estrutura: uma ideia por slide/seção, título que é afirmação',
17
+ briefing: `Você é o ROTEIRISTA. Parta do brief (tema, público, objetivo). Escreva primeiro a história em uma frase, depois a lista de slides com UMA afirmação cada (3 a 12 palavras, nunca "Visão geral"). O texto do slide é o que se lê de longe; o que se fala vai em "nota". Abra com capa, feche com ação concreta (campo "acao"). Cada número citado tem fonte [LI] ou vira "[VERIFICAR]". Prova: plano.json com slides e notas, sem conferência de texto ainda.`,
18
+ },
19
+ 'diretor-de-arte': {
20
+ tipos: ['slides', 'cartaz', 'documento'],
21
+ faz: 'escolhe a direção visual a partir de material solto, nunca de modelo pronto',
22
+ briefing: `Você é o DIRETOR DE ARTE. NÃO use template: junte material solto (objetos, cores e textura do assunto; referências que o dono aprovou) e escreva a direção em "direcao": conceito (por que este visual é DESTE assunto, >= 6 palavras), paleta {fundo,texto,destaque,destaque2?,suave?} em hex, fonteTitulo, fonteCorpo (fonte que a outra máquina tem, ou a que será embutida no PDF). Título gigante em cor de destaque (degradê de texto ainda não sai no PDF). Todo elemento visual extra leva "visual.motivo": o que ele tem a ver com o conteúdo. Prova: contraste texto/fundo >= 4.5 calculado, não olhado.`,
23
+ },
24
+ tipografo: {
25
+ tipos: ['slides', 'cartaz', 'documento'],
26
+ faz: 'hierarquia, tamanhos e encaixe do texto',
27
+ briefing: `Você é o TIPÓGRAFO. Defina a escala (título 40-80pt, corpo >= 24pt em slide) e dê UM peso de destaque por slide. Encurte até o texto caber na caixa (a conferência estima o encaixe). Nada de três fontes. Prova: regras "tamanho-da-letra" e "encaixe" sem falha.`,
28
+ },
29
+ revisor: {
30
+ tipos: ['slides', 'cartaz', 'documento'],
31
+ faz: 'confere o resultado com os números e aponta; não reescreve',
32
+ briefing: `Você é o REVISOR e não fez este plano: aponte, não conserte. ${NUCLEO} Depois abra o PDF exportado e olhe de verdade 3 slides (capa, o mais cheio, o último): texto vazando, contraste, layout repetido. Liste falha por slide com o número. Prova: relatório da conferência + o que viu no PDF.`,
33
+ },
34
+ };
35
+
36
+ const NOMES = Object.keys(ESPECIALISTAS_ENTREGAVEIS);
37
+
38
+ function porNome(nome) {
39
+ const n = String(nome || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, '-');
40
+ const mapa = { diretor: 'diretor-de-arte', arte: 'diretor-de-arte', designer: 'diretor-de-arte', roteiro: 'roteirista', tipografia: 'tipografo', review: 'revisor', reviewer: 'revisor' };
41
+ const k = mapa[n] || n;
42
+ return ESPECIALISTAS_ENTREGAVEIS[k] ? { nome: k, ...ESPECIALISTAS_ENTREGAVEIS[k] } : null;
43
+ }
44
+
45
+ /** A ordem em que os especialistas trabalham num tipo de entregável. */
46
+ function equipe(tipo) {
47
+ return ['roteirista', 'diretor-de-arte', 'tipografo', 'revisor'].filter((n) => ESPECIALISTAS_ENTREGAVEIS[n].tipos.includes(tipo));
48
+ }
49
+
50
+ module.exports = { ESPECIALISTAS_ENTREGAVEIS, NOMES, porNome, equipe };