[{"data":1,"prerenderedAt":911},["ShallowReactive",2],{"authors":3,"article-2026-05-05-conception-d-une-fsm-maison":331},[4,23,35,48,61,73,85,98,111,124,136,148,161,173,185,197,209,221,233,245,258,270,282,295,307,319],{"id":5,"title":6,"body":7,"description":11,"extension":14,"meta":15,"name":16,"navigation":17,"path":18,"readingTime":19,"seo":20,"stem":21,"__hash__":22},"authors\u002Fauthors\u002Falexandre-guillon.md","Software Engineer",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"md",{},"Alexandre Guillon",true,"\u002Fauthors\u002Falexandre-guillon",1,{"title":6,"description":11},"authors\u002Falexandre-guillon","4tf48mjyjFNqItOHaulICbrjeCyMag1o6801uHeTz98",{"id":24,"title":6,"body":25,"description":11,"extension":14,"meta":29,"name":30,"navigation":17,"path":31,"readingTime":19,"seo":32,"stem":33,"__hash__":34},"authors\u002Fauthors\u002Falexis-ablain.md",{"type":8,"value":26,"toc":27},[],{"title":11,"searchDepth":12,"depth":12,"links":28},[],{},"Alexis Ablain","\u002Fauthors\u002Falexis-ablain",{"title":6,"description":11},"authors\u002Falexis-ablain","_SIAtB7f-39e5t3GiJof81NP47s6MGo2n4gaHkTy1uQ",{"id":36,"title":37,"body":38,"description":11,"extension":14,"meta":42,"name":43,"navigation":17,"path":44,"readingTime":19,"seo":45,"stem":46,"__hash__":47},"authors\u002Fauthors\u002Faxel-shaita.md","Engineering Manager",{"type":8,"value":39,"toc":40},[],{"title":11,"searchDepth":12,"depth":12,"links":41},[],{},"Axel Shaïta","\u002Fauthors\u002Faxel-shaita",{"title":37,"description":11},"authors\u002Faxel-shaita","fK0argUhsBkWLjpTAhY13oYLVzQthcEYkCEdtHWmIgE",{"id":49,"title":50,"body":51,"description":11,"extension":14,"meta":55,"name":56,"navigation":17,"path":57,"readingTime":19,"seo":58,"stem":59,"__hash__":60},"authors\u002Fauthors\u002Fbaptiste-faure.md","Head of Talent Acquisition",{"type":8,"value":52,"toc":53},[],{"title":11,"searchDepth":12,"depth":12,"links":54},[],{},"Baptiste Faure","\u002Fauthors\u002Fbaptiste-faure",{"title":50,"description":11},"authors\u002Fbaptiste-faure","ELisToYtcgHmgdVWZkCclTPV6exZtfyXqhpx1jjbJHs",{"id":62,"title":6,"body":63,"description":11,"extension":14,"meta":67,"name":68,"navigation":17,"path":69,"readingTime":19,"seo":70,"stem":71,"__hash__":72},"authors\u002Fauthors\u002Fbenjamin-bouillot.md",{"type":8,"value":64,"toc":65},[],{"title":11,"searchDepth":12,"depth":12,"links":66},[],{},"Benjamin Bouillot","\u002Fauthors\u002Fbenjamin-bouillot",{"title":6,"description":11},"authors\u002Fbenjamin-bouillot","tbhCFZyfTt7ZM5b5YgqQ2nhgnSTl8BweaQQryc87fHo",{"id":74,"title":37,"body":75,"description":11,"extension":14,"meta":79,"name":80,"navigation":17,"path":81,"readingTime":19,"seo":82,"stem":83,"__hash__":84},"authors\u002Fauthors\u002Fcedric-nicoloso.md",{"type":8,"value":76,"toc":77},[],{"title":11,"searchDepth":12,"depth":12,"links":78},[],{},"Cédric Nicoloso","\u002Fauthors\u002Fcedric-nicoloso",{"title":37,"description":11},"authors\u002Fcedric-nicoloso","ibSoh4VZYiWYTuLOnZTedaAfcnvet1Q9H7ogW0LgorY",{"id":86,"title":87,"body":88,"description":11,"extension":14,"meta":92,"name":93,"navigation":17,"path":94,"readingTime":19,"seo":95,"stem":96,"__hash__":97},"authors\u002Fauthors\u002Fdavid-touzet.md","Staff Engineer",{"type":8,"value":89,"toc":90},[],{"title":11,"searchDepth":12,"depth":12,"links":91},[],{},"David Touzet","\u002Fauthors\u002Fdavid-touzet",{"title":87,"description":11},"authors\u002Fdavid-touzet","dHWwnQxb1Ubt-WwXWEODGEo9AFoq1cJUhfg3kdnYSBM",{"id":99,"title":100,"body":101,"description":11,"extension":14,"meta":105,"name":106,"navigation":17,"path":107,"readingTime":19,"seo":108,"stem":109,"__hash__":110},"authors\u002Fauthors\u002Feloise-chizat.md","Data Engineer",{"type":8,"value":102,"toc":103},[],{"title":11,"searchDepth":12,"depth":12,"links":104},[],{},"Eloïse Chizat","\u002Fauthors\u002Feloise-chizat",{"title":100,"description":11},"authors\u002Feloise-chizat","Utd72Vm9qT4hh2ZbFi6a2_nXw5Wb494Ed_HL1ra5yw8",{"id":112,"title":113,"body":114,"description":11,"extension":14,"meta":118,"name":119,"navigation":17,"path":120,"readingTime":19,"seo":121,"stem":122,"__hash__":123},"authors\u002Fauthors\u002Femmanuel-auclair.md","Staff engineer",{"type":8,"value":115,"toc":116},[],{"title":11,"searchDepth":12,"depth":12,"links":117},[],{},"Emmanuel Auclair","\u002Fauthors\u002Femmanuel-auclair",{"title":113,"description":11},"authors\u002Femmanuel-auclair","MtsA8THNLEn0dTtYEIQaGwDuf7MjQL55IOeei5gugEg",{"id":125,"title":6,"body":126,"description":11,"extension":14,"meta":130,"name":131,"navigation":17,"path":132,"readingTime":19,"seo":133,"stem":134,"__hash__":135},"authors\u002Fauthors\u002Fhoreb-parraud.md",{"type":8,"value":127,"toc":128},[],{"title":11,"searchDepth":12,"depth":12,"links":129},[],{},"Horeb Parraud","\u002Fauthors\u002Fhoreb-parraud",{"title":6,"description":11},"authors\u002Fhoreb-parraud","ajjsnUX4ohZI-ghMdbb92q_taWDkKXVZSLZXoAeLQtg",{"id":137,"title":37,"body":138,"description":11,"extension":14,"meta":142,"name":143,"navigation":17,"path":144,"readingTime":19,"seo":145,"stem":146,"__hash__":147},"authors\u002Fauthors\u002Fhugo-contreras.md",{"type":8,"value":139,"toc":140},[],{"title":11,"searchDepth":12,"depth":12,"links":141},[],{},"Hugo Contreras","\u002Fauthors\u002Fhugo-contreras",{"title":37,"description":11},"authors\u002Fhugo-contreras","2nc3VMu9ASq9Z6Pwx2-7-Ye991Pww4p-UEDBQFfjF-Q",{"id":149,"title":150,"body":151,"description":11,"extension":14,"meta":155,"name":156,"navigation":17,"path":157,"readingTime":19,"seo":158,"stem":159,"__hash__":160},"authors\u002Fauthors\u002Fjulien-tassin.md","Head of Engineering",{"type":8,"value":152,"toc":153},[],{"title":11,"searchDepth":12,"depth":12,"links":154},[],{},"Julien Tassin","\u002Fauthors\u002Fjulien-tassin",{"title":150,"description":11},"authors\u002Fjulien-tassin","iUIHI7SITje38Jh9X9uvYs4-VsHx4eCdt6hAlyLFG_o",{"id":162,"title":6,"body":163,"description":11,"extension":14,"meta":167,"name":168,"navigation":17,"path":169,"readingTime":19,"seo":170,"stem":171,"__hash__":172},"authors\u002Fauthors\u002Flaurent-renard.md",{"type":8,"value":164,"toc":165},[],{"title":11,"searchDepth":12,"depth":12,"links":166},[],{},"Laurent Renard","\u002Fauthors\u002Flaurent-renard",{"title":6,"description":11},"authors\u002Flaurent-renard","5BP7Ed-pt1SQHjh0UJ1XUrlLTcdlFaDoKBCP4deHq8A",{"id":174,"title":6,"body":175,"description":11,"extension":14,"meta":179,"name":180,"navigation":17,"path":181,"readingTime":19,"seo":182,"stem":183,"__hash__":184},"authors\u002Fauthors\u002Fleo-martin.md",{"type":8,"value":176,"toc":177},[],{"title":11,"searchDepth":12,"depth":12,"links":178},[],{},"Léo Martin","\u002Fauthors\u002Fleo-martin",{"title":6,"description":11},"authors\u002Fleo-martin","eYxCHkRgbGDV7shKdTA9s7Tu0zGV4yDGFoKR5MHQntY",{"id":186,"title":6,"body":187,"description":11,"extension":14,"meta":191,"name":192,"navigation":17,"path":193,"readingTime":19,"seo":194,"stem":195,"__hash__":196},"authors\u002Fauthors\u002Floic-bousquet.md",{"type":8,"value":188,"toc":189},[],{"title":11,"searchDepth":12,"depth":12,"links":190},[],{},"Loïc Bousquet","\u002Fauthors\u002Floic-bousquet",{"title":6,"description":11},"authors\u002Floic-bousquet","ko12qZwiGL8XNjAoy9oWypPkIjr29Pbq7vhdtgldqeQ",{"id":198,"title":6,"body":199,"description":11,"extension":14,"meta":203,"name":204,"navigation":17,"path":205,"readingTime":19,"seo":206,"stem":207,"__hash__":208},"authors\u002Fauthors\u002Floic-poullain.md",{"type":8,"value":200,"toc":201},[],{"title":11,"searchDepth":12,"depth":12,"links":202},[],{},"Loïc Poullain","\u002Fauthors\u002Floic-poullain",{"title":6,"description":11},"authors\u002Floic-poullain","oRIyJhFRTqxy5dLCYQ2OnYZ1DB-gLDUM-85vTSYuTF0",{"id":210,"title":100,"body":211,"description":11,"extension":14,"meta":215,"name":216,"navigation":17,"path":217,"readingTime":19,"seo":218,"stem":219,"__hash__":220},"authors\u002Fauthors\u002Fmaud-lelu.md",{"type":8,"value":212,"toc":213},[],{"title":11,"searchDepth":12,"depth":12,"links":214},[],{},"Maud Lélu","\u002Fauthors\u002Fmaud-lelu",{"title":100,"description":11},"authors\u002Fmaud-lelu","MMbsCKuE41OMHusrl12FIEsI-Trx7l8Nn_ANhvj2_y4",{"id":222,"title":37,"body":223,"description":11,"extension":14,"meta":227,"name":228,"navigation":17,"path":229,"readingTime":19,"seo":230,"stem":231,"__hash__":232},"authors\u002Fauthors\u002Fnicolas-poirier.md",{"type":8,"value":224,"toc":225},[],{"title":11,"searchDepth":12,"depth":12,"links":226},[],{},"Nicolas Poirier","\u002Fauthors\u002Fnicolas-poirier",{"title":37,"description":11},"authors\u002Fnicolas-poirier","dXrJkYo8az4SN_D23aYc3fQ7z8s1dR2a0lt1ogjAjJs",{"id":234,"title":37,"body":235,"description":11,"extension":14,"meta":239,"name":240,"navigation":17,"path":241,"readingTime":19,"seo":242,"stem":243,"__hash__":244},"authors\u002Fauthors\u002Fraphael-sauget.md",{"type":8,"value":236,"toc":237},[],{"title":11,"searchDepth":12,"depth":12,"links":238},[],{},"Raphaël Sauget","\u002Fauthors\u002Fraphael-sauget",{"title":37,"description":11},"authors\u002Fraphael-sauget","Uri9bcq0QDuxRA0PbBoNtu7p_5L3dALu4kzcXVW0xyM",{"id":246,"title":247,"body":248,"description":11,"extension":14,"meta":252,"name":253,"navigation":17,"path":254,"readingTime":19,"seo":255,"stem":256,"__hash__":257},"authors\u002Fauthors\u002Fromain-koenig.md","Co-funder & Head of innovation",{"type":8,"value":249,"toc":250},[],{"title":11,"searchDepth":12,"depth":12,"links":251},[],{},"Romain Koenig","\u002Fauthors\u002Fromain-koenig",{"title":247,"description":11},"authors\u002Fromain-koenig","uyS8--eG2_ezyqRABcJnMJmQKKuSArhPWd14aUvFeEw",{"id":259,"title":37,"body":260,"description":11,"extension":14,"meta":264,"name":265,"navigation":17,"path":266,"readingTime":19,"seo":267,"stem":268,"__hash__":269},"authors\u002Fauthors\u002Fromaric-juniet.md",{"type":8,"value":261,"toc":262},[],{"title":11,"searchDepth":12,"depth":12,"links":263},[],{},"Romaric Juniet","\u002Fauthors\u002Fromaric-juniet",{"title":37,"description":11},"authors\u002Fromaric-juniet","4Zb2artgT-eo-PHLXi3xi4d5t7s6PfhUxeSfXIikSUY",{"id":271,"title":6,"body":272,"description":11,"extension":14,"meta":276,"name":277,"navigation":17,"path":278,"readingTime":19,"seo":279,"stem":280,"__hash__":281},"authors\u002Fauthors\u002Fstanyslas-bres.md",{"type":8,"value":273,"toc":274},[],{"title":11,"searchDepth":12,"depth":12,"links":275},[],{},"Stanyslas Bres","\u002Fauthors\u002Fstanyslas-bres",{"title":6,"description":11},"authors\u002Fstanyslas-bres","Xa0SahETuiN4q1jrmR2ych3moAqcZ2LbU7vSfEt2RuU",{"id":283,"title":284,"body":285,"description":11,"extension":14,"meta":289,"name":290,"navigation":17,"path":291,"readingTime":19,"seo":292,"stem":293,"__hash__":294},"authors\u002Fauthors\u002Ftalent-acquisition.md","Talent Acquisition",{"type":8,"value":286,"toc":287},[],{"title":11,"searchDepth":12,"depth":12,"links":288},[],{},"Équipe Talent Acquisition","\u002Fauthors\u002Ftalent-acquisition",{"description":11},"authors\u002Ftalent-acquisition","doDfE76txftQ4wIiKjJoDmSpyzSKk0tzlgVAp6-opAY",{"id":296,"title":6,"body":297,"description":11,"extension":14,"meta":301,"name":302,"navigation":17,"path":303,"readingTime":19,"seo":304,"stem":305,"__hash__":306},"authors\u002Fauthors\u002Fvictor-borg.md",{"type":8,"value":298,"toc":299},[],{"title":11,"searchDepth":12,"depth":12,"links":300},[],{},"Victor Borg","\u002Fauthors\u002Fvictor-borg",{"title":6,"description":11},"authors\u002Fvictor-borg","-Za-JweoiP6hyclue_WkxMXdRUDTczPGlJf6AZckjUc",{"id":308,"title":6,"body":309,"description":11,"extension":14,"meta":313,"name":314,"navigation":17,"path":315,"readingTime":19,"seo":316,"stem":317,"__hash__":318},"authors\u002Fauthors\u002Fvirgil-roger.md",{"type":8,"value":310,"toc":311},[],{"title":11,"searchDepth":12,"depth":12,"links":312},[],{},"Virgil Roger","\u002Fauthors\u002Fvirgil-roger",{"title":6,"description":11},"authors\u002Fvirgil-roger","DfVFe5j0bCgXeEr381ZYOM5DP4m-pWb93J9-m_muKJ0",{"id":320,"title":6,"body":321,"description":11,"extension":14,"meta":325,"name":326,"navigation":17,"path":327,"readingTime":19,"seo":328,"stem":329,"__hash__":330},"authors\u002Fauthors\u002Fyukan-zhao.md",{"type":8,"value":322,"toc":323},[],{"title":11,"searchDepth":12,"depth":12,"links":324},[],{},"Yukan Zhao","\u002Fauthors\u002Fyukan-zhao",{"title":6,"description":11},"authors\u002Fyukan-zhao","LRPHugtAJnWHsmHxy9_SR5Zas_C5p-GR_uHEs1Fhk_E",{"id":332,"title":333,"author":334,"body":335,"date":899,"description":900,"extension":14,"lang":901,"meta":902,"navigation":17,"path":903,"published":17,"readingTime":507,"seo":904,"stem":905,"tags":906,"__hash__":910},"articles\u002Farticles\u002F2026-05-05-conception-d-une-fsm-maison.md","Conception d'une FSM* maison","horeb-parraud",{"type":8,"value":336,"toc":889},[337,342,352,370,374,377,380,394,398,401,417,420,423,427,438,441,583,590,608,611,700,703,706,783,791,797,800,815,829,841,860,864,867,874,885],[338,339],"img",{"src":340,"alt":341},"\u002Fimages\u002Fconception-d-une-fsm-maison\u002Fcoupe-crane.jpeg","Schéma de la coupe d'un crâne",[343,344,346,347,351],"h2",{"id":345},"cest-quoi-une-finite-state-machine-fsm","C'est quoi une ",[348,349,350],"em",{},"finite state machine (FSM)"," ?",[353,354,355,356,363,364,369],"p",{},"Une FSM (ou machine à états finis 🇫🇷) est une\n",[357,358,362],"a",{"href":359,"rel":360},"https:\u002F\u002Ffr.wikipedia.org\u002Fwiki\u002FAutomate_fini",[361],"nofollow","modélisation informatique"," qui permet, à partir d’un\nensemble d’états et de transitions, de connaître l’état courant, d’identifier la prochaine étape\npossible et de mémoriser la progression, comme un GPS. On peut aussi parler de\n",[357,365,368],{"href":366,"rel":367},"https:\u002F\u002Frefactoring.guru\u002Fdesign-patterns\u002Fstate",[361],"State Pattern",".",[343,371,373],{"id":372},"contexte","Contexte",[353,375,376],{},"Chez Indy, plusieurs parcours utilisateur reposent sur des formulaires, comme l’inscription ou le\nparamétrage du compte. Les réponses y sont enregistrées progressivement, certaines questions peuvent\ndépendre du contexte, que ce soit des données extérieures ou simplement les réponses précédentes.\nL’utilisateur doit pouvoir reprendre un formulaire là où il l’avait laissé.",[353,378,379],{},"Historiquement, les formulaires n’avaient pas grand-chose en commun dans leur fonctionnement ou leur\nimplémentation.",[353,381,382,383,387,388,393],{},"Lors de l’ajout du formulaire de souscription client, l’objectif n’était pas seulement d’ajouter un\nformulaire de plus, mais de factoriser cette logique pour pouvoir en construire, modifier et\nmaintenir facilement plusieurs. En modélisant le problème,\n",[357,384,386],{"href":366,"rel":385},[361],"la machine à états est vite apparue comme un pattern naturel",".\nAprès tout, un formulaire de ce type n’est qu’une suite d’étapes, avec des règles de passage, un\nétat courant et un contexte persistant. C’est donc tout naturellement que nous avons commencé par\nexplorer des solutions dédiées à ce type de modélisation. Mais les librairies externes envisagées,\ncomme ",[357,389,392],{"href":390,"rel":391},"https:\u002F\u002Fstately.ai\u002Fdocs\u002Fxstate",[361],"XState",", ne sont pas adaptées à notre besoin, souvent peu\nconfigurables ou parfois trop “usine à gaz”.",[343,395,397],{"id":396},"définir-les-specs","Définir les specs",[353,399,400],{},"Le cahier des charges est simple pour permettre une adoption totale et homogénéiser le\nfonctionnement des formulaires :",[402,403,404,408,411,414],"ul",{},[405,406,407],"li",{},"facile d’ajouter \u002F modifier \u002F supprimer une question (évolution fréquente des formulaires)",[405,409,410],{},"une question peut avoir une condition d’affichage (question précédente, élément externe)",[405,412,413],{},"si on quitte le formulaire, on revient à la même question",[405,415,416],{},"pouvoir revenir en arrière pour modifier une réponse",[353,418,419],{},"En résumé, on veut un formulaire, avec des questions (états), conditionnable (contexte), la\npossibilité de quitter et de revenir (état courant), et amplement modulable.",[353,421,422],{},"Pour limiter le coût initial de développement et limiter au maximum la dette technique, il est aussi\ndéfini que des fonctionnalités plus avancées, comme la mise en cache des réponses en cas de retour\nen arrière, seraient déléguées au consommateur du module.",[343,424,426],{"id":425},"conceptuellement-ça-donne-quoi","Conceptuellement ça donne quoi ?",[353,428,429,430,434,435,369],{},"Notre machine à états est volontairement minimaliste : une ",[431,432,433],"strong",{},"liste ordonnée\nd’étapes"," et un ",[431,436,437],{},"contexte persistant",[353,439,440],{},"Chaque étape embarque ses propres règles. Elle sait si elle est disponible, si elle est déjà\ncomplétée, comment enregistrer une réponse dans le contexte, comment l’annuler, et quel format de\ndonnées elle accepte. De son côté, le contexte n’a besoin que de pouvoir être lu et mis à jour.",[442,443,447],"pre",{"className":444,"code":445,"language":446,"meta":11,"style":11},"language-ts shiki shiki-themes github-light github-dark","state = {\n    key,\n    payloadSchema,\n    isAvailable(context),\n    isCompleted(context),\n    setContext(context, payload),\n    unsetContext(context),\n};\n\ncontextStore = {\n    get(),\n    set(context),\n};\n","ts",[448,449,450,465,470,476,493,505,523,535,541,547,557,566,578],"code",{"__ignoreMap":11},[451,452,454,458,462],"span",{"class":453,"line":19},"line",[451,455,457],{"class":456},"sVt8B","state ",[451,459,461],{"class":460},"szBVR","=",[451,463,464],{"class":456}," {\n",[451,466,467],{"class":453,"line":12},[451,468,469],{"class":456},"    key,\n",[451,471,473],{"class":453,"line":472},3,[451,474,475],{"class":456},"    payloadSchema,\n",[451,477,479,483,486,490],{"class":453,"line":478},4,[451,480,482],{"class":481},"sScJk","    isAvailable",[451,484,485],{"class":456},"(",[451,487,489],{"class":488},"s4XuR","context",[451,491,492],{"class":456},"),\n",[451,494,496,499,501,503],{"class":453,"line":495},5,[451,497,498],{"class":481},"    isCompleted",[451,500,485],{"class":456},[451,502,489],{"class":488},[451,504,492],{"class":456},[451,506,508,511,513,515,518,521],{"class":453,"line":507},6,[451,509,510],{"class":481},"    setContext",[451,512,485],{"class":456},[451,514,489],{"class":488},[451,516,517],{"class":456},", ",[451,519,520],{"class":488},"payload",[451,522,492],{"class":456},[451,524,526,529,531,533],{"class":453,"line":525},7,[451,527,528],{"class":481},"    unsetContext",[451,530,485],{"class":456},[451,532,489],{"class":488},[451,534,492],{"class":456},[451,536,538],{"class":453,"line":537},8,[451,539,540],{"class":456},"};\n",[451,542,544],{"class":453,"line":543},9,[451,545,546],{"emptyLinePlaceholder":17},"\n",[451,548,550,553,555],{"class":453,"line":549},10,[451,551,552],{"class":456},"contextStore ",[451,554,461],{"class":460},[451,556,464],{"class":456},[451,558,560,563],{"class":453,"line":559},11,[451,561,562],{"class":481},"    get",[451,564,565],{"class":456},"(),\n",[451,567,569,572,574,576],{"class":453,"line":568},12,[451,570,571],{"class":481},"    set",[451,573,485],{"class":456},[451,575,489],{"class":488},[451,577,492],{"class":456},[451,579,581],{"class":453,"line":580},13,[451,582,540],{"class":456},[353,584,585,586,589],{},"À partir de là, on reste sur un fonctionnement volontairement très léger. On ne stocke pas l’étape\ncourante comme une vérité absolue : on la ",[431,587,588],{},"recalcule"," à partir du contexte et de la liste des\nétapes :",[402,591,592,599,605],{},[405,593,594,595,598],{},"l’",[431,596,597],{},"état courant"," est la première étape disponible encore incomplète ;",[405,600,594,601,604],{},[431,602,603],{},"état précédent"," est la dernière étape déjà validée ;",[405,606,607],{},"si aucune étape ne reste à compléter, alors le parcours est terminé.",[353,609,610],{},"Lorsqu’un utilisateur interagit avec un formulaire, on suit toujours la même séquence :",[442,612,614],{"className":444,"code":613,"language":446,"meta":11,"style":11},"context = contextStore.get()\ncurrentState = getCurrentState(context, states)\n\nsubmit(stateKey, payload):\n    vérifier que stateKey == currentState.key\n    valider payload avec payloadSchema\n    newContext = currentState.setContext(context, payload)\n    contextStore.set(newContext)\n",[448,615,616,632,645,649,657,668,673,689],{"__ignoreMap":11},[451,617,618,621,623,626,629],{"class":453,"line":19},[451,619,620],{"class":456},"context ",[451,622,461],{"class":460},[451,624,625],{"class":456}," contextStore.",[451,627,628],{"class":481},"get",[451,630,631],{"class":456},"()\n",[451,633,634,637,639,642],{"class":453,"line":12},[451,635,636],{"class":456},"currentState ",[451,638,461],{"class":460},[451,640,641],{"class":481}," getCurrentState",[451,643,644],{"class":456},"(context, states)\n",[451,646,647],{"class":453,"line":472},[451,648,546],{"emptyLinePlaceholder":17},[451,650,651,654],{"class":453,"line":478},[451,652,653],{"class":481},"submit",[451,655,656],{"class":456},"(stateKey, payload):\n",[451,658,659,662,665],{"class":453,"line":495},[451,660,661],{"class":456},"    vérifier que stateKey ",[451,663,664],{"class":460},"==",[451,666,667],{"class":456}," currentState.key\n",[451,669,670],{"class":453,"line":507},[451,671,672],{"class":456},"    valider payload avec payloadSchema\n",[451,674,675,678,680,683,686],{"class":453,"line":525},[451,676,677],{"class":456},"    newContext ",[451,679,461],{"class":460},[451,681,682],{"class":456}," currentState.",[451,684,685],{"class":481},"setContext",[451,687,688],{"class":456},"(context, payload)\n",[451,690,691,694,697],{"class":453,"line":537},[451,692,693],{"class":456},"    contextStore.",[451,695,696],{"class":481},"set",[451,698,699],{"class":456},"(newContext)\n",[353,701,702],{},"Ce fonctionnement apporte trois garanties : on ne peut soumettre que l’étape en cours, on ne peut\nrevenir en arrière que sur la dernière étape complétée, chaque donnée envoyée est validée avant\nd’être enregistrée.",[353,704,705],{},"C’est ce qui rend l’approche intéressante : notre step machine reste générique, tandis que\nl’intelligence du parcours est portée par les étapes elles-mêmes. Ajouter ou modifier un formulaire\nrevient alors surtout à décrire de nouvelles étapes, sans avoir à complexifier le cœur de la\nmachine.",[442,707,711],{"className":708,"code":709,"language":710,"meta":11,"style":11},"language-mermaid shiki shiki-themes github-light github-dark","flowchart TB\n    A[Contexte courant] --> B[Filtrer les étapes disponibles\u003Cbr\u002F>isAvailable]\n    B --> C[Trouver l'étape courante\u003Cbr\u002F>1re disponible + incomplète]\n    C --> D{Action utilisateur}\n    D -->|Soumettre| E[Valider payload\u003Cbr\u002F>JSON Schema]\n    E --> F[setContext]\n    D -->|Annuler| G[Étape précédente]\n    G --> H[unsetContext]\n    F --> I[Sauvegarder le contexte]\n    H --> I\n    I --> A\n\n    classDef highlight fill:#FFE8A3,stroke:#B7791F,stroke-width:2px,color:#1A202C;\n    class A highlight;\n","mermaid",[448,712,713,718,723,728,733,738,743,748,753,758,763,768,772,777],{"__ignoreMap":11},[451,714,715],{"class":453,"line":19},[451,716,717],{"class":456},"flowchart TB\n",[451,719,720],{"class":453,"line":12},[451,721,722],{"class":456},"    A[Contexte courant] --> B[Filtrer les étapes disponibles\u003Cbr\u002F>isAvailable]\n",[451,724,725],{"class":453,"line":472},[451,726,727],{"class":456},"    B --> C[Trouver l'étape courante\u003Cbr\u002F>1re disponible + incomplète]\n",[451,729,730],{"class":453,"line":478},[451,731,732],{"class":456},"    C --> D{Action utilisateur}\n",[451,734,735],{"class":453,"line":495},[451,736,737],{"class":456},"    D -->|Soumettre| E[Valider payload\u003Cbr\u002F>JSON Schema]\n",[451,739,740],{"class":453,"line":507},[451,741,742],{"class":456},"    E --> F[setContext]\n",[451,744,745],{"class":453,"line":525},[451,746,747],{"class":456},"    D -->|Annuler| G[Étape précédente]\n",[451,749,750],{"class":453,"line":537},[451,751,752],{"class":456},"    G --> H[unsetContext]\n",[451,754,755],{"class":453,"line":543},[451,756,757],{"class":456},"    F --> I[Sauvegarder le contexte]\n",[451,759,760],{"class":453,"line":549},[451,761,762],{"class":456},"    H --> I\n",[451,764,765],{"class":453,"line":559},[451,766,767],{"class":456},"    I --> A\n",[451,769,770],{"class":453,"line":568},[451,771,546],{"emptyLinePlaceholder":17},[451,773,774],{"class":453,"line":580},[451,775,776],{"class":456},"    classDef highlight fill:#FFE8A3,stroke:#B7791F,stroke-width:2px,color:#1A202C;\n",[451,778,780],{"class":453,"line":779},14,[451,781,782],{"class":456},"    class A highlight;\n",[343,784,786,787,790],{"id":785},"finite-step-machine-quand-les-étapes-remplacent-les-états","Finite ",[348,788,789],{},"Step"," Machine : quand les étapes remplacent les états",[353,792,793,794],{},"Cette réflexion est née en rédigeant cet article : ",[348,795,796],{},"« C’est toujours une finite state machine ? »",[353,798,799],{},"Et la réponse est… non, pas vraiment !",[353,801,802,803,806,807,810,811,814],{},"Une ",[431,804,805],{},"machine à états classique"," modélise des ",[431,808,809],{},"états métier"," et les ",[431,812,813],{},"transitions"," qui les\nrelient. Chaque état y représente une situation stable du système, et chaque événement déclenche une\ntransition vers un nouvel état défini et prévisible.",[353,816,817,818,821,822,825,826,828],{},"Dans notre cas, nous ne manipulons pas des ",[348,819,820],{},"états"," au sens strict. Nous gérons plutôt des ",[431,823,824],{},"étapes\nde parcours"," qui contribuent à faire évoluer un ",[348,827,372],{}," dynamique, sans pour autant incarner un\n« état atteignable ».",[353,830,831,832,837,838,840],{},"Notre implémentation s’apparente plus à un\n",[357,833,836],{"href":834,"rel":835},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWizard_(software)",[361],"wizard-pattern"," qu’à une FSM classique. Le\nmoteur ne décrit pas un graphe complexe de transitions : il parcourt une ",[431,839,433],{},", en déterminant dynamiquement la suivante en fonction du contexte.",[353,842,843,844,847,848,851,852,859],{},"Nous nous sommes inspirés des ",[348,845,846],{},"finite state machines",", mais pour en proposer une version ",[431,849,850],{},"linéaire,\npragmatique et adaptée à nos besoins."," Une approche que nous aimons, avec un brin de mauvaise foi,\nsurnommer ",[348,853,854,855,858],{},"finite ",[431,856,857],{},"step"," machine"," (donc bien une FSM* 😛 !).",[343,861,863],{"id":862},"conclusion","Conclusion",[353,865,866],{},"Notre besoin était simple. Dans ce contexte, construire une FSM minimaliste en interne était plus\npertinent que d’adopter une grosse librairie, avec son coût d’apprentissage et sa complexité\nd’usage.",[353,868,869,870,873],{},"Après deux années d’utilisation, le constat reste le même : cette ",[348,871,872],{},"finite step machine"," répond\ntoujours à notre besoin, s’adapte à des parcours variés, et n’a pratiquement nécessité aucune\nmaintenance.",[875,876,877],"blockquote",{},[353,878,879],{},[348,880,881,882,884],{},"« Même la plus petite ",[451,883,857],{}," peut changer le cours de l’avenir. » Galadriel, Le Seigneur des\nanneaux : La Communauté de l’anneau",[886,887,888],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":11,"searchDepth":12,"depth":12,"links":890},[891,893,894,895,896,898],{"id":345,"depth":12,"text":892},"C'est quoi une finite state machine (FSM) ?",{"id":372,"depth":12,"text":373},{"id":396,"depth":12,"text":397},{"id":425,"depth":12,"text":426},{"id":785,"depth":12,"text":897},"Finite Step Machine : quand les étapes remplacent les états",{"id":862,"depth":12,"text":863},"2026-05-05","Comment concevoir une finite step machine minimaliste pour piloter des formulaires dynamiques, persistants et faciles à faire évoluer.","fr",{},"\u002Farticles\u002F2026-05-05-conception-d-une-fsm-maison",{"title":333,"description":900},"articles\u002F2026-05-05-conception-d-une-fsm-maison",[907,908,909],"Tech","Architecture","UX","9Qgy0-Apx8z_7mkNjqy9WyldT70eNTtqECVLZTR6WQ4",1778159243192]