Open Challenge Protocol (OCP) | Ride Cave

Especificação

Open Challenge Protocol

Um formato JSON declarativo para descrever desafios de fitness entre plataformas.

RascunhoVersão 0.1.0

Visão geral

O Open Challenge Protocol (OCP) é um formato JSON declarativo para descrever desafios de fitness. Ele define o que conta como sucesso, não como um aplicativo host deve implementar a experiência.

Um desafio é uma meta estruturada que avalia o desempenho do ciclista com base em critérios definidos, com fases de ciclo de vida, diretivas em tempo de execução e lógica de iteração/loop opcionais.

Desafios vs treinos

O OCP foi pensado especificamente para Desafios, não para treinos. Entender essa diferença é fundamental.

Desafios

  • Dinâmico e orientado a metas: O sucesso é definido pelo cumprimento de critérios específicos
  • Baseado em lógica: Baseado em condições, limites e critérios de saída
  • Duração muitas vezes indeterminada: Muitos terminam quando você não consegue manter uma condição
  • Pass/fail ou quebra de recorde: Ou você atinge a meta, ou disputa contra seu melhor resultado pessoal

Exemplos

  • Manter 5.0 w/kg por 10 segundos
  • Sprintar 200m o mais rápido possível
  • Pedale o mais longe possível mantendo a FC abaixo da zona 4
  • Acelerar do zero até 40 km/h

Treinos

  • Estruturado e predeterminado: Seguir uma sequência fixa de intervalos
  • Baseado em tempo/duração: Os intervalos têm durações específicas
  • Conclusão = sucesso: Se você termina todos os intervalos, você "completou" o treino
  • Focado em treino: Pensado para a adaptação fisiológica

Não adequado ao OCP

  • 5x (30 segundos forte / 30 segundos leve)
  • Tabata: 8x (20 segundos / 10 segundos)
  • Intervalos em escada de 1-2-3-2-1 minutos
  • 2x20 minutos no FTP

Na dúvida: Pergunte: "O sucesso depende de como o ciclista se sai, ou só que ele complete a estrutura?" Se o sucesso depende de manter condições → Desafio. Se o sucesso é só completar os intervalos → Treino.

Estrutura do desafio

Um desafio é um objeto JSON com os seguintes campos de nível superior:

{
  "id": "string",
  "version": "0.1.0",
  "name": "string",
  "description": "string (optional)",
  "sport": "cycling | running | rowing | any (optional)",

  "mode": "record | pass-fail",

  "config": { },
  "phases": [ ],
  "criteria": { },
  "loops": [ ],
  "directives": [ ],
  "record": { },
  "completion": { },

  "metadata": { }
}

Campos obrigatórios

CampoTipoDescrição
idstringIdentificador único deste desafio
versionstringVersão do protocolo (semver)
namestringNome do desafio legível para pessoas
phasesarrayLista ordenada de fases do desafio (OU criteria para desafios simples)

Campos opcionais

CampoTipoDescrição
descriptionstringDescrição mais longa do desafio
sportstringTipo de esporte (cycling, running, rowing, any)
modestringrecord (melhor esforço) ou pass-fail (sucesso binário). Padrão: pass-fail
configobjectConfiguração geral do desafio e valores calculados
criteriaobjectCritérios de sucesso (e de falha, opcionais) para desafios simples
loopsarrayBlocos de avaliação repetidos
directivesarrayComportamentos em tempo de execução (notificações, sugestões)
recordobjectQuais métricas capturar como resultado do desafio
completionobjectComo o desafio termina e o que exibir
metadataobjectExibir dicas, dificuldade, tags etc.

Modos de desafio

pass-fail (padrão): desafio tradicional com critérios de sucesso/falha. Resultado binário.

record: desafio de melhor esforço, em que o objetivo é alcançar o melhor resultado possível. Não há estado de falha: o desafio registra o seu desempenho.

Métricas

As métricas são os dados canônicos que os desafios avaliam. Os aplicativos host mapeiam seus dados brutos para esses caminhos.

Formato do caminho da métrica

As métricas usam notação de ponto: category.metric

power.instant     → Current power in watts
power.wkg         → Current power in watts per kilogram
lap.index         → Current lap number (0-indexed or 1-indexed per host)
position.current  → Race position (1 = first place)

Métricas básicas

Tempo

CaminhoTipoDescrição
time.elapsednumberSegundos desde o início do desafio
time.sessionnumberSegundos desde o início da sessão de pedal

Potência

CaminhoTipoDescrição
power.instantnumberPotência atual (watts)
power.avgnumberPotência média da sessão
power.normalizednumberPotência normalizada
power.wkgnumberWatts por quilo atuais
power.zonenumberZona de potência atual (1-7)
power.percentFtpnumberPotência atual em % do FTP

Velocidade, cadência, frequência cardíaca

CaminhoTipoDescrição
speed.instantnumberVelocidade atual (m/s ou km/h, conforme o host)
cadence.instantnumberCadência atual (RPM)
heartRate.instantnumberFrequência cardíaca atual (BPM)
heartRate.zonenumberZona de FC atual (1-5)

Distância, volta, pista

CaminhoTipoDescrição
distance.totalnumberDistância total (metros)
lap.indexnumberNúmero da volta atual
lap.countnumberTotal de voltas completadas
track.lengthnumberComprimento da pista (metros)

Métricas de prova/multijogador

Estes só estão disponíveis em contextos de prova:

CaminhoTipoDescrição
position.currentnumberPosição atual na prova (1 = primeiro)
gap.aheadnumberDiferença para o ciclista à frente
gap.behindnumberDiferença para o ciclista atrás
overtakes.countnumberNúmero de ultrapassagens feitas

Fases

As fases definem a estrutura de um desafio como uma sequência ordenada de segmentos. Cada fase tem sua própria duração, metas, condições de saída e diretivas.

Estrutura da fase

{
  "id": "effort",
  "name": "Max Effort",
  "duration": 30,
  "description": "Give everything for 30 seconds",
  "targets": { },
  "exitConditions": { },
  "directives": [ ]
}
CampoTipoObrigatórioDescrição
idstringSimIdentificador único desta fase
namestringSimNome de exibição
durationnumber/stringNãoDuração da fase em segundos, ou "unlimited"
distancenumberNãoDistância da fase em metros (alternativa à duração)
targetsobjectNãoMétricas-alvo desta fase
exitConditionsobjectNãoCondições que encerram esta fase antes da hora

Metas

As metas definem as métricas esperadas ou exigidas durante uma fase:

{
  "targets": {
    "power": { "min": 80, "max": 150 },
    "cadence": { "min": 90, "max": 100 }
  }
}

Metas em rampa

{
  "power": {
    "mode": "ramp",
    "start": 100,
    "increment": 1,
    "per": "second"
  }
}

Condições

Uma condição é uma única expressão testável que resulta em verdadeiro ou falso.

Estrutura da condição

{
  "metric": "power.wkg",
  "operator": ">=",
  "value": 5.0,
  "duration": 10,
  "window": "sustained"
}
CampoTipoObrigatórioDescrição
metricstringSimCaminho da métrica a avaliar
operatorstringSimOperador de comparação
valuenumber/arraySimValor(es)-alvo
durationnumberNãoRestrição de tempo em segundos
windowstringNãoComo a duração é avaliada

Operadores

Comparação

  • == Igual a
  • != Diferente de
  • > Maior que
  • < Menor que
  • >= Maior ou igual a
  • <= Menor ou igual a

Faixa

  • between Intervalo inclusivo
  • outside Fora do intervalo

Alterar

  • increasedBy Aumento delta
  • decreasedBy Redução delta

Janelas de duração

JanelaDescrição
sustainedA condição precisa ser verdadeira continuamente durante a duração (padrão)
cumulativeA condição precisa ser verdadeira pelo tempo total (pode ser descontínuo)
rollingAvaliado numa janela móvel com a duração especificada
anyA condição precisa ser verdadeira em algum momento durante a duração

Portas lógicas

As condições podem ser combinadas com portas lógicas.

Tipos de porta

PortãoDescrição
allTodas as condições precisam ser verdadeiras (AND)
anyPelo menos uma condição precisa ser verdadeira (OR)
noneNenhuma condição pode ser verdadeira (NOT ANY)
sequenceAs condições precisam se tornar verdadeiras em ordem

Exemplo: portas aninhadas

{
  "all": [
    { "metric": "time.elapsed", "operator": ">=", "value": 60 },
    {
      "any": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0 },
        { "metric": "position.current", "operator": "==", "value": 1 }
      ]
    }
  ]
}

"Depois de 60 segundos, atinja 5.0 w/kg OU esteja em primeiro lugar"

Ciclo de vida

O ciclo de vida define quando a avaliação do desafio começa e termina.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   PENDING   │ ──▶ │   ARMING    │ ──▶ │   ACTIVE    │ ──▶ │  COMPLETE   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                          │                    │
                          │                    ▼
                          │              ┌─────────────┐
                          └────────────▶ │   FAILED    │
                                         └─────────────┘
FaseDescrição
pendingDesafio carregado, mas não iniciado
armingAguardando condições de início
activeAvaliando os critérios de sucesso/falha
completeCritérios de sucesso atingidos
failedCritérios de falha atingidos ou desafio abandonado

Estrutura do ciclo de vida

{
  "lifecycle": {
    "arming": {
      "all": [
        { "metric": "time.elapsed", "operator": ">=", "value": 120 }
      ]
    },
    "timeout": 600,
    "maxAttempts": 3
  }
}

Critérios de sucesso e falha

Estrutura dos critérios

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
      ]
    }
  }
}

Desafios só com sucesso

A maioria dos desafios só define critérios de sucesso. O desafio continua até o sucesso ou até o usuário desistir.

Desafios com condições de falha

{
  "criteria": {
    "success": {
      "all": [
        { "metric": "lap.count", "operator": ">=", "value": 5 }
      ]
    },
    "failure": {
      "any": [
        { "metric": "position.current", "operator": ">", "value": 3 }
      ]
    }
  }
}

"Complete 5 voltas sem sair do top 3. Cair para 4º ou pior = falha."

Loops

Os loops permitem repetir blocos de avaliação com lógica que leva a iteração em conta. Isso é essencial para desafios que envolvem vários intervalos, dificuldade progressiva ou ações repetidas.

Estrutura do loop

{
  "loops": [
    {
      "id": "sprint_intervals",
      "count": 5,
      "trigger": {
        "metric": "lap.index",
        "operator": "increasedBy",
        "value": 1
      },
      "onIteration": {
        "directives": [ ],
        "criteria": { }
      },
      "onComplete": {
        "directives": [ ]
      }
    }
  ]
}

Variáveis de contexto da iteração

VariávelDescrição
{{loopIndex}}Iteração atual (começando em 0)
{{loopCount}}Total de iterações configuradas
{{loopRemaining}}Iterações restantes

Exemplo de dificuldade progressiva

{
  "loops": [
    {
      "id": "progressive_power",
      "count": 5,
      "trigger": {
        "metric": "time.elapsed",
        "operator": ">=",
        "value": "{{loopIndex * 60}}"
      },
      "onIteration": {
        "directives": [
          {
            "type": "notify",
            "message": "Interval {{loopIndex + 1}}/{{loopCount}}: Hold {{3.0 + loopIndex * 0.5}} w/kg"
          }
        ]
      }
    }
  ]
}

"5 intervalos com potência crescente: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"

Diretivas

Diretivas são comportamentos opcionais em tempo de execução que o aplicativo host pode executar. Elas são advisory: o host decide se vai segui-las.

Estrutura da diretiva

{
  "directives": [
    {
      "id": "gap_warning",
      "type": "notify",
      "when": {
        "metric": "gap.ahead",
        "operator": ">",
        "value": 3.0
      },
      "params": {
        "message": "Gap widening! Push harder.",
        "priority": "high"
      },
      "cooldown": 10
    }
  ]
}

Tipos de diretiva

notify

Exibir uma mensagem para o ciclista

{
  "type": "notify",
  "params": {
    "message": "Push harder!",
    "priority": "normal | high | critical"
  }
}

sound

Tocar um aviso sonoro

{
  "type": "sound",
  "params": {
    "sound": "bell | chime | warning | success"
  }
}

suggest

Sugerir uma meta ao ciclista (advisory)

{
  "type": "suggest",
  "params": {
    "metric": "power.instant",
    "target": 250
  }
}

highlight

Destacar uma métrica na interface

{
  "type": "highlight",
  "params": {
    "metric": "power.wkg",
    "style": "pulse | glow | flash"
  }
}

Record (desafios de melhor esforço)

Para desafios mode: "record", o objeto record define quais métricas capturar como resultado do desafio.

Estrutura do record

{
  "record": {
    "primary": {
      "metric": "power.max",
      "label": "Peak Power",
      "unit": "watts",
      "phase": "effort"
    },
    "secondary": [
      { "metric": "power.peak", "label": "Peak Power", "unit": "watts" },
      { "metric": "cadence.max", "label": "Max Cadence", "unit": "rpm" }
    ]
  }
}

Métricas primárias comuns

Tipo de desafioMétrica principalExemplo
Esforço máximopower.maxPotência de pico
Contrarrelógioduration para a faseTempo de sprint de 200m
Manutenção em enduranceduration para a faseTempo mantido na meta
Distânciadistance.totalMetros percorridos

Conclusão

O objeto completion define como um desafio termina e o que exibir no resumo.

Estrutura de conclusão

{
  "completion": {
    "condition": "phases-complete | exit-or-phases-complete | criteria-met",
    "summary": {
      "headline": "{{record.primary.value}}W",
      "subheadline": "Peak Power"
    }
  }
}

Condições de conclusão

CondiçãoDescrição
phases-completeO desafio termina quando todas as fases terminam
exit-or-phases-completeO desafio termina na condição de saída da fase OU quando todas as fases terminam
criteria-metO desafio termina quando os critérios de sucesso são atingidos (modo pass-fail)

Metadados

Os metadados fornecem dicas de exibição e categorização para os aplicativos host.

{
  "metadata": {
    "difficulty": 4,
    "estimatedDuration": 120,
    "tags": ["sprint", "power", "short"],
    "author": "Ride Cave",
    "icon": "bolt",
    "color": "#FF6B00",
    "shortDescription": "Hold 5.0 w/kg for 10 seconds",
    "requirements": {
      "powerMeter": true,
      "heartRateMonitor": false
    }
  }
}
CampoTipoDescrição
difficultynumberNota de dificuldade de 1 a 5
estimatedDurationnumberDuração esperada em segundos
tagsarrayTags de categorização
authorstringCriador do desafio
iconstringIdentificador do ícone
colorstringCor de destaque (hex)
requirementsobjectSensores/equipamentos necessários

Exemplo completo

{
  "id": "power-surge",
  "version": "0.1.0",
  "name": "Power Surge",
  "description": "After a 2-minute warmup, hold 5.0 w/kg for 10 continuous seconds.",
  "sport": "cycling",

  "lifecycle": {
    "arming": {
      "all": [
        { "metric": "time.elapsed", "operator": ">=", "value": 120 }
      ]
    },
    "timeout": 300
  },

  "criteria": {
    "success": {
      "all": [
        {
          "metric": "power.wkg",
          "operator": ">=",
          "value": 5.0,
          "duration": 10,
          "window": "sustained"
        }
      ]
    }
  },

  "directives": [
    {
      "type": "notify",
      "params": {
        "message": "Warmup complete. Go for 5.0 w/kg!"
      },
      "once": true
    },
    {
      "type": "notify",
      "when": {
        "metric": "power.wkg",
        "operator": ">=",
        "value": 4.5
      },
      "params": {
        "message": "Almost there! Push to 5.0!",
        "priority": "high"
      },
      "cooldown": 5
    }
  ],

  "metadata": {
    "difficulty": 4,
    "estimatedDuration": 180,
    "tags": ["sprint", "power", "short"],
    "author": "Ride Cave",
    "shortDescription": "Hold 5.0 w/kg for 10 seconds"
  }
}

Modelo de avaliação

Loop de avaliação

O avaliador do desafio roda numa frequência fixa (recomendado: 10Hz).

┌─────────────────────────────────────────────────────────────┐
│                      EVALUATION TICK                        │
├─────────────────────────────────────────────────────────────┤
│  1. Collect current metrics from host                       │
│  2. Check lifecycle phase                                   │
│     - If PENDING: check arming conditions                   │
│     - If ARMING: check arming conditions → transition       │
│     - If ACTIVE: evaluate criteria + loops + directives     │
│  3. Update loop states                                      │
│  4. Check success criteria                                  │
│  5. Check failure criteria (if defined)                     │
│  6. Fire applicable directives                              │
│  7. Return evaluation result                                │
└─────────────────────────────────────────────────────────────┘

Resultado da avaliação

{
  "phase": "active",
  "success": false,
  "failed": false,
  "progress": {
    "percent": 75,
    "conditions": [
      {
        "metric": "power.wkg",
        "target": 5.0,
        "current": 5.2,
        "durationElapsed": 7.5,
        "durationRequired": 10,
        "met": false
      }
    ]
  },
  "loops": [
    {
      "id": "sprint_intervals",
      "iteration": 2,
      "count": 5,
      "status": "active"
    }
  ],
  "firedDirectives": ["gap_warning"],
  "elapsed": 145.3
}

Apêndices

A. Ambiente e portabilidade

Os desafios podem referenciar métricas de ambiente, valores fornecidos pelo aplicativo host que descrevem o contexto em que o desafio roda.

Espectro de portabilidade

  • Totalmente portátil: Desafios que só referenciam métricas básicas (potência, cadência, frequência cardíaca, tempo, distância) funcionam em qualquer host compatível
  • Dependente do ambiente: Desafios que referenciam métricas de ambiente exigem que o host forneça esses valores
  • Específico da plataforma: Alguns desafios podem referenciar recursos exclusivos de uma plataforma específica

Valores de reserva

{
  "config": {
    "approachDistance": {
      "value": "{{track.length}}",
      "fallback": 250
    }
  }
}

B. Caminhos de métricas reservados

Os seguintes caminhos estão reservados para uso futuro:

  • rowing.* - Métricas específicas de remo
  • running.* - Métricas específicas de corrida
  • strength.* - Métricas de treino de força
  • custom.* - Métricas personalizadas definidas pelo host

C. Notas de implementação para o host

Os hosts precisam implementar um adaptador de métricas que mapeie seus dados internos para os caminhos de métricas do OCP:

interface MetricAdapter {
  resolve(path: string): number | undefined;
  getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}

D. Histórico de versões

VersãoDataAlterações
0.1.0-draft2025-01Especificação inicial em rascunho