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.
Pilar
Agrupador estratégico. Contém um ou mais modules. Identificado por slug único.
Module
Unidade de produto (ex.: admin, hub). Slug único globalmente, usado como filtro em quase todas as queries.
Capability
Conjunto de epics relacionados dentro de um theme. Possui tags[] e status active|cancelled.
Story
Unidade executável. Tem status lifecycle completo, prioridade, assignee, comments, attachments e kb_refs.
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 dadescriptionté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 paracancelled.kb_refs[]: slugs deReferenceda 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
cancelledStoryStatus (story):
backlog
ready_for_dev
in_progress
review
done
cancelledTransiçõ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
Catálogo de tools MCP
Visão geral das tools MCP expostas pelo simplafy-admin, organizadas por categoria — PM, GitHub, secrets, infraestrutura, ops, observabilidade e analytics.
KB References
Base de conhecimento cross-projeto do Simplafy Admin com módulos, criticality, semantic search via pgvector e ciclo de verificação periódica.