raptor-aios 0.8.3 → 0.10.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 +30 -0
- package/README.md +26 -26
- package/dist/_core/dist/agents/context-file.js +8 -0
- package/dist/_core/dist/extensions/types.js +2 -0
- package/dist/_core/dist/jira/dialects.js +2 -2
- package/dist/_core/dist/jira/index.js +1 -1
- package/dist/_core/dist/jira/mapper.js +152 -13
- package/dist/_core/dist/jira/mcp-client.js +1 -0
- package/dist/_core/dist/presets/gol-smiles-deep.js +268 -0
- package/dist/_core/dist/presets/gol-smiles-gates.js +341 -0
- package/dist/_core/dist/presets/gol-smiles.js +77 -0
- package/dist/_core/dist/presets/index.js +4 -1
- package/dist/_core/dist/presets/registry.js +4 -0
- package/dist/_core/dist/presets/renderer.js +6 -0
- package/dist/_core/dist/verify/arch.js +82 -0
- package/dist/_core/dist/verify/i18n.js +182 -0
- package/dist/_core/dist/verify/index.js +3 -0
- package/dist/_core/dist/verify/tangerina.js +645 -0
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/plan.md.hbs +23 -2
- package/dist/_core/templates/raptor.yml.hbs +1 -1
- package/dist/_core/templates/spec.md.hbs +24 -3
- package/dist/commands/init.js +51 -18
- package/dist/commands/jira/pull.js +7 -1
- package/dist/commands/new.js +3 -0
- package/dist/commands/plan.js +3 -1
- package/dist/commands/preset/add.js +26 -1
- package/dist/commands/upgrade.js +13 -3
- package/dist/commands/verify/arch.js +116 -0
- package/dist/commands/verify/i18n.js +79 -0
- package/dist/commands/verify/tangerina.js +111 -0
- package/dist/shared/gol-verify.js +51 -0
- package/dist/shared/jira.js +18 -7
- package/dist/shared/scaffold.js +60 -5
- package/extensions/gol-smiles/extension.yml +28 -0
- package/extensions/gol-smiles/knowledge/app-kit-catalog.md +54 -0
- package/extensions/gol-smiles/knowledge/composition-recipes.md +84 -0
- package/extensions/gol-smiles/knowledge/conformity-checklist.md +34 -0
- package/extensions/gol-smiles/knowledge/figma-tangerina-map.json +927 -0
- package/extensions/gol-smiles/knowledge/tangerina-catalog.md +39 -0
- package/extensions/gol-smiles/knowledge/tokens-reference.md +32 -0
- package/package.json +1 -1
- package/scripts/generate-tangerina-design-map.mjs +147 -0
- package/scripts/prepare-npm.mjs +1 -1
- package/templates/plan-template.md +3 -0
- package/templates/spec-template.md +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.10.0] - 2026-06-11
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Jira → spec: nomes de campo resolvidos + filtro de relevância (`packages/core/src/jira/`).** Um card de Jira corporativo carrega centenas de custom fields de configuração (o `CDPOS-6532` real: 1408 campos, 515 não-nulos) que antes inundavam a `## Problem Statement` com blocos `### customfield_NNNNN` ilegíveis. Agora:
|
|
11
|
+
- **Nomes resolvidos.** O dialeto Rovo (hospedado, OAuth) passa a enviar `expand: "names"`, então cada `customfield_NNNNN` ganha seu rótulo humano ("Critérios de Aceite"). O `mcp-atlassian` (≥ 0.11.2) tem o rótulo desembrulhado do wrapper `{value, name}` (ele nunca ecoa o mapa `names`); `renderedFields` deixou de ser pedido (o servidor o descarta e a combinação suprime custom fields em algumas versões).
|
|
12
|
+
- **Filtro de relevância (`filterCustomFields`).** Camadas determinísticas (PT/EN, singular+plural): valor-ruído (enums, datas, placeholders) e rótulos de config (Responsável, Relator, Categorias, Branch, Repositório, EI/EO/EQ, CMDB, Nova Data…) caem; allowlist de história (Requisitos, Cenários, Critérios de Aceite, Ressalva de testes, Figma, Traduções, Documentação, Anexo…) e links de ferramenta de design (figma/miro/zeplin/notion/confluence) ficam — avaliados ANTES do denylist. Desconhecidos só ficam com prosa substantiva (rede *lose-nothing*).
|
|
13
|
+
- **`jira.custom_fields` ganha `exclude` e force-keep** (chaves case-insensitive): `<campo>: exclude` derruba; `<campo>: <bucket>` força a manter.
|
|
14
|
+
- **Anexos de imagem** (`image/*`) entram como `### Attachments (images)` (links, não baixados); outros tipos ficam no card.
|
|
15
|
+
- **Critérios de Aceite agrupados por cenário** (`acceptanceLines`): blocos "Cenário N …" viram um `[AC-#]` cada (não um por linha de Gherkin).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`flattenAdf` rende `taskItem` e `table`.** Uma matriz de aplicabilidade do Jira (grade de checkboxes) agora vira texto legível: **só os itens marcados `[x]` sobrevivem** (os desmarcados são ruído) e tabelas viram uma linha por row com células separadas por `|`. No `CDPOS-6532`, "Cenários aplicáveis" encolheu de 2528 → 607 chars, mostrando o escopo real.
|
|
20
|
+
- **URL do card browsable.** `resolveUrl` prefere o `base_url` do `raptor.yml` ao `self` do gateway `api.atlassian.com` (que não abre no navegador); `jira.base_url` é encanado do `raptor.yml` ao cliente OAuth.
|
|
21
|
+
- **`raptor jira pull`** imprime quantos custom fields foram filtrados (descoberta para o escape hatch); `--json` segue despejando tudo sem filtro. O `jira-refresh.md` (clarify) usa o mesmo filtro — os três consumidores (`new`, `pull`, `jira-refresh`) mostram o mesmo card.
|
|
22
|
+
|
|
23
|
+
Doc: [`docs/jira-spec-enrichment.md`](docs/jira-spec-enrichment.md) (seções F1–F4 + F3½ + §4b card corporativo). Validado E2E contra o `CDPOS-6532` real via OAuth.
|
|
24
|
+
|
|
25
|
+
## [0.9.0] - 2026-06-11
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Preset `gol-smiles` (G1–G6), empilhável sobre `mobile-opinionated`.** Novo preset bundled (`packages/core/src/presets/gol-smiles.ts` + `gol-smiles-gates.ts`) focado no ecossistema Gol/Smiles (RN + Design System Tangerina, arquitetura `modernization/`): **G1** Tangerina-first UI (vocabulário fechado de componentes/receitas), **G2** arquitetura moderna (Zustand + RHF/Zod), **G3** i18n pt-BR/en/es, **G4** contrato de analytics, **G5** rollout por feature flag, **G6** paridade multi-marca — todos com opt-out honesto. Registrado no `REGISTRY` com aliases `gol`/`smiles`. `init --preset mobile-opinionated,gol-smiles` empilha os 12 artigos e 12 gates; `raptor preset add gol-smiles` ativa num projeto existente.
|
|
30
|
+
- **Render multi-preset na constituição (D-10).** `raptor init --preset a,b` aceita um stack comma-separated e renderiza os artigos de **todos** os presets (`renderPresetsArticles`, atribuição de fonte por preset), gravando `presets:` no `raptor.yml`; `raptor preset add` **anexa** os artigos do novo preset à constituição (append seguro e idempotente, sem sobrescrever artigos do projeto).
|
|
31
|
+
- **Knowledge pack via extensão de preset (D-3).** O schema `raptor.extension/v1` ganhou `provides.knowledge[]` e `requires.preset`; o scaffold instala extensões **condicionalmente ao preset ativo** (`installBundledExtensions`/`collectFrameworkFiles` filtram; `collectKnowledgeRefs` alimenta o bloco de contexto). `buildContextBlock` ganhou a seção "Knowledge packs". A extensão `extensions/gol-smiles/` traz 5 catálogos (Tangerina, kit interno com classificação A/B/C/D, receitas de composição, tokens, 19 regras de conformidade) + o dicionário Figma→Tangerina, referenciados no `CLAUDE.md`/`AGENTS.md`.
|
|
32
|
+
- **Frontmatter ciente do preset (D-2).** `spec.md.hbs`/`plan.md.hbs` emitem os blocos G* (`ui`/`i18n`/`analytics`/`brands` na spec; `architecture`/`rollout` no plan) **somente** quando `gol-smiles` está ativo — projetos non-gol ficam intactos. Mirrors de doc sincronizados.
|
|
33
|
+
- **Verificadores `raptor verify tangerina | i18n | arch` (Fase 3).** Runners reais que medem o código (`packages/core/src/verify/`): tokens literais/lista negra do kit/receita-base e receitas declaradas (tangerina, com modo `--design`), paridade de chaves pt-BR/en/es (i18n), imports banidos e aliases (arch). Achados carregam `regra`/`receita#asserção` + `arquivo:linha` + severidade (`error` bloqueia; `warning` só com `--strict`).
|
|
34
|
+
- **Modo deep gate→verify (D-7), opt-in.** Com `gol_smiles.deep_verify: true` no `raptor.yml`, os gates G1/G2/G3 também executam os runners na avaliação e agregam o resultado em `details` (erro determinístico no código reprova o gate). Sem o opt-in, os gates seguem puramente declarativos (padrão dos gates mobile).
|
|
35
|
+
|
|
6
36
|
## [0.8.3] - 2026-06-10
|
|
7
37
|
|
|
8
38
|
### Documentation
|
package/README.md
CHANGED
|
@@ -360,14 +360,14 @@ Gates são checagens versionadas com um **nível**: 🔴 `critical` (bloqueia, i
|
|
|
360
360
|
|
|
361
361
|
Cada gate exige campos no *frontmatter* de `spec.md` ou `plan.md` — declarativos, verificáveis e (para alguns) **mensuráveis** via `verify`.
|
|
362
362
|
|
|
363
|
-
| Gate | Nível |
|
|
364
|
-
| --------------------------------------------- | ----- |
|
|
363
|
+
| Gate | Nível | Exige no frontmatter | Verificável por |
|
|
364
|
+
| --------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
|
365
365
|
| **M1 — App/Play Store Compliance** | 🟡 | `spec.stores.ios_permissions`, `.android_permissions` (ou `stores.no_restricted_apis: "<motivo>"` p/ opt-out honesto) | `verify stores` |
|
|
366
|
-
| **M2 — Privacidade & Residência (LGPD/GDPR)** | 🟡 | `plan.privacy.lawful_basis`, `.residency`, `.retention`
|
|
367
|
-
| **M3 — Acessibilidade (WCAG AA)** | 🟡 | `spec.a11y.wcag_level`, `.criteria`
|
|
368
|
-
| **M4 — Matriz de SO** | 🟡 | `plan.os_matrix.ios_min`, `.android_min`
|
|
369
|
-
| **M5 — Orçamento de Performance** | 🟡 | `plan.perf_budget.ttfi_ms`, `.interactions_ms`
|
|
370
|
-
| **M6 — Crash-Free SLO & Observabilidade** | 🔵 | `plan.crash_free_slo` (≥99.5), `plan.observability.provider`
|
|
366
|
+
| **M2 — Privacidade & Residência (LGPD/GDPR)** | 🟡 | `plan.privacy.lawful_basis`, `.residency`, `.retention` | — |
|
|
367
|
+
| **M3 — Acessibilidade (WCAG AA)** | 🟡 | `spec.a11y.wcag_level`, `.criteria` | `verify a11y` |
|
|
368
|
+
| **M4 — Matriz de SO** | 🟡 | `plan.os_matrix.ios_min`, `.android_min` | `verify os-matrix` |
|
|
369
|
+
| **M5 — Orçamento de Performance** | 🟡 | `plan.perf_budget.ttfi_ms`, `.interactions_ms` | `verify perf` |
|
|
370
|
+
| **M6 — Crash-Free SLO & Observabilidade** | 🔵 | `plan.crash_free_slo` (≥99.5), `plan.observability.provider` | — |
|
|
371
371
|
|
|
372
372
|
### 🔗 M7 — Rastreabilidade código↔spec
|
|
373
373
|
|
|
@@ -454,17 +454,17 @@ Declarar não é cumprir. O Raptor **mede** e compara com o declarado:
|
|
|
454
454
|
|
|
455
455
|
## 🧰 Capacidades do Raptor
|
|
456
456
|
|
|
457
|
-
| Capacidade |
|
|
458
|
-
| ----------------------- |
|
|
459
|
-
| 🎚️ **Presets** | Conjuntos reusáveis de artigos + gates, empilháveis. Bundled: `mobile-opinionated`, `lean`.
|
|
457
|
+
| Capacidade | Para que serve | Comandos |
|
|
458
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
459
|
+
| 🎚️ **Presets** | Conjuntos reusáveis de artigos + gates, empilháveis. Bundled: `mobile-opinionated`, `lean`. | `raptor preset list · add · info · remove` |
|
|
460
460
|
| 🔁 **Workflows** | Orquestram o ciclo via YAML, com gates de revisão humana. Bundled: `sdd-cycle`, `sdd-cycle-quick`. | `raptor workflow catalog · run · status · resume · list` |
|
|
461
|
-
| 🤖 **Agentes** | Materializa os slash commands por agente.
|
|
462
|
-
| 🧠 **Skills** | Habilidades reusáveis materializadas para os agentes.
|
|
463
|
-
| 🔌 **MCP** | Registra servidores MCP e materializa nas configs nativas dos agentes.
|
|
464
|
-
| 🪝 **Hooks** | Scripts disparados em **34 pontos** do ciclo (`before_*`/`after_*` de 17 comandos).
|
|
465
|
-
| 📦 **Extensões** | Pacotes self-contained (templates/gates/hooks/workflows).
|
|
466
|
-
| 🎟️ **Jira** | Puxa issues do Jira (somente leitura) para semear specs.
|
|
467
|
-
| 📜 **Auditoria & trace** | Consulta a trilha de eventos e a rastreabilidade.
|
|
461
|
+
| 🤖 **Agentes** | Materializa os slash commands por agente. | `raptor add-agent` · `raptor list-agents` |
|
|
462
|
+
| 🧠 **Skills** | Habilidades reusáveis materializadas para os agentes. | `raptor skill add · list · sync · remove` |
|
|
463
|
+
| 🔌 **MCP** | Registra servidores MCP e materializa nas configs nativas dos agentes. | `raptor mcp add · list · sync · remove` |
|
|
464
|
+
| 🪝 **Hooks** | Scripts disparados em **34 pontos** do ciclo (`before_*`/`after_*` de 17 comandos). | `raptor hook list · run` |
|
|
465
|
+
| 📦 **Extensões** | Pacotes self-contained (templates/gates/hooks/workflows). | `raptor extension add · list · info · remove` |
|
|
466
|
+
| 🎟️ **Jira** | Puxa issues do Jira (somente leitura) para semear specs. | `raptor jira connect · status · pull · disconnect` |
|
|
467
|
+
| 📜 **Auditoria & trace** | Consulta a trilha de eventos e a rastreabilidade. | `raptor audit query · show` · `raptor trace` |
|
|
468
468
|
|
|
469
469
|
### 🎟️ Integração com Jira
|
|
470
470
|
|
|
@@ -554,15 +554,15 @@ CLAUDE.md # 📎 bloco de contexto do agente
|
|
|
554
554
|
|
|
555
555
|
Comece pelo **[📖 Glossário Canônico](docs/glossary.md)** — a referência normativa de todos os termos, gates, hierarquias e anti-padrões do Raptor (Gate, ADR, Override, Canonical, Preset, M1–M8, C1–C5, traceability…). É a base de onboarding para usuários, mantenedores e agentes IA.
|
|
556
556
|
|
|
557
|
-
|
|
|
558
|
-
|
|
|
559
|
-
| **[📖 docs/glossary.md](docs/glossary.md)**
|
|
560
|
-
| [🛡️ docs/readiness-gates.md](docs/readiness-gates.md)
|
|
561
|
-
| [🎨 docs/design-gate.md](docs/design-gate.md)
|
|
562
|
-
| [🔗 docs/artifact-chain.md](docs/artifact-chain.md)
|
|
563
|
-
| [🎟️ docs/jira-spec-enrichment.md](docs/jira-spec-enrichment.md)
|
|
564
|
-
| [📐 docs/templates-architecture.md](docs/templates-architecture.md) | Dualidade de templates (`.hbs` vs `*-template.md`) e Priority Stack
|
|
565
|
-
| [✅ docs/spec-kit-parity.md](docs/spec-kit-parity.md)
|
|
557
|
+
| Documento | Sobre |
|
|
558
|
+
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
559
|
+
| **[📖 docs/glossary.md](docs/glossary.md)** | Glossário canônico: conceitos, hierarquias, catálogo de 42 gates e de comandos CLI |
|
|
560
|
+
| [🛡️ docs/readiness-gates.md](docs/readiness-gates.md) | Gates de prontidão (`spec/plan/tasks.ready`) em detalhe |
|
|
561
|
+
| [🎨 docs/design-gate.md](docs/design-gate.md) | Design gate, integração Figma e `assets-manifest.json` |
|
|
562
|
+
| [🔗 docs/artifact-chain.md](docs/artifact-chain.md) | Cadeia de artefatos e rastreabilidade spec→plan→tasks→código |
|
|
563
|
+
| [🎟️ docs/jira-spec-enrichment.md](docs/jira-spec-enrichment.md) | Enriquecimento de spec a partir de cards do Jira |
|
|
564
|
+
| [📐 docs/templates-architecture.md](docs/templates-architecture.md) | Dualidade de templates (`.hbs` vs `*-template.md`) e Priority Stack |
|
|
565
|
+
| [✅ docs/spec-kit-parity.md](docs/spec-kit-parity.md) | Paridade com o GitHub Spec Kit |
|
|
566
566
|
|
|
567
567
|
### Hierarquia de governança (quem manda)
|
|
568
568
|
|
|
@@ -21,6 +21,14 @@ export function buildContextBlock(opts) {
|
|
|
21
21
|
lines.push(`\n## Applicable articles\n`);
|
|
22
22
|
lines.push(`- ${opts.constitutionArticles.join(", ")}`);
|
|
23
23
|
}
|
|
24
|
+
if (opts.knowledge && opts.knowledge.length > 0) {
|
|
25
|
+
lines.push(`\n## Knowledge packs\n`);
|
|
26
|
+
lines.push("Preset knowledge installed for this project. READ the relevant file before generating code so the output matches the house patterns (components, recipes, tokens, conventions):");
|
|
27
|
+
lines.push("");
|
|
28
|
+
for (const ref of opts.knowledge) {
|
|
29
|
+
lines.push(`- \`${ref}\``);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
lines.push("\n## Raptor Commands (slash commands)\n");
|
|
25
33
|
lines.push("- `/raptor-new` — create a new feature spec");
|
|
26
34
|
lines.push("- `/raptor-clarify` — detect and resolve [NEEDS CLARIFICATION] in spec");
|
|
@@ -26,6 +26,7 @@ export const EXTENSION_JSON_SCHEMA = {
|
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
28
28
|
raptor_version: { type: "string" },
|
|
29
|
+
preset: { type: "string" },
|
|
29
30
|
},
|
|
30
31
|
additionalProperties: false,
|
|
31
32
|
},
|
|
@@ -37,6 +38,7 @@ export const EXTENSION_JSON_SCHEMA = {
|
|
|
37
38
|
hooks: { type: "array", items: { type: "string" } },
|
|
38
39
|
workflows: { type: "array", items: { type: "string" } },
|
|
39
40
|
presets: { type: "array", items: { type: "string" } },
|
|
41
|
+
knowledge: { type: "array", items: { type: "string" } },
|
|
40
42
|
},
|
|
41
43
|
additionalProperties: false,
|
|
42
44
|
},
|
|
@@ -12,7 +12,7 @@ export const ROVO_DIALECT = {
|
|
|
12
12
|
transition: ["transitionJiraIssue"],
|
|
13
13
|
},
|
|
14
14
|
args: {
|
|
15
|
-
getIssue: (cloudId, key) => ({ cloudId, issueIdOrKey: key }),
|
|
15
|
+
getIssue: (cloudId, key) => ({ cloudId, issueIdOrKey: key, expand: "names" }),
|
|
16
16
|
search: (cloudId, jql, max) => ({ cloudId, jql, maxResults: max }),
|
|
17
17
|
projects: (cloudId) => ({ cloudId }),
|
|
18
18
|
createIssue: (input) => ({
|
|
@@ -53,7 +53,7 @@ export const MCP_ATLASSIAN_DIALECT = {
|
|
|
53
53
|
getIssue: (_cloudId, key) => ({
|
|
54
54
|
issue_key: key,
|
|
55
55
|
fields: "*all",
|
|
56
|
-
expand: "names
|
|
56
|
+
expand: "names",
|
|
57
57
|
comment_limit: 10,
|
|
58
58
|
}),
|
|
59
59
|
search: (_cloudId, jql, max) => ({ jql, limit: max }),
|
|
@@ -3,4 +3,4 @@ export * from "./credentials.js";
|
|
|
3
3
|
export { AtlassianOAuthProvider, isAccessTokenExpired, openBrowser, startCallbackServer, } from "./oauth.js";
|
|
4
4
|
export { connectJira, connectMcpServer, makeJiraClient, clientToToolCaller, decodeToolResult, unwrapEnvelope, expandEnv, missingEnvRefs, } from "./mcp-client.js";
|
|
5
5
|
export { ROVO_DIALECT, MCP_ATLASSIAN_DIALECT, DIALECTS, resolveDialect, detectDialect, } from "./dialects.js";
|
|
6
|
-
export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, classifyField, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
|
|
6
|
+
export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, extractAttachments, classifyField, filterCustomFields, isBoilerplateValue, isTemplateValue, acceptanceLines, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
|
|
@@ -33,18 +33,49 @@ export function parseJiraIssue(raw, baseUrl) {
|
|
|
33
33
|
acceptanceCriteria: extractAcceptanceCriteria(fields, description),
|
|
34
34
|
comments: extractComments(fields),
|
|
35
35
|
customFields,
|
|
36
|
+
attachments: extractAttachments(fields),
|
|
36
37
|
...(url ? { url } : {}),
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
export function extractAttachments(fields) {
|
|
41
|
+
const list = Array.isArray(fields["attachment"])
|
|
42
|
+
? fields["attachment"]
|
|
43
|
+
: Array.isArray(fields["attachments"])
|
|
44
|
+
? fields["attachments"]
|
|
45
|
+
: [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const a of list) {
|
|
48
|
+
if (!isRecord(a))
|
|
49
|
+
continue;
|
|
50
|
+
const filename = str(a["filename"]);
|
|
51
|
+
if (!filename)
|
|
52
|
+
continue;
|
|
53
|
+
out.push({
|
|
54
|
+
filename,
|
|
55
|
+
mimeType: str(a["mimeType"]) || str(a["content_type"]),
|
|
56
|
+
url: str(a["content"]) || str(a["url"]),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
39
61
|
export function extractCustomFields(fields, names) {
|
|
40
62
|
const out = [];
|
|
41
63
|
for (const [id, raw] of Object.entries(fields)) {
|
|
42
64
|
if (!/^customfield_/i.test(id))
|
|
43
65
|
continue;
|
|
44
|
-
|
|
66
|
+
let label = str(names[id]);
|
|
67
|
+
let inner = raw;
|
|
68
|
+
if (isRecord(raw) &&
|
|
69
|
+
"value" in raw &&
|
|
70
|
+
Object.keys(raw).every((k) => k === "value" || k === "name")) {
|
|
71
|
+
if (!label)
|
|
72
|
+
label = str(raw["name"]);
|
|
73
|
+
inner = raw["value"];
|
|
74
|
+
}
|
|
75
|
+
const value = flattenFieldValue(inner);
|
|
45
76
|
if (!value)
|
|
46
77
|
continue;
|
|
47
|
-
const name =
|
|
78
|
+
const name = label || id;
|
|
48
79
|
out.push({ id, name, value, bucket: classifyField(name) });
|
|
49
80
|
}
|
|
50
81
|
return out;
|
|
@@ -55,21 +86,102 @@ export function classifyField(label) {
|
|
|
55
86
|
return "acceptance";
|
|
56
87
|
if (/cen[áa]rio|scenario/.test(l))
|
|
57
88
|
return "scenarios";
|
|
58
|
-
if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b/.test(l))
|
|
89
|
+
if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b|ressalva/.test(l))
|
|
59
90
|
return "nonFunctional";
|
|
60
91
|
if (/requisito|requirement|\brf\b/.test(l))
|
|
61
92
|
return "functional";
|
|
62
93
|
if (/fora do escopo|out of scope|n[ãa]o escopo/.test(l))
|
|
63
94
|
return "outOfScope";
|
|
64
|
-
if (/documenta|documentation|link|refer[êe]ncia|design|figma/.test(l))
|
|
95
|
+
if (/documenta|documentation|link|refer[êe]ncia|design|figma|tradu[çc]|translation/.test(l))
|
|
65
96
|
return "docs";
|
|
66
97
|
return "other";
|
|
67
98
|
}
|
|
99
|
+
const ENUM_NOISE = /^(?:sim|n[ãa]o|yes|no|true|false|n\/a|na|none|nenhum|classificar|backlog|portal|no prazo|⏳|✅|❌|-+)$/i;
|
|
100
|
+
const PLACEHOLDER_LINE = /^(?:inserir|informar|descrever|preencher|selecionar|indicar|exportar|insira|informe|descreva|preencha|selecione|indique)\b/i;
|
|
101
|
+
const CARD_CONFIG_LABEL = /\b(?:respons[áa]ve(?:l|is)|relator(?:a|es|as)?|reporter|assignee|aprovador(?:a|es|as)?|squads?|categorias?|data\s+limite|nova\s+data|due\s*date|prazos?|branch(?:es)?|reposit[óo]rios?|repositor(?:y|ies)|entradas?\s+externas?|sa[íi]das?\s+externas?|consultas?\s+externas?|\bei\b|\beo\b|\beq\b|cmdb|aplica[çc][ãa]o\s+afetada|epic\s*links?|rank|development|sprints?|itera[çc](?:[ãa]o|[õo]es)|iterations?|quarters?|garantias?|story\s*points?|time\s*tracking)\b/i;
|
|
102
|
+
const ESSENTIAL_LABEL = /aceite|aceita[çc][ãa]o|acceptance|cen[áa]rio|scenario|requisito|requirement|\brnf\b|\brf\b|ressalva|figma|tradu[çc]|translation|documenta[çc]|documentation|refer[êe]ncia|\bdesign\b|anexo|attachment|prot[óo]tipo|prototype|hist[óo]ria|user\s*stor|fora\s+do\s+escopo|out\s+of\s+scope/i;
|
|
103
|
+
const DOC_TOOL_URL = /\b(?:figma\.com|miro\.com|zeplin\.(?:io|app)|notion\.so|atlassian\.net\/wiki)\b/i;
|
|
104
|
+
export function isBoilerplateValue(value) {
|
|
105
|
+
const v = value.trim();
|
|
106
|
+
if (!v)
|
|
107
|
+
return true;
|
|
108
|
+
if (ENUM_NOISE.test(v))
|
|
109
|
+
return true;
|
|
110
|
+
if (!v.includes("\n") && /^[\d|:.,\-T+Z\s/]+$/.test(v))
|
|
111
|
+
return true;
|
|
112
|
+
if (/^(?:espa[çc]o para inser[çc][ãa]o|campos a seguir)/i.test(v))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
export function isTemplateValue(value) {
|
|
117
|
+
const v = value.trim();
|
|
118
|
+
if (v.length < 200)
|
|
119
|
+
return true;
|
|
120
|
+
if (v.includes("{panel") || v.includes("🚨"))
|
|
121
|
+
return true;
|
|
122
|
+
if (/^\{(?:pullrequest|dataType|json)\b/.test(v))
|
|
123
|
+
return true;
|
|
124
|
+
const segments = v
|
|
125
|
+
.split("\n")
|
|
126
|
+
.flatMap((l) => l.split(" | "))
|
|
127
|
+
.map((s) => s.replace(/^[-*\d.)\s]+/, "").trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
return segments.filter((s) => PLACEHOLDER_LINE.test(s)).length >= 2;
|
|
130
|
+
}
|
|
131
|
+
export function filterCustomFields(fields, overrides = {}) {
|
|
132
|
+
const ovByKey = {};
|
|
133
|
+
for (const [k, v] of Object.entries(overrides))
|
|
134
|
+
ovByKey[k.toLowerCase()] = v;
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const cf of fields) {
|
|
137
|
+
const ov = ovByKey[cf.id.toLowerCase()] ?? ovByKey[cf.name.toLowerCase()];
|
|
138
|
+
if (ov && ov.toLowerCase() === "exclude")
|
|
139
|
+
continue;
|
|
140
|
+
if (ov) {
|
|
141
|
+
out.push({ ...cf, bucket: ov });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isBoilerplateValue(cf.value))
|
|
145
|
+
continue;
|
|
146
|
+
if (ESSENTIAL_LABEL.test(cf.name)) {
|
|
147
|
+
out.push(cf);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (DOC_TOOL_URL.test(cf.value)) {
|
|
151
|
+
out.push({ ...cf, bucket: "docs" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (CARD_CONFIG_LABEL.test(cf.name))
|
|
155
|
+
continue;
|
|
156
|
+
if (isTemplateValue(cf.value))
|
|
157
|
+
continue;
|
|
158
|
+
out.push(cf);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
export function acceptanceLines(value) {
|
|
163
|
+
const lines = toLines(value);
|
|
164
|
+
const isStart = (l) => /^(?:cen[áa]rio|scenario)\b/i.test(l);
|
|
165
|
+
const starts = lines.filter(isStart).length;
|
|
166
|
+
const hasGherkin = lines.some((l) => /^(?:dado|quando|ent[ãa]o|given|when|then)\b/i.test(l));
|
|
167
|
+
if (starts === 0 || (starts === 1 && !hasGherkin))
|
|
168
|
+
return lines;
|
|
169
|
+
const blocks = [];
|
|
170
|
+
for (const l of lines) {
|
|
171
|
+
if (isStart(l) || blocks.length === 0)
|
|
172
|
+
blocks.push([l]);
|
|
173
|
+
else
|
|
174
|
+
blocks[blocks.length - 1].push(l);
|
|
175
|
+
}
|
|
176
|
+
return blocks.map((b) => b.join(" "));
|
|
177
|
+
}
|
|
68
178
|
export function flattenFieldValue(raw) {
|
|
69
179
|
if (raw == null)
|
|
70
180
|
return "";
|
|
71
181
|
if (typeof raw === "string")
|
|
72
182
|
return raw.trim();
|
|
183
|
+
if (typeof raw === "number" || typeof raw === "boolean")
|
|
184
|
+
return String(raw);
|
|
73
185
|
if (Array.isArray(raw)) {
|
|
74
186
|
return raw.map(flattenFieldValue).filter(Boolean).join("\n").trim();
|
|
75
187
|
}
|
|
@@ -113,11 +225,7 @@ export function extractComments(fields) {
|
|
|
113
225
|
return out;
|
|
114
226
|
}
|
|
115
227
|
export function mapIssueToSpecContext(issue, opts = {}) {
|
|
116
|
-
const
|
|
117
|
-
const custom = (issue.customFields ?? []).map((cf) => {
|
|
118
|
-
const ov = overrides[cf.id] ?? overrides[cf.name.toLowerCase()];
|
|
119
|
-
return ov ? { ...cf, bucket: ov } : cf;
|
|
120
|
-
});
|
|
228
|
+
const custom = filterCustomFields(issue.customFields ?? [], opts.fieldBuckets);
|
|
121
229
|
const ac = [];
|
|
122
230
|
const seen = new Set();
|
|
123
231
|
const pushAc = (line) => {
|
|
@@ -130,13 +238,19 @@ export function mapIssueToSpecContext(issue, opts = {}) {
|
|
|
130
238
|
issue.acceptanceCriteria.forEach(pushAc);
|
|
131
239
|
for (const cf of custom)
|
|
132
240
|
if (cf.bucket === "acceptance")
|
|
133
|
-
|
|
241
|
+
acceptanceLines(cf.value).forEach(pushAc);
|
|
134
242
|
const sections = custom
|
|
135
243
|
.filter((cf) => cf.bucket !== "acceptance")
|
|
136
244
|
.map((cf) => ({ title: cf.name, body: cf.value, bucket: cf.bucket }));
|
|
245
|
+
const images = (issue.attachments ?? []).filter((a) => /^image\//i.test(a.mimeType));
|
|
137
246
|
const ticketBody = [
|
|
138
247
|
issue.description,
|
|
139
248
|
...sections.map((s) => `### ${s.title}\n${s.body}`),
|
|
249
|
+
images.length
|
|
250
|
+
? `### Attachments (images)\n${images
|
|
251
|
+
.map((a) => `- [${a.filename}](${a.url})`)
|
|
252
|
+
.join("\n")}`
|
|
253
|
+
: "",
|
|
140
254
|
]
|
|
141
255
|
.filter((s) => s && s.trim())
|
|
142
256
|
.join("\n\n");
|
|
@@ -175,6 +289,18 @@ export function flattenAdf(node) {
|
|
|
175
289
|
if (url)
|
|
176
290
|
return type === "inlineCard" ? url : `${url}\n`;
|
|
177
291
|
}
|
|
292
|
+
if (type === "taskItem") {
|
|
293
|
+
const attrs = isRecord(node["attrs"]) ? node["attrs"] : {};
|
|
294
|
+
if (attrs["state"] !== "DONE")
|
|
295
|
+
return "";
|
|
296
|
+
return `- [x] ${flattenAdf(node["content"]).trim()}\n`;
|
|
297
|
+
}
|
|
298
|
+
if (type === "tableRow") {
|
|
299
|
+
const cells = Array.isArray(node["content"])
|
|
300
|
+
? node["content"].map((c) => flattenAdf(c).trim().replace(/\s*\n\s*/g, " · "))
|
|
301
|
+
: [];
|
|
302
|
+
return `${cells.join(" | ")}\n`;
|
|
303
|
+
}
|
|
178
304
|
const inner = flattenAdf(node["content"]);
|
|
179
305
|
const blockTypes = new Set([
|
|
180
306
|
"paragraph",
|
|
@@ -184,6 +310,8 @@ export function flattenAdf(node) {
|
|
|
184
310
|
"listItem",
|
|
185
311
|
"codeBlock",
|
|
186
312
|
"blockquote",
|
|
313
|
+
"table",
|
|
314
|
+
"taskList",
|
|
187
315
|
]);
|
|
188
316
|
if (type === "hardBreak")
|
|
189
317
|
return "\n";
|
|
@@ -256,13 +384,17 @@ function resolveUrl(issue, fields, key, baseUrl) {
|
|
|
256
384
|
if (direct)
|
|
257
385
|
return direct;
|
|
258
386
|
const self = str(issue["self"]);
|
|
259
|
-
|
|
387
|
+
const selfOrigin = (() => {
|
|
260
388
|
try {
|
|
261
|
-
|
|
389
|
+
const u = new URL(self);
|
|
390
|
+
return u.hostname === "api.atlassian.com" ? undefined : u.origin;
|
|
262
391
|
}
|
|
263
392
|
catch {
|
|
393
|
+
return undefined;
|
|
264
394
|
}
|
|
265
|
-
}
|
|
395
|
+
})();
|
|
396
|
+
if (selfOrigin)
|
|
397
|
+
return `${selfOrigin}/browse/${key}`;
|
|
266
398
|
if (baseUrl) {
|
|
267
399
|
try {
|
|
268
400
|
return `${new URL(baseUrl).origin}/browse/${key}`;
|
|
@@ -270,6 +402,13 @@ function resolveUrl(issue, fields, key, baseUrl) {
|
|
|
270
402
|
catch {
|
|
271
403
|
}
|
|
272
404
|
}
|
|
405
|
+
if (self) {
|
|
406
|
+
try {
|
|
407
|
+
return `${new URL(self).origin}/browse/${key}`;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
273
412
|
return undefined;
|
|
274
413
|
}
|
|
275
414
|
function nestedName(v) {
|