Statut : actif · Dernière revue : 31 octobre 2026 · Portée : ce dépôt (
phpurs/phpurs: compilateur, CLI, tests, docs, outils). Hors portée : les dépôts frèresphpurs-*et le fork PBO, sauf lorsqu’un contrat les traverse (chantier M7).
Ce document est le backlog unique de la maintenabilité. Les rapports de
audit/ sont des archives de mesures : ils documentent le passé, ils ne
pilotent plus les priorités. Une tâche = une PR ; la case est cochée dans la PR
qui la ferme, avec la date.
- Priorités :
P0bloque le reste,P1nécessaire à l’objectif,P2amélioration. - Effort : ordre de grandeur en jours-homme, pour un mainteneur qui connaît déjà le dépôt.
- Statut :
[ ]à faire,[~]en cours,[x]fait,[!]bloqué (motif écrit à côté),[-]abandonné (décision écrite). - Les identifiants (
M1-01…) sont stables : ils servent de référence dans les PR, les issues et les ADR. - Une tâche n’est pas terminée tant que code + test + documentation ne sont pas dans la même PR.
Un humain qui n’a pas écrit le code doit pouvoir, sans aide :
- Builder et tester un clone en moins de 15 minutes, avec une seule commande et un résultat non ambigu ;
- localiser en moins de 10 minutes le module et la fonction responsables d’un comportement observé ;
- ajouter une passe d’optimisation en moins d’une journée, en copiant un modèle documenté, sans toucher aux cinq autres passes ;
- modifier un contrat FFI (représentation, arité, annulation) sans casser les bibliothèques sœurs, et être averti au build quand il le fait ;
- comprendre pourquoi une décision existe (ordre des passes, budget, ABI, clé de cache) sans lire l’historique Git ni les rapports d’audit ;
- être arrêté par la CI dès qu’il casse un invariant, plutôt qu’à l’exécution d’une application en production.
Mesures de départ au 31 octobre 2026, cibles à atteindre à la fin du plan.
| Indicateur | Départ | Cible |
|---|---|---|
| Modules > 400 lignes | 0 | 0 |
| Fonctions > 60 lignes | 16 | 0 hors exceptions listées |
Modules avec en-tête -- | |
81 / 81 | 81 / 81 |
| Passes avec fiche (preuve, repli, budget, test) | 11 / 11 | 11 / 11 |
PhpRaw "" (sentinelle) |
0 | 0 |
Constructions PhpRaw |
4 (documentées) | 0 hors exceptions documentées (≤ 4) |
Seuils numériques hors module Budget |
0 (src/Phpurs) | 0 |
| Sorties PHP figées (golden) | 7 cas / 7 fichiers | 12 cas, diff octet |
| Tests exécutés | 149 | ≥ 200 |
| CI | workflow complet, pas encore exécuté | obligatoire sur main, < 20 min |
| Docs | 27 fichiers, 1 langue (docs), compiler.md 116 l. |
index, hub ≤ 250 l. |
| Commits « misc » | 168 / ~176 | 0 nouveau |
npm run check |
local seulement | vert sur clone propre + CI |
- Performance : aucun nouvel audit de performance, aucune comparaison de
temps dans les critères d’acceptation. L’archive reste
audit/2026-10-20/todo-archive/performance-backlog-2026-10-20.md. - Nouvelles optimisations : ce plan ne change pas les algorithmes ; il rend ceux qui existent lisibles et vérifiables.
- Réécriture du runtime PHP ou changement d’ABI non versionné.
- Suppression des audits historiques ou réécriture de l’historique Git.
- Migration de langage/outillage (le fork PBO et
purs-tidyrestent).
Ces règles s’appliquent à toutes les tâches ; une PR qui les viole est refusée.
- Comportement observable préservé. À entrée identique, la sortie PHP, les manifestes et les erreurs restent identiques, octet pour octet, sauf tâche explicitement contractuelle (M7). Le golden (M1-03) le prouve.
- Déplacer ≠ changer. Une extraction ou un renommage ne modifie ni l’algorithme, ni l’ordre d’évaluation, ni les budgets. Un changement de comportement a sa propre PR.
- Une PR = une tâche. Diff ≤ 400 lignes hors déplacements ; sinon découper.
- Tests et docs dans la PR. Pas de « documentation dans un suivi ».
- Pas d’abstraction à un seul usage. Une factorisation n’est introduite qu’avec au moins deux appelants réels (règle déjà en vigueur).
- Budgets = garde-fous, pas des réglages de performance. Déplacer une constante ne change pas sa valeur ; la changer exige un test de frontière et une entrée d’ADR.
- Tout
PhpRawnouveau est justifié par un commentaire et couvert par un test ; il ne peut pas être vide ni servir d’opérande. - Documentation en une langue (voir ADR-005) ; toute extraction met à jour le pointeur d’architecture dans la même PR.
- Commits conventionnels :
type(scope): résuméavectype∈feat|fix|refactor|docs|test|build|perf|choreetscope∈passes|codegen|printer|ast|ffi|runtime|cache|cli|docs|tests|tools. - La CI est la seule autorité. Un test vert en local ne remplace pas la CI ; un test instable est corrigé ou supprimé, jamais ignoré.
| Élément | Valeur |
|---|---|
| Modules PureScript | 81 (src/), 9 884 lignes |
| FFI JavaScript | 13 fichiers, tous documentés dans docs/ffi-boundaries.md |
| Plus gros modules | CodeGen/Expr 752, EnumRegions 516, Printer/Expr 513, StateFusion 432, ArrayRefs/Analysis 363 |
| Fonctions les plus longues | translateSyntax 475 l., printExprWith 142, TailInline.optimize 131, translateWithContracts 121, Main.compile 103 |
| Tests | 166 réussis (60 direct+FFI ; 106 CLI) |
| Format | npm run format:check ✅ |
| Build | spago build ✅, 0 avertissement, 0 erreur |
| Docs | 27 fichiers, tous en anglais (hors todo.md) ; compiler.md 116 lignes |
| Modules sans en-tête | 0 / 61 |
| CI | workflow ajouté, pas encore exécuté |
| Historique | 168 commits « misc » sur ~176 |
| Audits suivis | 7,7 Mo, 10 dossiers datés |
| Scratch non suivi | tests/runner 432 Mo, output/ 98 Mo, .purmeta/ 54 Mo |
Le build et les tests ci-dessus ont été rejoués le 31 octobre 2026 sur l’arbre
courant. Note d’environnement : spago build échoue avec
unable to open database file si le cache global Spago
(~/Library/Caches/spago-nodejs sur macOS, emplacement XDG sur Linux) n’est pas
inscriptible ; ce n’est pas un défaut du dépôt, mais cela doit être documenté
pour la CI et les environnements sandboxés.
- Formatage automatique et versions épinglées (
purs-tidy, Prettier, plugin PHP). - Frontières FFI JavaScript décrites entrée/sortie/erreur dans
docs/ffi-boundaries.md. - Traversée d’AST avec frontières de portée explicites dans
PhpAst/Traversal.purs(choixAllScopes/LocalScope, match exhaustif). - Une suite de tests par passe, avec refus, replis, budgets, idempotence et ordre d’évaluation.
- Invariants du pipeline écrits dans
docs/architecture.md. - Zéro
unsafeCoerce,unsafeCrashWith,Debug.todoouunsafePartialdanssrc/(vérifié le 31 octobre 2026). - Noms runtime centralisés dans
RuntimeNames.purs. .gitignorecouvreoutput/,.purmeta/,bin/phpurs.js,tests/runner/*.- Cache de modules versionné et checksumé, avec tests de corruption.
- 146 tests et 0 avertissement de build : la base est saine, le problème est l’organisation et la garantie de cet état.
Le nettoyage d’octobre 2026 a déjà : découpé CodeGen, ArrayRefs, Printer
et Main en sous-modules ; centralisé les noms runtime ; introduit PhpNull ;
classé les tests en trois familles ; documenté l’architecture et les frontières
JS ; unifié les runners de bibliothèques. Voir
audit/2026-10-20/cleanup-closeout/report.md
et les bilans voisins (codegen-*, arrayrefs-*, printer-split,
main-*, runtime-*, test-families).
Le présent plan reprend après ces extractions : il ne propose pas de redécouper ce qui l’est déjà.
Chaque friction est datée et reliée à un chantier.
| ID | Friction | Preuve | Impact | Chantier |
|---|---|---|---|---|
| F1 | Dispatch géant | translateSyntax : 475 lignes, 24 cas, dans un module de 752 lignes ; printExprWith 142 l. |
Lecture et revue difficiles ; conflits de merge ; impossible de tester une famille de syntaxe isolément | M3 |
| F2 | Protocole de passes dupliqué 5× | Même garde bindings > 256, même collecte des groupes récursifs à un binding, même runState { fuel: 32768 }, mêmes préfixes ; Result nommé seulement dans StateFusion |
Toute évolution (stats, budget, ordre) doit être répliquée 6 fois ; risque de divergence silencieuse | M2 |
| F3 | Budgets dispersés | Résolu le 31/10 : nodeBudget, workerBudget, fuel, profondeurs et largeurs vivent dans Phpurs/Budget.purs ; aucune passe n’en code |
Le risque de divergence des seuils est supprimé | M2-03 |
| F4 | Helpers dupliqués | require dans Optimize.NeutralExpr et EnumRegions ; bounded pour NeutralExpr et pour PhpExpr ; size vs countNodes |
Mêmes noms, sémantiques différentes ; unification déplacée en M3 avec les découpages | M3 |
| F5 | Invariant AST violé | Résolu le 31/10 : PhpUnaryOp remplace les 4 encodages PhpBinOp "op" (PhpRaw "") expr |
Plus d’enfant fantôme ; les traversées voient un seul opérande | M4-01 |
| F6 | PhpRaw polymorphe |
Résolu le 31/10 : 4 PhpRaw documentés (gardes et template Array, message d’erreur) ; appels, $GLOBALS et unset ont des constructeurs typés |
La règle est écrite dans docs/compiler.md |
M4-03 |
| F7 | Docs non maintenables | Résolu le 31/10 : docs en anglais, source map générée, hub compiler.md à 116 l. et sous-pages docs/compiler/*. |
Le hub et la source map sont vérifiés en CI | M5 |
| F8 | Couverture inégale | Résolu le 31/10 : tests directs consolidés (M6-02), 5 suites de propriétés et couverture c8 à 98,6 %. |
Les contrats critiques sont testés et mesurés | M6 |
| F9 | CI non activée | Résolu le 31/10 : workflow check vert sur main ; protection de branche non requise par décision du mainteneur. |
La CI tourne sur chaque push, sans blocage de merge | M1 |
| F10 | Dépendance PBO par chemin relatif | Résolu le 31/10 : tools/toolchain.json, check:toolchain, bin/setup clone la révision épinglée ; premier run CI vert. |
Dépendance reproductible | M8-02 |
| F11 | Hygiène du dépôt | Résolu le 31/10 : CONTRIBUTING, CHANGELOG, engines, prepublishOnly, npm run clean, rétention des audits et flake Nix vérifié |
Release outillée | M8 |
| F12 | Décisions non écrites | Résolu le 31/10 : ADR-001 à 007 dans docs/decisions/ |
Le « pourquoi » est consultable | M9 |
Objectif : rendre l’état « vert » vérifiable par une machine, sur chaque PR.
- M1-01 — CI
check(P0, 1–2 j). Fait (31/10). Workflow vert surmain(badgecheck: passing) après checkout du fork PBO et des paquetsphpurs-prelude/phpurs-foldable-traversable/phpurs-arraysaux révisions épinglées,npm ci, build et tests. Échec éventuel : étapes nommées, logcodegen-tests.logpublié en artefact..github/workflows/check.yml: Node 22, PHP 8.4,npm ci, checkout du fork PBO au chemin attendu à une révision épinglée (dans le workflow d’abord, puis danstools/toolchain.jsonen M8-02), puisnpm run check(format → build + bundle → tests). Cache npm et cache Spago (chemin selon l’OS). Journaux et répertoires temporaires publiés en cas d’échec. Fait (31/10) : layout miroir (phpurs/phpurs+purescript-backend-optimizer-phpurs+ paquetsphpurs-*), caches npm/Spago,check:toolchain, premier run vert et étapes nommées. - M1-02 —
npm run checkcomplet (P0, 0,5 j). Ajoutercheck:conventions,check:docs,test:goldenau fur et à mesure que M1-03/M1-04/M1-05 existent. Les scripts ne doivent jamais masquer un code de sortie (pas de| taildans les scripts npm ;set -o pipefaildans les scripts bash). Fait (31/10) :check:conventions,check:docs,check:shellettest:goldenbranchés dansnpm run check; aucun script ne masque un code de sortie (set -euo pipefaildans les entrées shell). - M1-03 — Corpus golden (P0, 2 j).
tests/golden/cases/*(entrées CoreFn/FFI minimales) ettests/golden/expected/*(PHP + manifestes). 12 cas : ADT simple, région scalaire, constructeur nullable, record + update, TCO, Aff, FFI, modular, bundle, cache froid, cache chaud, sans main.npm run test:goldencompare octet pour octet ;npm run golden:updaterégénère pour une PR qui change volontairement la sortie. Fait (31/10) : harnaistests/golden/run.mjs(diff ligne à ligne, détection des fichiers orphelins) et 12 cas / 13 fichiers :adt,tco,records,strings,numbers,arrays,branches,nullable,ffi,runtime(Aff/promise),entrypointsetbundle. Cache froid/chaud et manifestes restent couverts par les tests CLI (module-cache.mjs,cache-cli.mjs,package-runner.mjs) plutôt que par un rendu pur. - M1-04 —
tools/check-conventions.mjs(P0, 1,5 j). Vérifie : en-tête-- |sur chaque module ; module ≤ 400 lignes ; fonction ≤ 60 lignes ; matrice d’imports autorisés par couche ;PhpRaw ""interdit ; seuils numériques interdits horsPhpurs.Budget(mode avertissement avant M2-03, bloquant ensuite). Sortiefichier:ligne: règle: message. Fait (31/10) : en-têtes, tailles, sentinelles, imports, règle « aucun seuil numérique horsPhpurs.Budget» et baseline ratchet (tools/conventions-baseline.json). Dette restante : 4 modules et 16 fonctions ; en-têtes et sentinelles à zéro. - M1-05 —
tools/check-docs.mjs(P1, 1 j). Fait (31/10).check:docsvérifie liens, ancres, index (avertissement jusqu’à M5-02), taille du hubcompiler.md(avertissement) et mélange de langues._ Liens relatifs et ancres valides,docs/index.mdà jour (créé en M5-02),compiler.md≤ 250 lignes, pas de mélange de langues dans un même document. - M1-06 — Shellcheck (P2, 0,5 j). Fait (31/10).
tools/check-shell.sh:bash -nsur les 5 entrées,shellcheck -xquand il est présent,SHELLCHECK_REQUIRED=1en CI._shellcheck -x bin/*etbash -nen CI ; corriger les scripts concernés. - M1-07 — Templates GitHub (P1, 0,5 j). Fait (31/10).
Template de PR + issues « pass bug », « FFI contract », « documentation »._
PR (checklist : invariant, golden, test, doc, ADR si décision), issues
(bug de passe, contrat FFI, documentation),
CODEOWNERSsi plusieurs mainteneurs. - [-] M1-08 — Protection de branche (P1, 0,2 j, manuel). Abandonné (31/10). Décision du mainteneur : pas de protection de branche pour l’instant. La CI reste informative sur chaque push ; à rouvrir si une revue obligatoire ou un merge protégé devient nécessaire.
Acceptation : une PR ouvre la CI en moins de 20 minutes ; un module de 450 lignes, un module sans en-tête ou une sortie PHP modifiée échouent ; le résultat est reproductible sur un clone propre.
Objectif : une passe = un module court, un contrat explicite, un budget nommé.
Le protocole Result unifié couvre les passes qui transforment le TAST
(6 passes) ; les passes qui transforment l’AST PHP (TailInline,
Nullable.lower, CopyCleanup, ArrayRefs, CompactLoops) gardent leur
signature mais reçoivent la même fiche et les mêmes budgets.
- M2-01 — Module
Phpurs.Pass(P0, 1,5 j). Fait (31/10).Phpurs.PassfournitResult,Pass,unchanged,smallModule(256 bindings / 64 par groupe),scanGroups(budget de scan 8 192),scanNodesetprivateName.PartialBindingsmigré sans changement de sortie ; test directtests/codegen/direct/pass-protocol.mjs.type Result r = { module_ :: BackendModule, privateNames :: Set Ident | r },type Pass r = BackendModule -> Result r,unchanged,guardModule(seuils documentés : 256 bindings, 64 par groupe, 128 dataDecls, 64 constructeurs/champs),scanBindings(traverse les groupes, applique le seuil de scan),copyWorker(préfixe__phpurs_*, nom privé, recopie),freshPrefix. - M2-02 — Migrer les 4 passes restantes (P0, 2 j). Fait (31/10).
ThunkFusion,StateFusion,CallbackSpecialization,ArrayCallbacksutilisentsmallModule,scanGroupsetprivateName;EnumRegionsconserve son résultat étendu et utilisescanGroupsWithin(son compteursize <= nodeBudgetest préservé) +privateName. Aucun changement de sortie : golden identique, 147 tests verts. Baseline resserrée (EnumRegions516→511,StateFusion432→422). - M2-03 —
Phpurs.Budget(P0, 1 j). Constantes nommées et justifiées :proofNodes(1024),scanNodes(8192),stateFuel(32768),workerLimit(32),moduleBindings(256),groupBindings(64),dataDecls(128),depth,width. Supprimer les littéraux8192,32768,256,64des passes ; fusionnerrequire/bounded/size/countNodesderrière des noms distincts (requirepourMaybe Unit,withinNodespourNeutralExpr,withinPhpNodespourPhpExpr,nodeCountunique). _Fait (31/10) :Phpurs.Budgetcouvre les passes TAST,NullableConstructors,CopyCleanup,TailInline,CompactLoopset le helperbounded; aucun littéral de budget ne reste hors dePhpurs/Budget.pursdanssrc/Phpurs. Fait (31/10, suite) :requireCheck,regionNodeCountetboundedPhpnomment les variantes locales ;requireetboundeddocumentent leur portée dansOptimize.NeutralExpr. - M2-04 — Pipeline tabulaire + statistiques (P1, 2 j). Fait (31/10).
CodeGen/Pipeline: table des 6 passes TAST (name,precondition,budget,fallback,run),runordonné,PassStatsagrégé ettranslateWithStats. La sortie--verboseaffichepasses: proved/unchanged/refused. Les refus (budget/preuve) restent à 0 tant qu’une passe ne les expose pas ;translateWithStats(131 l.) est l’exception de fonction documentée. - M2-05 — Documenter le protocole (P1, 1 j). Fait (31/10).
docs/compiler/passes.md: fiches des 11 passes (preuve, budgets, repli, test), liées depuiscompiler.md._docs/compiler/passes.md: cycle de vie d’une passe, invariants, recette « ajouter une passe », recette « tester un refus », et une fiche pour chacune des 11 passes (y compris les 5 passes sur l’AST PHP).
Acceptation : ajouter une passe = un module d’environ 30 lignes plus une
entrée dans la table ; sortie PHP identique (golden) ; PassStats explique
pourquoi une passe ne s’applique pas à un module.
Objectif : aucun module au-dessus de 400 lignes, aucune fonction au-dessus de 60 lignes, sauf exception écrite.
- M3-01 —
CodeGen/Expr(P1, 3 j). Fait (31/10).Expr/Support(169 l.),Terms(270 l.),Statements(304 l.),Prims(151 l.),Update(144 l.) ; la façadeExprfait 80 lignes ettranslateSyntax37 lignes (473 → 37, cible ≤ 40). Toutes les fonctions de famille sont ≤ 80 lignes (maxtranslateLetRec72). Golden et 149 tests identiques.translateBranch(69) ettryFusedUpdate(68) restent > 60 et sont dans la baseline. - M3-02 —
Printer/Expr(P1, 2 j). Fait (31/10).Printer/Context(18 l.),Names(50 l., FFI déplacée),Support(164 l. : captures, returns, switch,printCallparamétré par un renderer) etExpr(353 l.).printExprWith: 143 → 78 lignes ; golden et 149 tests identiques. - M3-03 —
EnumRegions(P1, 2 j). Fait (31/10).EnumRegions/Types(47 l.),Proof(inference + worker, 223 l.),Rewrite(context, scan, optimize, 303 l.) et une façade de 12 lignes.regionNodeCountvit dansTypes. Sortie identique (goldenadt). - M3-04 —
StateFusion(P1, 1,5 j). Fait (31/10).StateFusion/Types(13 l.),Prove(239 l.),Rewrite(174 l.) et une façadeStateFusion(45 l.) qui gardeResult,optimizeetworkerBudget. Sortie identique (golden, 149 tests). - M3-05 —
TailInline(P2, 1 j). Fait (31/10).TailInline/Proof(114 l. : nœuds, captures,leafNode,terminates,flattenCall) etTailInline/Rewrite(126 l. :simplifyReturns,mapLocals,rename,compactCopies). La façadeTailInline(161 l.) gardeoptimize(129 l., exception existante) et les budgets. - M3-06 —
ArrayRefs/Analysis(P1, 1,5 j). Fait (31/10).ArrayRefs/CallSites(89 l.),Decision(200 l.),Fixpoint(130 l.) ;Analysis(44 l.) agrège les 7 entrées publiques.docs/architecture.mdpointe vers les trois modules. - M3-07 — Exceptions de taille (P1, 0,2 j). Fait (31/10).
docs/conventions.mdne liste aucune exception de module depuis M3-01. Les exceptions de fonction sont dans la baseline.
Acceptation : check:conventions vert ; golden identique ; les pointeurs
de docs/architecture.md et la source map générée pointent vers les nouveaux
modules.
Objectif : l’AST ne contient plus de nœuds fantômes ni de convention implicite.
- M4-01 —
PhpUnaryOp(P1, 1 j). Fait (31/10).PhpUnaryOp String PhpExprremplace les 4 encodagesPhpBinOp "op" (PhpRaw "") expr.Traversal,Printer/Expr,CodeGen/Operators,CopyCleanup,TailInline,CompactLoopsetArrayRefs/Ownershiptraitent le nouveau nœud. Sortie imprimée inchangée (( ! $x), testunary-operators.mjs). - M4-02 — Invariants testés (P1, 0,5 j). Fait (31/10).
PhpRaw ""a disparu desrc/;check:conventionsle refuse et la dette correspondante est retirée de la baseline. Le testunary-operators.mjsvérifie le nœud, l’enfant unique, la traversée et l’impression. Reste : un test global « aucune AST générée n’imprime/* ERROR */» (M6). - M4-03 — Classer les
PhpRaw(P1, 1,5 j). Fait (31/10).PhpCall (PhpRaw "\\phpurs_*")→PhpDirectCallpour un global généré ouPhpNamedCallpour un nom de fonction littéral (count,\intdiv,\fmod, helpers runtime) ;$GLOBALS[...]→PhpGlobalSlot;unset(...)→PhpUnset. Restent 4PhpRawdocumentés : deux gardes de traversal Array, le templateforeachArray et le message d’erreur généré. Règle écrite dansdocs/compiler.md. - M4-04 — Exhaustivité de
Traversal(P1, 1 j). Fait (31/10).tests/codegen/direct/ast-traversal.mjscompare la table d’échantillons aux constructeurs exportés (41 formes), vérifiemapChildrenidentité, le mapping de chaque enfant direct et les frontièreslocalChildren. - [-] M4-05 — (optionnel)
newtype PhpName/PhpNamespace(P2, 2 j). Abandonné (31/10). M4-03 a introduitPhpNamedCall,PhpDirectCalletPhpGlobalSlot, qui lèvent les ambiguïtés nom/expression ; un nouveau type ne justifie pas la migration.
Acceptation : grep 'PhpRaw ""' = 0 ; ≤ 4 PhpRaw documentés ; test
d’exhaustivité ; sortie identique.
Objectif : une seule source de vérité par sujet, une langue, des liens testés.
- M5-01 — Langue et plan (P1, 3 j). Fait (31/10).
ADR-005 : anglais pour
README.mdetdocs/, français pourtodo.md.architecture.md,development.mdetffi-boundaries.mdtraduits ; le paragraphe français restant decompiler.mdl’est aussi. - M5-02 — Découper
compiler.md(P1, 3 j). Fait (31/10).docs/compiler/{pipeline,lowering,printing,emission,diagnostics}.md+ hubcompiler.md(116 lignes) +docs/index.mdglobal. Le README reste le hub applicatif (27 Ko) ; sa découpe engetting-started.mdest reportée à M5-02b si nécessaire. - M5-03 — Source map générée (P1, 2 j). Fait (31/10).
tools/docs-source-map.mjsgénèredocs/compiler/source-map.mddepuis les en-têtes ;check:source-mapéchoue si la page est périmée. La table manuelle decompiler.mdest remplacée par le lien (642 → 595 lignes). En-têtes de modules normalisés (-- | Responsabilité. Invariants. Budgets.),tools/docs-source-map.mjsproduit la table des modules et vérifie en CI (--check). Supprimer la table manuelle decompiler.md. - M5-04 — En-têtes manquants (P1, 0,5 j). Fait (31/10).
14 en-têtes ajoutés ; la baseline d’en-têtes est vide (61/61)._
Les 14 modules sans en-tête (liste mesurée :
CompactLoops,NativeCallbacks,StateFusion,NullableConstructors,ArrayRefs,EnumRegions,CopyCleanup,ForeignTraversals,ThunkFusion,CallbackSpecialization,ArrayCallbacks,TailInline,PartialBindings,Metrics). - M5-05 — ADR (P1, 1 j). Fait (31/10).
docs/decisions/: index, template et ADR-001 à 007._docs/decisions/+ template (contexte, décision, alternatives, conséquences, date, statut) + ADR-001 à 007 (voir M9). - M5-06 —
CONTRIBUTING.md(P1, 1,5 j). Fait (31/10). Setup, layout, recettes (passe, budget, AST, FFI, golden, déplacement) et règles de PR. Setup (dont le fork PBO), gates, conventions de commit, checklist de revue, recettes : ajouter une passe, ajouter un helper runtime, modifier l’ABI FFI, ajouter un golden, mettre à jour la doc. - M5-07 —
docs/conventions.md(P1, 1 j). Fait (31/10). Tailles, noms,PhpRaw, budgets, tests, docs, commits, exceptions._ Tailles, nommage (dontmodule_, imposé par PBO),PhpRaw, budgets, tests, langue, liens, exceptions.
Acceptation : check:docs vert ; compiler.md ≤ 250 lignes ; 59/59 modules
avec en-tête ; un nouveau contributeur suit CONTRIBUTING sans question.
Objectif : chaque contrat critique a un test nommé et une panne explicite.
- M6-01 — Matrice passe → contrats (P1, 1 j). Fait (31/10).
Table des 11 passes dans
tests/codegen/README.md(preuve, repli, budgets, test), pluspass-protocol.mjsetast-traversal.mjs. Danstests/codegen/README.md: pour chaque passe, les contrats couverts (preuve, repli, budget, idempotence, ordre d’évaluation, erreurs) et le fichier de test correspondant. Les cases vides deviennent des tâches. - M6-02 — Comboler les trous (P1, 5 j). Fait (31/10).
Couverture consolidée :
Traversal(ast-traversal),Printer(golden +unary-callables),CodeGen.{Tco,Bindings,Effects,Declarations,Types, Operators}(tco-direct,recursive-initialization,effect-results,adt,exact-arity,unary-operators),ArrayRefs.{Analysis,Ownership, Rebuild,Support}(array-refs,array-refs-ownership),Main.Options(cli/options.mjs),BuildCache/BuildInputs/OutputManifest/FileEmission/Metrics(module-cache,cache-cli,file-emission,build-profile,ast-metrics). - M6-03 — Tests de propriété
fast-check(P1, 2 j). Fait (31/10).direct/properties.mjs: 5 suites (identitémapChildren, monotonie des free vars, composition du renommage, clés de cache déterministes/sensibles, imprimante sansERROR/Impossibleet parenthèses équilibrées). - M6-04 — Golden étendu (P1, 1 j). Fait (31/10).
Cas ajoutés :
nullable,ffi,runtime(Aff/promise),entrypoints,bundle. Manifestes et cache froid/chaud restent couverts par les tests CLI (package-runner.mjs,module-cache.mjs,cache-cli.mjs). - M6-05 — Couverture mesurée (P2, 1 j). Fait (31/10).
npm run test:coverage(c8) : 98,64 % de statements, 89,87 % de branches. Étape CI non bloquante ; cible 70 % largement dépassée. - M6-06 — Smoke TAST (P2, 2 j). Fait (31/10).
npm run test:tast-smokedétecte le fork (../../purescriptouPHPURS_TAST_PURS) et les paquetsphpurs-*, puis lancebin/testsur une fixture. Skip explicite et motivé quand ils manquent ; étape CI séparée et non bloquante. - [-] M6-07 — Messages d’échec normalisés (P2, 0,5 j). Abandonné (31/10).
Les tests récents nomment déjà la fixture et le contrat ; une passe globale
sur 159 tests apporterait peu. Convention consignée dans
tests/codegen/README.md; à rouvrir si un échec CI reste opaque.
Acceptation : matrice remplie à 100 % ; 5 suites de propriétés ; trous fermés ou waiver daté.
Objectif : les frontières PHP sont des contrats écrits, validés au build et testés, pas des conventions découvertes à l’exécution. Le détail des constats est en annexe A.
- M7-01 — Forme du fichier FFI (P1, 1 j). Décidé (31/10).
ADR-002 : garder le fragment inline, documenter
$exports/__DIR__, tracer la voie « vrai fichier PHP ». Les diagnostics d’export relèvent de M7-04. Décider : garder le fragment inline (recommandé à court terme) et documenter portée de$exports, fusion de deuxrequire, valeurs brutes autorisées ; tracer la trajectoire « vrai fichier PHP » dans l’ADR-002. - M7-02 — Racine de module (P1, 1 j). Fait (31/10).
runtime.phpdéfinitPHPURS_ROOTetphpurs_root()(résolus depuis__DIR__du fichier runtime) ; sémantique du layout généré documentée dansdocs/ffi-boundaries.mdet testée partests/codegen/ffi/runtime-preamble.mjs. - M7-03 — Forme de
$exports(P1, 1,5 j). Fait (31/10).GenNativeForeign.validateanalyse le fragment : arité des littérauxfunction(seule une sur-arité est certaine),declare(strict_types=1)etnamespacerejetés. Les tables générées en boucle, alias, exports conditionnels, clés manquantes et helpers supplémentaires restent acceptés ; le wrapper runtime fournit le diagnostic de clé manquante. - M7-04 — Validation des exports au build (P1, 2 j). Fait (31/10).
Les erreurs structurelles remontent dans
TranslatedModule.foreignErrorsetMainéchoue avant émission : sur-arité d’un littéral,declare(strict_types=1)ounamespace. Les clés manquantes restent des diagnostics runtime, car les paquets réels utilisent aliases, tables générées et helpers. Tests :export-validation.mjs,missing-ffi.mjs,cache-cli.mjs;bin/test -cetbin/modtestvalident les paquets voisins. Clé manquante et arité incohérente détectées avant l’exécution, y compris pour une application partielle au top-level (_parseParisTime). Diagnostic clair pourdeclare(strict_types=1)ounamespacedans un fragment. À planifier tôt : c’est le correctif FFI au meilleur rapport valeur/risque. - M7-05 —
phpurs_applyet callbacks (P1, 1 j). Fait (31/10). Helper runtimephpurs_apply($fn, array $args): vérifie le nombre de paramètres déclarés (variadiques exemptés) et lèveTypeErroravec les comptes attendus/reçus. Documenté dansdocs/ffi-contract.md, testé dansruntime-preamble.mjs(succès et arité incorrecte). - M7-06 — ABI runtime versionnée (P1, 3 j). Fait (31/10).
docs/ffi-contract.md: ABI v1,PHPURS_ABI_VERSION/phpurs_abi_version(), accesseursphpurs_variant_tag/valueetphpurs_record_get, formesMaybe/Either/Effect/EffectFnAff, callbacks et$exports. Tests dansruntime-preamble.mjs. Le source runtime entre dans l’empreinte backend de la clé de cache ; un changement d’ABI l’invalide. - M7-07 — Aff (P1, 2 j). Fait (31/10).
phpurs_aff_from_operation(thenable rejouable,then= nouvelle opération) etphpurs_non_cancellable(canceler idempotent) dans le runtime ;docs/ffi-contract.mddocumente replay, annulation et échec ; tests PHP replay (deuxthen), annulation double et$reject. Aucun$runAsyncrecopié ne subsiste dans le dépôt. - [-] M7-08 — Distribution des helpers de test (P2, 2 j). Abandonné (31/10).
Aucun test ne fait de
requireen dur vers un checkout voisin : les fixtures chargent le runtime en mémoire (tests/codegen/fixtures/runtime.mjs) et les exécutions PHP utilisentvendor/autoload.phpdu projet généré. Un paquetphpurs/test-supportn’aurait aucun consommateur ; à rouvrir si un projet externe en a besoin. - M7-09 — Figer la documentation (P1, 1 j). Fait (31/10).
docs/ffi-contract.mdcréé et lié depuisdocs/ffi-boundaries.mdetdocs/libraries.md;check:docs: 29 pages, 0 erreur.
Acceptation : b8x peut supprimer ses contournements ; un export manquant échoue au build ; l’ABI a une version, des accesseurs et des tests.
- M8-01 — Paquet npm (P2, 1 j). Fait (31/10).
engines,prepublishOnly,CHANGELOG.md;npm pack --dry-runvérifié (129 fichiers, sans caches temporaires ni.github).enginesaligné sur la version de CI (Node 22), npm ≥ 10,filesexplicite ou.npmignorevérifié,prepublishOnly: npm run check, version semver,CHANGELOG.md(Keep a Changelog). - M8-02 —
bin/setup+ PBO (P1, 1 j). Fait (31/10).tools/toolchain.jsonépingle dépôt, chemin et révision ;bin/setupclone le fork à cette révision ;tools/check-toolchain.mjscompare le checkout et le cheminspago.yaml, et échoue avec un message clair (utilisé par la CI). - M8-03 —
npm run clean(P2, 0,5 j). Fait (31/10).tools/clean.mjs(garde de chemin,--dry-run), documenté dansdevelopment.md; scratch.tmp-*ignoré par Git et npm. Supprimeoutput,.spago,.purmeta,tests/runner/*,bin/phpurs.jsavec garde de chemin et confirmation ; documenté. - M8-04 — Rétention des audits (P2, 0,5 j). Fait (31/10).
Politique ajoutée en tête de
audit/README.md: rapports immuables, pas deraw/, revue à chaque release, archivage > 6 mois. Politique : rapports immuables, pas deraw/, revue à chaque release, archivage au-delà de 6 mois dans un tag ;audit/README.mdmis à jour. - M8-05 — Nix (P2, 1 j). Fait (31/10).
nix build .#defaultetnix flake checkpassent suraarch64-darwin;nix developfournit le compilateur TASTpurs0.15.16 (fork105f5752), Spago 1.0.4, PHP 8.4 et Node 24. Le flake épingle le fork PBO (pbo) et exige le fork TAST (purs-tast) ; le bundle Nix utiliseesbuild(le bundlepurs-backend-escrashait au démarrage). Job CInixajouté, non bloquant. - M8-06 — Convention de commit (P2, 0,5 j). Fait (31/10).
tools/check-commit-msg.mjs, scriptcheck:commit, étape CI sur le titre de PR, règle écrite dansdocs/conventions.md._ Conventional Commits + lint du message en CI ; portées fermées. - M8-07 — Fins de ligne (P2, 0,2 j). Fait (31/10).
git ls-files --eol: tout le texte est en LF, seuls les PNG sont binaires ; règle documentée dansdocs/conventions.md. Vérifier.gitattributeset.editorconfig; documenter la règle. - M8-08 — Templates d’issue (P2, 0,3 j). Fait (31/10).
Voir M1-07 : trois modèles d’issue dans
.github/ISSUE_TEMPLATE/._ Voir M1-07.
Objectif : le « pourquoi » devient consultable et stable.
- M9-01 — ADR-001 : passes pures
BackendModule -> Result, sans IO, sans mutation du TAST. - M9-02 — ADR-002 : FFI = fragment inline ;
$exports,__DIR__, valeurs brutes ; trajectoire « fichier PHP ». - M9-03 — ADR-003 : ABI runtime versionnée, accesseurs stables, politique de rupture.
- M9-04 — ADR-004 : budgets = garde-fous ; valeurs et justification ; procédure pour les changer.
- M9-05 — ADR-005 : langue et structure des docs ; source map générée.
- M9-06 — ADR-006 : la clé de cache inclut le flux de modules précédent ; raison et conséquences.
- M9-07 — ADR-007 :
EffectFnAffrejouable, annulation coopérative, sémantique PHP. - M9-08 — Revue trimestrielle (P2, récurrent, 0,5 j). Première revue (31/10).
Journal dans
docs/decisions/README.md: ADR relus, budgets centralisés, 3 exceptions de module et 16 de fonction suivies par M3. Prochaine revue : 2027-01-31.
Charge totale estimée : ≈ 75–85 j-h. Calendrier : environ 8 semaines pour deux contributeurs à temps plein (les chantiers M5/M8/M9 sont parallélisables), ou 4 à 5 mois pour un mainteneur seul à mi-temps.
| Jalon | Semaine | Contenu | Critère de sortie |
|---|---|---|---|
| J0 — Socle | S1 | M1-01…M1-04, M5-04, M5-05 (template) | CI verte sur une PR de test ; golden et conventions bloquants |
| J1 — Passes | S1–S2 | M2-01…M2-03, M4-01, M4-02 | 6 passes TAST migrées, budgets centralisés, sortie identique |
| J2 — Taille | S2–S4 | M3-01…M3-04, M3-06, M4-03, M4-04 | Aucun module > 400 l. hors exceptions ; AST sans sentinelle |
| J3 — Preuves | S4–S5 | M6-01…M6-04, M1-05, M5-01, M5-02 | Matrice de tests remplie ; docs en une langue ; hub ≤ 250 l. |
| J4 — Contrats | S5–S6 | M7-01…M7-05, M7-09, M5-03, M5-06 | Validation FFI au build ; source map générée |
| J5 — ABI et release | S6–S8 | M7-06…M7-08, M8-01…M8-06, M5-07, M9 | ABI documentée et testée ; npm run check vert sur clone propre ; CHANGELOG ; tag |
Chemin critique : M1-03 → M1-04 → M2 → M4 → M3 → M6 → M7-06.
Parallélisable sans risque : M5 (docs), M8 (outillage), M9 (ADR).
build(ci): workflow +npm run check(M1-01, M1-02).test(golden): 3 cas d’abord (ADT, TCO, records), puis 12 (M1-03).build(tools):check-conventions+ baseline ratchet (M1-04).docs(modules): 14 en-têtes + template ADR (M5-04, M5-05).refactor(passes):Phpurs.Pass, migration dePartialBindingsseule (M2-01).refactor(passes): les 4 autres passes (M2-02).refactor(passes):Phpurs.Budget(M2-03).refactor(ast):PhpUnaryOp+ suppression des sentinelles (M4-01, M4-02).refactor(codegen): extraction d’une famille detranslateSyntax(M3-01).refactor(printer): extraction d’une famille deprintExprWith(M3-02).test(ast): exhaustivité deTraversal(M4-04).docs(compiler): hub + première sous-page (M5-02).
Chaque PR coche sa case ici et met à jour la source map.
| Risque | Prob. | Impact | Mitigation |
|---|---|---|---|
| Le golden ne couvre pas assez de sorties | Moyenne | Élevé | 12 cas + comparaison de la sortie de référence b8x (2 686 entrées, 1 498 fichiers) à chaque release |
| Un découpage crée des cycles d’imports | Moyenne | Moyen | Matrice de couches (M1-04) + revue avant extraction |
| Déplacer un budget change la sortie | Faible | Élevé | Valeur inchangée, test de frontière, golden, ADR-004 |
| La traduction des docs introduit des erreurs | Moyenne | Faible | Revue bilingue, check:docs, terminologie dans conventions.md |
| La CI est lente ou coûteuse | Moyenne | Moyen | Cache npm/Spago, jobs séparés, tests ciblés par famille |
| Une version d’ABI casse les bibliothèques sœurs | Faible | Élevé | Version d’ABI + bin/modtest + période de double support |
| Dérive vers la performance | Moyenne | Moyen | Non-objectifs explicites ; refus en revue ; archive consultable |
| Bus factor 1 | Élevée | Élevé | CONTRIBUTING, ADR, revue externe, ce document à jour |
Le plan est terminé quand, sur un clone propre :
-
npm ci && npm run checkpasse en moins de 15 minutes, sans variable d’environnement cachée (hors fork PBO et PHP) ; - la CI est requise sur
mainet reproductible (même résultat sur deux machines) ; - aucun module ne dépasse 400 lignes hors exceptions listées et datées ;
- chaque module a un en-tête, chaque passe a une fiche, chaque décision structurante a un ADR ;
- les 12 golden sont stables et une variation de sortie est détectée ;
-
PhpRaw ""a disparu, lesPhpRawrestants sont classés et testés ; - les budgets vivent dans un seul module, avec une justification ;
- la matrice de tests est complète, les trous fermés ou waivés ;
- les contrats FFI (exports, arité,
__DIR__, ABI, Aff, replay) sont écrits, validés au build et testés ; - la documentation est monolingue, indexée, à liens vérifiés, et
compiler.mdtient en un hub ; -
CHANGELOG.md,CONTRIBUTING.mdet le semver existent, le paquet npm est publiable ; - un nouveau contributeur a réalisé une PR de bout en bout sans aide.
Constats issus de la revue des 32 fichiers .php écrits à la main de b8x,
filtrés pour ne garder que ce qui révèle un contrat ou un manque phpurs.
Les correctifs applicatifs (cookie, hash, configs, Encode, Clean, tests
b8x, php -l) sont hors périmètre. Chaque point est repris par une tâche M7.
- Le fichier est inliné dans une closure de module :
declare(strict_types=1),namespace, classes et fonctions nommées sont impossibles ;$exportsest initialisé par le wrapper, pas par le fichier. Deuxrequiredans la même portée fusionnent les exports (constaté). __DIR__désigne l’emplacement généré, passrc/: b8x code le layoutrun/bak/php/outputdansPath.phppour retrouver sa racine.$exportspeut contenir des valeurs brutes (empty,_rootDirAbsolutePath), pas seulement des fonctions ;Auth.phpretourne un tableau littéral hors contrat et fonctionne quand même.- Appeler une fonction PureScript depuis PHP (
$encodeJson($v),$just($ms),$mimeTypeToExtension($m)) dépend de l’uncurrying décidé par l’optimiseur : si la fonction reste curryfiée, l’appel retourne une closure silencieusement.
Décision recommandée : garder le fragment, écrire le contrat, ajouter
phpurs_root() et les diagnostics de build (M7-01, M7-02, M7-05).
- Le PHP connaît
->type/->value(Variant, dansRouter.phpetTaggedSum.php), les records commestdClass,Maybe/Eithercomme callbacks$just/$nothing/$left/$right. C’est un ABI de fait, non documenté. Effect= closure zéro-argument ;EffectFnAff= closure à deux callbacks retournant un canceler ;Unit= valeur libre.- Un
EffectFnAffdoit être rejouable (b8x teste « Hash could not replay ») : contrat implicite, jamais écrit.
Décision recommandée : accesseurs stables + version d’ABI + tests de stabilité (M7-06).
- Aucun helper PHP pour fabriquer un
EffectFnAffdepuis une opération sync/async : chaque application réécrit le même$runAsync(Postgres, S3, Ses, RabbitMq) et les mêmes cancelers, avec des sémantiques divergentes — certains abandonnent l’opération à l’annulation, d’autres la laissent courir. - Le seul exemple du contrat est en JavaScript, dans le commentaire de
phpurs-aff/src/Effect/Aff/Compat.purs; aucun équivalent PHP documenté. - Pas de notion « non annulable » standard : b8x réimplémente
$onCancelSuccess(null)à chaque fois, etProjector.phpdocumente que l’annulation ne libère pas le verrou. phpurs_promiseexiste dans le runtime (thenable JS-like) mais n’est ni utilisé ni documenté pour ce cas.
Décision recommandée : phpurs_aff_from_operation +
phpurs_non_cancellable + contrat de replay, testés (M7-07).
- Une clé
$exportsmanquante n’échoue qu’à la demande ; avec une application partielle au top-level (parseParisTime = _parseParisTime Just Nothing), cela fait échouer l’initialisation du module — cas_parseParisTimedans b8x. - Les fragments contenant
declare(strict_types=1)ounamespaceéchouent au parse silencieusement à l’inlining : le diagnostic doit être explicite.
Décision recommandée : validation clé+arité au build, diagnostic dédié (M7-04).
test/ffi/effects.phpde b8xrequireen dur../../phpurs/phpurs-effect/src/Effect/Uncurried.php(checkout voisin) pour obtenirrunEffectFn1/2: fournir un paquet Composer ou un helper de test supporté.- Les tests FFI d’une application ne peuvent pas instancier
Effect/EffectFnAffsans recopier les conventions : fournir des doubles (phpurs_test_*).
Décision recommandée : paquet phpurs/test-support + doubles (M7-08).
docs/libraries.md doit figer : inlining et portée de $exports, sémantique
de __DIR__, représentations record/Variant/Maybe/Effect, convention
EffectFnAff + annulation + replay, callbacks PureScript uncurried, et les
valeurs brutes autorisées dans $exports.
_parseParisTime manquant dans Instant.php, détecté seulement à
l’initialisation du module généré → argument pour la validation au build
(§A.4, M7-04).
# Gate complet (format, build + bundle, tests)
npm run check
# Tests seuls, par famille
npm run test:codegen
node --test tests/codegen/direct/enum-regions.mjs
node --test tests/codegen/cli/cache-cli.mjs
node --test tests/codegen/ffi/missing-ffi.mjs
# Golden et conventions (après M1)
npm run test:golden
npm run golden:update
npm run check:conventions
npm run check:docs
# Mesures rapides
find src -name '*.purs' | wc -l
find src -name '*.purs' -exec wc -l {} + | sort -rn | head -20
node --test --test-concurrency=1 tests/codegen/direct/*.mjs \
tests/codegen/ffi/*.mjs
node --test --test-concurrency=1 tests/codegen/cli/*.mjs
# Build isolé
npm run build # spago build && node tools/bundle.mjsNotes d’environnement :
spago builda besoin d’un cache global inscriptible (~/Library/Caches/spago-nodejssur macOS, emplacement XDG sur Linux). Dans un environnement sandboxé, utiliser unHOMElocal au workspace pour les mesures, sans modifier le dépôt.- Les tests CLI utilisent
bin/phpurs.js; après une modification desrc, relancernpm run buildavantnpm run test:codegen. bin/testetbin/modtestexigent le fork TAST et les dépôts frères ; ils ne font pas partie du gatecheck.
-- | <Nom> — <responsabilité en une phrase>.
-- |
-- | Précondition : <ce qui doit être vrai pour tenter la passe>
-- | Preuve : <ce que la passe démontre avant de réécrire>
-- | Budget : <constantes importées de Phpurs.Budget>
-- | Repli : <ce qui est laissé intact quand la preuve échoue>
-- | Test : <fichier de test, contrats couverts>
# ADR-00N — Titre
- Statut : proposé | accepté | remplacé
- Date : AAAA-MM-JJ
- Contexte : ...
- Décision : ...
- Alternatives : ...
- Conséquences : ...
- Références : tâche(s) Mx-yy, tests, PR- La tâche
Mx-yyest référencée et cochée dans ce document. - Le comportement observable est inchangé (golden), ou le changement est explicitement contractuel et documenté.
- Les tests couvrent le contrat modifié (nom du test = contrat).
- Les budgets n’ont pas changé, ou l’ADR est mis à jour.
- La documentation (module,
docs/, source map) est à jour. -
npm run checkest vert ; la CI est verte. - Le commit suit
type(scope): résumé.