raptor-aios 0.7.1 → 0.8.2
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 +48 -0
- package/README.md +63 -21
- package/dist/_core/dist/gates/builtin.js +2 -0
- package/dist/_core/dist/gates/m7-gates.js +3 -15
- package/dist/_core/dist/gates/phase-gates.js +32 -5
- package/dist/_core/dist/jira/dialects.js +9 -1
- package/dist/_core/dist/jira/index.js +3 -3
- package/dist/_core/dist/jira/mapper.js +121 -7
- package/dist/_core/dist/jira/mcp-client.js +27 -5
- package/dist/_core/dist/models/tasks.js +11 -0
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/spec.md.hbs +9 -5
- package/dist/commands/doctor.js +19 -19
- package/dist/commands/gate/approve.js +3 -3
- package/dist/commands/gate/list.js +6 -6
- package/dist/commands/gate/skip.js +3 -3
- package/dist/commands/jira/pull.js +6 -1
- package/dist/commands/new.js +7 -2
- package/dist/commands/plan.js +3 -2
- package/dist/commands/status.js +4 -4
- package/dist/commands/verify.js +6 -6
- package/dist/shared/jira.js +4 -0
- package/dist/shared/presets.js +16 -0
- package/package.json +1 -1
- package/scripts/prepare-npm.mjs +1 -1
- package/templates/tasks-template.md +4 -1
- package/templates/checklist-template.md +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
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.8.2] - 2026-06-10
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **`raptor preset add` no longer silently disables the preset's gates (A1).** The command writes the `presets:` array and removes the singular `preset:`, but every gate consumer (`verify`, `plan`, `gate skip/approve/list`, `doctor`, `status`) read only `config.preset` — so after `preset add` the mobile M1–M6 gates stopped running with no warning. A single resolver `resolveActivePresets` (`packages/cli/src/shared/presets.ts`) now reads both shapes via `activePresetIds` and stacks the resolved presets (`stackPresets`), so the array path yields the same gates and multiple presets compose. Unknown (e.g. disk-based) ids surface as an honest warning instead of being dropped.
|
|
11
|
+
- **M7 acceptance coverage now works from each task's `[AC-#]` tags, not a broken heuristic (A2).** The M7 gates derived coverage with `t.story.replace(/^US-/, "A")` → `A1`, while specs use `AC-1`; the fallback never matched, so `gate.m7.acceptance_has_coverage` failed spuriously whenever the impl-log lacked explicit `traces_acceptance`, and `gate.m7.coverage_evidence_valid` went inert. `parseTaskList` now captures `acceptance[]` from each task's `[AC-#]` tags, and a new `buildTaskAcceptanceMap` feeds the coverage projection. Explicit `traces_acceptance` still wins.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`raptor verify` now runs the clarify/analyze/checklist phase gates (A3).** `WIRED_PHASE_GATES` (a 6-gate subset of `PHASE_GATES`) is wired into `BUILTIN_GATES`. Guards keep this from breaking existing flows: `gate.clarify.acceptance_still_covered` now skips draft specs (only bites after approval, like the readiness gates), the redundant `gate.clarify.no_contradictions` is excluded (already covered by `gate.spec.clarifications`), and the checklist gates only validate structured CK checklists (those with frontmatter) — the `/rpt.checklist`-generated free-prose `requirements.md` is skipped, not failed.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- **Dropped the orphaned `templates/checklist-template.md`.** It was a generic, backend-flavoured "Quality Checklist" (endpoints, N+1 queries, pagination, OWASP, connection pools…) misaligned with Raptor's mobile/React Native focus, and nothing consumed it: it isn't a bundled `.hbs` template, `raptor checklist --init` generates its own structured `CK-#` checklist, and `/rpt.checklist` carries its categories inline (writing `checklists/requirements.md`). Removing it cuts dead, off-tone duplication without changing any live behaviour (`installTemplates` copies `*-template.md` by glob; the suite stays green).
|
|
20
|
+
|
|
21
|
+
### Internal
|
|
22
|
+
|
|
23
|
+
- New `resolveActivePresets` (cli), `buildTaskAcceptanceMap` + `TaskItem.acceptance` (core), `WIRED_PHASE_GATES` (core) — all unit-tested. Suite: core 784, cli 201, e2e 34 — all green. Verified end-to-end on a scratch project (`preset add` keeps the mobile gates active; a prose `requirements.md` is skipped, not failed).
|
|
24
|
+
|
|
25
|
+
## [0.8.1] - 2026-06-09
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`raptor jira pull` / `new --jira` against a self-hosted MCP server now work without a `dialect:` and fail clearly when misconfigured.** Three silent failures in the MCP connect path are fixed:
|
|
30
|
+
- **Dialect auto-detection** — a `jira.mcp` block without `dialect:` used to default to the hosted Rovo dialect, so a snake-case `mcp-atlassian` server received the camelCase `getJiraIssue` and rejected it with an opaque *"Unknown tool: getJiraIssue"*. The client now infers the dialect from the tools the server advertises (`detectDialect`); an explicit `dialect:` still wins.
|
|
31
|
+
- **Fail-loud tool resolution** — when no advertised tool matches an operation, the client throws an actionable error (naming the op, the dialect and the advertised tools, pointing at `jira.mcp.dialect`) instead of sending a bogus name.
|
|
32
|
+
- **Empty-env warning** — `connectMcpServer` now warns (via `missingEnvRefs`) when a `${VAR}` reference without a default resolves to unset/empty, so a missing or misnamed token surfaces at connect time instead of as the server's opaque *"Jira configuration could not be resolved"*.
|
|
33
|
+
|
|
34
|
+
### Internal
|
|
35
|
+
|
|
36
|
+
- New `detectDialect` (dialects) and `missingEnvRefs` (mcp-client), both exported and unit-tested. Verified end-to-end against the real KAN-2 card: a dialect-less config pulls successfully, and a missing token prints the warning.
|
|
37
|
+
|
|
38
|
+
## [0.8.0] - 2026-06-09
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **`raptor new --jira` now seeds the spec from the WHOLE card, not just its description.** A ticket's scenarios, requirements, acceptance criteria, test caveats and design/documentation links — wherever they live — are captured, kept verbatim, and routed into the spec for the agent to redistribute (enforced by `gate.spec.ready`, not a brittle extractor). Four parts:
|
|
43
|
+
- **Fetch-all** — the `mcp-atlassian` dialect requests `fields="*all"` with `expand="names,renderedFields"` instead of six fixed fields, so custom fields and the id→label map come back.
|
|
44
|
+
- **Link preservation** — `flattenAdf` now surfaces a link mark's `href` (as `[text](url)`) and smart-link card URLs (`inlineCard`/`blockCard`/`embedCard`), so a Figma link embedded in the body survives flattening and re-enables the `design/` scaffold + `gate.design.ready`.
|
|
45
|
+
- **Rich custom fields** — `JiraIssue` gains `customFields[]` (id, human label, value, canonical bucket). A PT/EN classifier (`classifyField`) routes "Critérios de aceite", "Cenários", "Requisitos", "Fora do escopo", "Documentação"… into the right section; `flattenFieldValue` tolerates ADF/select/multi-select shapes. Nothing is dropped — an unrecognised field falls to an appendix.
|
|
46
|
+
- **Verbatim ticket body** — `mapIssueToSpecContext` builds `jiraTicketBody` (description + labelled `### <field>` sections), folds acceptance-bucket fields into the AC list (de-duped, feeding `acceptance.ids`/M7), and the `spec.md` Problem Statement dumps it with explicit redistribution instructions. `raptor jira pull` and the clarify-time `jira-refresh.md` surface the same fields.
|
|
47
|
+
- **New `jira.custom_fields` config** — deterministic `id`/`label → bucket` overrides for servers that don't honour `expand=names`.
|
|
48
|
+
- **New docs** — `docs/jira-spec-enrichment.md` documents how the card is captured, how the agent is instructed, and how the gates validate it (anchored on a real card).
|
|
49
|
+
|
|
50
|
+
### Internal
|
|
51
|
+
|
|
52
|
+
- New `extractCustomFields` / `classifyField` / `flattenFieldValue` and `MapIssueOptions` in `jira/mapper.ts`; `JiraCustomField` / `JiraFieldBucket` types. A test proves `gate.spec.ready` blocks an approved-but-un-redistributed Jira seed on three independent axes (leftover `[PREENCHER]`, uncovered user stories, unmirrored `acceptance.ids`) — locking in "Raptor delivers verbatim, the agent redistributes, the gate enforces". Verified end-to-end against the real KAN-2 card.
|
|
53
|
+
|
|
6
54
|
## [0.7.1] - 2026-06-08
|
|
7
55
|
|
|
8
56
|
### Fixed
|
package/README.md
CHANGED
|
@@ -7,7 +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)
|
|
11
12
|
[](LICENSE)
|
|
12
13
|
|
|
13
14
|
</div>
|
|
@@ -42,6 +43,7 @@ raptor new login-biometrico -d "Permitir login com Face ID / digital"
|
|
|
42
43
|
- [🧰 Capacidades do Raptor](#-capacidades-do-raptor)
|
|
43
44
|
- [📖 Referência de comandos CLI](#-referência-de-comandos-cli)
|
|
44
45
|
- [🗂️ Estrutura criada no projeto](#️-estrutura-criada-no-projeto)
|
|
46
|
+
- [📚 Documentação & glossário](#-documentação--glossário)
|
|
45
47
|
- [🛠️ Desenvolvimento local](#️-desenvolvimento-local)
|
|
46
48
|
- [⚖️ Governança & contribuição](#️-governança--contribuição)
|
|
47
49
|
|
|
@@ -420,6 +422,7 @@ observability:
|
|
|
420
422
|
</details>
|
|
421
423
|
|
|
422
424
|
> 🧯 **Feature sem APIs restritas?** Não invente permissões. Há dois caminhos honestos e auditáveis para o M1:
|
|
425
|
+
>
|
|
423
426
|
> 1. **Opt-out no spec** — deixe as listas vazias e declare `stores.no_restricted_apis: "<motivo>"` (espelha `a11y.wcag_level: "n/a"` e `perf_budget.scope: "none"`).
|
|
424
427
|
> 2. **Override por ADR** — registre um ADR aceito em `.raptor/memory/decisions.md` com `Overrides: gate.mobile.stores`; o `verify` o lê em runtime e marca o gate como `⊝ overridden by ADR-NNN`. Gates **críticos** nunca são waivados por ADR (exigem assinatura humana).
|
|
425
428
|
>
|
|
@@ -451,17 +454,17 @@ Declarar não é cumprir. O Raptor **mede** e compara com o declarado:
|
|
|
451
454
|
|
|
452
455
|
## 🧰 Capacidades do Raptor
|
|
453
456
|
|
|
454
|
-
| Capacidade | Para que serve
|
|
455
|
-
| ----------------------- |
|
|
456
|
-
| 🎚️ **Presets** | Conjuntos reusáveis de artigos + gates, empilháveis. Bundled: `mobile-opinionated`.
|
|
457
|
-
| 🔁 **Workflows** | Orquestram o ciclo via YAML, com gates de revisão humana. Bundled: `sdd-cycle`, `quick
|
|
458
|
-
| 🤖 **Agentes** | Materializa os slash commands por agente.
|
|
459
|
-
| 🧠 **Skills** | Habilidades reusáveis materializadas para os agentes.
|
|
460
|
-
| 🔌 **MCP** | Registra servidores MCP e materializa nas configs nativas dos agentes.
|
|
461
|
-
| 🪝 **Hooks** | Scripts disparados em
|
|
462
|
-
| 📦 **Extensões** | Pacotes self-contained (templates/gates/hooks/workflows).
|
|
463
|
-
| 🎟️ **Jira** | Puxa issues do Jira (somente leitura) para semear specs.
|
|
464
|
-
| 📜 **Auditoria & trace** | Consulta a trilha de eventos e a rastreabilidade.
|
|
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
|
+
| 🔁 **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. | `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` |
|
|
465
468
|
|
|
466
469
|
### 🎟️ Integração com Jira
|
|
467
470
|
|
|
@@ -471,20 +474,22 @@ raptor jira pull APP-1234 # importa a issue
|
|
|
471
474
|
raptor new login --jira APP-1234 # semeia a spec a partir da issue
|
|
472
475
|
```
|
|
473
476
|
|
|
477
|
+
> 📖 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).
|
|
478
|
+
|
|
474
479
|
---
|
|
475
480
|
|
|
476
481
|
## 📖 Referência de comandos CLI
|
|
477
482
|
|
|
478
483
|
```text
|
|
479
|
-
🔄 Ciclo
|
|
484
|
+
🔄 Ciclo init · new · clarify · plan · tasks · analyze · checklist · implement · approve · taskstoissues
|
|
480
485
|
🌿 Branch/commit commit · scan
|
|
481
|
-
🔬 Verificação
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
🧩 Extensão
|
|
486
|
-
|
|
487
|
-
🎟️ Jira
|
|
486
|
+
🔬 Verificação verify · verify a11y · verify perf · verify stores · verify os-matrix
|
|
487
|
+
verify architecture · verify audit · verify constitution
|
|
488
|
+
📊 Status status · doctor · trace · audit query · audit show
|
|
489
|
+
⚖️ Governança gate list · gate approve · gate skip · repair constitution · hotfix · resync · upgrade
|
|
490
|
+
🧩 Extensão preset … · workflow … · skill … · mcp … · hook … · extension …
|
|
491
|
+
add-agent · list-agents · add-extension
|
|
492
|
+
🎟️ Jira jira connect · status · pull · disconnect
|
|
488
493
|
```
|
|
489
494
|
|
|
490
495
|
Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um.
|
|
@@ -545,6 +550,43 @@ CLAUDE.md # 📎 bloco de contexto do agente
|
|
|
545
550
|
|
|
546
551
|
---
|
|
547
552
|
|
|
553
|
+
## 📚 Documentação & glossário
|
|
554
|
+
|
|
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
|
+
|
|
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
|
+
|
|
567
|
+
### Hierarquia de governança (quem manda)
|
|
568
|
+
|
|
569
|
+
```mermaid
|
|
570
|
+
flowchart TD
|
|
571
|
+
A["🧭 Constitution — Artigos C1–C5<br/><i>inviolável; só major release altera o core</i>"]
|
|
572
|
+
B["🔎 Phase -1 / Constitution Check<br/><i>confronta decisões com a constituição</i>"]
|
|
573
|
+
C["🛡️ Gates — 🔴 critical > 🟡 required > 🔵 advisory"]
|
|
574
|
+
D["📝 ADR / Override<br/><i>dispensa required/advisory; nunca critical</i>"]
|
|
575
|
+
E["👤 Human Review (C5)<br/><i>veto humano nos críticos</i>"]
|
|
576
|
+
A --> B --> C --> D --> E
|
|
577
|
+
|
|
578
|
+
classDef law fill:#fee2e2,stroke:#ef4444,color:#7f1d1d;
|
|
579
|
+
classDef enf fill:#fef9c3,stroke:#eab308,color:#713f12;
|
|
580
|
+
classDef human fill:#dcfce7,stroke:#22c55e,color:#14532d;
|
|
581
|
+
class A,B law;
|
|
582
|
+
class C,D enf;
|
|
583
|
+
class E human;
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
> Customização sem fork segue a **Priority Stack**: `overrides do projeto → presets → extensions → core`. Detalhes e justificativa de cada nível no [glossário](docs/glossary.md#hierarquias).
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
548
590
|
## 🛠️ Desenvolvimento local
|
|
549
591
|
|
|
550
592
|
Monorepo pnpm (`packages/core` = `@raptor/core`, `packages/cli` = binário `raptor`).
|
|
@@ -552,7 +594,7 @@ Monorepo pnpm (`packages/core` = `@raptor/core`, `packages/cli` = binário `rapt
|
|
|
552
594
|
```bash
|
|
553
595
|
pnpm install
|
|
554
596
|
pnpm build
|
|
555
|
-
pnpm -r test && pnpm test:e2e #
|
|
597
|
+
pnpm -r test && pnpm test:e2e # 1019 testes (784 core + 201 cli + 34 e2e)
|
|
556
598
|
|
|
557
599
|
# usar a CLI local em outro projeto:
|
|
558
600
|
cd packages/cli && npm link # ou: pnpm link --global
|
|
@@ -1119,6 +1119,7 @@ export const gateHotfixUnreconciled = {
|
|
|
1119
1119
|
};
|
|
1120
1120
|
import { M7_GATES } from "./m7-gates.js";
|
|
1121
1121
|
import { DESIGN_GATES } from "./design-gates.js";
|
|
1122
|
+
import { WIRED_PHASE_GATES } from "./phase-gates.js";
|
|
1122
1123
|
export const PROJECT_GATES = [
|
|
1123
1124
|
gateConstitutionIntegrity,
|
|
1124
1125
|
gateAmendmentCore,
|
|
@@ -1144,6 +1145,7 @@ export const BUILTIN_GATES = [
|
|
|
1144
1145
|
gateTasksReady,
|
|
1145
1146
|
...M7_GATES,
|
|
1146
1147
|
...DESIGN_GATES,
|
|
1148
|
+
...WIRED_PHASE_GATES,
|
|
1147
1149
|
];
|
|
1148
1150
|
export function gateById(id) {
|
|
1149
1151
|
return BUILTIN_GATES.find((g) => g.id === id);
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseImplLog, computeAcceptanceCoverage, } from "../models/impl-log.js";
|
|
4
4
|
import { parseSpec } from "../models/spec.js";
|
|
5
|
-
import { parseTasks, parseTaskList } from "../models/tasks.js";
|
|
5
|
+
import { parseTasks, parseTaskList, buildTaskAcceptanceMap } from "../models/tasks.js";
|
|
6
6
|
import { parseFrontmatter } from "../frontmatter/index.js";
|
|
7
7
|
import { hashFile } from "../audit/hash.js";
|
|
8
8
|
function result(gate, status, message) {
|
|
@@ -64,13 +64,7 @@ export const gateM7AcceptanceHasCoverage = {
|
|
|
64
64
|
if (acceptanceIds.length === 0)
|
|
65
65
|
return result(this, "pass", "No acceptance criteria to cover.");
|
|
66
66
|
const taskList = tasks ? parseTaskList(tasks.body) : [];
|
|
67
|
-
const taskToAcceptance =
|
|
68
|
-
for (const t of taskList) {
|
|
69
|
-
if (t.story) {
|
|
70
|
-
const aId = t.story.replace(/^US-/, "A").replace(/^US/, "A");
|
|
71
|
-
taskToAcceptance[t.id] = [aId];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
67
|
+
const taskToAcceptance = buildTaskAcceptanceMap(taskList);
|
|
74
68
|
const coverage = computeAcceptanceCoverage(acceptanceIds, taskToAcceptance, implLog);
|
|
75
69
|
const missing = acceptanceIds.filter((id) => coverage[id].length === 0);
|
|
76
70
|
if (missing.length > 0) {
|
|
@@ -134,13 +128,7 @@ export const gateM7CoverageEvidenceValid = {
|
|
|
134
128
|
if (acceptanceIds.length === 0)
|
|
135
129
|
return result(this, "pass", "No acceptance criteria to validate.");
|
|
136
130
|
const taskList = tasks ? parseTaskList(tasks.body) : [];
|
|
137
|
-
const taskToAcceptance =
|
|
138
|
-
for (const t of taskList) {
|
|
139
|
-
if (t.story) {
|
|
140
|
-
const aId = t.story.replace(/^US-?/, "A");
|
|
141
|
-
taskToAcceptance[t.id] = [aId];
|
|
142
|
-
}
|
|
143
|
-
}
|
|
131
|
+
const taskToAcceptance = buildTaskAcceptanceMap(taskList);
|
|
144
132
|
const coverage = computeAcceptanceCoverage(acceptanceIds, taskToAcceptance, implLog);
|
|
145
133
|
const errors = [];
|
|
146
134
|
for (const aId of acceptanceIds) {
|
|
@@ -50,6 +50,9 @@ export const gateClarifyAcceptanceCovered = {
|
|
|
50
50
|
}
|
|
51
51
|
const raw = readFileSync(specPath, "utf8");
|
|
52
52
|
const { data } = parseFrontmatter(raw);
|
|
53
|
+
if (data?.status !== "approved") {
|
|
54
|
+
return result(this, "skipped", "spec not yet approved; acceptance set is finalised at approval.");
|
|
55
|
+
}
|
|
53
56
|
const acceptanceIds = data?.acceptance?.ids ?? [];
|
|
54
57
|
if (acceptanceIds.length === 0) {
|
|
55
58
|
return result(this, "fail", "No acceptance criteria IDs found in spec.md frontmatter after clarify.");
|
|
@@ -76,10 +79,14 @@ export const gateChecklistFrontmatter = {
|
|
|
76
79
|
return result(this, "skipped", "No checklists found; skipping.");
|
|
77
80
|
}
|
|
78
81
|
const errors = [];
|
|
82
|
+
let validated = 0;
|
|
79
83
|
for (const { kind, path } of checklists) {
|
|
80
84
|
try {
|
|
81
85
|
const raw = readFileSync(path, "utf8");
|
|
82
|
-
const { data } = parseFrontmatter(raw);
|
|
86
|
+
const { data, hasFrontmatter } = parseFrontmatter(raw);
|
|
87
|
+
if (!hasFrontmatter)
|
|
88
|
+
continue;
|
|
89
|
+
validated++;
|
|
83
90
|
if (!data.id)
|
|
84
91
|
errors.push(`${kind}.md: missing 'id' in frontmatter`);
|
|
85
92
|
if (!data.kind)
|
|
@@ -92,7 +99,10 @@ export const gateChecklistFrontmatter = {
|
|
|
92
99
|
if (errors.length > 0) {
|
|
93
100
|
return result(this, "fail", `Checklist frontmatter errors:\n ${errors.join("\n ")}`);
|
|
94
101
|
}
|
|
95
|
-
|
|
102
|
+
if (validated === 0) {
|
|
103
|
+
return result(this, "skipped", "No structured checklists (with frontmatter) found; skipping.");
|
|
104
|
+
}
|
|
105
|
+
return result(this, "pass", `${validated} checklist(s) have valid frontmatter.`);
|
|
96
106
|
},
|
|
97
107
|
};
|
|
98
108
|
export const gateChecklistLinksToDecisions = {
|
|
@@ -112,7 +122,9 @@ export const gateChecklistLinksToDecisions = {
|
|
|
112
122
|
let totalItems = 0;
|
|
113
123
|
for (const { kind, path } of checklists) {
|
|
114
124
|
const raw = readFileSync(path, "utf8");
|
|
115
|
-
const { body } = parseFrontmatter(raw);
|
|
125
|
+
const { body, hasFrontmatter } = parseFrontmatter(raw);
|
|
126
|
+
if (!hasFrontmatter)
|
|
127
|
+
continue;
|
|
116
128
|
const items = parseChecklistItems(body);
|
|
117
129
|
totalItems += items.length;
|
|
118
130
|
for (const item of items) {
|
|
@@ -141,9 +153,13 @@ export const gateChecklistNonEmpty = {
|
|
|
141
153
|
return result(this, "skipped", "No checklists found; skipping.");
|
|
142
154
|
}
|
|
143
155
|
const empty = [];
|
|
156
|
+
let structured = 0;
|
|
144
157
|
for (const { kind, path } of checklists) {
|
|
145
158
|
const raw = readFileSync(path, "utf8");
|
|
146
|
-
const { body } = parseFrontmatter(raw);
|
|
159
|
+
const { body, hasFrontmatter } = parseFrontmatter(raw);
|
|
160
|
+
if (!hasFrontmatter)
|
|
161
|
+
continue;
|
|
162
|
+
structured++;
|
|
147
163
|
const items = parseChecklistItems(body);
|
|
148
164
|
if (items.length === 0) {
|
|
149
165
|
empty.push(kind);
|
|
@@ -152,7 +168,10 @@ export const gateChecklistNonEmpty = {
|
|
|
152
168
|
if (empty.length > 0) {
|
|
153
169
|
return result(this, "fail", `Empty checklist(s): ${empty.join(", ")}`);
|
|
154
170
|
}
|
|
155
|
-
|
|
171
|
+
if (structured === 0) {
|
|
172
|
+
return result(this, "skipped", "No structured checklists found; skipping.");
|
|
173
|
+
}
|
|
174
|
+
return result(this, "pass", `All ${structured} checklist(s) have items.`);
|
|
156
175
|
},
|
|
157
176
|
};
|
|
158
177
|
export const gateAnalyzeFrontmatter = {
|
|
@@ -235,3 +254,11 @@ export const PHASE_GATES = [
|
|
|
235
254
|
gateAnalyzeFrontmatter,
|
|
236
255
|
gateAnalyzeFindingsTraced,
|
|
237
256
|
];
|
|
257
|
+
export const WIRED_PHASE_GATES = [
|
|
258
|
+
gateClarifyAcceptanceCovered,
|
|
259
|
+
gateChecklistFrontmatter,
|
|
260
|
+
gateChecklistLinksToDecisions,
|
|
261
|
+
gateChecklistNonEmpty,
|
|
262
|
+
gateAnalyzeFrontmatter,
|
|
263
|
+
gateAnalyzeFindingsTraced,
|
|
264
|
+
];
|
|
@@ -52,7 +52,8 @@ export const MCP_ATLASSIAN_DIALECT = {
|
|
|
52
52
|
args: {
|
|
53
53
|
getIssue: (_cloudId, key) => ({
|
|
54
54
|
issue_key: key,
|
|
55
|
-
fields: "
|
|
55
|
+
fields: "*all",
|
|
56
|
+
expand: "names,renderedFields",
|
|
56
57
|
comment_limit: 10,
|
|
57
58
|
}),
|
|
58
59
|
search: (_cloudId, jql, max) => ({ jql, limit: max }),
|
|
@@ -81,3 +82,10 @@ export const DIALECTS = {
|
|
|
81
82
|
export function resolveDialect(name) {
|
|
82
83
|
return (name && DIALECTS[name]) || ROVO_DIALECT;
|
|
83
84
|
}
|
|
85
|
+
export function detectDialect(toolNames) {
|
|
86
|
+
for (const d of [MCP_ATLASSIAN_DIALECT, ROVO_DIALECT]) {
|
|
87
|
+
if (d.toolNames.getIssue.some((n) => toolNames.includes(n)))
|
|
88
|
+
return d;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./credentials.js";
|
|
3
3
|
export { AtlassianOAuthProvider, isAccessTokenExpired, openBrowser, startCallbackServer, } from "./oauth.js";
|
|
4
|
-
export { connectJira, connectMcpServer, makeJiraClient, clientToToolCaller, decodeToolResult, unwrapEnvelope, expandEnv, } from "./mcp-client.js";
|
|
5
|
-
export { ROVO_DIALECT, MCP_ATLASSIAN_DIALECT, DIALECTS, resolveDialect, } from "./dialects.js";
|
|
6
|
-
export { parseJiraIssue, parseCreatedIssue, extractComments, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
|
|
4
|
+
export { connectJira, connectMcpServer, makeJiraClient, clientToToolCaller, decodeToolResult, unwrapEnvelope, expandEnv, missingEnvRefs, } from "./mcp-client.js";
|
|
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";
|
|
@@ -17,6 +17,12 @@ export function parseJiraIssue(raw, baseUrl) {
|
|
|
17
17
|
nestedName(fields["issue_type"]);
|
|
18
18
|
const labels = strArray(fields["labels"]);
|
|
19
19
|
const url = resolveUrl(issue, fields, key, baseUrl);
|
|
20
|
+
const names = isRecord(issue["names"])
|
|
21
|
+
? issue["names"]
|
|
22
|
+
: isRecord(obj["names"])
|
|
23
|
+
? obj["names"]
|
|
24
|
+
: {};
|
|
25
|
+
const customFields = extractCustomFields(fields, names);
|
|
20
26
|
return {
|
|
21
27
|
key,
|
|
22
28
|
summary,
|
|
@@ -26,9 +32,57 @@ export function parseJiraIssue(raw, baseUrl) {
|
|
|
26
32
|
labels,
|
|
27
33
|
acceptanceCriteria: extractAcceptanceCriteria(fields, description),
|
|
28
34
|
comments: extractComments(fields),
|
|
35
|
+
customFields,
|
|
29
36
|
...(url ? { url } : {}),
|
|
30
37
|
};
|
|
31
38
|
}
|
|
39
|
+
export function extractCustomFields(fields, names) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const [id, raw] of Object.entries(fields)) {
|
|
42
|
+
if (!/^customfield_/i.test(id))
|
|
43
|
+
continue;
|
|
44
|
+
const value = flattenFieldValue(raw);
|
|
45
|
+
if (!value)
|
|
46
|
+
continue;
|
|
47
|
+
const name = str(names[id]) || id;
|
|
48
|
+
out.push({ id, name, value, bucket: classifyField(name) });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function classifyField(label) {
|
|
53
|
+
const l = label.toLowerCase();
|
|
54
|
+
if (/aceite|aceita[çc][ãa]o|acceptance/.test(l))
|
|
55
|
+
return "acceptance";
|
|
56
|
+
if (/cen[áa]rio|scenario/.test(l))
|
|
57
|
+
return "scenarios";
|
|
58
|
+
if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b/.test(l))
|
|
59
|
+
return "nonFunctional";
|
|
60
|
+
if (/requisito|requirement|\brf\b/.test(l))
|
|
61
|
+
return "functional";
|
|
62
|
+
if (/fora do escopo|out of scope|n[ãa]o escopo/.test(l))
|
|
63
|
+
return "outOfScope";
|
|
64
|
+
if (/documenta|documentation|link|refer[êe]ncia|design|figma/.test(l))
|
|
65
|
+
return "docs";
|
|
66
|
+
return "other";
|
|
67
|
+
}
|
|
68
|
+
export function flattenFieldValue(raw) {
|
|
69
|
+
if (raw == null)
|
|
70
|
+
return "";
|
|
71
|
+
if (typeof raw === "string")
|
|
72
|
+
return raw.trim();
|
|
73
|
+
if (Array.isArray(raw)) {
|
|
74
|
+
return raw.map(flattenFieldValue).filter(Boolean).join("\n").trim();
|
|
75
|
+
}
|
|
76
|
+
if (isRecord(raw)) {
|
|
77
|
+
if ("content" in raw || raw["type"] === "doc")
|
|
78
|
+
return flattenAdf(raw).trim();
|
|
79
|
+
const opt = str(raw["value"]) || str(raw["name"]);
|
|
80
|
+
if (opt)
|
|
81
|
+
return opt;
|
|
82
|
+
return flattenAdf(raw).trim();
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
32
86
|
export function parseCreatedIssue(raw) {
|
|
33
87
|
const obj = coerceObject(raw);
|
|
34
88
|
if (!obj)
|
|
@@ -58,11 +112,38 @@ export function extractComments(fields) {
|
|
|
58
112
|
}
|
|
59
113
|
return out;
|
|
60
114
|
}
|
|
61
|
-
export function mapIssueToSpecContext(issue) {
|
|
62
|
-
const
|
|
115
|
+
export function mapIssueToSpecContext(issue, opts = {}) {
|
|
116
|
+
const overrides = opts.fieldBuckets ?? {};
|
|
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
|
+
});
|
|
121
|
+
const ac = [];
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const pushAc = (line) => {
|
|
124
|
+
const t = line.trim();
|
|
125
|
+
if (t && !seen.has(t)) {
|
|
126
|
+
seen.add(t);
|
|
127
|
+
ac.push(t);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
issue.acceptanceCriteria.forEach(pushAc);
|
|
131
|
+
for (const cf of custom)
|
|
132
|
+
if (cf.bucket === "acceptance")
|
|
133
|
+
toLines(cf.value).forEach(pushAc);
|
|
134
|
+
const sections = custom
|
|
135
|
+
.filter((cf) => cf.bucket !== "acceptance")
|
|
136
|
+
.map((cf) => ({ title: cf.name, body: cf.value, bucket: cf.bucket }));
|
|
137
|
+
const ticketBody = [
|
|
138
|
+
issue.description,
|
|
139
|
+
...sections.map((s) => `### ${s.title}\n${s.body}`),
|
|
140
|
+
]
|
|
141
|
+
.filter((s) => s && s.trim())
|
|
142
|
+
.join("\n\n");
|
|
63
143
|
return {
|
|
64
144
|
jiraSummary: issue.summary,
|
|
65
145
|
jiraDescription: issue.description,
|
|
146
|
+
jiraTicketBody: ticketBody,
|
|
66
147
|
jiraStatus: issue.status ?? "",
|
|
67
148
|
jiraIssueType: issue.issueType ?? "",
|
|
68
149
|
jiraUrl: issue.url ?? "",
|
|
@@ -71,6 +152,7 @@ export function mapIssueToSpecContext(issue) {
|
|
|
71
152
|
jiraAcceptanceNumbered: ac.map((text, i) => ({ n: i + 1, text })),
|
|
72
153
|
jiraAcceptanceNextN: ac.length + 1,
|
|
73
154
|
jiraHasAcceptance: ac.length > 0,
|
|
155
|
+
jiraCustomSections: sections,
|
|
74
156
|
};
|
|
75
157
|
}
|
|
76
158
|
export function flattenAdf(node) {
|
|
@@ -82,8 +164,17 @@ export function flattenAdf(node) {
|
|
|
82
164
|
return node.map(flattenAdf).join("");
|
|
83
165
|
if (!isRecord(node))
|
|
84
166
|
return "";
|
|
85
|
-
if (typeof node["text"] === "string")
|
|
86
|
-
|
|
167
|
+
if (typeof node["text"] === "string") {
|
|
168
|
+
const text = node["text"];
|
|
169
|
+
const href = linkHref(node["marks"]);
|
|
170
|
+
return href && href !== text ? `[${text}](${href})` : text;
|
|
171
|
+
}
|
|
172
|
+
const type = str(node["type"]);
|
|
173
|
+
if (type === "inlineCard" || type === "blockCard" || type === "embedCard") {
|
|
174
|
+
const url = cardUrl(node["attrs"]);
|
|
175
|
+
if (url)
|
|
176
|
+
return type === "inlineCard" ? url : `${url}\n`;
|
|
177
|
+
}
|
|
87
178
|
const inner = flattenAdf(node["content"]);
|
|
88
179
|
const blockTypes = new Set([
|
|
89
180
|
"paragraph",
|
|
@@ -94,7 +185,6 @@ export function flattenAdf(node) {
|
|
|
94
185
|
"codeBlock",
|
|
95
186
|
"blockquote",
|
|
96
187
|
]);
|
|
97
|
-
const type = str(node["type"]);
|
|
98
188
|
if (type === "hardBreak")
|
|
99
189
|
return "\n";
|
|
100
190
|
if (type === "listItem")
|
|
@@ -103,16 +193,40 @@ export function flattenAdf(node) {
|
|
|
103
193
|
return `${inner}\n`;
|
|
104
194
|
return inner;
|
|
105
195
|
}
|
|
196
|
+
function linkHref(marks) {
|
|
197
|
+
if (!Array.isArray(marks))
|
|
198
|
+
return undefined;
|
|
199
|
+
for (const m of marks) {
|
|
200
|
+
if (isRecord(m) && m["type"] === "link" && isRecord(m["attrs"])) {
|
|
201
|
+
const href = m["attrs"]["href"];
|
|
202
|
+
if (typeof href === "string" && href)
|
|
203
|
+
return href;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
function cardUrl(attrs) {
|
|
209
|
+
if (!isRecord(attrs))
|
|
210
|
+
return undefined;
|
|
211
|
+
const url = attrs["url"];
|
|
212
|
+
if (typeof url === "string" && url)
|
|
213
|
+
return url;
|
|
214
|
+
const data = attrs["data"];
|
|
215
|
+
if (isRecord(data) && typeof data["url"] === "string" && data["url"]) {
|
|
216
|
+
return data["url"];
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
106
220
|
export function extractAcceptanceCriteria(fields, description) {
|
|
107
221
|
for (const [k, v] of Object.entries(fields)) {
|
|
108
|
-
if (!/acceptance/i.test(k))
|
|
222
|
+
if (!/acceptance|aceite|aceita[çc]/i.test(k))
|
|
109
223
|
continue;
|
|
110
224
|
const flat = flattenAdf(v);
|
|
111
225
|
const lines = toLines(flat);
|
|
112
226
|
if (lines.length)
|
|
113
227
|
return lines;
|
|
114
228
|
}
|
|
115
|
-
const m = description.match(/acceptance\s+criteria\s*:?\s*\n([\s\S]+?)(?:\n\s*\n|$)/i);
|
|
229
|
+
const m = description.match(/(?:acceptance\s+criteria|crit[ée]rios?\s+de\s+aceita[çc][ãa]o|crit[ée]rios?\s+de\s+aceite)\s*:?\s*\n([\s\S]+?)(?:\n\s*\n|$)/i);
|
|
116
230
|
if (m && m[1]) {
|
|
117
231
|
const lines = toLines(m[1]);
|
|
118
232
|
if (lines.length)
|
|
@@ -4,14 +4,20 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
4
4
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
5
5
|
import { AtlassianOAuthProvider, openBrowser, startCallbackServer, } from "./oauth.js";
|
|
6
6
|
import { mapIssueToSpecContext, parseCreatedIssue, parseJiraIssue, } from "./mapper.js";
|
|
7
|
-
import { ROVO_DIALECT, resolveDialect, } from "./dialects.js";
|
|
7
|
+
import { ROVO_DIALECT, resolveDialect, detectDialect, } from "./dialects.js";
|
|
8
8
|
import { ATLASSIAN_MCP_URL, } from "./types.js";
|
|
9
9
|
export function makeJiraClient(caller, opts = {}) {
|
|
10
|
-
const dialect = opts.dialect ?? ROVO_DIALECT;
|
|
10
|
+
const dialect = opts.dialect ?? detectDialect(caller.toolNames) ?? ROVO_DIALECT;
|
|
11
11
|
const resolve = (kind) => {
|
|
12
12
|
const candidates = dialect.toolNames[kind];
|
|
13
13
|
const found = candidates.find((n) => caller.toolNames.includes(n));
|
|
14
|
-
|
|
14
|
+
if (found)
|
|
15
|
+
return found;
|
|
16
|
+
const advertised = caller.toolNames.slice(0, 12).join(", ");
|
|
17
|
+
throw new Error(`No MCP tool for "${kind}": dialect "${dialect.name}" expects one of ` +
|
|
18
|
+
`[${candidates.join(", ")}], but the server advertises [${advertised}` +
|
|
19
|
+
`${caller.toolNames.length > 12 ? ", …" : ""}]. ` +
|
|
20
|
+
`Set jira.mcp.dialect in raptor.yml to match your server.`);
|
|
15
21
|
};
|
|
16
22
|
const parseIssue = (data) => parseJiraIssue(data, opts.baseUrl);
|
|
17
23
|
return {
|
|
@@ -87,7 +93,11 @@ export async function connectJira(opts = {}) {
|
|
|
87
93
|
});
|
|
88
94
|
}
|
|
89
95
|
export async function connectMcpServer(cfg) {
|
|
90
|
-
const
|
|
96
|
+
const missing = missingEnvRefs(cfg.env ?? {});
|
|
97
|
+
if (missing.length) {
|
|
98
|
+
process.stderr.write(`Warning: Jira MCP env var(s) referenced but unset/empty: ${missing.join(", ")} ` +
|
|
99
|
+
`— the spawned server may fail to authenticate.\n`);
|
|
100
|
+
}
|
|
91
101
|
const transport = new StdioClientTransport({
|
|
92
102
|
command: cfg.command,
|
|
93
103
|
args: cfg.args ?? [],
|
|
@@ -97,10 +107,22 @@ export async function connectMcpServer(cfg) {
|
|
|
97
107
|
const client = new Client({ name: "raptor-cli", version: "0.1.0" }, { capabilities: {} });
|
|
98
108
|
await client.connect(transport);
|
|
99
109
|
return makeJiraClient(await clientToToolCaller(client), {
|
|
100
|
-
dialect,
|
|
110
|
+
...(cfg.dialect ? { dialect: resolveDialect(cfg.dialect) } : {}),
|
|
101
111
|
...(cfg.base_url ? { baseUrl: cfg.base_url } : {}),
|
|
102
112
|
});
|
|
103
113
|
}
|
|
114
|
+
export function missingEnvRefs(env, source = process.env) {
|
|
115
|
+
const missing = new Set();
|
|
116
|
+
for (const raw of Object.values(env)) {
|
|
117
|
+
for (const m of raw.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(:-[^}]*)?\}/g)) {
|
|
118
|
+
const name = m[1];
|
|
119
|
+
const hasDefault = m[2] !== undefined;
|
|
120
|
+
if (!hasDefault && !source[name])
|
|
121
|
+
missing.add(name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...missing];
|
|
125
|
+
}
|
|
104
126
|
export function expandEnv(env, source = process.env) {
|
|
105
127
|
const out = {};
|
|
106
128
|
for (const [k, raw] of Object.entries(env)) {
|
|
@@ -30,15 +30,18 @@ export function parseTaskList(body) {
|
|
|
30
30
|
const rest = m[2] ?? "";
|
|
31
31
|
const stories = [...rest.matchAll(/\[(US\d+)\]/gi)].map((mm) => mm[1]);
|
|
32
32
|
const story = stories[0];
|
|
33
|
+
const acceptance = [...rest.matchAll(/\[(AC-\d+)\]/gi)].map((mm) => mm[1]);
|
|
33
34
|
const parallel = /\[P\]/i.test(rest);
|
|
34
35
|
const description = rest
|
|
35
36
|
.replace(/\[US\d+\]/gi, "")
|
|
37
|
+
.replace(/\[AC-\d+\]/gi, "")
|
|
36
38
|
.replace(/\[P\]/gi, "")
|
|
37
39
|
.trim();
|
|
38
40
|
items.push({
|
|
39
41
|
id,
|
|
40
42
|
...(story ? { story } : {}),
|
|
41
43
|
stories,
|
|
44
|
+
acceptance,
|
|
42
45
|
parallel,
|
|
43
46
|
description,
|
|
44
47
|
status,
|
|
@@ -72,3 +75,11 @@ export function markTaskCompleted(body, taskId) {
|
|
|
72
75
|
}
|
|
73
76
|
return lines.join("\n");
|
|
74
77
|
}
|
|
78
|
+
export function buildTaskAcceptanceMap(taskList) {
|
|
79
|
+
const map = {};
|
|
80
|
+
for (const t of taskList) {
|
|
81
|
+
if (t.acceptance.length > 0)
|
|
82
|
+
map[t.id] = t.acceptance;
|
|
83
|
+
}
|
|
84
|
+
return map;
|
|
85
|
+
}
|
package/dist/_core/package.json
CHANGED
|
@@ -88,18 +88,22 @@ and assets are incorporated, catalogued and present on disk — no loose ends.
|
|
|
88
88
|
{{/if}}
|
|
89
89
|
|
|
90
90
|
## Problem Statement
|
|
91
|
-
{{#if
|
|
92
|
-
{{
|
|
91
|
+
{{#if jiraTicketBody}}
|
|
92
|
+
{{jiraTicketBody}}
|
|
93
93
|
|
|
94
94
|
<!--
|
|
95
|
-
Imported VERBATIM from Jira {{jira}} —
|
|
96
|
-
|
|
95
|
+
Imported VERBATIM from Jira {{jira}} — the description PLUS every rich custom field
|
|
96
|
+
(scenarios, requirements, test caveats, documentation/design links) the card holds.
|
|
97
|
+
This is the authoritative source for this feature. The agent MUST redistribute ALL of
|
|
98
|
+
it into the canonical sections below, LOSING NOTHING (each `### <field>` block too):
|
|
97
99
|
- each user story / "História" → ## User Stories (as [US#] with priority, why, independent test)
|
|
98
100
|
- each functional requirement / "Requisito Funcional / RF" → ## Functional Requirements (as [FR-#])
|
|
99
101
|
- each non-functional requirement / "Requisito Não Funcional / RNF" → ## Non-Functional Requirements (as [NFR-#])
|
|
102
|
+
- each applicable scenario / "Cenário aplicável" → ## User Scenarios & Testing (and a covering [AC-#])
|
|
100
103
|
- each acceptance scenario / "Critério de Aceitação / Cenário" → ## Acceptance Criteria (as [AC-#] Given/When/Then, covering a [US#])
|
|
104
|
+
- test caveats / "Ressalvas de testes" → ## Edge Cases (or the relevant [NFR-#])
|
|
101
105
|
- the ticket's out-of-scope / "Fora do Escopo" list → ## Out of Scope
|
|
102
|
-
- design links (Figma etc.) → ## Dependencies & Assumptions
|
|
106
|
+
- design / documentation links (Figma etc.) → ## Dependencies & Assumptions
|
|
103
107
|
Then REDUCE this section to a crisp problem statement (who is affected today and how). Do not leave the raw dump here.
|
|
104
108
|
-->
|
|
105
109
|
{{else}}{{#if description}}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,8 +2,9 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { BaseCommand } from "../base-command.js";
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { CORE_VERSION, coreHash, extractCoreBlock,
|
|
5
|
+
import { CORE_VERSION, coreHash, extractCoreBlock, knownPresetIds, MANIFEST_SCHEMA_VERSION, validateManifest, validateFeatureState, validateInitOptions, VERSION, detectInstalledAgents, discoverSkills, loadSkillsConfig, validateSkillsConfig, skillTargetPath, loadMcpConfig, validateMcpConfig, isServerMaterialized, mcpTargetFor, } from "../_core/dist/index.js";
|
|
6
6
|
import { findProjectRoot, readConfig } from "../shared/project.js";
|
|
7
|
+
import { resolveActivePresets } from "../shared/presets.js";
|
|
7
8
|
import { configuredAdapters } from "../shared/agents.js";
|
|
8
9
|
import { configuredMcpAgentIds } from "../shared/mcp.js";
|
|
9
10
|
export default class Doctor extends BaseCommand {
|
|
@@ -140,30 +141,29 @@ export default class Doctor extends BaseCommand {
|
|
|
140
141
|
detail: "Missing",
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (preset) {
|
|
146
|
-
checks.push({
|
|
147
|
-
name: "Preset",
|
|
148
|
-
status: "pass",
|
|
149
|
-
detail: `${preset.id} v${preset.version} (${preset.articles.length} articles, ${preset.gates.length} gates)`,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
checks.push({
|
|
154
|
-
name: "Preset",
|
|
155
|
-
status: "warn",
|
|
156
|
-
detail: `"${config.preset}" not registered — known: ${knownPresetIds().join(", ")}`,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
144
|
+
const resolvedPresets = resolveActivePresets(config);
|
|
145
|
+
if (resolvedPresets.ids.length === 0) {
|
|
161
146
|
checks.push({
|
|
162
147
|
name: "Preset",
|
|
163
148
|
status: "warn",
|
|
164
149
|
detail: "No preset configured",
|
|
165
150
|
});
|
|
166
151
|
}
|
|
152
|
+
else if (resolvedPresets.unknownIds.length > 0) {
|
|
153
|
+
checks.push({
|
|
154
|
+
name: "Preset",
|
|
155
|
+
status: "warn",
|
|
156
|
+
detail: `not registered: ${resolvedPresets.unknownIds.join(", ")} — known: ${knownPresetIds().join(", ")}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const articleCount = resolvedPresets.presets.reduce((n, p) => n + p.articles.length, 0);
|
|
161
|
+
checks.push({
|
|
162
|
+
name: "Preset",
|
|
163
|
+
status: "pass",
|
|
164
|
+
detail: `${resolvedPresets.ids.join(", ")} (${articleCount} articles, ${resolvedPresets.gates.length} gates)`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
167
|
try {
|
|
168
168
|
const agents = detectInstalledAgents();
|
|
169
169
|
if (agents.length > 0) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from "@oclif/core";
|
|
2
2
|
import { BaseCommand } from "../../base-command.js";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
-
import { appendAuditEvent, gateById,
|
|
4
|
+
import { appendAuditEvent, gateById, } from "../../_core/dist/index.js";
|
|
5
5
|
import { auditableDir, currentActor, readConfig, requireProjectRoot, } from "../../shared/project.js";
|
|
6
|
+
import { resolveActivePresets } from "../../shared/presets.js";
|
|
6
7
|
import { runAfterHook, runBeforeHook } from "../../shared/hooks.js";
|
|
7
8
|
export default class GateApprove extends BaseCommand {
|
|
8
9
|
static description = "Record human approval of a gate against a feature (emits gate.approved). " +
|
|
@@ -39,8 +40,7 @@ export default class GateApprove extends BaseCommand {
|
|
|
39
40
|
const dir = auditableDir(root, args.feature);
|
|
40
41
|
const featureName = basename(dir);
|
|
41
42
|
const config = readConfig(root);
|
|
42
|
-
const
|
|
43
|
-
const presetGates = preset?.gates ?? [];
|
|
43
|
+
const { gates: presetGates } = resolveActivePresets(config);
|
|
44
44
|
const gate = gateById(args.gateId) ?? presetGates.find((g) => g.id === args.gateId);
|
|
45
45
|
if (!gate) {
|
|
46
46
|
this.error(`unknown gate: ${args.gateId}. Run 'raptor gate list' to see valid ids.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { BUILTIN_GATES,
|
|
1
|
+
import { BUILTIN_GATES, listPresets } from "../../_core/dist/index.js";
|
|
2
2
|
import { BaseCommand } from "../../base-command.js";
|
|
3
3
|
import { findProjectRoot, readConfig } from "../../shared/project.js";
|
|
4
|
+
import { resolveActivePresets } from "../../shared/presets.js";
|
|
4
5
|
export default class GateList extends BaseCommand {
|
|
5
6
|
static description = "List all built-in gates, their levels and article references";
|
|
6
7
|
async run() {
|
|
@@ -13,13 +14,12 @@ export default class GateList extends BaseCommand {
|
|
|
13
14
|
this.log(`${g.id.padEnd(30)} ${g.level.padEnd(8)} ${art.padEnd(7)} ${g.title}`);
|
|
14
15
|
}
|
|
15
16
|
const root = findProjectRoot();
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
: listPresets();
|
|
17
|
+
const resolved = root ? resolveActivePresets(readConfig(root)) : undefined;
|
|
18
|
+
const activeIds = new Set(resolved?.ids ?? []);
|
|
19
|
+
const presets = resolved && resolved.presets.length ? resolved.presets : listPresets();
|
|
20
20
|
for (const p of presets) {
|
|
21
21
|
this.log("");
|
|
22
|
-
this.log(`=== Preset: ${p.id} v${p.version}${
|
|
22
|
+
this.log(`=== Preset: ${p.id} v${p.version}${activeIds.has(p.id) ? " (active)" : ""} ===`);
|
|
23
23
|
this.log("");
|
|
24
24
|
this.log("ID LEVEL ARTICLE TITLE");
|
|
25
25
|
this.log("────────────────────────────── ──────── ─────── ─────────────────────────────");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from "@oclif/core";
|
|
2
2
|
import { BaseCommand } from "../../base-command.js";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
-
import { appendAuditEvent, gateById,
|
|
4
|
+
import { appendAuditEvent, gateById, } from "../../_core/dist/index.js";
|
|
5
5
|
import { auditableDir, currentActor, readConfig, requireProjectRoot, } from "../../shared/project.js";
|
|
6
|
+
import { resolveActivePresets } from "../../shared/presets.js";
|
|
6
7
|
import { runAfterHook, runBeforeHook } from "../../shared/hooks.js";
|
|
7
8
|
export default class GateSkip extends BaseCommand {
|
|
8
9
|
static description = "Record a justified skip of a non-critical gate (emits gate.skipped). " +
|
|
@@ -32,8 +33,7 @@ export default class GateSkip extends BaseCommand {
|
|
|
32
33
|
const dir = auditableDir(root, args.feature);
|
|
33
34
|
const featureName = basename(dir);
|
|
34
35
|
const config = readConfig(root);
|
|
35
|
-
const
|
|
36
|
-
const presetGates = preset?.gates ?? [];
|
|
36
|
+
const { gates: presetGates } = resolveActivePresets(config);
|
|
37
37
|
const gate = gateById(args.gateId) ?? presetGates.find((g) => g.id === args.gateId);
|
|
38
38
|
if (!gate) {
|
|
39
39
|
this.error(`unknown gate: ${args.gateId}. Run 'raptor gate list' to see valid ids.`);
|
|
@@ -36,7 +36,7 @@ export default class JiraPull extends BaseCommand {
|
|
|
36
36
|
this.log(JSON.stringify(issue, null, 2));
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
const ctx = mapIssueToSpecContext(issue);
|
|
39
|
+
const ctx = mapIssueToSpecContext(issue, { fieldBuckets: conn.customFields });
|
|
40
40
|
this.log(`=== ${issue.key} — ${issue.summary} ===\n`);
|
|
41
41
|
if (issue.issueType)
|
|
42
42
|
this.log(` Type: ${issue.issueType}`);
|
|
@@ -56,5 +56,10 @@ export default class JiraPull extends BaseCommand {
|
|
|
56
56
|
for (const ac of ctx.jiraAcceptance)
|
|
57
57
|
this.log(` - ${ac}`);
|
|
58
58
|
}
|
|
59
|
+
for (const s of ctx.jiraCustomSections) {
|
|
60
|
+
this.log(`\n ${s.title} [${s.bucket}]:`);
|
|
61
|
+
for (const line of s.body.split("\n"))
|
|
62
|
+
this.log(` ${line}`);
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
package/dist/commands/new.js
CHANGED
|
@@ -138,7 +138,7 @@ export default class New extends BaseCommand {
|
|
|
138
138
|
? [
|
|
139
139
|
flags.desc,
|
|
140
140
|
jiraContext?.jiraSummary,
|
|
141
|
-
jiraContext?.
|
|
141
|
+
jiraContext?.jiraTicketBody,
|
|
142
142
|
]
|
|
143
143
|
.filter(Boolean)
|
|
144
144
|
.join("\n")
|
|
@@ -447,7 +447,12 @@ export default class New extends BaseCommand {
|
|
|
447
447
|
}
|
|
448
448
|
try {
|
|
449
449
|
const issue = await client.getJiraIssue(conn.cloudId, key);
|
|
450
|
-
return {
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
context: mapIssueToSpecContext(issue, {
|
|
453
|
+
fieldBuckets: conn.customFields,
|
|
454
|
+
}),
|
|
455
|
+
};
|
|
451
456
|
}
|
|
452
457
|
catch (err) {
|
|
453
458
|
return {
|
package/dist/commands/plan.js
CHANGED
|
@@ -3,8 +3,9 @@ import { BaseCommand } from "../base-command.js";
|
|
|
3
3
|
import { step, heading } from "../shared/ui.js";
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
|
-
import { appendAuditEvent, buildCanonicalPrompt, coreHash, extractCoreBlock,
|
|
6
|
+
import { appendAuditEvent, buildCanonicalPrompt, coreHash, extractCoreBlock, hashString, loadAgentsConfig, parseSpec, renderBundled, renderPresetGatesForPlan, selectAgent, } from "../_core/dist/index.js";
|
|
7
7
|
import { currentActor, featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
|
|
8
|
+
import { resolveActivePresets } from "../shared/presets.js";
|
|
8
9
|
import { writeFeaturePrompt } from "../shared/feature-prompt.js";
|
|
9
10
|
import { runAfterHook, runBeforeHook } from "../shared/hooks.js";
|
|
10
11
|
import { backupPhaseFile, waitForPhaseArtifact, } from "../shared/artifact-io.js";
|
|
@@ -82,7 +83,7 @@ export default class Plan extends BaseCommand {
|
|
|
82
83
|
}
|
|
83
84
|
const constitutionHash = extracted.declaredHash;
|
|
84
85
|
const config = readConfig(root);
|
|
85
|
-
const
|
|
86
|
+
const { stacked: preset } = resolveActivePresets(config);
|
|
86
87
|
const presetGatesBlock = preset
|
|
87
88
|
? renderPresetGatesForPlan(preset)
|
|
88
89
|
: "*(none)*";
|
package/dist/commands/status.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { computeAcceptanceCoverage, discoverChecklists, hasUnresolvedClarifications, parseChecklistItems, parseImplLog, parsePlan, parseSpec, parseTasks, parseTaskList, summarizeChecklist, } from "../_core/dist/index.js";
|
|
6
6
|
import { parseFrontmatter } from "../_core/dist/index.js";
|
|
7
|
-
import { featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
|
|
7
|
+
import { activePresetIds, featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
|
|
8
8
|
export default class Status extends BaseCommand {
|
|
9
9
|
static description = "Show project or feature status dashboard";
|
|
10
10
|
static examples = [
|
|
@@ -31,7 +31,8 @@ export default class Status extends BaseCommand {
|
|
|
31
31
|
const config = readConfig(root);
|
|
32
32
|
this.log("=== Raptor Project Status ===\n");
|
|
33
33
|
this.log(` Project: ${config.project?.name ?? basename(root)}`);
|
|
34
|
-
|
|
34
|
+
const presetIds = activePresetIds(config);
|
|
35
|
+
this.log(` Preset: ${presetIds.length ? presetIds.join(", ") : "(none)"}`);
|
|
35
36
|
const specsDir = join(root, ".raptor", "specs");
|
|
36
37
|
const features = existsSync(specsDir)
|
|
37
38
|
? readdirSync(specsDir).filter((d) => /^\d{3}-/.test(d))
|
|
@@ -137,8 +138,7 @@ export default class Status extends BaseCommand {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
const config = readConfig(root);
|
|
140
|
-
|
|
141
|
-
if (preset.includes("mobile")) {
|
|
141
|
+
if (activePresetIds(config).some((id) => id.includes("mobile"))) {
|
|
142
142
|
await this.showM7Dashboard(dir, featureName);
|
|
143
143
|
}
|
|
144
144
|
}
|
package/dist/commands/verify.js
CHANGED
|
@@ -2,9 +2,10 @@ import { Args, Flags } from "@oclif/core";
|
|
|
2
2
|
import { BaseCommand } from "../base-command.js";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
|
-
import { BUILTIN_GATES, formatReport, gateById,
|
|
5
|
+
import { BUILTIN_GATES, formatReport, gateById, PROJECT_GATES, readAdrOverrides, runGates, } from "../_core/dist/index.js";
|
|
6
6
|
const PROJECT_GATE_IDS = new Set(PROJECT_GATES.map((g) => g.id));
|
|
7
7
|
import { featureDir, readConfig, requireProjectRoot, } from "../shared/project.js";
|
|
8
|
+
import { resolveActivePresets } from "../shared/presets.js";
|
|
8
9
|
export default class Verify extends BaseCommand {
|
|
9
10
|
static description = "Run built-in gates against the project or a specific feature (C1–C5 + required checks)";
|
|
10
11
|
static examples = [
|
|
@@ -30,11 +31,10 @@ export default class Verify extends BaseCommand {
|
|
|
30
31
|
const { args, flags } = await this.parse(Verify);
|
|
31
32
|
const root = requireProjectRoot();
|
|
32
33
|
const config = readConfig(root);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
this.warn(`raptor.yml declares preset "${
|
|
34
|
+
const { gates: presetGates, unknownIds: unknownPresetIds, ids: presetIds, } = resolveActivePresets(config);
|
|
35
|
+
for (const id of unknownPresetIds) {
|
|
36
|
+
this.warn(`raptor.yml declares preset "${id}" but it is not registered — its gates will not run.`);
|
|
36
37
|
}
|
|
37
|
-
const presetGates = preset?.gates ?? [];
|
|
38
38
|
const skipSet = new Set((flags.skip ?? "")
|
|
39
39
|
.split(",")
|
|
40
40
|
.map((s) => s.trim())
|
|
@@ -95,7 +95,7 @@ export default class Verify extends BaseCommand {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
const report = await runGates(gatesToRun, { projectRoot: root, featureDir: dir }, { skip: skipSet, skipJustifications, overrides });
|
|
98
|
-
this.log(`=== Gates for feature ${featureName}${
|
|
98
|
+
this.log(`=== Gates for feature ${featureName}${presetIds.length ? ` (presets: ${presetIds.join(", ")})` : ""} ===`);
|
|
99
99
|
this.log(formatReport(report));
|
|
100
100
|
if (report.blocked)
|
|
101
101
|
this.exit(1);
|
package/dist/shared/jira.js
CHANGED
|
@@ -12,6 +12,7 @@ export function jiraConn(config) {
|
|
|
12
12
|
...(j.project_key ? { projectKey: j.project_key } : {}),
|
|
13
13
|
statusSync: j.status_sync !== false,
|
|
14
14
|
transitions: j.transitions ?? {},
|
|
15
|
+
customFields: j.custom_fields ?? {},
|
|
15
16
|
};
|
|
16
17
|
if (provider === "mcp") {
|
|
17
18
|
if (!j.mcp?.command)
|
|
@@ -112,6 +113,9 @@ function renderRefresh(issue) {
|
|
|
112
113
|
for (const ac of issue.acceptanceCriteria)
|
|
113
114
|
lines.push(`- ${ac}`);
|
|
114
115
|
}
|
|
116
|
+
for (const cf of issue.customFields ?? []) {
|
|
117
|
+
lines.push("", `## ${cf.name}`, "", cf.value);
|
|
118
|
+
}
|
|
115
119
|
if (issue.comments.length) {
|
|
116
120
|
lines.push("", "## Recent comments");
|
|
117
121
|
for (const c of issue.comments.slice(-5)) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getPreset, stackPresets } from "../_core/dist/index.js";
|
|
2
|
+
import { activePresetIds } from "./project.js";
|
|
3
|
+
export function resolveActivePresets(cfg) {
|
|
4
|
+
const ids = activePresetIds(cfg);
|
|
5
|
+
const presets = [];
|
|
6
|
+
const unknownIds = [];
|
|
7
|
+
for (const id of ids) {
|
|
8
|
+
const p = getPreset(id);
|
|
9
|
+
if (p)
|
|
10
|
+
presets.push(p);
|
|
11
|
+
else
|
|
12
|
+
unknownIds.push(id);
|
|
13
|
+
}
|
|
14
|
+
const stacked = presets.length ? stackPresets(presets).preset : undefined;
|
|
15
|
+
return { ids, presets, unknownIds, stacked, gates: stacked?.gates ?? [] };
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raptor-aios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Raptor — Spec-Driven Development (SDD) CLI for modern mobile apps. Constitutional gates, audit trail, real verification (a11y/perf/stores/OS matrix), and AI-agent slash commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/prepare-npm.mjs
CHANGED
|
@@ -35,13 +35,16 @@ Rules:
|
|
|
35
35
|
<!-- Format: - **T###** [US#] [AC-#] [P?] — <imperative> in <path/to/file.ext> -->
|
|
36
36
|
|
|
37
37
|
### Phase: Setup
|
|
38
|
+
|
|
38
39
|
- **T001** [meta] — Initialize feature module structure in src/<slug>/
|
|
39
40
|
|
|
40
41
|
### Phase: MVP — US1 (P1)
|
|
42
|
+
|
|
41
43
|
- **T002** [US1] [AC-1] — <first MVP task> in src/<slug>/<file>.ts
|
|
42
|
-
- **T003** [US1] [AC-1] [P] — <add the test> in
|
|
44
|
+
- **T003** [US1] [AC-1] [P] — <add the test> in **tests**/<slug>/<file>.test.ts
|
|
43
45
|
|
|
44
46
|
### Phase: US2 (P2)
|
|
47
|
+
|
|
45
48
|
- **T004** [US2] [AC-2] — <task> in src/<slug>/<file>.ts
|
|
46
49
|
|
|
47
50
|
## 3. Dependency Graph
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Quality Checklist — {{featureName}}
|
|
2
|
-
|
|
3
|
-
> Generated by `/rpt.checklist`. Source: spec.md + plan.md (+ tasks.md if present).
|
|
4
|
-
> Spec Kit ref: E09 — quality gate before `/rpt.implement` proceeds.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Metadata
|
|
9
|
-
|
|
10
|
-
- **Feature**: `{{featureDirectory}}`
|
|
11
|
-
- **Generated**: `{{generatedAt}}`
|
|
12
|
-
- **Spec status**: {{specStatus}}
|
|
13
|
-
- **Plan status**: {{planStatus}}
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Code Quality
|
|
18
|
-
|
|
19
|
-
- [ ] All files follow project coding standards (lint clean)
|
|
20
|
-
- [ ] No `TODO` / `FIXME` markers remain in production code
|
|
21
|
-
- [ ] Error handling covers both expected and unexpected failures
|
|
22
|
-
- [ ] Logging follows project conventions (no console.log in prod paths)
|
|
23
|
-
- [ ] Public APIs have type signatures
|
|
24
|
-
- [ ] No dead code or unused imports
|
|
25
|
-
|
|
26
|
-
## Testing
|
|
27
|
-
|
|
28
|
-
- [ ] Unit tests cover business logic (target ≥ 80% coverage)
|
|
29
|
-
- [ ] Integration tests for external dependencies
|
|
30
|
-
- [ ] Edge cases documented and tested
|
|
31
|
-
- [ ] Test fixtures are realistic (no `foo`/`bar` placeholders)
|
|
32
|
-
- [ ] Tests are deterministic (no flaky timing/ordering)
|
|
33
|
-
- [ ] Tests run independently (no shared mutable state)
|
|
34
|
-
|
|
35
|
-
## Security
|
|
36
|
-
|
|
37
|
-
- [ ] Input validation on all endpoints / boundaries
|
|
38
|
-
- [ ] Authentication and authorization verified
|
|
39
|
-
- [ ] No secrets in code, fixtures, or configuration files
|
|
40
|
-
- [ ] OWASP Top 10 reviewed (injection, XSS, auth, etc.)
|
|
41
|
-
- [ ] Sensitive data handling (PII, tokens) audited
|
|
42
|
-
- [ ] Dependency vulnerabilities checked
|
|
43
|
-
|
|
44
|
-
## Performance
|
|
45
|
-
|
|
46
|
-
- [ ] No N+1 query patterns
|
|
47
|
-
- [ ] Pagination on list endpoints
|
|
48
|
-
- [ ] Caching strategy documented (where applicable)
|
|
49
|
-
- [ ] Load characteristics measured for critical paths
|
|
50
|
-
- [ ] Memory / connection pools sized appropriately
|
|
51
|
-
|
|
52
|
-
## Documentation
|
|
53
|
-
|
|
54
|
-
- [ ] API documentation reflects new contracts
|
|
55
|
-
- [ ] README updated when surface changes
|
|
56
|
-
- [ ] Architecture decisions recorded (in `plan.md` Decisions section)
|
|
57
|
-
- [ ] Operational runbook (if applicable)
|
|
58
|
-
- [ ] Migration / rollback procedure documented
|
|
59
|
-
|
|
60
|
-
## Accessibility (UI features)
|
|
61
|
-
|
|
62
|
-
- [ ] Screen-reader compatible
|
|
63
|
-
- [ ] Keyboard navigation supported (no mouse-only flows)
|
|
64
|
-
- [ ] Color contrast meets WCAG AA
|
|
65
|
-
- [ ] Touch targets ≥ 44px on mobile
|
|
66
|
-
- [ ] Localizable strings (no hardcoded user-facing text)
|
|
67
|
-
|
|
68
|
-
## Mobile (when applicable)
|
|
69
|
-
|
|
70
|
-
- [ ] Tested on iOS minimum supported version
|
|
71
|
-
- [ ] Tested on Android minimum supported version
|
|
72
|
-
- [ ] Offline-friendly (graceful degradation)
|
|
73
|
-
- [ ] Battery / data usage acceptable
|
|
74
|
-
- [ ] Push / notification permissions handled correctly
|
|
75
|
-
|
|
76
|
-
## Constitution
|
|
77
|
-
|
|
78
|
-
- [ ] No principle violated without explicit, documented mitigation
|
|
79
|
-
- [ ] Test-First respected (where mandated)
|
|
80
|
-
- [ ] Library-First respected (no in-house reimplementation of common libs)
|
|
81
|
-
|
|
82
|
-
## Observability
|
|
83
|
-
|
|
84
|
-
- [ ] Critical paths emit logs at appropriate level
|
|
85
|
-
- [ ] Key metrics exported (latency, errors, throughput)
|
|
86
|
-
- [ ] Distributed tracing in place (where applicable)
|
|
87
|
-
- [ ] Alerts wired for SLO violations
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Sign-Off
|
|
92
|
-
|
|
93
|
-
- [ ] All boxes above checked OR justified inline with `<!-- justification: ... -->`
|
|
94
|
-
- [ ] Reviewer: `{{reviewerEmail}}`
|
|
95
|
-
- [ ] Date: `{{signOffDate}}`
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Rules
|
|
100
|
-
|
|
101
|
-
1. **No partial sign-off**: every box must be ticked OR justified.
|
|
102
|
-
2. **Justifications are auditable**: inline justifications must reference a decision in `plan.md` or an issue number.
|
|
103
|
-
3. **Re-runnable**: `/rpt.checklist` regenerates this file when artifacts change; user-justifications are preserved between runs.
|