Pular para o conteúdo

/dev — Developer Experience

Developer Experience

Domain kernel puro. Event sourcing. MCP como única surface até v1.0.

Se você é dev e quer entender como o sistema é desenhado antes de cogitar contribuir ou self-host: esta página é o ponto de entrada.

Arquitetura em uma tela

Capability vira surface, não o contrário.

┌─────────────────────────────────────────────────────────────────┐
│  SURFACES (humano + IA falam aqui)                              │
│  ┌─────────────────┐  ┌─────────────────┐  ┌────────────────┐   │
│  │ Open WebUI chat │  │   MCP server    │  │  REST Fastify  │   │
│  │  + MCP Apps     │  │  (níveis 1-3)   │  │ (nível 4 TOTP) │   │
│  └────────┬────────┘  └────────┬────────┘  └────────┬───────┘   │
│           │                    │                    │           │
└───────────┼────────────────────┼────────────────────┼───────────┘
          ▼                    ▼                    ▼
     ┌──────────────────────────────────────────────────┐
     │  APPLICATION LAYER  (@canna/app-services)        │
     │  Orquestra comandos → domain → projections → fx  │
     └────────────────────────┬─────────────────────────┘
                              ▼
     ┌──────────────────────────────────────────────────┐
     │  DOMAIN KERNEL  (@canna/domain — TS puro)        │
     │   decide() · evolve() — zero side effects        │
     │   Bounded contexts: membership · inventory ·     │
     │   dispensation · compliance · cultivation        │
     └────────────────────────┬─────────────────────────┘
                              ▼
     ┌──────────────────────────────────────────────────┐
     │  EVENT STORE  (Emmett + Postgres)                │
     │   Append-only. Source of truth. Audit por design.│
     └────────────────────────┬─────────────────────────┘
                              ▼
     ┌──────────────────────────────────────────────────┐
     │  PROJECTIONS  (Drizzle read models)              │
     │  member-quota · inventory-lot · dispensation-hist│
     └──────────────────────────────────────────────────┘

     Background: @canna/worker (BullMQ)
       → SNGPC XML, PDF render, email

Exemplo MCP call

Registrar uma dispensação = um tool call.

Request — MCP tool call

{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
  "name": "request_record_dispensation",
  "arguments": {
    "memberId": "MBR-01HXX...",
    "lotId":    "LOT-01HXY...",
    "quantityG": 30,
    "dispenserId": "USR-01HXZ...",
    "prescriptionId": "RX-01HX0..."
  }
},
"id": "call-42"
}

Response — eventos emitidos atomicamente

{
"jsonrpc": "2.0",
"result": {
  "content": [{
    "type": "resource",
    "resource": {
      "uri": "canna://dispensation/DSP-01HY0",
      "mimeType": "application/json",
      "text": "{\n  \"events\": [\n    \"DispensationRecorded\",\n    \"LotQuantityDeducted\",\n    \"MemberQuotaConsumed\"\n  ]\n}"
    }
  }],
  "structuredContent": {
    "dispensationId": "DSP-01HY0...",
    "remainingQuotaG": 0,
    "lotRemainingG": 1450
  }
},
"id": "call-42"
}

O que aconteceu por baixo: Domain kernel valida invariantes (quota suficiente? lote liberado? prescrição vigente?), produz 3 eventos atômicos (DispensationRecorded + LotQuantityDeducted + MemberQuotaConsumed), Emmett persiste no Postgres event store, projections atualizam read models (quota restante, histórico). Se algo falhar, nada é gravado — atomicidade event-sourced. Mesma capability é alcançável via REST /api/dispensations quando o caller é máquina não-conversacional.

Eventos canonical (do código)

20 eventos no domain kernel — fonte: packages/domain

Lista extraída automaticamente da @canna/domain. Cada evento é imutável, append-only, e participa do audit trail seed-to-dispensação.

Membership

  • MemberRegistered
  • ConsentGranted
  • ConsentRevoked
  • PrescriptionValidated
  • QuotaUpdated
  • MemberQuotaConsumed
  • QuotaExceededAttempt
  • MemberSuspended
  • MemberReinstated
  • MemberAnonymized

Inventory (lote)

  • LotCreated
  • LotQuarantined
  • LotReleased
  • LotQuantityDeducted
  • LotQuantityDeductedByDispensation
  • LotRecalled
  • LotExhausted
  • LotInsufficientQuantity
  • LotInsufficientForDispensation

Dispensation

  • DispensationRecorded

Dispensation triggers compound transaction: append DispensationRecorded + LotQuantityDeducted + MemberQuotaConsumed atomicamente. Falha de qualquer invariant → 0 eventos persistidos.

Próximos bounded contexts (v0.5+): Cultivation (PlantRegistered, StageAdvanced, HarvestRecorded), Processing (BatchProcessed, COASubmitted, COAApproved), Compliance (SngpcXmlSubmitted, BspoSigned, RipdGenerated). Veja /build/domain para o modelo completo.

Setup local · 60s

Rode tudo em pnpm dev.

# Pré-requisitos: Node 22+, pnpm, Docker (para Postgres testcontainers)
git clone https://github.com/biliboss/obsidian.git
cd obsidian/99-development/canna-oss

pnpm install
pnpm verify          # typecheck + tests (154 specs) across all workspaces
pnpm test:domain     # @canna/domain vitest scenario coverage
pnpm dev             # docs site em http://localhost:4335

# MCP server stdio
pnpm --filter @canna/mcp dev

# Open WebUI sidecar end-to-end (Docker Compose)
cd ops/openwebui && docker compose up -d