raptor-aios 0.14.0 → 0.15.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 (46) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +51 -17
  3. package/dist/_core/dist/agents/prompt-builder.js +10 -0
  4. package/dist/_core/dist/design/anatomy.js +4 -1
  5. package/dist/_core/dist/design/asset-normalize.js +10 -2
  6. package/dist/_core/dist/design/downloader.js +38 -2
  7. package/dist/_core/dist/design/fetcher.js +35 -9
  8. package/dist/_core/dist/design/fidelity.js +8 -1
  9. package/dist/_core/dist/design/figma-rest.js +5 -2
  10. package/dist/_core/dist/design/mapper.js +2 -0
  11. package/dist/_core/dist/design/scaffold.js +36 -7
  12. package/dist/_core/dist/design/screens.js +18 -0
  13. package/dist/_core/dist/design/token-diff.js +4 -2
  14. package/dist/_core/dist/gates/builtin.js +32 -9
  15. package/dist/_core/dist/gates/design-gates.js +64 -16
  16. package/dist/_core/dist/gates/m7-gates.js +23 -5
  17. package/dist/_core/dist/jira/mapper.js +100 -2
  18. package/dist/_core/dist/models/checklist.js +10 -6
  19. package/dist/_core/dist/models/clarify.js +69 -1
  20. package/dist/_core/dist/models/impl-log.js +71 -0
  21. package/dist/_core/dist/prompts/resumption.js +16 -2
  22. package/dist/_core/dist/state/feature.js +1 -1
  23. package/dist/_core/dist/transport/mcp.js +1 -0
  24. package/dist/_core/package.json +1 -1
  25. package/dist/_core/templates/raptor.yml.hbs +0 -1
  26. package/dist/_core/templates/spec.md.hbs +14 -12
  27. package/dist/commands/approve.js +22 -8
  28. package/dist/commands/design/pull.js +9 -1
  29. package/dist/commands/design/sync.js +26 -5
  30. package/dist/commands/implement.js +71 -7
  31. package/dist/commands/new.js +34 -3
  32. package/dist/commands/taskstoissues.js +16 -5
  33. package/dist/shared/design.js +2 -1
  34. package/dist/shared/project.js +4 -4
  35. package/package.json +1 -1
  36. package/scripts/app/screenshot.sh +6 -2
  37. package/scripts/bash/common.sh +64 -23
  38. package/scripts/bash/create-new-feature.sh +11 -6
  39. package/scripts/bash/setup-plan.sh +3 -3
  40. package/scripts/check-naming.sh +16 -8
  41. package/scripts/powershell/common.ps1 +51 -9
  42. package/scripts/powershell/create-new-feature.ps1 +3 -1
  43. package/scripts/prepare-npm.mjs +1 -1
  44. package/scripts/review/hex-scan.mjs +6 -2
  45. package/templates/commands/implement.md +38 -9
  46. package/templates/commands/specify.md +8 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,98 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.15.0] - 2026-06-22
9
+
10
+ Tema do épico KAN-20: **determinismo e consentimento**. Tudo que antes "passava em
11
+ silêncio", "escrevia no card sem perguntar" ou "despejava o arquivo Figma inteiro"
12
+ agora é explícito, opt-in ou escopado por node.
13
+
14
+ ### Added
15
+
16
+ - **`--design-scope node|file`** (em `new`, `design sync`, `design pull`). Controla o
17
+ escopo da captura de telas/assets. Em `new` e `design sync` o default é `node` (só o
18
+ frame linkado pela URL); em `design pull` o default é `file` (inspeção do arquivo
19
+ inteiro). `--design-scope file` é o escape hatch para o comportamento legado (arquivo
20
+ todo). Uma ref COM node-id captura aquele node sob qualquer scope.
21
+ - **`raptor new --activate` / `--no-activate`.** `new` agora aponta
22
+ `.raptor/feature.json` para a feature recém-criada (default ligado). `--no-activate`
23
+ preserva a feature anterior.
24
+ - **`--sync-jira`** (em `approve`, `implement`, `taskstoissues`). Opt-in para escrever
25
+ de volta no card Jira (comentário de ciclo de vida + transição; em `taskstoissues`,
26
+ comentário-resumo dos issues criados). Sem a flag, só loga a ação pretendida.
27
+ - **`raptor implement --finalize`.** Reconstrói `impl-log.md` a partir dos eventos
28
+ `code.generated` do `audit.jsonl` quando as tasks foram fechadas sem `--task`.
29
+ Recupera traces de AC das tags `[AC-#]` do `tasks.md`; quando não há, deixa vazio
30
+ para o gate M7 cobrar (não fabrica pass). Respeita `--dry-run`.
31
+
32
+ ### Changed
33
+
34
+ - **`--design-fetch` agora é escopo por node-id por default** (era arquivo inteiro,
35
+ até 107 telas). Card sem node-id pula a captura com aviso acionável; a spec referencia
36
+ `assets-manifest.json` em vez de listar tudo. **Migração:** use `--design-scope file`
37
+ para o comportamento antigo.
38
+ - **`raptor new` ativa a feature criada por default.** `feature.json` é dobrado no
39
+ auto-commit para empilhar specs sem sujar a árvore. **Migração:** use `--no-activate`
40
+ se contava com a feature anterior continuar ativa.
41
+ - **Escrita no Jira é opt-in (`--sync-jira`)** em `approve`/`implement`/`taskstoissues`.
42
+ **Migração:** pipelines que esperavam status-sync automático precisam adicionar a flag.
43
+ - **`gate.design.fidelity` bloqueia por scorecards.** Avalia scorecards sempre que
44
+ existirem; retorna `skipped` (nunca mais `pass` falso) só quando não há scorecards
45
+ nem impl-log. **Migração:** projetos cujo fidelity passava verde sem evidência podem
46
+ ver o gate bloquear agora.
47
+ - **Gates M7 falham (não skipam) quando há tasks concluídas mas falta `impl-log.md`.**
48
+ Sem tasks concluídas continua `skipped`. A skill `/raptor-implement` foi reescrita
49
+ para registrar cada task via `raptor implement --task`.
50
+ - **Namespace de specs unificado em `.raptor/specs`.** `get_next_feature_number` deixa
51
+ de saltar numeração por varrer `specs/` legado (002 → 011); legado vira só fallback
52
+ de leitura. Espelhado no PowerShell.
53
+ - **`branch_prefix` removido do schema/`raptor.yml`** (dead config, 0 leituras).
54
+ **Migração:** pode remover do `raptor.yml` — não tinha efeito.
55
+ - **stderr do MCP silenciado por default** (`StdioClientTransport` usa `stderr:'ignore'`).
56
+ Reative diagnóstico com `RAPTOR_MCP_DEBUG=1`.
57
+ - **Assets do fetch isolados com `.gitignore` RAPTOR-managed + guarda de colisão** —
58
+ binários baixados não entram em versionamento por acidente; a captura nunca
59
+ sobrescreve arte já versionada.
60
+
61
+ ### Fixed
62
+
63
+ - **Falso negativo de cobertura de US eliminado** — `gate.spec.ready` normaliza IDs:
64
+ header `US-001` casa com `[US1]`.
65
+ - **Markers falsos não acusam mais** — detector ignora code-spans (`` `[PREENCHER]` ``)
66
+ e linhas de checklist negadoras; helper compartilhado em spec/plan/tasks.ready.
67
+ - **Checklist sem ID conta corretamente** — parser conta itens `[x]` mesmo sem `CK-#`
68
+ (contido em `status`; gates estruturados inalterados).
69
+ - **Path in-place no redesign** — `gate.tasks.ready` herda path por fase; reset por
70
+ heading preserva detecção de drift.
71
+ - **Hash stale agora orienta a correção** — `gate.plan/tasks.input_hash` sugere
72
+ `raptor resync <feature> --artifact plan|tasks`.
73
+ - **`clarify --apply` popula `## Clarifications`** (upsert das Q&A; CLI é dono único,
74
+ skill só produz `answers.json`), preserva frontmatter e não mascara marker real.
75
+ - **Wiki-markup do Jira → Markdown no seeding** — `flattenAdf` converte `{quote}`,
76
+ `{{...}}`, `||tabela||`, `hN.`, `{code}`/`{noformat}`, gateado por detecção de wiki
77
+ (não toca Markdown existente; preserva `5 * 4`).
78
+ - **Ator canônico único no audit** — `currentActor` fixa o `user` no `git user.email`
79
+ (fallback name → `USER`).
80
+ - **`HAS_GIT` correto no `--dry-run`** — `create-new-feature.sh` detecta git antes da
81
+ bifurcação dry-run. Skill `specify` passa `--short-name` (slug kebab-case).
82
+ - **Robustez do seeding de design** — tokens vazios direcionam ao MCP `get_variable_defs`
83
+ e o gate bloqueia `tokens.json` vazio (`radii` sempre presente); `gate.design.ready`
84
+ casa telas por `name` do frontmatter (tolerante a sufixo node-id); `design sync`
85
+ avisa (não apaga) telas órfãs da convenção antiga de slug.
86
+
87
+ ## [0.14.1] - 2026-06-21
88
+
89
+ ### Fixed
90
+
91
+ - **Correções do code-review xhigh do épico de fidelidade visual.** `gate.design.fidelity`
92
+ resiliente a race (`readFileSync`/`statSync` não crasham mais) e com contenção de
93
+ evidência à prova de symlink (`realpathSync`); duplicata de screenshot comparada por
94
+ path resolvido. Parser do scorecard mais estrito: divergência com enum inválido e
95
+ região sem `id` são **descartadas** (em vez de retornar objeto com tipo inválido).
96
+ `token-diff` normaliza hex de 4 dígitos (`#rgba`). Scripts `hex-scan`/`screenshot.sh`
97
+ validam argumentos (sem crash de `shift`; resolução do core com mensagem clara). Doc:
98
+ diagrama Mermaid corrigido (linter havia transformado arestas em tabela).
99
+
8
100
  ## [0.14.0] - 2026-06-21
9
101
 
10
102
  ### Added
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  [![npm](https://img.shields.io/npm/v/raptor-aios?color=ef4444&label=raptor-aios&logo=npm)](https://www.npmjs.com/package/raptor-aios)
8
8
  [![node](https://img.shields.io/badge/node-%E2%89%A5%2020-3c873a?logo=node.js&logoColor=white)](https://nodejs.org)
9
9
  [![platform](https://img.shields.io/badge/target-React%20Native-61dafb?logo=react&logoColor=black)](https://reactnative.dev)
10
- [![tests](https://img.shields.io/badge/tests-1374%20passing-22c55e)](#️-desenvolvimento-local)
11
- [![version](https://img.shields.io/badge/version-0.13.0-ef4444)](CHANGELOG.md)
10
+ [![tests](https://img.shields.io/badge/tests-1597%20passing-22c55e)](#️-desenvolvimento-local)
11
+ [![version](https://img.shields.io/badge/version-0.15.0-ef4444)](CHANGELOG.md)
12
12
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
13
13
 
14
14
  </div>
@@ -24,7 +24,7 @@ npm install -g raptor-aios
24
24
  cd meu-app-react-native
25
25
  raptor setup # 🧭 assistente guiado: IA/IDE, preset, Jira/Figma (ou 'raptor init' direto)
26
26
  raptor new login-biometrico -d "Permitir login com Face ID / digital"
27
- # → cria a feature E já troca para a branch feat/001-login-biometrico
27
+ # → cria a feature, forka a branch feat/001-login-biometrico do HEAD e a ativa
28
28
  ```
29
29
 
30
30
  ---
@@ -40,6 +40,7 @@ raptor new login-biometrico -d "Permitir login com Face ID / digital"
40
40
  - [📱 Fluxo correto com React Native](#-fluxo-correto-com-react-native)
41
41
  - [🛡️ Gates: o que bloqueia o quê](#️-gates-o-que-bloqueia-o-quê)
42
42
  - [🔬 A ponta de verificação (`verify`)](#-a-ponta-de-verificação-verify)
43
+ - [🎯 Fidelidade visual (Figma → React Native)](#-fidelidade-visual-figma--react-native)
43
44
  - [🧰 Capacidades do Raptor](#-capacidades-do-raptor)
44
45
  - [📖 Referência de comandos CLI](#-referência-de-comandos-cli)
45
46
  - [🗂️ Estrutura criada no projeto](#️-estrutura-criada-no-projeto)
@@ -61,6 +62,7 @@ O Raptor leva o SDD a sério para o mundo mobile, adicionando o que falta para u
61
62
  | 📜 **Trilha de auditoria** | Cada passo emite eventos em `audit.jsonl`, validáveis contra um JSON Schema canônico. |
62
63
  | 🔗 **Rastreabilidade código↔spec (M7)** | Cada artefato de código aponta para os critérios de aceitação que satisfaz, com evidência pareada (*code + test*). |
63
64
  | 🔬 **Verificação real** | `verify a11y/perf/stores/os-matrix` medem o app de verdade e comparam com o que a spec/plan declararam. |
65
+ | 🎯 **Fidelidade visual (Figma→RN)** | Decompõe a tela em **anatomia**, dá **nota por micro-região** (mín. 95) e **trava o merge** via `gate.design.fidelity` até a implementação replicar o Figma. Guia: [docs/visual-fidelity-loop.md](docs/visual-fidelity-loop.md). |
64
66
  | 🤖 **Agnóstico de agente** | Materializa slash commands para Claude Code, Cursor, Copilot, Codex, Gemini e Antigravity. |
65
67
  | ✨ **Specs sem pontas soltas** | A spec nasce preenchida (defaults informados), com teto de 3 dúvidas de alto impacto, e um `/raptor-clarify` investigativo que caça lacunas antes do planejamento. |
66
68
  | 🌿 **Branch & commit sob controle** | Cada feature ganha sua branch (`feat/001-…`) automaticamente, e hooks *warn-only* alertam sobre arquivos sensíveis ou fora do contexto — sem travar seu fluxo git. |
@@ -199,7 +201,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
199
201
 
200
202
  ### Uma branch por unidade de trabalho
201
203
 
202
- `raptor new` cria e troca para a branch certa, sempre ramificando de `main`:
204
+ `raptor new` cria e troca para a branch certa, **forkando do HEAD atual** (modelo empilhado, não de `main`): cada nova feature ramifica de onde você está, então specs se **empilham** umas sobre as outras. Ao final, a feature recém-criada vira a **ativa** em `.raptor/feature.json` (comandos seguintes a resolvem sem `--feature`); use `--no-activate` para manter a feature anterior ativa (`--activate` é o default).
203
205
 
204
206
  | Você roda | Branch criada |
205
207
  | -------------------------------------- | --------------------------------------------------- |
@@ -208,7 +210,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
208
210
  | `raptor new refactor-auth --kind=debt` | `debt/003-refactor-auth` |
209
211
  | `raptor new login --jira APP-1234` | `feat/004-login.app-1234` *(tipo inferido do Jira)* |
210
212
 
211
- Numeração sequencial global, prefixo por tipo (`feat`/`bugfix`/`debt`), e a chave do Jira no fim quando houver. Já vem ligado (`git.branch_per_feature`); desligue com `--no-branch` num comando ou `no_git` no projeto.
213
+ Numeração sequencial global, prefixo por tipo (`feat`/`bugfix`/`debt`), e a chave do Jira no fim quando houver. Já vem ligado (`git.branch_per_feature`); desligue com `--no-branch` num comando ou `no_git` no projeto. Por padrão a spec scaffoldada é **commitada** na própria branch (para o próximo `raptor new` empilhar em cima); `--no-commit` deixa a spec sem commit.
212
214
 
213
215
  ### Commits coerentes, sem travar seu fluxo
214
216
 
@@ -269,9 +271,11 @@ Gera checklists de qualidade temáticos (code quality, testes, segurança, perfo
269
271
 
270
272
  Executa as tarefas em ordem de fase, respeitando paralelização e (se exigido) TDD. Marca cada tarefa `[x]` ao concluir e registra a **evidência M7** (qual arquivo cobre qual critério de aceitação).
271
273
 
274
+ > ⚠️ **`impl-log.md` é inegociável.** Cada `raptor implement --task <id> --files … --acceptance …` atualiza o `impl-log.md` com as execuções, artefatos e traces. Se ele estiver **ausente após a implementação**, os gates M7 **bloqueiam** o `verify` (deixam de "pular" por arquivo inexistente e passam a falhar). Se o agente esqueceu de registrar tarefa por tarefa via `--task`, rode `raptor implement <feat> --finalize` para **reconstruir** o `impl-log.md` a partir dos eventos `code.generated` do `audit.jsonl` — traces de aceitação ausentes ficam vazios para o gate M7 cobrar (complete-os com `--amend --acceptance <AC-#>`).
275
+
272
276
  ### 🎫 `/raptor-taskstoissues`
273
277
 
274
- Converte as tarefas em issues de um tracker (GitHub via MCP, Jira via integração).
278
+ Converte as tarefas em issues de um tracker (GitHub via MCP, Jira via integração). Criar as issues é o propósito do comando, mas **escrever de volta no card pai** (comentário de resumo) é **opt-in via `--sync-jira`** — sem a flag, nada é mutado no ticket de origem.
275
279
 
276
280
  ### 🌿 `/raptor-commit-review`
277
281
 
@@ -461,6 +465,41 @@ Declarar não é cumprir. O Raptor **mede** e compara com o declarado:
461
465
 
462
466
  ---
463
467
 
468
+ ## 🎯 Fidelidade visual (Figma → React Native)
469
+
470
+ > **Novo na v0.14.0.** Transforma _"a tela tem os tokens"_ em _"a tela **replica** o Figma"_.
471
+
472
+ <p align="center"><img src="docs/assets/visual-fidelity-loop.svg" alt="Visual Fidelity Loop — anatomia, loop de fidelidade e gate" width="760"></p>
473
+
474
+ Capturar o design não garante fidelidade. O Raptor decompõe cada tela em **anatomia**
475
+ (regiões → elementos amarrados a token), pontua cada **micro-região de 60 a 100** (mínimo
476
+ **95**) e **trava o `verify`** via `gate.design.fidelity` até a implementação replicar o
477
+ protótipo — com um **backbone determinístico** (hex / `rgb()` / token + screenshots de
478
+ evidência) que o Raptor mede sozinho, então o agente **não consegue inflar a nota**.
479
+
480
+ ```bash
481
+ raptor design fidelity 001-home # driver do loop: reporta a fidelidade + emite o prompt do scorer
482
+ raptor verify fidelity 001-home # lente read-only: nota por micro-região (60–100, mín. 95)
483
+ raptor verify 001-home # gate.design.fidelity (estático) trava se alguma região < mínimo
484
+ ```
485
+
486
+ ```
487
+ === fidelity: 001-home ===
488
+ ✅ home-unavailable [unavailable] header: 97/95
489
+ ✅ home-unavailable [unavailable] earnings-card: 99/95
490
+ ✅ home-unavailable [unavailable] status-component: 98/95
491
+ Summary: 5 region(s) scored — 5 pass, 0 fail.
492
+ ```
493
+
494
+ O agente `design-fidelity` (read-only, _"na dúvida, reprova"_) é acionado **depois do
495
+ implement**: extrai os valores reais do Figma, captura o app rodando, pontua cada
496
+ micro-região e escreve `design/visual-review/<slug>.fidelity.json`. O gate **estático** lê
497
+ esse scorecard (como os gates M7 leem `impl-log.md`) e nunca roda o app.
498
+
499
+ > 📖 **Guia completo** — use cases, workflow, exemplos e diagramas: [docs/visual-fidelity-loop.md](docs/visual-fidelity-loop.md).
500
+
501
+ ---
502
+
464
503
  ## 🧰 Capacidades do Raptor
465
504
 
466
505
  | Capacidade | Para que serve | Comandos |
@@ -484,6 +523,8 @@ raptor jira pull APP-1234 # importa a issue
484
523
  raptor new login --jira APP-1234 # semeia a spec a partir da issue
485
524
  ```
486
525
 
526
+ > 🔒 **Escrita no Jira é opt-in.** O `raptor new --jira` só **lê** o card. Os comandos do ciclo que poderiam escrever de volta no ticket de origem (`approve`, `implement`, `taskstoissues`) **só mutam o Jira** — comentário de lifecycle e transição opcional — quando você passa **`--sync-jira`**. Sem a flag, o Raptor apenas anuncia o que faria.
527
+
487
528
  > 📖 Como o card é capturado **por inteiro** (campos ricos, links de Figma) e redistribuído na spec — com os gates cobrando: veja [docs/jira-spec-enrichment.md](docs/jira-spec-enrichment.md).
488
529
 
489
530
  ### 🎨 Integração com Figma (design determinístico)
@@ -498,17 +539,9 @@ raptor design sync home # re-captura idempotente (nunca sobrescreve
498
539
 
499
540
  O provider **`figma-rest`** usa a REST API da Figma com um PAT (`${FIGMA_TOKEN}` no `raptor.yml`), é determinístico e roda em CI. A captura gera `design/tokens.json` (colors/typography/spacing/radii), `design/screens/<slug>.md` (frames de topo) e `design/assets-manifest.json` (assets baixados com SHA-256, guarda anti-SSRF). Quando há captura real, os prompts fazem o **flip REDISTRIBUIR**: o agente passa a *refinar e redistribuir* os artefatos em disco em vez de "olhar o Figma" — cobrado pelo `gate.design.ready`.
500
541
 
501
- #### 🎯 Loop de fidelidade visual (Figma React Native)
502
-
503
- Para garantir que a tela implementada **replica o Figma** (e não só "tem os tokens"), o Raptor adota anatomia + nota por micro-região:
504
-
505
- ```bash
506
- raptor design fidelity home-entregador # driver do loop: reporta a fidelidade e emite o prompt do scorer
507
- raptor verify fidelity home-entregador # lente read-only: nota por micro-região (60–100, mín. 95)
508
- raptor verify home-entregador # gate.design.fidelity (estático) trava se alguma região < mínimo
509
- ```
542
+ A captura no `raptor new` é **opt-in via `--design-fetch`** (default ligado): com o Figma conectado, ela puxa tokens/telas/assets e semeia `design/`; com `--no-design-fetch` apenas scaffolda o link, sem buscar. O **escopo** é controlado por `--design-scope`: `node` (default) captura **só o frame linkado** (o node-id na URL); `file` é uma escotilha de fuga que puxa o **arquivo Figma inteiro** (comportamento legado). O mesmo `--design-scope` vale para `raptor design sync`.
510
543
 
511
- Cada tela ganha uma seção `## Anatomy` em `design/screens/<slug>.md` (regiões→elementos amarrados a token) — o **contrato** de fidelidade. O agente `design-fidelity` (read-only, adversarial "na dúvida, reprova") extrai os valores reais do Figma, captura o app rodando (`scripts/app/screenshot.sh`), pontua cada micro-região e escreve `design/visual-review/<slug>.fidelity.json` (schema `raptor.fidelity/v1`). O **backbone determinístico** que o Raptor mede sozinho hex hardcoded (`scripts/review/hex-scan.mjs`), drift de token (`scripts/review/diff-tokens.mjs`), screenshots de evidência — impede inflar a nota. O `gate.design.fidelity` é **estático** (lê o scorecard, nunca roda o app), como os gates M7 leem o `impl-log.md`. Detalhes: [docs/visual-fidelity-loop.md](docs/visual-fidelity-loop.md).
544
+ > 🎯 **Fidelidade visual:** depois do `implement`, um loop garante que a tela **replica** o Figma (anatomia + nota por micro-região + `gate.design.fidelity`) — veja [🎯 Fidelidade visual (Figma React Native)](#-fidelidade-visual-figma--react-native).
512
545
 
513
546
  > 📖 Detalhes, providers e limitações: [docs/design-figma-sync.md](docs/design-figma-sync.md) e [docs/design-gate.md](docs/design-gate.md).
514
547
 
@@ -530,7 +563,7 @@ Cada tela ganha uma seção `## Anatomy` em `design/screens/<slug>.md` (regiões
530
563
  🎨 Figma/Design design connect · status · pull · sync · disconnect
531
564
  ```
532
565
 
533
- Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um.
566
+ Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um, ou veja a **[📖 Referência de flags (docs/cli-reference.md)](docs/cli-reference.md)** — flags relevantes e novas da v0.15.0, por comando, com defaults.
534
567
 
535
568
  ### 💬 Quando você erra um comando
536
569
 
@@ -595,6 +628,7 @@ Comece pelo **[📖 Glossário Canônico](docs/glossary.md)** — a referência
595
628
  | Documento | Sobre |
596
629
  | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
597
630
  | **[📖 docs/glossary.md](docs/glossary.md)** | Glossário canônico: conceitos, hierarquias, catálogo de 48 gates e de comandos CLI |
631
+ | [📖 docs/cli-reference.md](docs/cli-reference.md) | Referência de flags por comando (novas/relevantes da v0.15.0) com defaults |
598
632
  | [🧭 docs/setup-wizard.md](docs/setup-wizard.md) | Assistente de instalação `raptor setup`: onboarding, IA/IDE, preset, Jira/Figma |
599
633
  | [🛡️ docs/readiness-gates.md](docs/readiness-gates.md) | Gates de prontidão (`spec/plan/tasks.ready`) em detalhe |
600
634
  | [🎨 docs/design-figma-sync.md](docs/design-figma-sync.md) | Captura determinística do Figma (`figma-rest`): tokens/telas/assets/fontes + flip REDISTRIBUIR |
@@ -158,6 +158,9 @@ const PHASE_MAP = {
158
158
  "6. Se a feature tiver design (pasta `design/`): consuma os assets já catalogados em",
159
159
  " `design/assets-manifest.json` (baixados em `assets/{images,icons,fonts}`) e baixe via",
160
160
  " Figma MCP só o que faltar; referencie por caminho concreto (tags `[screen: X]` / `[asset: X]`).",
161
+ "7. PRESERVE o frontmatter existente de `tasks.md` ao reescrevê-lo — em especial o",
162
+ " campo `plan_hash` (e qualquer outro já presente). NÃO o remova nem o reescreva:",
163
+ " o gate de staleness o usa para detectar `tasks.md` desatualizado. (NÃO NEGOCIÁVEL)",
161
164
  ].join("\n"),
162
165
  },
163
166
  implement: {
@@ -215,6 +218,13 @@ const PHASE_MAP = {
215
218
  "",
216
219
  "### Saída esperada:",
217
220
  'Um arquivo JSON contendo um array de objetos `{ "label": string, "answer": string }`.',
221
+ "",
222
+ "### Regras de escrita (NÃO NEGOCIÁVEL):",
223
+ "1. Produza APENAS `answers.json`. NÃO edite `spec.md` — nem a seção",
224
+ " `## Clarifications` nem os marcadores inline.",
225
+ "2. A escrita no spec é determinística e exclusiva do CLI (`raptor clarify --apply`):",
226
+ " ele resolve os marcadores e popula `## Clarifications` a partir do seu `answers.json`.",
227
+ " Editar o spec aqui causaria dupla-escrita e regrediria o determinismo.",
218
228
  ].join("\n"),
219
229
  },
220
230
  analyze: {
@@ -4,8 +4,11 @@ const REGION_PREFIX = /^(?:region|regi[aã]o)\s*:\s*(.+)$/i;
4
4
  const DELIM = /\s+[—–-]\s+|\s*:\s+/;
5
5
  function splitHead(text) {
6
6
  const m = DELIM.exec(text);
7
- if (!m || m.index <= 0)
7
+ if (!m)
8
8
  return { head: text.trim() };
9
+ if (m.index <= 0) {
10
+ return { head: "", tail: text.slice(m.index + m[0].length).trim() || undefined };
11
+ }
9
12
  return {
10
13
  head: text.slice(0, m.index).trim(),
11
14
  tail: text.slice(m.index + m[0].length).trim() || undefined,
@@ -7,6 +7,7 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
7
7
  const renderType = classifyAssetType(fmt);
8
8
  const renderExt = extForFormat(fmt);
9
9
  const renderDir = assetDirFor(renderType);
10
+ const screen = opts.screen?.trim() ? opts.screen.trim() : undefined;
10
11
  for (const [nodeId, url] of Object.entries(renders)) {
11
12
  if (!url)
12
13
  continue;
@@ -18,7 +19,14 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
18
19
  path = `${renderDir}/${base}.${renderExt}`;
19
20
  }
20
21
  seen.add(path);
21
- out.push({ name: human || base, type: renderType, path, url, figma_node_id: nodeId });
22
+ out.push({
23
+ name: human || base,
24
+ type: renderType,
25
+ path,
26
+ url,
27
+ figma_node_id: nodeId,
28
+ ...(screen ? { screen } : {}),
29
+ });
22
30
  }
23
31
  for (const [ref, url] of Object.entries(fills)) {
24
32
  if (!url)
@@ -31,7 +39,7 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
31
39
  path = `assets/images/${base}.${ext}`;
32
40
  }
33
41
  seen.add(path);
34
- out.push({ name: base, type: "image", path, url });
42
+ out.push({ name: base, type: "image", path, url, ...(screen ? { screen } : {}) });
35
43
  }
36
44
  return out;
37
45
  }
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { mkdirSync, writeFileSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
@@ -64,16 +65,31 @@ export async function downloadAll(pending, root, opts = {}) {
64
65
  if (isAbsolute(rel) || (within !== rootAbs && !within.startsWith(rootAbs + sep))) {
65
66
  throw new DownloadError(`asset path escapes project root: ${a.path}`, 0);
66
67
  }
68
+ if (isGitTracked(rootAbs, rel)) {
69
+ const w = `asset "${a.name}" colidiria com arquivo versionado ${rel} — não sobrescrito`;
70
+ if (opts.onWarn)
71
+ opts.onWarn(w);
72
+ else
73
+ process.stderr.write(`Warning: ${w}\n`);
74
+ results[i] = { ...base, status: "pending" };
75
+ continue;
76
+ }
67
77
  const destAbs = join(root, rel);
68
78
  const { hash } = await downloadAsset(a.url, destAbs, opts);
69
79
  results[i] = { ...base, hash, status: "downloaded" };
70
80
  }
71
81
  catch (err) {
72
82
  const msg = err instanceof Error ? err.message : String(err);
83
+ const where = assetWhere(a);
84
+ const oversized = err instanceof DownloadError && /exceeds max_bytes/.test(msg);
85
+ const advice = oversized
86
+ ? " — pending, fora do manifest; eleve design.assets.max_bytes ou rebaixe via MCP"
87
+ : "";
88
+ const line = `asset "${a.name}"${where} not downloaded: ${msg}${advice}`;
73
89
  if (opts.onWarn)
74
- opts.onWarn(`asset "${a.name}" not downloaded: ${msg}`);
90
+ opts.onWarn(line);
75
91
  else
76
- process.stderr.write(`Warning: asset "${a.name}" not downloaded: ${msg}\n`);
92
+ process.stderr.write(`Warning: ${line}\n`);
77
93
  results[i] = { ...base, status: "pending" };
78
94
  }
79
95
  }
@@ -81,6 +97,26 @@ export async function downloadAll(pending, root, opts = {}) {
81
97
  await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
82
98
  return results;
83
99
  }
100
+ function isGitTracked(rootAbs, rel) {
101
+ try {
102
+ const r = spawnSync("git", ["ls-files", "--error-unmatch", "--", rel], {
103
+ cwd: rootAbs,
104
+ stdio: "ignore",
105
+ });
106
+ return r.status === 0;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ function assetWhere(a) {
113
+ const parts = [];
114
+ if (a.screen)
115
+ parts.push(`screen: ${a.screen}`);
116
+ if (a.figma_node_id)
117
+ parts.push(`node ${a.figma_node_id}`);
118
+ return parts.length ? ` (${parts.join(", ")})` : "";
119
+ }
84
120
  function assertSafeAssetUrl(url, allowHttp) {
85
121
  let u;
86
122
  try {
@@ -6,6 +6,16 @@ function warn(opts, message) {
6
6
  else
7
7
  process.stderr.write(`Warning: ${message}\n`);
8
8
  }
9
+ function scopeSkipWarning(url) {
10
+ return (`ref sem node-id (link sem ?node-id=); telas/assets não capturados — ` +
11
+ `rode com --design-scope file para o arquivo inteiro, ou linke o frame no card: ${url}`);
12
+ }
13
+ function warnOnce(opts, seen, url) {
14
+ if (seen.has(url))
15
+ return;
16
+ seen.add(url);
17
+ warn(opts, scopeSkipWarning(url));
18
+ }
9
19
  export async function fetchDesignData(refs, client, opts = {}) {
10
20
  const varMap = {};
11
21
  for (const ref of refs) {
@@ -30,10 +40,11 @@ export async function fetchDesignData(refs, client, opts = {}) {
30
40
  }
31
41
  }
32
42
  const tokens = normalizeVariablesToTokens(varMap, opts.source);
33
- const screens = await captureScreens(refs, client, opts);
43
+ const scopeWarned = new Set();
44
+ const screens = await captureScreens(refs, client, opts, scopeWarned);
34
45
  const assets = [];
35
46
  if (opts.root) {
36
- assets.push(...(await captureAssets(refs, client, tokens.typography, opts)));
47
+ assets.push(...(await captureAssets(refs, client, tokens.typography, opts, scopeWarned)));
37
48
  }
38
49
  return {
39
50
  tokens,
@@ -42,12 +53,17 @@ export async function fetchDesignData(refs, client, opts = {}) {
42
53
  libraries: [...libraries],
43
54
  };
44
55
  }
45
- async function captureScreens(refs, client, opts) {
56
+ async function captureScreens(refs, client, opts, scopeWarned) {
46
57
  if (!client.captureScreens)
47
58
  return [];
59
+ const scope = opts.scope ?? "node";
48
60
  const out = [];
49
61
  const seen = new Set();
50
62
  for (const ref of refs) {
63
+ if (scope === "node" && !ref.nodeId) {
64
+ warnOnce(opts, scopeWarned, ref.url);
65
+ continue;
66
+ }
51
67
  try {
52
68
  for (const s of await client.captureScreens(ref.fileKey, ref.nodeId)) {
53
69
  const key = s.nodeId
@@ -65,7 +81,7 @@ async function captureScreens(refs, client, opts) {
65
81
  }
66
82
  return out;
67
83
  }
68
- async function captureAssets(refs, client, typography, opts) {
84
+ async function captureAssets(refs, client, typography, opts, scopeWarned) {
69
85
  const root = opts.root;
70
86
  const format = opts.assets?.format ?? "svg";
71
87
  const scale = opts.assets?.scale ?? 2;
@@ -73,8 +89,13 @@ async function captureAssets(refs, client, typography, opts) {
73
89
  const fetchImpl = opts.deps?.fetchImpl;
74
90
  const pending = [];
75
91
  const seenPaths = new Set();
92
+ const scope = opts.scope ?? "node";
76
93
  if (client.captureAssets) {
77
94
  for (const ref of refs) {
95
+ if (scope === "node" && !ref.nodeId) {
96
+ warnOnce(opts, scopeWarned, ref.url);
97
+ continue;
98
+ }
78
99
  try {
79
100
  const found = await client.captureAssets(ref.fileKey, ref.nodeId, { format, scale });
80
101
  for (const p of found) {
@@ -100,8 +121,15 @@ async function captureAssets(refs, client, typography, opts) {
100
121
  for (const e of entries) {
101
122
  if (e.status === "downloaded")
102
123
  out.push(e);
103
- else
104
- warn(opts, `asset "${e.name}" could not be downloaded — left pending (not catalogued)`);
124
+ else {
125
+ const where = [
126
+ e.screen ? `screen: ${e.screen}` : "",
127
+ e.figma_node_id ? `node ${e.figma_node_id}` : "",
128
+ ]
129
+ .filter(Boolean)
130
+ .join(", ");
131
+ warn(opts, `asset "${e.name}"${where ? ` (${where})` : ""} could not be downloaded — left pending (not catalogued)`);
132
+ }
105
133
  }
106
134
  }
107
135
  const fontEntries = await resolveFonts(typography, {
@@ -183,9 +211,7 @@ export function normalizeVariablesToTokens(map, source) {
183
211
  break;
184
212
  }
185
213
  }
186
- const tokens = { colors, typography, spacing };
187
- if (radii.length)
188
- tokens.radii = radii;
214
+ const tokens = { colors, typography, spacing, radii };
189
215
  if (source) {
190
216
  tokens.source = {
191
217
  provider: source.provider,
@@ -22,18 +22,24 @@ function validateDivergence(d, label, problems) {
22
22
  problems.push(`${label}: divergence must be an object`);
23
23
  return null;
24
24
  }
25
+ let valid = true;
25
26
  const dim = d["dimension"];
26
27
  if (typeof dim !== "string" || !FIDELITY_DIMENSIONS.includes(dim)) {
27
28
  problems.push(`${label}: divergence "dimension" must be one of ${FIDELITY_DIMENSIONS.join("|")}`);
29
+ valid = false;
28
30
  }
29
31
  const sev = d["severity"];
30
32
  if (typeof sev !== "string" || !DIVERGENCE_SEVERITIES.includes(sev)) {
31
33
  problems.push(`${label}: divergence "severity" must be one of ${DIVERGENCE_SEVERITIES.join("|")}`);
34
+ valid = false;
32
35
  }
33
36
  const status = d["status"];
34
37
  if (typeof status !== "string" || !DIVERGENCE_STATUSES.includes(status)) {
35
38
  problems.push(`${label}: divergence "status" must be one of ${DIVERGENCE_STATUSES.join("|")}`);
39
+ valid = false;
36
40
  }
41
+ if (!valid)
42
+ return null;
37
43
  return {
38
44
  dimension: dim,
39
45
  figma: typeof d["figma"] === "string" ? d["figma"] : "",
@@ -51,6 +57,7 @@ function validateRegion(r, label, problems) {
51
57
  const id = r["id"];
52
58
  if (typeof id !== "string" || !id.trim()) {
53
59
  problems.push(`${label}: missing "id"`);
60
+ return null;
54
61
  }
55
62
  if (!isScore(r["score"])) {
56
63
  problems.push(`${label}: "score" must be a number 0–100`);
@@ -90,7 +97,7 @@ function validateRegion(r, label, problems) {
90
97
  }
91
98
  }
92
99
  return {
93
- id: typeof id === "string" ? id : "",
100
+ id,
94
101
  ...(typeof r["label"] === "string" ? { label: r["label"] } : {}),
95
102
  score: isScore(r["score"]) ? r["score"] : 0,
96
103
  ...(dimensions ? { dimensions } : {}),
@@ -62,6 +62,7 @@ export function makeFigmaRest(token, opts = {}) {
62
62
  const scale = assetOpts.scale ?? 2;
63
63
  let renders = {};
64
64
  let scopedRefs = null;
65
+ let screen;
65
66
  if (nodeId) {
66
67
  const q = `ids=${encodeURIComponent(nodeId)}&format=${encodeURIComponent(format)}&scale=${scale}`;
67
68
  const data = await getJson(`/v1/images/${encodeURIComponent(fileKey)}?${q}`);
@@ -70,7 +71,9 @@ export function makeFigmaRest(token, opts = {}) {
70
71
  }
71
72
  renders = parseImagesResponse(data);
72
73
  try {
73
- scopedRefs = collectImageRefs(await readNodes(fileKey, nodeId));
74
+ const nodesDoc = await readNodes(fileKey, nodeId);
75
+ scopedRefs = collectImageRefs(nodesDoc);
76
+ screen = screensFromNodes(nodesDoc, fileKey)[0]?.name;
74
77
  }
75
78
  catch {
76
79
  scopedRefs = new Set();
@@ -88,7 +91,7 @@ export function makeFigmaRest(token, opts = {}) {
88
91
  const refs = scopedRefs;
89
92
  fills = Object.fromEntries(Object.entries(fills).filter(([ref]) => refs.has(ref)));
90
93
  }
91
- return normalizeImagesToAssets(renders, fills, { format });
94
+ return normalizeImagesToAssets(renders, fills, { format, ...(screen ? { screen } : {}) });
92
95
  },
93
96
  async captureScreens(fileKey, nodeId) {
94
97
  if (!fileKey)
@@ -1,4 +1,5 @@
1
1
  import { seededTokensCount } from "./fetcher.js";
2
+ import { isTokensEmpty } from "./tokens.js";
2
3
  export function mapDesignToSpecContext(refs, capture) {
3
4
  const designRefs = refs.map((r) => ({
4
5
  url: r.url,
@@ -20,6 +21,7 @@ export function mapDesignToSpecContext(refs, capture) {
20
21
  return {
21
22
  ...base,
22
23
  designSeeded: true,
24
+ seededTokensEmpty: isTokensEmpty(t),
23
25
  seededTokensCount: seededTokensCount(t),
24
26
  seededScreens: capture.screens.map((s) => ({
25
27
  name: s.name,
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { isTokensEmpty, parseDesignTokens } from "./tokens.js";
4
4
  import { parseAssetManifest } from "./manifest.js";
@@ -14,6 +14,7 @@ const EMPTY_TOKENS = {
14
14
  colors: [],
15
15
  typography: [],
16
16
  spacing: [],
17
+ radii: [],
17
18
  };
18
19
  function writeIfMissing(path, content) {
19
20
  if (existsSync(path))
@@ -34,8 +35,11 @@ by the agent through its Figma MCP tools and enforced by \`gate.design.ready\`.
34
35
  ${links}
35
36
 
36
37
  ## Files
37
- - \`tokens.json\` — design tokens (colors, typography, spacing). Fill via
38
- \`get_variable_defs\`.
38
+ - \`tokens.json\` — design tokens (colors, typography, spacing, radii). Fill via
39
+ \`get_variable_defs\`. NOTE: the deterministic capture reads Figma *Variables*
40
+ (Enterprise-plan REST) and falls back to published *styles*; a file with
41
+ neither yields an EMPTY tokens.json — populate it via the Figma MCP
42
+ \`get_variable_defs\` (it can read what the REST capture could not).
39
43
  - \`screens/<name>.md\` — one file per screen, with frontmatter (name, nodeId) and
40
44
  a short spec. Create from \`get_design_context\` / \`get_screenshot\`.
41
45
  - \`assets-manifest.json\` — catalogue of every downloaded asset
@@ -77,8 +81,34 @@ export function writeDesignScaffold(featureDir, root, refs) {
77
81
  created.push(`${dir}/.gitkeep`);
78
82
  }
79
83
  }
84
+ if (ensureAssetGitignore(root)) {
85
+ created.push(".gitignore");
86
+ }
80
87
  return created;
81
88
  }
89
+ const GITIGNORE_BEGIN = "# RAPTOR design assets";
90
+ const GITIGNORE_END = "# /RAPTOR design assets";
91
+ function ensureAssetGitignore(root) {
92
+ try {
93
+ const gitignorePath = join(root, ".gitignore");
94
+ const existing = existsSync(gitignorePath)
95
+ ? readFileSync(gitignorePath, "utf8")
96
+ : "";
97
+ if (existing.includes(GITIGNORE_BEGIN))
98
+ return false;
99
+ const lines = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
100
+ const missing = ASSET_DIRS.filter((d) => !lines.has(d) && !lines.has(`/${d}`));
101
+ if (missing.length === 0)
102
+ return false;
103
+ const block = (existing.length && !existing.endsWith("\n") ? "\n" : "") +
104
+ `${GITIGNORE_BEGIN}\n${missing.join("\n")}\n${GITIGNORE_END}\n`;
105
+ appendFileSync(gitignorePath, block);
106
+ return true;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
82
112
  function writeJson(path, data) {
83
113
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
84
114
  }
@@ -117,11 +147,10 @@ export function seedDesignArtifacts(featureDir, root, capture, refs) {
117
147
  const used = new Set();
118
148
  for (const screen of capture.screens) {
119
149
  const base = screenSlug(screen);
120
- let slug = base;
121
- let n = 1;
150
+ let slug = screen.nodeId ? `${base}-${slugify(screen.nodeId)}` : base;
151
+ let n = 2;
122
152
  while (used.has(slug)) {
123
- const suffix = n === 1 && screen.nodeId ? slugify(screen.nodeId) : String(n);
124
- slug = `${base}-${suffix}`;
153
+ slug = `${base}-${n}`;
125
154
  n++;
126
155
  }
127
156
  used.add(slug);