👋 Webinaire  | IA & ROI : de l'identification du projet au calcul de la rentabilité · Lundi 13/07 à 11h30 ·  Je m'inscris →

Publicodes : comment Puncto a codé son moteur de paie

Une fiche de paie est sans doute l'un des documents les plus lus et les moins compris de la vie d'un salarié. On la reçoit tous les mois, on la conserve jusqu'à la retraite, et pourtant presque personne n'est capable d'expliquer comment on passe du salaire brut à la somme réellement versée. Cette opacité n'est pas un hasard : derrière chaque ligne se cache un empilement de règles, d'exceptions et de cas particuliers accumulés depuis des décennies.
Pour un ingénieur, cette observation en cache une autre, plus intéressante. Une fiche de paie, ce n'est rien d'autre que l'exécution déterministe d'un arbre de règles : une fonction qui prend en entrée une centaine de variables (salaire, statut, ancienneté, mutuelle, absences, cumuls depuis janvier…) et produit un ensemble de montants. Le vrai sujet n'est donc pas le calcul — il est parfaitement déterministe — mais la manière d'encoder des milliers de règles interconnectées sans que la base de code devienne, à son tour, une magie noire.
C'est le pari qu'a fait Puncto, un logiciel de paie SaaS français : plutôt que de traduire le Code du travail en TypeScript impératif, décrire la réglementation dans un DSL déclaratif, Publicodes, puis l'exécuter depuis un back-end typé. Voici comment, et les problèmes d'ingénierie que ça soulève réellement.

Le Code du travail est une base de code legacy

La réglementation de la paie a exactement les propriétés qu'on redoute dans un vieux monolithe :
  • Des règles empilées : chaque cotisation (santé, retraite, chômage, CSG-CRDS, AT/MP…) a son taux, son assiette, ses plafonds, ses abattements.
  • Des exceptions partout : apprenti, cadre dirigeant, stagiaire, forfait jour… la même cotisation se calcule différemment selon le statut.
  • Un modèle social qui évolue : taux, SMIC et plafond de la Sécurité sociale changent au moins une fois par an, parfois en cours d'année.
  • Des dépendances circulaires : certaines valeurs se définissent en fonction d'elles-mêmes.
Encoder ça en impératif est faisable, mais on obtient vite des milliers de lignes de conditions imbriquées : ordre d'évaluation à gérer à la main, dépendances implicites, et une base illisible pour les seules personnes capables de la valider — les experts paie, qui ne sont pas développeurs. Le problème n'est pas d'écrire le calcul, c'est de le garder vérifiable.

Publicodes : rendre une réglementation exécutable

Publicodes est un langage déclaratif open source, développé à l'origine par l'équipe de mon-entreprise.urssaf.fr pour modéliser le droit social français. Le principe : on décrit des règles et leurs dépendances, pas un algorithme. Le moteur construit le graphe et l'évalue lui-même, comme un tableur recalcule ses cellules.
Une règle « feuille » se réduit à une question :
salaire de base:
  question: Salaire de base ce mois ?
  unité: €
Une règle calculée compose d'autres règles par leur nom :
rémunération brute:
  unité: €
  somme:
    - salaire de base
    - total primes
    - indemnisations d'absences
Trois points en découlent directement. D'abord, le nom de la règle est son identifiant : on référence salaire de base par son libellé français, le modèle est lisible tel quel. Ensuite, on ne code jamais l'ordre de calcul : le moteur en déduit le graphe de dépendances et l'évalue à la demande. Enfin, ce fichier est la spécification : un expert paie le relit et le valide ligne à ligne — impossible avec l'équivalent impératif. Chez Puncto, ce modèle représente aujourd'hui quelques centaines de règles, réparties sur plusieurs fichiers et organisées en grands modules.

Les vrais défis

Un « hello world » Publicodes est trompeusement simple. La difficulté surgit quand les règles s'entremêlent. Quelques exemples, tirés de la vraie codebase, l'illustrent.

1. Les dépendances circulaires

En cas d'arrêt maladie, l'employeur « maintient » souvent le salaire : il complète les indemnités journalières (IJSS) versées par la Sécurité sociale. Ces IJSS doivent être soustraites du brut pour éviter un double comptage… mais le brut sert justement à calculer une partie de ce qui en dépend. Référence circulaire. En impératif, on la traiterait avec une passe de calcul en deux temps. Publicodes l'expose comme une propriété déclarative :
rémunération brute:
  résoudre la référence circulaire: oui
  unité: €
  somme:
    - salaire de base
    - total primes
    - indemnisations d'absences
    - IJSS brutes à soustraire
  avec:
    IJSS brutes à soustraire:
      applicable si: maintien du salaire = oui
      valeur: total IJSS ce mois * -1
Avec résoudre la référence circulaire, le moteur détecte le cycle et itère jusqu'à ce que la valeur se stabilise (point fixe). La complexité est déclarée, pas bricolée dans une orchestration maison qu'il faudrait re-tester à chaque évolution.

2. L'inversion numérique : retrouver le brut à partir du net

Sur le marché du travail, on négocie presque toujours en net : « je te propose 2 500 € net par mois ». Mais toute la machinerie de la paie tourne dans l'autre sens : on part du brut, on retire les cotisations puis l'impôt, et on obtient le net. Comment retrouver le brut qui produira exactement un net donné ?
Le réflexe serait d'écrire la formule inverse. Sauf qu'il n'y en a pas : le net en fonction du brut est une fonction en escalier, non linéaire — tranches, plafonds, seuils d'exonération. On ne l'inverse pas algébriquement. La seule voie fiable est numérique : essayer un brut, calculer le net correspondant, comparer à la cible, ajuster, recommencer jusqu'à converger. Là encore, Publicodes fournit le mécanisme, l'inversion numérique : on déclare qu'une règle habituellement calculée « vers l'avant » peut être retrouvée à partir d'une de ses sorties.
rémunération brute calculée depuis le net:
  unité: €
  applicable si: option de régularisation du net
  inversion numérique:
    - net à payer avant impôt sur le revenu
  remplace:
    références à:
      rémunération brute
Le moteur fait alors tourner tout le calcul brut → net en boucle, en ajustant le brut à chaque itération jusqu'à ce que le net calculé colle à la cible (une recherche de zéro). Deux détails élégants : remplace fait que ce brut « inversé » se substitue automatiquement au brut partout dans le modèle dès qu'on fournit un net, et applicable si garde le mécanisme dormant le reste du temps. Surtout, on ne réécrit pas un second modèle « à l'envers » : c'est le même arbre de règles, exécuté dans l'autre sens. Cousine de la référence circulaire vue plus haut, l'inversion numérique dit bien ce qu'est ce moteur — un solveur itératif, pas une simple cascade de formules.

Le pont entre Publicodes et TypeScript

C'est ici que se joue la partie la plus intéressante côté ingénierie : faire dialoguer un modèle écrit en français avec un back-end typé, sans perdre les garanties de l'un ni de l'autre.
À la compilation, les fichiers .publicodes sont transformés en un graphe JSON et en types TypeScript générés : l'union de tous les noms de règles existe désormais comme type. Concrètement, l'appel d'une règle est autocomplété, et demander une règle qui n'existe pas devient une erreur de compilation — pas une exception au runtime en pleine génération de bulletin. On instancie un moteur par bulletin, puis on évalue à la demande :
const { payslipCalculator } = getPayslipCalculator(payslipVariables)

const net       = payslipCalculator('net payé')       // typé, autocomplété
const superBrut = payslipCalculator('super brut')
// payslipCalculator('net paye')  ← faute de frappe : erreur de compilation
Reste l'impédance : Publicodes a ses conventions, héritées du droit (un booléen s'écrit oui/non, une date JJ/MM/AAAA, une énumération se cite : 'CDI'). Un wrapper typé fait le pont dans les deux sens — et surtout, il refuse de rendre un résultat si une variable requise manque :
calculateValue(value: T) {
  const res = super.evaluate(value)
  const missingVariables = Object.keys(res.missingVariables)
    .filter((key) => key.includes('régularisation') === false)

  if (missingVariables.length > 0) {
    throw new Error(`Missing publicodes variables ${JSON.stringify(missingVariables)}`)
  }
  return res.nodeValue as RuleValue[T]
}
Ce choix de fail-fast est une décision de conception à part entière. Dans un domaine où une variable oubliée ne fait pas planter mais produit un montant faux, mieux vaut une exception explicite qu'un bulletin silencieusement erroné.

Tester un calcul où l'erreur coûte cher

Une fois ce socle en place, une bascule mentale s'opère, et elle est contre-intuitive : le calcul ne se trompe pratiquement jamais. Un arbre de règles validé donne toujours le bon résultat pour des entrées correctes. En pratique, les erreurs de paie viennent presque exclusivement d'un paramétrage ou d'une saisie : mutuelle mal affiliée, ancienneté fausse, prime oubliée, arrêt mal reporté.
La stratégie de test suit cette logique : on fige des situations complètes et on vérifie chaque ligne au centime près. Un bulletin de cadre à 64 k€/an d'avril 2025 produit ainsi une centaine d'assertions du type :
test('CSG déductible', () => {
  assert.strictEqual(
    payslipCalculator("CSG-CRDS . CSG déductible de l'impôt sur le revenu"),
    360.7,
  )
})
Ces tests jouent double rôle : filet anti-régression, et spécification exécutable. Quand un taux change au 1er janvier, on ajoute une situation datée avec les nouveaux montants attendus ; la non-régression sur les anciens millésimes est garantie par construction grâce à un versionnement par date au niveau publicodes. Résultat : modifier une règle de cotisation, d'ordinaire redouté dans un logiciel de paie, devient une opération maîtrisée — si un centime bouge quelque part, un test le signale immédiatement.

Ce que le déclaratif apporte, et ce qu'il coûte

Le bilan est net. Au crédit de Publicodes : un modèle auditable par les experts métier eux-mêmes, des diffs Git lisibles quand une règle change, une résolution automatique des dépendances (aucun tri topologique à la main), un versionnement réglementaire quasi gratuit via des variations sur la date — le tout enveloppé dans une couche de types qui rapatrie la sécurité statique de TypeScript.
Le coût existe : apprendre les conventions du langage, écrire et maintenir la couche d'impédance, et accepter qu'un moteur résolvant des cycles par itération soit un peu moins transparent qu'une fonction déroulée à la main. Rapporté à un droit social vivant, rétroactif et truffé d'exceptions, l'échange est très favorable.
La leçon dépasse la paie : quand un domaine ressemble à une base de code legacy que personne ne maîtrise entièrement, la bonne réponse n'est pas toujours d'écrire plus de code — c'est parfois de trouver le bon langage pour le décrire, et de laisser une machine l'exécuter. C'est exactement ce qui fait tourner le moteur de paie de Puncto au quotidien.
Vous souhaitez être accompagné pour lancer votre projet digital ?
Déposez votre projet dès maintenant
Article presentation image
Mêler OCR et LLM pour mettre en lumière une preuve visuelle dans les tâches d'extraction IA
Les LLMs couplés à des systèmes OCR permettent aujourd'hui d'automatiser le traitement de documents PDF ou images : résumés, ...
Louis Betzer
Louis Betzer
Ingénieur IA @ Galadrim
Article presentation image
Quel langage pour un backend performant en 2026 ? Benchmarker avec Grafana K6
Chez Galadrim, nous avons l'habitude de travailler avec de nombreux langages et frameworks Web différents. Si chaque équipe ...
Julien Chapuy
Julien Chapuy
Ingénieur IA @ Galadrim
Article presentation image
Comment des couleurs dans un terminal peuvent vous compromettre
Imaginez la scène : vous lancez un outil CLI pour diagnostiquer un serveur en prod. Il affiche des informations sur un process ...
Molly Allerhand
Molly Allerhand
Développeuse full-stack @ Galadrim