simpla.fydocs
Admin API

PM Board

Hierarquia themes, modules, capabilities, epics e stories do PM Board do Simplafy Admin — modelo de dados, status lifecycle, tags e filtros.

PM Board

O PM Board é a estrutura de planejamento e execução do Simplafy Admin. Ele organiza o trabalho de produto em uma hierarquia tipada — de pilares estratégicos até stories individuais — e expõe essa hierarquia via API REST sob /api/pm/* e via tools MCP pm_* para agentes de coding.

Esta página descreve o modelo de dados, como os recursos se relacionam e quais campos governam ordenação, filtros e o lifecycle de cada nível.

A base do PM Board é o schema Prisma em apps/web/prisma/schema.prisma. As rotas REST vivem em apps/web/src/app/api/pm/ e replicam, com autorização, o mesmo modelo exposto pelo MCP server.

Modelo de hierarquia

O PM Board organiza trabalho em seis níveis encadeados, cada um com sua entidade Prisma:

Pilar
 └── Module        (slug único, agrupa themes)
      └── Theme         (num + position por module)
           └── Capability    (status active|cancelled)
                └── Epic          (status active|cancelled)
                     └── Story         (status lifecycle completo)

Cada nível tem um id cuid(), timestamps createdAt e — a partir de Theme — campos num e position que controlam numeração estável e ordenação visual.

Themes e modules

Module é a fronteira principal de navegação no PM Board. Praticamente todas as queries de listagem aceitam ?module=<slug> como filtro obrigatório ou opcional.

model Module {
  id          String   @id @default(cuid())
  name        String
  slug        String   @unique
  description String?
  pilarId     String
  pilar       Pilar    @relation(fields: [pilarId], references: [id])
  themes      Theme[]
}

Theme herda escopo do Module e introduz numeração estável por meio de num:

model Theme {
  id          String   @id @default(cuid())
  name        String
  slug        String   @unique
  num         Int      @default(0)
  position    Int      @default(0)
  moduleId    String
  module      Module   @relation(fields: [moduleId], references: [id])
  capabilities Capability[]

  @@unique([moduleId, num])
  @@index([moduleId])
}

A constraint @@unique([moduleId, num]) garante numeração não duplicada por module — é a base do identificador human-friendly Admin#8.1.1 exibido na UI.

curl https://admin.simplafy.com.br/api/pm/themes?module=admin \
  -H "Cookie: $SESSION_COOKIE"

Retorna todos os themes do module, ordenados por name asc, com _count.capabilities.

curl -X POST https://admin.simplafy.com.br/api/pm/themes \
  -H "Content-Type: application/json" \
  -H "Cookie: $SESSION_COOKIE" \
  -d '{
    "name": "Observability",
    "description": "Logs, traces, metrics",
    "moduleSlug": "admin"
  }'

Requer session.user.role === 'admin'. O slug é derivado: <moduleSlug>-<name-kebab>. Registra EventLog com entityType: 'theme' e action: 'theme_created'.

Capabilities

Capability é o primeiro nível com status e tags[]. Ela vive sob um Theme mas mantém referência redundante a moduleId para acelerar filtros cross-theme.

model Capability {
  id          String          @id @default(cuid())
  name        String
  slug        String          @unique
  description String?
  status      HierarchyStatus @default(active)
  num         Int             @default(0)
  position    Int             @default(0)
  tags        String[]        @default([])
  themeId     String
  moduleId    String
  theme       Theme           @relation(fields: [themeId], references: [id])
  epics       Epic[]

  @@index([themeId])
}

A listagem em GET /api/pm/capabilities exige ?module=<slug> e retorna _count.epics por capability, ordenada por name asc.

status aceita apenas active ou cancelled (enum HierarchyStatus). Não há estado intermediário no nível capability — granularidade fina de execução vive em Story.

curl -X POST https://admin.simplafy.com.br/api/pm/capabilities \
  -H "Content-Type: application/json" \
  -H "Cookie: $SESSION_COOKIE" \
  -d '{
    "name": "Tracing distribuído",
    "moduleSlug": "admin",
    "themeId": "clx..."
  }'

A rota valida que themeId pertence ao module informado antes de criar a capability, evitando vínculos cross-module inválidos.

Epics e Stories

Epic é o pai direto das stories e replica o padrão status + num + position + tags[] de capability:

model Epic {
  id           String          @id @default(cuid())
  title        String
  description  String?
  status       HierarchyStatus @default(active)
  num          Int             @default(0)
  position     Int             @default(0)
  tags         String[]        @default([])
  capabilityId String
  capability   Capability      @relation(fields: [capabilityId], references: [id])
  stories      Story[]
}

Story é a unidade executável e o nível mais rico do modelo:

model Story {
  id              String       @id @default(cuid())
  title           String
  description     String?
  userSummary     String?
  status          StoryStatus  @default(backlog)
  priority        Int          @default(0)
  assigneeId      String?
  blocked         Boolean      @default(false)
  blockedReason   String?
  cancelledReason String?      @db.Text
  tags            String[]     @default([])
  num             Int          @default(0)
  position        Int          @default(0)
  metadata        Json?
  kb_refs         String[]     @default([])
  epicId          String
  epic            Epic         @relation(fields: [epicId], references: [id])
  comments        StoryComment[]
  attachments     StoryAttachment[]

  @@index([epicId])
  @@index([status])
  @@index([kb_refs])
}

Campos a destacar:

  • userSummary: descrição curta orientada ao usuário final, separada da description técnica.
  • priority: inteiro de 0 a 3 (validado no zod schema do POST).
  • blocked / blockedReason: flag operacional independente de status.
  • cancelledReason: texto longo preenchido quando uma story transiciona para cancelled.
  • kb_refs[]: slugs de Reference da Knowledge Base que a story implementa ou documenta. Validado contra o vocabulário de refs na escrita.
  • metadata: payload JSON livre para integrações (ex.: webhook GitHub).

Listagem com contexto

GET /api/pm/stories aceita module, status e tags/tags[] como filtros e retorna cada story já com epicTitle, capabilityName, capabilityId e moduleId resolvidos, além do último EventLog (lastActorType, lastActorId, lastReason, lastEventAt).

curl "https://admin.simplafy.com.br/api/pm/stories?module=admin&status=in_progress&tags[]=infra" \
  -H "Cookie: $SESSION_COOKIE"

A ordenação é estável: status asc, depois priority desc, depois createdAt asc.

Criação

curl -X POST https://admin.simplafy.com.br/api/pm/stories \
  -H "Content-Type: application/json" \
  -H "Cookie: $SESSION_COOKIE" \
  -d '{
    "title": "Adicionar tracing ao webhook GitHub",
    "epicId": "clx...",
    "priority": 2,
    "tags": ["observability", "github"]
  }'

O num da nova story é calculado em transação como max(num) + 1 dentro do epic, garantindo numeração contígua sem race conditions.

Status lifecycle

O PM Board usa dois enums de status distintos:

HierarchyStatus (capability e epic):

active
cancelled

StoryStatus (story):

backlog
ready_for_dev
in_progress
review
done
cancelled

Transições aceitas em POST /api/pm/stories e nas mutations subsequentes seguem o lifecycle natural backlog → ready_for_dev → in_progress → review → done, com cancelled como estado terminal alternativo (e cancelledReason obrigatório no fluxo de cancelamento).

A flag blocked é ortogonal ao status. Uma story pode estar in_progress e blocked: true simultaneamente, com blockedReason descrevendo o motivo. Isso preserva o status real do trabalho enquanto sinaliza impedimento.

Toda mutação relevante grava um registro em EventLog:

{
  entityType: 'story' | 'epic' | 'capability' | 'theme',
  entityId: string,
  action: string,           // ex.: 'story_created', 'status_changed'
  actorType: 'agent' | 'human',
  actorId: string,
  reason: string | null,
  payload: Json | null,
  createdAt: DateTime
}

Esse log é consultado pela listagem de stories para popular lastActorType / lastActorId / lastReason, permitindo distinguir alterações feitas por humanos via UI de alterações feitas por agentes via MCP.

Tags e filtros

tags[] é um campo String[] presente em Capability, Epic e Story. Não há tabela separada de tags — o vocabulário é livre, mas a UI consolida valores distintos via GET /api/pm/tags.

curl "https://admin.simplafy.com.br/api/pm/tags?module=admin" \
  -H "Cookie: $SESSION_COOKIE"

A rota retorna o conjunto distinto e ordenado de tags em uso por todas as stories do module:

{
  "success": true,
  "data": ["github", "infra", "observability", "ux"]
}

Omitir ?module= retorna o vocabulário global de tags entre todos os modules.

Filtros combinados em stories

GET /api/pm/stories aceita tags[] repetido para filtrar por interseção (hasEvery):

curl "https://admin.simplafy.com.br/api/pm/stories?module=admin&tags[]=infra&tags[]=observability" \
  -H "Cookie: $SESSION_COOKIE"

Esse exemplo retorna apenas stories do module admin que tenham tanto infra quanto observability em tags[]. Quando tags é fornecido sem module, a busca é cross-module — útil para encontrar trabalho relacionado entre verticais.

Stories indexam tags no campo Postgres array, mas não há índice GIN dedicado em tags no schema atual. Para filtros muito grandes, prefira combinar module + status antes de aplicar tags.

Próximos passos

On this page