Open Challenge Protocol (OCP) | Ride Cave

Spécification

Open Challenge Protocol

Un format JSON déclaratif pour décrire des défis sportifs sur toutes les plateformes.

BrouillonVersion 0.1.0

Aperçu

L'Open Challenge Protocol (OCP) est un format JSON déclaratif pour décrire des défis sportifs. Il définit ce qui constitue la réussite, pas comment une application hôte doit mettre en œuvre l'expérience.

Un défi est un objectif structuré qui évalue la performance du cycliste selon des critères définis, avec en option des phases de cycle de vie, des directives d'exécution et une logique d'itération/boucle.

Défis et séances

L'OCP est conçu spécifiquement pour les Défis, pas pour les séances. Il est essentiel de bien comprendre cette distinction.

Défis

  • Dynamiques et orientés objectif : La réussite dépend du respect de critères précis
  • Basé sur la logique : Construit autour de conditions, de seuils et de critères de sortie
  • Durée souvent indéterminée : Beaucoup se terminent quand tu ne parviens plus à maintenir une condition
  • Réussite/échec ou record à battre : Soit tu atteins l'objectif, soit tu te mesures à un record personnel

Exemples

  • Tenir 5,0 w/kg pendant 10 secondes
  • Sprinter 200 m le plus vite possible
  • Rouler le plus loin possible en gardant la FC sous la zone 4
  • Accélérer de l'arrêt à 40 km/h

Séances

  • Structurées et prédéterminées : Suivre une séquence fixe d'intervalles
  • Basé sur le temps/la durée : Les intervalles ont des durées précises
  • Achèvement = réussite : Si tu termines tous les intervalles, tu as « terminé » la séance
  • Orientées entraînement : Conçu pour l'adaptation physiologique

Inadapté à l'OCP

  • 5x (30 secondes d'effort / 30 secondes de récup)
  • Tabata : 8x (20 secondes / 10 secondes)
  • Intervalles en pyramide de 1-2-3-2-1 minutes
  • 2x20 minutes à la FTP

En cas de doute : Demande-toi : « La réussite dépend-elle de comment le cycliste performe, ou simplement du fait que le cycliste termine la structure ? » Si la réussite dépend du maintien de conditions → Défi. Si la réussite consiste juste à terminer les intervalles → Séance.

Structure du défi

Un défi est un objet JSON avec les champs de premier niveau suivants :

{
  "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": { }
}

Champs obligatoires

ChampTypeDescription
idchaîneIdentifiant unique de ce défi
versionchaîneVersion du protocole (semver)
namechaîneNom du défi lisible par un humain
phasestableauListe ordonnée des phases du défi (OU criteria pour les défis simples)

Champs facultatifs

ChampTypeDescription
descriptionchaîneDescription détaillée du défi
sportchaîneType de sport (cycling, running, rowing, any)
modechaînerecord (meilleur effort) ou pass-fail (réussite binaire). Par défaut : pass-fail
configobjetConfiguration et valeurs calculées à l'échelle du défi
criteriaobjetCritères de réussite (et d'échec en option) pour les défis simples
loopstableauBlocs d'évaluation répétés
directivestableauComportements d'exécution (notifications, suggestions)
recordobjetMétriques à enregistrer comme résultat du défi
completionobjetComment le défi se termine et quoi afficher
metadataobjetAfficher des indices, la difficulté, des tags, etc.

Modes de défi

pass-fail (par défaut) : défi classique avec critères de réussite/échec. Résultat binaire.

record : défi au meilleur effort, où le but est d'obtenir le meilleur résultat possible. Aucun état d'échec : le défi enregistre ta performance.

Mesures

Les métriques sont les points de données canoniques que les défis évaluent. Les applications hôtes relient leurs données brutes à ces chemins.

Format des chemins de métriques

Les métriques utilisent la notation pointée : 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étriques de base

Temps

CheminTypeDescription
time.elapsednombreSecondes depuis le début du défi
time.sessionnombreSecondes depuis le début de la sortie

Puissance

CheminTypeDescription
power.instantnombrePuissance actuelle (watts)
power.avgnombrePuissance moyenne sur la session
power.normalizednombrePuissance normalisée
power.wkgnombreWatts par kilogramme actuels
power.zonenombreZone de puissance actuelle (1-7)
power.percentFtpnombrePuissance actuelle en % de la FTP

Vitesse, cadence, fréquence cardiaque

CheminTypeDescription
speed.instantnombreVitesse actuelle (m/s ou km/h selon l'hôte)
cadence.instantnombreCadence actuelle (RPM)
heartRate.instantnombreFréquence cardiaque actuelle (BPM)
heartRate.zonenombreZone FC actuelle (1-5)

Distance, Lap, Track

CheminTypeDescription
distance.totalnombreDistance totale (mètres)
lap.indexnombreNuméro du tour actuel
lap.countnombreNombre total de tours effectués
track.lengthnombreLongueur de la piste (mètres)

Métriques compétition/multijoueur

Ces métriques ne sont disponibles qu'en contexte de compétition :

CheminTypeDescription
position.currentnombrePosition actuelle en compétition (1 = premier)
gap.aheadnombreÉcart avec le coureur devant
gap.behindnombreÉcart avec le coureur derrière
overtakes.countnombreNombre de dépassements effectués

Phases

Les phases définissent la structure d'un défi sous forme de séquence ordonnée de segments. Chaque phase a sa propre durée, ses cibles, ses conditions de sortie et ses directives.

Structure d'une phase

{
  "id": "effort",
  "name": "Max Effort",
  "duration": 30,
  "description": "Give everything for 30 seconds",
  "targets": { },
  "exitConditions": { },
  "directives": [ ]
}
ChampTypeObligatoireDescription
idchaîneOuiIdentifiant unique de cette phase
namechaîneOuiNom affiché
durationnombre/chaîneNonDurée de la phase en secondes, ou "unlimited"
distancenombreNonDistance de la phase en mètres (alternative à la durée)
targetsobjetNonMétriques cibles pour cette phase
exitConditionsobjetNonConditions qui terminent cette phase plus tôt

Cibles

Les cibles définissent les métriques attendues ou requises pendant une phase :

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

Cibles progressives

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

Conditions

Une condition est une expression testable unique qui renvoie vrai ou faux.

Structure d'une condition

{
  "metric": "power.wkg",
  "operator": ">=",
  "value": 5.0,
  "duration": 10,
  "window": "sustained"
}
ChampTypeObligatoireDescription
metricchaîneOuiChemin de la métrique à évaluer
operatorchaîneOuiOpérateur de comparaison
valuenombre/tableauOuiValeur(s) cible(s)
durationnombreNonContrainte de temps en secondes
windowchaîneNonComment la durée est évaluée

Opérateurs

Comparaison

  • == Égal à
  • != Différent de
  • > Supérieur à
  • < Inférieur à
  • >= Supérieur ou égal à
  • <= Inférieur ou égal à

Plage

  • between Plage inclusive
  • outside Hors plage

Changer

  • increasedBy Augmentation par delta
  • decreasedBy Diminution par delta

Fenêtres de durée

FenêtreDescription
sustainedLa condition doit être vraie en continu pendant la durée (par défaut)
cumulativeLa condition doit être vraie pendant la durée totale (pas forcément en continu)
rollingÉvalué sur une fenêtre glissante de la durée indiquée
anyLa condition doit être vraie à un moment donné pendant la durée

Portes logiques

Les conditions peuvent être combinées avec des portes logiques.

Types de portes

BarrièreDescription
allToutes les conditions doivent être vraies (ET)
anyAu moins une condition doit être vraie (OU)
noneAucune condition ne peut être vraie (AUCUNE)
sequenceLes conditions doivent devenir vraies dans l'ordre

Exemple : portes imbriquées

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

« Après 60 secondes, atteindre 5,0 w/kg OU être en tête »

Cycle de vie

Le cycle de vie définit quand l'évaluation du défi commence et se termine.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   PENDING   │ ──▶ │   ARMING    │ ──▶ │   ACTIVE    │ ──▶ │  COMPLETE   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                          │                    │
                          │                    ▼
                          │              ┌─────────────┐
                          └────────────▶ │   FAILED    │
                                         └─────────────┘
PhaseDescription
pendingDéfi chargé mais pas démarré
armingEn attente des conditions de départ
activeÉvaluation des critères de réussite/échec
completeCritères de réussite atteints
failedCritères d'échec atteints ou abandon

Structure du cycle de vie

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

Critères de réussite et d'échec

Structure des critères

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

Défis à réussite seule

La plupart des défis ne définissent que des critères de réussite. Le défi continue jusqu'à la réussite ou l'abandon de l'utilisateur.

Défis avec conditions d'échec

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

« Boucler 5 tours en restant dans le top 3. Tomber 4e ou pire = échec. »

Boucles

Les boucles permettent de répéter des blocs d'évaluation avec une logique tenant compte de l'itération. C'est indispensable pour les défis qui comportent plusieurs intervalles, une difficulté progressive ou des actions répétées.

Structure de boucle

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

Variables de contexte d'itération

IrrégulierDescription
{{loopIndex}}Itération actuelle (indexée à partir de 0)
{{loopCount}}Nombre total d'itérations configurées
{{loopRemaining}}Itérations restantes

Exemple de difficulté progressive

{
  "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 intervalles à puissance croissante : 3,0, 3,5, 4,0, 4,5, 5,0 w/kg »

Directives

Les directives sont des comportements d'exécution optionnels que l'application hôte peut appliquer. Elles sont indicatif : l'hôte décide de les suivre ou non.

Structure d'une directive

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

Types de directives

notify

Afficher un message au cycliste

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

sound

Jouer un signal sonore

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

suggest

Suggérer une cible au cycliste (indicatif)

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

highlight

Mettre une métrique en évidence dans l'interface

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

Record (défis au meilleur effort)

Pour les défis mode: "record", l'objet record définit les métriques à enregistrer comme résultat du défi.

Structure du 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étriques principales courantes

Type de défiMétrique principaleExemple
Effort maximalpower.maxPuissance max
Contre-la-montreduration pour la phaseTemps de sprint sur 200 m
Maintien d'enduranceduration pour la phaseTemps tenu à la cible
Distancedistance.totalMètres parcourus

Achèvement

L'objet completion définit comment un défi se termine et ce qu'il faut afficher dans le résumé.

Structure d'achèvement

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

Conditions d'achèvement

ConditionDescription
phases-completeLe défi se termine quand toutes les phases sont terminées
exit-or-phases-completeLe défi se termine sur la condition de sortie de phase OU quand toutes les phases sont terminées
criteria-metLe défi se termine quand les critères de réussite sont atteints (mode réussite-échec)

Métadonnées

Les métadonnées fournissent des indications d'affichage et une catégorisation pour les applications hôtes.

{
  "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
    }
  }
}
ChampTypeDescription
difficultynombreNote de difficulté de 1 à 5
estimatedDurationnombreDurée prévue en secondes
tagstableauTags de catégorisation
authorchaîneCréateur du défi
iconchaîneIdentifiant de l'icône
colorchaîneCouleur d'accent (hex)
requirementsobjetCapteurs/équipement requis

Exemple complet

{
  "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"
  }
}

Modèle d'évaluation

Boucle d'évaluation

L'évaluateur de défis tourne à une fréquence fixe (recommandée : 10 Hz).

┌─────────────────────────────────────────────────────────────┐
│                      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                                │
└─────────────────────────────────────────────────────────────┘

Résultat de l'évaluation

{
  "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
}

Annexes

A. Environnement et portabilité

Les défis peuvent faire référence à des métriques d'environnement, des valeurs fournies par l'application hôte qui décrivent le contexte dans lequel le défi se déroule.

Spectre de portabilité

  • Entièrement portable : Les défis qui ne font référence qu'aux métriques de base (puissance, cadence, fréquence cardiaque, temps, distance) fonctionnent sur n'importe quel hôte compatible
  • Dépendant de l'environnement : Les défis qui font référence à des métriques d'environnement exigent que l'hôte fournisse ces valeurs
  • Spécifique à la plateforme : Certains défis peuvent faire référence à des fonctionnalités propres à une plateforme

Valeurs de repli

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

B. Chemins de métriques réservés

Les chemins suivants sont réservés pour un usage futur :

  • rowing.* - Métriques propres à l'aviron
  • running.* - Métriques propres à la course à pied
  • strength.* - Métriques de renforcement
  • custom.* - Métriques personnalisées définies par l'hôte

C. Notes d'implémentation pour l'hôte

Les hôtes doivent implémenter un adaptateur de métriques qui relie leurs données internes aux chemins de métriques OCP :

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

D. Historique des versions

VersionDateModifications
0.1.0-draft2025-01Version initiale du projet de spécification