refactor(reports): extraire un constructeur d'arbre feuille-driven generique (#263) #267
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#267
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-263-generic-tree-builder"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resolves #263
Generated autonomously by /autopilot run of 2026-07-07
Objectif
Factoriser le tree-builder de
buildCompareTreeen un constructeur d'arbre feuille-driven generiquebuildLeafDrivenTree<T>(leaves, opts), reutilisable par #264 (arbre des tendances). Refactor behavior-preserving :getCompareMonthOverMonth/getCompareYearOverYearproduisent un output strictement identique.Ce qui change (
src/services/reportService.tsuniquement)buildLeafDrivenTree<T>(leaves, opts)exporte + typesTreeCatMeta,TreeSectionType,LeafDrivenTreeOptions<T>. Le squelette partage : split feuille/orphelin, set « relevant » (feuille + ancetres), adjacence en ordre DB, recursionbuildNodeavec gardeMAX_TREE_DEPTH+ loop-break conservee, tri des freres par magnitude, append des orphelins, tri stable par section (income -> expense -> transfer).buildCompareTreedevient une fine specialisation qui n'injecte que l'arithmetique des deltas (makeLeaf/makeSubtotal/decorateDirectLeaf/makeOrphan/sortKey/isSubtotal/sectionOf/sectionOrder).CompareCatMeta= alias deTreeCatMeta.Preuve behavior-preserving
git diff --name-onlyne touche quereportService.ts, zero fichier de test.reportService.test.ts(cas compare +buildCompareTreehierarchie) +compareResults.test.ts= 44 tests verts.makeLeaf== ancienleafRow,makeSubtotal== anciensubtotalRow,sortKey=Math.abs(deltaAbs),sectionOf=category_type ?? "expense",sectionOrder=COMPARE_TYPE_ORDER,unknownSectionRank=9 (defaut) ==?? 9,maxDepth=MAX_TREE_DEPTH(defaut).Decisions
reportService.ts(a cote debuildCompareTree), pas un nouveau fichier — suit le pattern existant (buildCompareTree y vit deja) et minimise le rayon d'explosion. Alternative ecartee :src/services/leafDrivenTree.tsdedie (plus isole mais plus de churn).Tsert a la fois de payload de feuille et de ligne de sortie (les champs hierarchie sont optionnels surCategoryDelta) — colle au compare et a ce dont #264 (sidecar id-keyed) a besoin. Deux type-params<TLeaf, TRow>ecartes (sur-ingenierie).CompareCatMetaconverti en alias deTreeCatMeta(shape identique) pour eviter la derive ; le commentaire compare-specifique (fetch sans filtre is_active) est conserve.Signature pour #264
export function buildLeafDrivenTree<T>(leaves: T[], opts: LeafDrivenTreeOptions<T>): T[]Aucune migration DB (v1->v16 inchangees). Refactor interne pur -> pas d'entree CHANGELOG, pas de cle i18n.
Revue /pr-review — APPROVE
Verdict : APPROVE ✅
Résumé
Extraction propre et behavior-preserving du tree-builder de
buildCompareTreevers un constructeur générique réutilisablebuildLeafDrivenTree<T>.buildCompareTreedevient une fine spécialisation qui n'injecte que l'arithmétique des deltas. La preuve la plus forte est là : seulsrc/services/reportService.tschange (git diff --name-only main..HEAD), le fichier de testreportService.test.tsest byte-identique — les 44 tests compare/hiérarchie sont le garde-fou de non-régression.Vérification behavior-preserving (mapping ligne-à-ligne)
J'ai retracé chaque point d'injection contre l'ancien code inline, tous exacts :
d.categoryId != null && catById.has(...)categoryIdOf+ même gardeMAX_TREE_DEPTHmaxDepth(défautMAX_TREE_DEPTH)leafRow(spread + stamp)makeLeafidemsubtotalRow(somme +deltaPctdénominateur signé)makeSubtotalarithmétique identique{ ...direct, categoryName: '(direct)' }decorateDirectLeaf(makeLeaf(...))subtotalRoworphelinsmakeOrphanidemCOMPARE_TYPE_ORDER[..] ?? 9sectionOrder[sectionOf] ?? unknownSectionRank(=9)Points scrutés de plus près (les deux seuls endroits où la forme change) :
descendantLeavesfilter :!r.is_parent→!isSubtotal(r)=!(r.is_parent === true). Équivalent : toute ligne construite dansbuildNodeporte unis_parentstrictement booléen (makeLeaf→false,makeSubtotal→true), jamaisundefined. Aucune divergence.sortKeydu direct leaf : désormais calculé sur la ligne décorée (post-categoryName), maisdeltaAbsn'est pas touché par le renommage → ordre des frères identique au byte.Autres contrôles
MAX_TREE_DEPTHavant le builder générique ;COMPARE_TYPE_ORDER(const) lu seulement au runtime parbuildCompareTree(pas de TDZ) ;buildLeafDrivenTreeest une function declaration hoistée.CategoryDelta(parent_id/is_parent/depth/category_type) sont optionnels →T = CategoryDeltatypecheck sans souci.buildCompareTreen'est consommé qu'en interne (getCompareMonthOverMonth/YoY) + tests ;COMPARE_TYPE_ORDER/COMPARE_CATEGORIES_SQLrestent module-private (seule mention = un commentaire doc dansoverTimeResults.ts).refactor(reports): …),Resolves #263, aucun artefact debug /.only/.skip. Refactor interne pur → pas de migration, pas d'i18n, pas de CHANGELOG (correct).main→ couverte parcheck.yml. C'est le maillon de base de la pile reports (#267 → #268/#269) : le builder générique atterrit d'abord pour que #264 le consomme.Suggestions (non bloquantes)
buildLeafDrivenTreen'a pas de test unitaire direct — il est couvert à 100 % transitivement par la suite compare, et #264 apportera la couverture du second consommateur. OK de différer.Beau refactor, bien commenté, rayon d'explosion minimal.
Mergée localement dans
main(pile #266/#267/#268/#269, tipfe87313). Fermée manuellement — merge local d'une pile stackée (l'API merge Forgejo échoue sur les bases non-main), tip cumulé validé build + 728 vitest verts.Pull request closed