raptor-aios 0.8.0 → 0.8.3

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 CHANGED
@@ -3,6 +3,45 @@
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.3] - 2026-06-10
7
+
8
+ ### Documentation
9
+
10
+ - **Glossário Canônico (`docs/glossary.md`) reescrito e certificado contra o código.** Passou de ~16 termos para ~60 conceitos cobrindo todo o ecossistema (Gate, GateLevel, ADR, Override, Canonical, Blueprint, Priority Stack, Preset, Extension, Hook, Skill, MCP, Harness, SDD, Stage, Workflow, Traceability/Coverage/Trace, Staleness, Readiness, M1–M6/M7/M8, C1–C5, D#, artefatos e memória), cada um no modelo Nome/Objetivo/Definição/Responsabilidades/Quando usar/Quando não usar/Relação/Prioridade/Exemplos/Anti-padrões. Inclui apêndices com as hierarquias de governança/decisão/documentos/execução/contexto, a distinção C/D/M, o catálogo dos 42 gates (nível + artigo) e o catálogo de comandos CLI. Cada entrada cita `arquivo:linha`.
11
+ - **README atualizado para refletir o código atual.** Nova seção "Documentação & glossário" (com link em destaque para o glossário, tabela dos docs e diagrama mermaid da hierarquia de governança) e entrada no índice. Correções confrontadas com `packages/core`/`packages/cli`: contagem de testes (1019 = 784 core + 201 cli + 34 e2e), workflows bundled (`sdd-cycle`, `sdd-cycle-quick`), hooks (34 pontos = before/after de 17 comandos), tabelas "Capacidades" e "Referência CLI" com markdown corrigido, badge de versão, e menção ao preset `lean`.
12
+
13
+ ## [0.8.2] - 2026-06-10
14
+
15
+ ### Fixed
16
+
17
+ - **`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.
18
+ - **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.
19
+
20
+ ### Changed
21
+
22
+ - **`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.
23
+
24
+ ### Removed
25
+
26
+ - **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).
27
+
28
+ ### Internal
29
+
30
+ - 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).
31
+
32
+ ## [0.8.1] - 2026-06-09
33
+
34
+ ### Fixed
35
+
36
+ - **`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:
37
+ - **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.
38
+ - **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.
39
+ - **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"*.
40
+
41
+ ### Internal
42
+
43
+ - 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.
44
+
6
45
  ## [0.8.0] - 2026-06-09
7
46
 
8
47
  ### Added
package/README.md CHANGED
@@ -7,7 +7,8 @@
7
7
  [![npm](https://img.shields.io/npm/v/raptor-aios?color=ef4444&label=raptor-aios&logo=npm)](https://www.npmjs.com/package/raptor-aios)
8
8
  [![node](https://img.shields.io/badge/node-%E2%89%A5%2020-3c873a?logo=node.js&logoColor=white)](https://nodejs.org)
9
9
  [![platform](https://img.shields.io/badge/target-React%20Native-61dafb?logo=react&logoColor=black)](https://reactnative.dev)
10
- [![tests](https://img.shields.io/badge/tests-881%20passing-22c55e)](#-desenvolvimento-local)
10
+ [![tests](https://img.shields.io/badge/tests-1019%20passing-22c55e)](#️-desenvolvimento-local)
11
+ [![version](https://img.shields.io/badge/version-0.8.3-ef4444)](CHANGELOG.md)
11
12
  [![license](https://img.shields.io/badge/license-MIT-blue)](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 | Comandos | | | |
455
- | ----------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------- | ------- | ----------- |
456
- | 🎚️ **Presets** | Conjuntos reusáveis de artigos + gates, empilháveis. Bundled: `mobile-opinionated`. | `raptor preset list\ | add\ | info\ | remove` |
457
- | 🔁 **Workflows** | Orquestram o ciclo via YAML, com gates de revisão humana. Bundled: `sdd-cycle`, `quick-fix`. | `raptor workflow catalog\ | run\ | status\ | resume` |
458
- | 🤖 **Agentes** | Materializa os slash commands por agente. | `raptor add-agent` · `raptor list-agents` | | | |
459
- | 🧠 **Skills** | Habilidades reusáveis materializadas para os agentes. | `raptor skill add\ | list\ | sync\ | remove` |
460
- | 🔌 **MCP** | Registra servidores MCP e materializa nas configs nativas dos agentes. | `raptor mcp add\ | list\ | sync\ | remove` |
461
- | 🪝 **Hooks** | Scripts disparados em ~20 pontos do ciclo (`before_*`/`after_*`). | `raptor hook list\ | run` | | |
462
- | 📦 **Extensões** | Pacotes self-contained (templates/gates/hooks/workflows). | `raptor extension add\ | list\ | info\ | remove` |
463
- | 🎟️ **Jira** | Puxa issues do Jira (somente leitura) para semear specs. | `raptor jira connect\ | status\ | pull\ | disconnect` |
464
- | 📜 **Auditoria & trace** | Consulta a trilha de eventos e a rastreabilidade. | `raptor audit query\ | show` · `raptor trace` | | |
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
 
@@ -478,15 +481,15 @@ raptor new login --jira APP-1234 # semeia a spec a partir da issue
478
481
  ## 📖 Referência de comandos CLI
479
482
 
480
483
  ```text
481
- 🔄 Ciclo init · new · clarify · plan · tasks · analyze · checklist · implement · approve · taskstoissues
484
+ 🔄 Ciclo init · new · clarify · plan · tasks · analyze · checklist · implement · approve · taskstoissues
482
485
  🌿 Branch/commit commit · scan
483
- 🔬 Verificação verify · verify a11y · verify perf · verify stores · verify os-matrix
484
- verify architecture · verify audit · verify constitution
485
- | 📊 Status status · doctor · trace · audit query | show | |
486
- | ⚖️ Governança gate list | approve | skip · repair constitution · hotfix · resync · upgrade |
487
- 🧩 Extensão preset … · workflow … · skill … · mcp … · hook … · extension …
488
- add-agent · list-agents · add-extension
489
- 🎟️ Jira jira connect|status|pull|disconnect
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
490
493
  ```
491
494
 
492
495
  Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um.
@@ -547,6 +550,43 @@ CLAUDE.md # 📎 bloco de contexto do agente
547
550
 
548
551
  ---
549
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 &gt; 🟡 required &gt; 🔵 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
+
550
590
  ## 🛠️ Desenvolvimento local
551
591
 
552
592
  Monorepo pnpm (`packages/core` = `@raptor/core`, `packages/cli` = binário `raptor`).
@@ -554,7 +594,7 @@ Monorepo pnpm (`packages/core` = `@raptor/core`, `packages/cli` = binário `rapt
554
594
  ```bash
555
595
  pnpm install
556
596
  pnpm build
557
- pnpm -r test && pnpm test:e2e # 881 testes (674 core + 183 cli + 24 e2e)
597
+ pnpm -r test && pnpm test:e2e # 1019 testes (784 core + 201 cli + 34 e2e)
558
598
 
559
599
  # usar a CLI local em outro projeto:
560
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
- return result(this, "pass", `${checklists.length} checklist(s) have valid frontmatter.`);
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
- return result(this, "pass", `All ${checklists.length} checklist(s) have items.`);
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
+ ];
@@ -82,3 +82,10 @@ export const DIALECTS = {
82
82
  export function resolveDialect(name) {
83
83
  return (name && DIALECTS[name]) || ROVO_DIALECT;
84
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";
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
6
  export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, classifyField, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
@@ -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
- return found ?? candidates[0] ?? kind;
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 dialect = resolveDialect(cfg.dialect);
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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raptor/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js"
6
6
  }
@@ -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, getPreset, knownPresetIds, MANIFEST_SCHEMA_VERSION, validateManifest, validateFeatureState, validateInitOptions, VERSION, detectInstalledAgents, discoverSkills, loadSkillsConfig, validateSkillsConfig, skillTargetPath, loadMcpConfig, validateMcpConfig, isServerMaterialized, mcpTargetFor, } from "../_core/dist/index.js";
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
- if (config.preset) {
144
- const preset = getPreset(config.preset);
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, getPreset, } from "../../_core/dist/index.js";
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 preset = config.preset ? getPreset(config.preset) : undefined;
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, getPreset, listPresets } from "../../_core/dist/index.js";
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 activePresetId = root ? readConfig(root).preset : undefined;
17
- const presets = activePresetId
18
- ? [getPreset(activePresetId)].filter(Boolean)
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}${activePresetId === p.id ? " (active)" : ""} ===`);
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, getPreset, } from "../../_core/dist/index.js";
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 preset = config.preset ? getPreset(config.preset) : undefined;
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.`);
@@ -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, getPreset, hashString, loadAgentsConfig, parseSpec, renderBundled, renderPresetGatesForPlan, selectAgent, } from "../_core/dist/index.js";
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 preset = config.preset ? getPreset(config.preset) : undefined;
86
+ const { stacked: preset } = resolveActivePresets(config);
86
87
  const presetGatesBlock = preset
87
88
  ? renderPresetGatesForPlan(preset)
88
89
  : "*(none)*";
@@ -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
- this.log(` Preset: ${config.preset ?? "(none)"}`);
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
- const preset = config.preset || "";
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
  }
@@ -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, getPreset, PROJECT_GATES, readAdrOverrides, runGates, } from "../_core/dist/index.js";
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 preset = config.preset ? getPreset(config.preset) : undefined;
34
- if (config.preset && !preset) {
35
- this.warn(`raptor.yml declares preset "${config.preset}" but it is not registered — its gates will not run.`);
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}${preset ? ` (preset: ${preset.id})` : ""} ===`);
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);
@@ -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.8.0",
3
+ "version": "0.8.3",
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": {
@@ -29,7 +29,7 @@ const CLI = join(ROOT, "packages", "cli");
29
29
  const CORE = join(ROOT, "packages", "core");
30
30
  const OUT = join(ROOT, "build", "npm");
31
31
 
32
- const VERSION = "0.8.0";
32
+ const VERSION = "0.8.3";
33
33
 
34
34
  function log(msg) {
35
35
  process.stdout.write(` ${msg}\n`);
@@ -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 __tests__/<slug>/<file>.test.ts
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.