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.
- package/CHANGELOG.md +92 -0
- package/README.md +51 -17
- package/dist/_core/dist/agents/prompt-builder.js +10 -0
- package/dist/_core/dist/design/anatomy.js +4 -1
- package/dist/_core/dist/design/asset-normalize.js +10 -2
- package/dist/_core/dist/design/downloader.js +38 -2
- package/dist/_core/dist/design/fetcher.js +35 -9
- package/dist/_core/dist/design/fidelity.js +8 -1
- package/dist/_core/dist/design/figma-rest.js +5 -2
- package/dist/_core/dist/design/mapper.js +2 -0
- package/dist/_core/dist/design/scaffold.js +36 -7
- package/dist/_core/dist/design/screens.js +18 -0
- package/dist/_core/dist/design/token-diff.js +4 -2
- package/dist/_core/dist/gates/builtin.js +32 -9
- package/dist/_core/dist/gates/design-gates.js +64 -16
- package/dist/_core/dist/gates/m7-gates.js +23 -5
- package/dist/_core/dist/jira/mapper.js +100 -2
- package/dist/_core/dist/models/checklist.js +10 -6
- package/dist/_core/dist/models/clarify.js +69 -1
- package/dist/_core/dist/models/impl-log.js +71 -0
- package/dist/_core/dist/prompts/resumption.js +16 -2
- package/dist/_core/dist/state/feature.js +1 -1
- package/dist/_core/dist/transport/mcp.js +1 -0
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/raptor.yml.hbs +0 -1
- package/dist/_core/templates/spec.md.hbs +14 -12
- package/dist/commands/approve.js +22 -8
- package/dist/commands/design/pull.js +9 -1
- package/dist/commands/design/sync.js +26 -5
- package/dist/commands/implement.js +71 -7
- package/dist/commands/new.js +34 -3
- package/dist/commands/taskstoissues.js +16 -5
- package/dist/shared/design.js +2 -1
- package/dist/shared/project.js +4 -4
- package/package.json +1 -1
- package/scripts/app/screenshot.sh +6 -2
- package/scripts/bash/common.sh +64 -23
- package/scripts/bash/create-new-feature.sh +11 -6
- package/scripts/bash/setup-plan.sh +3 -3
- package/scripts/check-naming.sh +16 -8
- package/scripts/powershell/common.ps1 +51 -9
- package/scripts/powershell/create-new-feature.ps1 +3 -1
- package/scripts/prepare-npm.mjs +1 -1
- package/scripts/review/hex-scan.mjs +6 -2
- package/templates/commands/implement.md +38 -9
- 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
|
[](https://www.npmjs.com/package/raptor-aios)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](https://reactnative.dev)
|
|
10
|
-
[](#️-desenvolvimento-local)
|
|
11
|
+
[](CHANGELOG.md)
|
|
12
12
|
[](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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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(
|
|
90
|
+
opts.onWarn(line);
|
|
75
91
|
else
|
|
76
|
-
process.stderr.write(`Warning:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
150
|
+
let slug = screen.nodeId ? `${base}-${slugify(screen.nodeId)}` : base;
|
|
151
|
+
let n = 2;
|
|
122
152
|
while (used.has(slug)) {
|
|
123
|
-
|
|
124
|
-
slug = `${base}-${suffix}`;
|
|
153
|
+
slug = `${base}-${n}`;
|
|
125
154
|
n++;
|
|
126
155
|
}
|
|
127
156
|
used.add(slug);
|