raptor-aios 0.14.1 → 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 +79 -0
- package/README.md +14 -7
- package/dist/_core/dist/agents/prompt-builder.js +10 -0
- 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/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/gates/builtin.js +32 -9
- package/dist/_core/dist/gates/design-gates.js +31 -8
- 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/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/templates/commands/implement.md +38 -9
- package/templates/commands/specify.md +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,85 @@ 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
|
+
|
|
8
87
|
## [0.14.1] - 2026-06-21
|
|
9
88
|
|
|
10
89
|
### Fixed
|
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
|
---
|
|
@@ -201,7 +201,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
|
|
|
201
201
|
|
|
202
202
|
### Uma branch por unidade de trabalho
|
|
203
203
|
|
|
204
|
-
`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).
|
|
205
205
|
|
|
206
206
|
| Você roda | Branch criada |
|
|
207
207
|
| -------------------------------------- | --------------------------------------------------- |
|
|
@@ -210,7 +210,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
|
|
|
210
210
|
| `raptor new refactor-auth --kind=debt` | `debt/003-refactor-auth` |
|
|
211
211
|
| `raptor new login --jira APP-1234` | `feat/004-login.app-1234` *(tipo inferido do Jira)* |
|
|
212
212
|
|
|
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.
|
|
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.
|
|
214
214
|
|
|
215
215
|
### Commits coerentes, sem travar seu fluxo
|
|
216
216
|
|
|
@@ -271,9 +271,11 @@ Gera checklists de qualidade temáticos (code quality, testes, segurança, perfo
|
|
|
271
271
|
|
|
272
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).
|
|
273
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
|
+
|
|
274
276
|
### 🎫 `/raptor-taskstoissues`
|
|
275
277
|
|
|
276
|
-
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.
|
|
277
279
|
|
|
278
280
|
### 🌿 `/raptor-commit-review`
|
|
279
281
|
|
|
@@ -521,6 +523,8 @@ raptor jira pull APP-1234 # importa a issue
|
|
|
521
523
|
raptor new login --jira APP-1234 # semeia a spec a partir da issue
|
|
522
524
|
```
|
|
523
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
|
+
|
|
524
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).
|
|
525
529
|
|
|
526
530
|
### 🎨 Integração com Figma (design determinístico)
|
|
@@ -535,6 +539,8 @@ raptor design sync home # re-captura idempotente (nunca sobrescreve
|
|
|
535
539
|
|
|
536
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`.
|
|
537
541
|
|
|
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`.
|
|
543
|
+
|
|
538
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).
|
|
539
545
|
|
|
540
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).
|
|
@@ -557,7 +563,7 @@ O provider **`figma-rest`** usa a REST API da Figma com um PAT (`${FIGMA_TOKEN}`
|
|
|
557
563
|
🎨 Figma/Design design connect · status · pull · sync · disconnect
|
|
558
564
|
```
|
|
559
565
|
|
|
560
|
-
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.
|
|
561
567
|
|
|
562
568
|
### 💬 Quando você erra um comando
|
|
563
569
|
|
|
@@ -622,6 +628,7 @@ Comece pelo **[📖 Glossário Canônico](docs/glossary.md)** — a referência
|
|
|
622
628
|
| Documento | Sobre |
|
|
623
629
|
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
624
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 |
|
|
625
632
|
| [🧭 docs/setup-wizard.md](docs/setup-wizard.md) | Assistente de instalação `raptor setup`: onboarding, IA/IDE, preset, Jira/Figma |
|
|
626
633
|
| [🛡️ docs/readiness-gates.md](docs/readiness-gates.md) | Gates de prontidão (`spec/plan/tasks.ready`) em detalhe |
|
|
627
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: {
|
|
@@ -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,
|
|
@@ -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);
|
|
@@ -177,6 +177,24 @@ export function pageIdsFromMetadata(data) {
|
|
|
177
177
|
export function screenSlug(screen) {
|
|
178
178
|
return slugify(screen.name) || slugify(screen.nodeId ?? "screen") || "screen";
|
|
179
179
|
}
|
|
180
|
+
export function orphanScreenStems(presentStems, screens) {
|
|
181
|
+
const present = new Set(presentStems);
|
|
182
|
+
const suffixedBases = new Set();
|
|
183
|
+
const bareBases = new Set();
|
|
184
|
+
for (const screen of screens) {
|
|
185
|
+
if (screen.nodeId)
|
|
186
|
+
suffixedBases.add(screenSlug(screen));
|
|
187
|
+
else
|
|
188
|
+
bareBases.add(screenSlug(screen));
|
|
189
|
+
}
|
|
190
|
+
const orphans = [];
|
|
191
|
+
for (const base of suffixedBases) {
|
|
192
|
+
if (present.has(base) && !bareBases.has(base) && !orphans.includes(base)) {
|
|
193
|
+
orphans.push(base);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return orphans;
|
|
197
|
+
}
|
|
180
198
|
function bodyName(name) {
|
|
181
199
|
return (name
|
|
182
200
|
.replace(/[\r\n]+/g, " ")
|
|
@@ -6,7 +6,7 @@ import { extractCoreBlock } from "../constitution/core.js";
|
|
|
6
6
|
import { parsePlan } from "../models/plan.js";
|
|
7
7
|
import { parseSpec } from "../models/spec.js";
|
|
8
8
|
import { parseTaskList, parseTasks } from "../models/tasks.js";
|
|
9
|
-
import { detectClarificationMarkers, stripHtmlComments, } from "../models/clarify.js";
|
|
9
|
+
import { detectClarificationMarkers, hasUnresolvedPlaceholder, stripHtmlComments, } from "../models/clarify.js";
|
|
10
10
|
import { gateBranchNaming } from "./branch-gate.js";
|
|
11
11
|
function extractIdsByPattern(source, re) {
|
|
12
12
|
const seen = new Set();
|
|
@@ -418,11 +418,11 @@ export const gateSpecReady = {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
const problems = [];
|
|
421
|
-
if (/\[PREENCHER\b/i
|
|
421
|
+
if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
|
|
422
422
|
problems.push("unfilled [PREENCHER] placeholder(s) remain");
|
|
423
423
|
}
|
|
424
|
-
const storyIds = new Set([...body.matchAll(/\bUS-?(\d+)\b/g)].map((m) => `US${m[1]}`));
|
|
425
|
-
const coveredStories = new Set([...body.matchAll(/covers\b[^.\n]*?\bUS-?(\d+)/gi)].map((m) => `US${m[1]}`));
|
|
424
|
+
const storyIds = new Set([...body.matchAll(/\bUS-?(\d+)\b/g)].map((m) => `US${Number(m[1])}`));
|
|
425
|
+
const coveredStories = new Set([...body.matchAll(/covers\b[^.\n]*?\bUS-?(\d+)/gi)].map((m) => `US${Number(m[1])}`));
|
|
426
426
|
const uncovered = [...storyIds].filter((s) => !coveredStories.has(s));
|
|
427
427
|
if (storyIds.size > 0 && uncovered.length > 0) {
|
|
428
428
|
problems.push(`user stor${uncovered.length > 1 ? "ies" : "y"} without acceptance coverage: ${uncovered.join(", ")}`);
|
|
@@ -523,7 +523,11 @@ export const gatePlanInputHash = {
|
|
|
523
523
|
level: this.level,
|
|
524
524
|
article: this.article,
|
|
525
525
|
status: "fail",
|
|
526
|
-
message: `stale: plan declares spec_hash=${plan.data.spec_hash.slice(0, 20)}…, current spec hash=${spec.hash.slice(0, 20)}
|
|
526
|
+
message: `stale: plan declares spec_hash=${plan.data.spec_hash.slice(0, 20)}…, current spec hash=${spec.hash.slice(0, 20)}…\n` +
|
|
527
|
+
` spec.md changed after plan.md was generated (likely a clarify/re-approve). ` +
|
|
528
|
+
`If the edit was intentional, run 'raptor resync <feature> --artifact plan' to re-bless plan.md's spec_hash, ` +
|
|
529
|
+
`then re-run 'raptor verify' (and re-approve if the change is material). ` +
|
|
530
|
+
`Otherwise regenerate plan.md with 'raptor plan <feature>'.`,
|
|
527
531
|
details: {
|
|
528
532
|
declared: plan.data.spec_hash,
|
|
529
533
|
current: spec.hash,
|
|
@@ -600,7 +604,11 @@ export const gateTasksInputHash = {
|
|
|
600
604
|
level: this.level,
|
|
601
605
|
article: this.article,
|
|
602
606
|
status: "fail",
|
|
603
|
-
message: `stale: tasks declares plan_hash=${tasks.data.plan_hash.slice(0, 20)}…, current plan hash=${plan.hash.slice(0, 20)}
|
|
607
|
+
message: `stale: tasks declares plan_hash=${tasks.data.plan_hash.slice(0, 20)}…, current plan hash=${plan.hash.slice(0, 20)}…\n` +
|
|
608
|
+
` plan.md changed after tasks.md was generated (likely a clarify/re-approve upstream). ` +
|
|
609
|
+
`If the edit was intentional, run 'raptor resync <feature> --artifact tasks' to re-bless tasks.md's plan_hash, ` +
|
|
610
|
+
`then re-run 'raptor verify' (and re-approve if the change is material). ` +
|
|
611
|
+
`Otherwise regenerate tasks.md with 'raptor tasks <feature>'.`,
|
|
604
612
|
details: {
|
|
605
613
|
declared: tasks.data.plan_hash,
|
|
606
614
|
current: plan.hash,
|
|
@@ -911,7 +919,7 @@ export const gatePlanReady = {
|
|
|
911
919
|
if (detectClarificationMarkers(body).length > 0) {
|
|
912
920
|
problems.push("unresolved [NEEDS CLARIFICATION] marker(s) remain");
|
|
913
921
|
}
|
|
914
|
-
if (/\[PREENCHER\b/i
|
|
922
|
+
if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
|
|
915
923
|
problems.push("unfilled [PREENCHER] placeholder(s) remain");
|
|
916
924
|
}
|
|
917
925
|
if (/\bStatus:\s*`?pending`?/i.test(stripped) || /\|\s*pending\s*\|/i.test(stripped)) {
|
|
@@ -1010,12 +1018,22 @@ export const gateTasksReady = {
|
|
|
1010
1018
|
if (detectClarificationMarkers(body).length > 0) {
|
|
1011
1019
|
problems.push("unresolved [NEEDS CLARIFICATION] marker(s) remain");
|
|
1012
1020
|
}
|
|
1013
|
-
if (/\[PREENCHER\b/i
|
|
1021
|
+
if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
|
|
1014
1022
|
problems.push("unfilled [PREENCHER] placeholder(s) remain");
|
|
1015
1023
|
}
|
|
1024
|
+
const PHASE_HEADING_RE = /^#{2,}\s/;
|
|
1025
|
+
const bodyLines = body.split("\n");
|
|
1026
|
+
const lineToPhase = new Array(bodyLines.length);
|
|
1027
|
+
let phaseBucket = 0;
|
|
1028
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
1029
|
+
if (PHASE_HEADING_RE.test(bodyLines[i]))
|
|
1030
|
+
phaseBucket++;
|
|
1031
|
+
lineToPhase[i] = phaseBucket;
|
|
1032
|
+
}
|
|
1016
1033
|
const noUS = [];
|
|
1017
1034
|
const noAC = [];
|
|
1018
1035
|
const noFile = [];
|
|
1036
|
+
const phaseHasPath = new Set();
|
|
1019
1037
|
for (const it of items) {
|
|
1020
1038
|
if (META_TASK_RE.test(it.raw))
|
|
1021
1039
|
continue;
|
|
@@ -1023,8 +1041,13 @@ export const gateTasksReady = {
|
|
|
1023
1041
|
noUS.push(it.id);
|
|
1024
1042
|
if (!/\[AC-\d+\]/i.test(it.raw))
|
|
1025
1043
|
noAC.push(it.id);
|
|
1026
|
-
|
|
1044
|
+
const phase = lineToPhase[it.lineIndex] ?? 0;
|
|
1045
|
+
if (FILE_RE.test(it.raw)) {
|
|
1046
|
+
phaseHasPath.add(phase);
|
|
1047
|
+
}
|
|
1048
|
+
else if (!phaseHasPath.has(phase)) {
|
|
1027
1049
|
noFile.push(it.id);
|
|
1050
|
+
}
|
|
1028
1051
|
}
|
|
1029
1052
|
if (noUS.length > 0) {
|
|
1030
1053
|
problems.push(`task(s) without a [US#] (orphan, not [meta]): ${noUS.join(", ")}`);
|