{
  "openapi": "3.1.0",
  "info": {
    "title": "Tharsan.me — API publique",
    "version": "1.0.0",
    "summary": "Profil, projets, expériences et tech stack de Tharsan Vishnukumar, développeur web & mobile freelance.",
    "description": "API REST publique, en lecture seule, sans clé ni authentification. Elle expose les mêmes données que le site tharsan.me (portfolio de Tharsan Vishnukumar) dans un format structuré pour les agents et les intégrations : profil et coordonnées professionnelles, projets réalisés, expériences et formation, technologies maîtrisées. Toutes les réponses sont en JSON UTF-8, enveloppées dans `{ data, meta }` ; les erreurs renvoient `{ error: { status, code, message, hint, docs } }`. Quota : 60 requêtes par minute et par adresse IP, annoncé par les en-têtes `RateLimit-Policy` / `RateLimit` (draft IETF) ; 429 + `Retry-After` au-delà. Version dans l'URL (/api/v1), changements additifs uniquement ; une version dépréciée envoie `Deprecation` (RFC 9745) et `Sunset` (RFC 8594) au moins 180 jours avant retrait — voir `x-versioning-policy`. Les réponses sont en cache CDN une heure.",
    "termsOfService": "https://www.tharsan.me/privacy",
    "contact": {
      "name": "Tharsan Vishnukumar",
      "url": "https://www.tharsan.me/contact",
      "email": "tharsan.vishnukumar@gmail.com"
    },
    "license": {
      "name": "Contenu © Tharsan Vishnukumar — consultation et réutilisation avec attribution autorisées",
      "url": "https://www.tharsan.me/developers"
    },
    "x-llms-txt": "https://www.tharsan.me/llms.txt",
    "x-agent-instructions": "https://www.tharsan.me/agents.md",
    "x-versioning-policy": {
      "current": "v1",
      "basePath": "/api/v1",
      "scheme": "url-path",
      "stability": "stable",
      "compatibility": "Dans une version donnée, aucun champ n'est renommé ni supprimé et aucun type ne change ; de nouveaux champs, paramètres optionnels ou endpoints peuvent apparaître sans préavis (additif uniquement).",
      "breakingChanges": "Toute rupture passe par une nouvelle version majeure (/api/v2). L'ancienne version reste servie pendant toute la période de dépréciation.",
      "deprecation": {
        "minimumNoticeDays": 180,
        "signals": [
          "En-tête `Deprecation` (RFC 9745) avec la date de dépréciation sur chaque réponse de la version dépréciée",
          "En-tête `Sunset` (RFC 8594) avec la date de retrait, au moins 180 jours plus tard",
          "En-tête `Link` rel=\"deprecation\" vers la page de migration",
          "Entrée dans `deprecated` de l'index /api/v1 et dans `x-versioning-policy` de la spécification OpenAPI"
        ],
        "docs": "https://www.tharsan.me/developers#versioning"
      },
      "deprecated": []
    },
    "x-rate-limit-policy": {
      "name": "api",
      "quota": 60,
      "windowSeconds": 60,
      "scope": "per-ip",
      "headers": [
        "RateLimit-Policy",
        "RateLimit",
        "Retry-After (429)"
      ],
      "specification": "draft-ietf-httpapi-ratelimit-headers"
    }
  },
  "security": [],
  "externalDocs": {
    "description": "Portail développeurs (quickstart, formats, négociation Markdown)",
    "url": "https://www.tharsan.me/developers"
  },
  "servers": [
    {
      "url": "https://www.tharsan.me",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "profile",
      "description": "Identité, coordonnées et activité professionnelle."
    },
    {
      "name": "projects",
      "description": "Projets web et mobile réalisés."
    },
    {
      "name": "experiences",
      "description": "Expériences professionnelles et formation."
    },
    {
      "name": "tech-stack",
      "description": "Technologies maîtrisées, par domaine, avec les années d'expérience."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "profile"
        ],
        "summary": "Index de l'API",
        "description": "Liste les endpoints disponibles (operationId, méthode, chemin, résumé) avec les liens vers la spécification OpenAPI, la documentation, llms.txt et agents.md, la politique de versionnement/dépréciation et la politique de quota. Filtrable par tag.",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Ne lister que les endpoints de ce tag.",
            "schema": {
              "type": "string",
              "enum": [
                "profile",
                "projects",
                "experiences",
                "tech-stack"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Index des ressources.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ApiIndex"
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "docs": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Paramètre `tag` hors de l'énumération.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Chemin inexact (la casse compte) : erreur JSON standard du catch-all /api.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profile": {
      "get": {
        "operationId": "getProfile",
        "tags": [
          "profile"
        ],
        "summary": "Profil de Tharsan Vishnukumar",
        "description": "Retourne l'identité publique, la présentation, la localisation, la disponibilité, les coordonnées (e-mail, téléphone), les liens (GitHub, LinkedIn, CV PDF, laboratoire Kouma Labs) et les informations légales de l'activité freelance. Le paramètre `fields` permet de ne recevoir qu'un sous-ensemble de champs (économie de tokens).",
        "parameters": [
          {
            "name": "fields",
            "in": "query",
            "required": false,
            "description": "Liste de champs de premier niveau à renvoyer, séparés par des virgules. Valeurs : name, headline, tagline, bio, location, availability, languages, softSkills, email, phone, links, business. Omettre pour tout recevoir.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z]+(,[a-zA-Z]+)*$",
              "examples": [
                "name,email,links",
                "availability,location"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Profil.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Profile"
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "docs": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Champ inconnu dans `fields` (la liste des champs valides est renvoyée dans `details.allowed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Chemin inexact (la casse compte) : erreur JSON standard du catch-all /api.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "projects"
        ],
        "summary": "Lister les projets",
        "description": "Retourne tous les projets du portfolio (du plus récent au plus ancien), avec description, année, technologies, liens publics et dépôt GitHub éventuel. Filtrable par technologie.",
        "parameters": [
          {
            "name": "stack",
            "in": "query",
            "required": false,
            "description": "Ne garder que les projets utilisant cette technologie (insensible à la casse et aux accents, ex. `react`, `flutter`, `nextjs`).",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64,
              "examples": [
                "react",
                "flutter"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Liste de projets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "count",
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Nombre d'éléments retournés."
                        },
                        "docs": {
                          "type": "string",
                          "format": "uri",
                          "description": "Portail développeurs."
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri",
                          "description": "URL de cette spécification."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Paramètre `stack` invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{slug}": {
      "get": {
        "operationId": "getProject",
        "tags": [
          "projects"
        ],
        "summary": "Détail d'un projet",
        "description": "Retourne un projet identifié par son slug (voir `listProjects` pour la liste des slugs).",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug du projet, ex. `filon`, `en-plein-air`, `govo-application-mobile`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
              "examples": [
                "filon"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Projet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Project"
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "docs": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Aucun projet pour ce slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/experiences": {
      "get": {
        "operationId": "listExperiences",
        "tags": [
          "experiences"
        ],
        "summary": "Expériences professionnelles et formation",
        "description": "Retourne les expériences professionnelles (freelance, alternance, stages) et les formations, triées de la plus récente à la plus ancienne, avec dates ISO 8601 et informations légales des organisations quand elles sont connues. Filtrable par catégorie, par type de contrat et par statut en cours.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Ne remplir que cette catégorie (l'autre tableau est renvoyé vide). Défaut : `all`.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "experiences",
                "educations"
              ],
              "default": "all"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Ne garder que les entrées de ce type : `full-time` (temps plein, alternance, activité freelance) ou `internship` (stage).",
            "schema": {
              "type": "string",
              "enum": [
                "full-time",
                "internship"
              ]
            }
          },
          {
            "name": "current",
            "in": "query",
            "required": false,
            "description": "`true` : uniquement les entrées en cours (sans date de fin) ; `false` : uniquement les entrées terminées.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Expériences et formations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "experiences",
                        "educations"
                      ],
                      "properties": {
                        "experiences": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Experience"
                          }
                        },
                        "educations": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Experience"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "docs": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Paramètre `category`, `kind` ou `current` invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Chemin inexact (la casse compte) : erreur JSON standard du catch-all /api.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tech-stack": {
      "get": {
        "operationId": "listTechStack",
        "tags": [
          "tech-stack"
        ],
        "summary": "Technologies maîtrisées",
        "description": "Retourne les technologies et outils avec le nombre d'années d'expérience et le domaine (langages, front & web, mobile, back & data, infra & cloud, outils & services), triés par années décroissantes. Filtrable par domaine.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "description": "Ne garder que ce domaine.",
            "schema": {
              "type": "string",
              "enum": [
                "langages",
                "front-web",
                "mobile",
                "back-data",
                "infra-cloud",
                "outils-services"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              }
            },
            "description": "Liste de technologies.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tech"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "count",
                        "docs",
                        "openapi"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Nombre d'éléments retournés."
                        },
                        "docs": {
                          "type": "string",
                          "format": "uri",
                          "description": "Portail développeurs."
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri",
                          "description": "URL de cette spécification."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Paramètre `domain` hors de l'énumération.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Quota dépassé (60 requêtes / 60 s par adresse IP). Attendre `Retry-After` secondes.",
            "headers": {
              "RateLimit-Policy": {
                "description": "Politique de quota (draft IETF RateLimit) : `\"api\";q=60;w=60`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";q=60;w=60"
                  ]
                }
              },
              "RateLimit": {
                "description": "Quota restant (r) et secondes avant réinitialisation (t) pour la politique courante.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"api\";r=59;t=60"
                  ]
                }
              },
              "Retry-After": {
                "description": "Secondes à attendre avant de réessayer.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "version",
          "openapi",
          "docs",
          "llmsTxt",
          "agentInstructions",
          "versioning",
          "rateLimit",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "description": "Version sémantique de l'implémentation."
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "llmsTxt": {
            "type": "string",
            "format": "uri"
          },
          "agentInstructions": {
            "type": "string",
            "format": "uri",
            "description": "Fichier agents.md : quand utiliser le site et comment l'appeler."
          },
          "versioning": {
            "type": "object",
            "description": "Politique de versionnement et de dépréciation.",
            "required": [
              "current",
              "basePath",
              "scheme",
              "stability",
              "compatibility",
              "breakingChanges",
              "deprecation",
              "deprecated"
            ],
            "properties": {
              "current": {
                "type": "string",
                "examples": [
                  "v1"
                ]
              },
              "basePath": {
                "type": "string",
                "examples": [
                  "/api/v1"
                ]
              },
              "scheme": {
                "type": "string",
                "enum": [
                  "url-path"
                ]
              },
              "stability": {
                "type": "string",
                "enum": [
                  "stable",
                  "beta",
                  "deprecated"
                ]
              },
              "compatibility": {
                "type": "string"
              },
              "breakingChanges": {
                "type": "string"
              },
              "deprecation": {
                "type": "object",
                "required": [
                  "minimumNoticeDays",
                  "signals",
                  "docs"
                ],
                "properties": {
                  "minimumNoticeDays": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "signals": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "docs": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              "deprecated": {
                "type": "array",
                "description": "Versions dépréciées encore servies, avec leurs dates Deprecation/Sunset.",
                "items": {
                  "type": "object",
                  "required": [
                    "version",
                    "basePath",
                    "deprecatedAt",
                    "sunsetAt",
                    "migration"
                  ],
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "basePath": {
                      "type": "string"
                    },
                    "deprecatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "sunsetAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "migration": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "rateLimit": {
            "type": "object",
            "required": [
              "policy",
              "quota",
              "windowSeconds",
              "scope",
              "headers"
            ],
            "properties": {
              "policy": {
                "type": "string"
              },
              "quota": {
                "type": "integer"
              },
              "windowSeconds": {
                "type": "integer"
              },
              "scope": {
                "type": "string",
                "enum": [
                  "per-ip"
                ]
              },
              "headers": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "operationId",
                "method",
                "path",
                "summary",
                "tags"
              ],
              "properties": {
                "operationId": {
                  "type": "string"
                },
                "method": {
                  "type": "string",
                  "enum": [
                    "GET"
                  ]
                },
                "path": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                },
                "tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "Profile": {
        "type": "object",
        "required": [
          "name",
          "headline",
          "tagline",
          "bio",
          "location",
          "availability",
          "languages",
          "softSkills",
          "email",
          "phone",
          "links",
          "business"
        ],
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "Tharsan Vishnukumar"
            ]
          },
          "headline": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "bio": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Paragraphes de présentation (français)."
          },
          "location": {
            "type": "string"
          },
          "availability": {
            "type": "string"
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Langues du contenu (codes BCP 47)."
          },
          "softSkills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "Numéro français, format national sans espaces."
          },
          "links": {
            "type": "object",
            "required": [
              "website",
              "github",
              "linkedin",
              "cv",
              "lab"
            ],
            "properties": {
              "website": {
                "type": "string",
                "format": "uri"
              },
              "github": {
                "type": "string",
                "format": "uri"
              },
              "linkedin": {
                "type": "string",
                "format": "uri"
              },
              "cv": {
                "type": "string",
                "format": "uri",
                "description": "CV au format PDF."
              },
              "lab": {
                "type": "string",
                "format": "uri",
                "description": "Kouma Labs, laboratoire d'applications privacy-first."
              }
            }
          },
          "business": {
            "type": "object",
            "required": [
              "name",
              "legalForm",
              "siren",
              "registeredSince",
              "headquarters"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "legalForm": {
                "type": "string"
              },
              "siren": {
                "type": "string",
                "pattern": "^[0-9]{9}$"
              },
              "registeredSince": {
                "type": "string",
                "format": "date"
              },
              "headquarters": {
                "type": "string"
              }
            }
          }
        }
      },
      "Project": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "description",
          "year",
          "stacks",
          "image",
          "github",
          "links",
          "url"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "examples": [
              "filon"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "Filon"
            ]
          },
          "description": {
            "type": "string"
          },
          "year": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]{4}$",
            "description": "Année de mise en avant dans la frise."
          },
          "stacks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "image": {
            "type": "string",
            "format": "uri",
            "description": "Capture du projet (WebP)."
          },
          "github": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "url"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Ancre du projet sur la page /projets."
          }
        }
      },
      "Experience": {
        "type": "object",
        "required": [
          "title",
          "organization",
          "organizationDescription",
          "location",
          "kind",
          "status",
          "startDate",
          "endDate",
          "current",
          "organizationInfo"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "organization": {
            "type": "string"
          },
          "organizationDescription": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "full-time",
              "internship"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "startDate": {
            "type": "string",
            "format": "date"
          },
          "endDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "current": {
            "type": "boolean",
            "description": "Vrai si l'expérience est en cours (pas de date de fin)."
          },
          "organizationInfo": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "url",
              "legalForm",
              "capital",
              "siren",
              "creationDate",
              "headquarters",
              "certification"
            ],
            "properties": {
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              },
              "legalForm": {
                "type": "string"
              },
              "capital": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "siren": {
                "type": "string"
              },
              "creationDate": {
                "type": "string",
                "description": "Date française JJ/MM/AAAA telle qu'affichée sur le site."
              },
              "headquarters": {
                "type": "string"
              },
              "certification": {
                "type": "string"
              }
            }
          }
        }
      },
      "Tech": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "years",
          "domain",
          "domainId"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "examples": [
              "typescript"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "Typescript"
            ]
          },
          "years": {
            "type": "integer",
            "minimum": 0,
            "description": "Années d'expérience."
          },
          "domain": {
            "type": "string",
            "enum": [
              "Langages",
              "Front & web",
              "Mobile",
              "Back & data",
              "Infra & cloud",
              "Outils & services"
            ]
          },
          "domainId": {
            "type": "string",
            "enum": [
              "langages",
              "front-web",
              "mobile",
              "back-data",
              "infra-cloud",
              "outils-services"
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "status",
              "code",
              "message",
              "hint",
              "docs",
              "openapi"
            ],
            "properties": {
              "status": {
                "type": "integer",
                "examples": [
                  404
                ]
              },
              "code": {
                "type": "string",
                "enum": [
                  "not_found",
                  "invalid_parameter",
                  "method_not_allowed",
                  "not_acceptable",
                  "rate_limited"
                ]
              },
              "message": {
                "type": "string",
                "description": "Description lisible de l'erreur."
              },
              "hint": {
                "type": "string",
                "description": "Comment corriger ou où chercher."
              },
              "docs": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      }
    }
  }
}