fix: check category parents and repair orphan categories (#393) #472
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#472
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-393-category-hierarchy"
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 #393
Generated autonomously by /autopilot run of 2026-10-18
Résumé
Maillon 5 sur 8 de la pile
planned-2026-10-16-lot-v018, basé surissue-429-category-combobox-portal(PR #471), dont il utilise le portail deCategoryCombobox: la liste d'un combobox de la fenêtre sort de la fenêtre.Un
parent_iden boucle (A sous B, B sous A, ou une catégorie sous elle-même) cachait la boucle et tout ce qui pend dessous partout où l'on part des racines : arbre, sélecteurs, budget, rapports. Rien ne permettait de le voir ni de le réparer, etupdateCategoryécrivait n'importe quelparent_id. Désormais :src/utils/categoryHierarchy.ts:depthsOf,subtreeHeightetdescendantsOf(une seule descente), sortis deparentOptions.ts, avec un ensemble de visités sur chaque remontée et chaque descente ; plusfitsUnder(trois niveaux, sous-arbre compris),parentChangeRefusal,findOrphans,displaysAsRoot.buildParentOptions, le diagnostic et la validation du service l'importent ; aucune autre implémentation.parentOptions.test.tsreste vert sans changement de ses tests existants. Une catégorie prise dans une boucle, ou dessous, n'a plus de profondeur (au lieu d'une profondeur inventée) : la liste des parents ne la propose jamais.findOrphanCategories: requête dédiéeSELECT id, parent_id, is_active FROM categories(actives et inactives), motifscycle(le membre de plus petitid, une entrée par boucle),inactive_parent,missing_parent; seule la tête de chaque groupe est rendue.checkNewParent, partagée) :self_parent,cycle(parent dans la descendance, ou parent sans profondeur, pris dans une autre boucle),too_deep(sur les catégories actives, sous-arbre compris),parent_has_transactions(transactions directes du nouveau parent). Sinon, le nouveau parent passe àis_inputable = 0comme danscreateCategory. Lectures et écritures sous unwithTransaction, par son handle, même pour une ligne.updateCategoryne vérifie qu'un parent changé ;reattachCategory(id, parentId | null)vérifie toujours. La racine est toujours acceptée.CategoryHierarchyError(code), sur le modèle deTransactionLinkedToBalanceError;useCategoriesla reconnaît parinstanceofet dispatche la clécategories.hierarchy.errors.<code>(étatCategoriesError = { key } | { message }, toutes les erreurs du hook passent parcategoriesErrorOf).getCategoryDepthgarde lesidvus et rendInfinitysur une boucle (se termine ;createCategoryrefuse alors ce parent).HierarchyCheckModal: nombre d'orphelines ou « aucune », une ligne par orpheline (nom traduit, motif traduit,CategoryComboboxalimenté parbuildParentOptionsavec l'orpheline en catégorie éditée, « Racine (niveau supérieur) » présélectionnée enextraOptions, bouton « Rattacher »), bouton « Fermer ». Après un rattachement : une lignelogInfo(motif et nombre de catégories remises en place, aucun nom), puis la liste et l'arbre se rechargent (l'arbre même si la nouvelle vérification échoue, l'écriture étant faite). Un refus s'affiche traduit dans la fenêtre ; à l'enregistrement du formulaire, dans le bandeau d'erreur existant de la page, juste au-dessus du formulaire, qui reste ouvert.buildBudgetYearRows(useBudget.ts) etgetBudgetVsActualData(budgetService.ts) placent au premier niveau une catégorie active dont le parent est désactivé ou absent (displaysAsRoot), comme l'arbre et les sélecteurs.Fichiers
src/utils/categoryHierarchy.tssrc/utils/categoryHierarchy.test.tssrc/components/categories/parentOptions.tssrc/components/categories/parentOptions.test.tssrc/services/categoryService.tsCategoryHierarchyError,findOrphanCategories,checkNewParent,updateCategorysous transaction,reattachCategory, garde degetCategoryDepth,markAsParentpartagé, commentaire deupdateCategorySortOrderssrc/services/categoryService.test.tsgetDb())src/hooks/useCategories.tsCategoriesError,categoriesErrorOf,categoriesErrorText, étathierarchyCheck,checkHierarchy,reattachOrphan,closeHierarchyCheck,buildTreeexporté,reattachLogLinesrc/hooks/useCategories.test.tshookHarness)src/components/categories/HierarchyCheckModal.tsxsrc/components/categories/HierarchyCheckModal.render.test.tsxrenderToStaticMarkup)src/components/categories/HierarchyCheckModal.wiring.test.tssrc/pages/CategoriesPage.tsxsrc/pages/CategoriesPage.render.test.tsxsrc/hooks/useBudget.ts,src/services/budgetService.tssrc/hooks/useBudget.test.ts,src/services/budgetService.test.tssrc/components/shared/CategoryCombobox.test.tssrc/i18n/locales/fr.json,en.jsoncategories.hierarchy.*;docs.categories: 1 fonctionnalité, 1 étape, 2 astucesdocs/guide-utilisateur.mddocs/architecture.mdcategoryService,useCategories,/categories, section « Hiérarchie des catégories (#393) » (dont le hors portée)CHANGELOG.md,CHANGELOG.fr.mdAucune migration (v1→v17 inchangées), aucun fichier Rust, aucun appel réseau, aucune écriture d'une table lue par
COUNTERS_SQL(seulecategoriesest écrite).Hors portée
updateCategorySortOrdersécritparent_idsans cette validation ; le chemin reste gardé par l'interface (moveCategoryne peut pas déposer une catégorie dans son propre sous-arbre : la cible n'est pas trouvée, rien n'est écrit). Il ne refuse pas non plus un parent avec transactions et ne touche pasis_inputable. Écrit dansdocs/architecture.mdet en commentaire de la fonction.restoreCategories) : réinsère lesparent_idtels quels, boucles comprises ; la fenêtre répare après coup.parentOptions.ts) : seulement branché sur le module.Décisions
Toutes consignées dans
reports/run-2026-10-18-lot-v018/decisions-log.md: 5 HIGH, 11 MEDIUM, 0 LOW.v0.17.2) ; sites repérés par leur contenu.computeDepthsdeCategoryCombobox(indentation seulement) laissé à part : ce n'est pas l'un des trois consommateurs nommés, et le maillon 4 vient de toucher le fichier.depthsOfne donne aucune profondeur à une catégorie dans ou sous une boucle, au lieu de la profondeur inventée de l'ancienseen → 0(dans une boucle 1 ↔ 2, 2 recevait la profondeur 1 et était proposé comme parent à la création, masqué seulement par le DFS du combobox). Un parent sans profondeur est refusé encycle. Écarté : un cinquième code (parent_in_cycle) ; le plan fixe quatre codes et le messagecyclecouvre les deux cas.updateCategoryne vérifie qu'un parent changé : un renommage d'une catégorie enregistrée avant ces règles (4e niveau, parent avec transactions, quecreateCategoryn'a jamais vérifié) ne doit pas être refusé.CategoryHierarchyError: hors d'atteinte de l'interface (les deux sélecteurs ne listent que des catégories actives), et le plan fixe les quatre codes.getCategoryDepthrendInfinitysur une boucle, pour que le contrôle existant decreateCategory(>= 2) refuse un membre de boucle comme parent (chemin hors d'atteinte : les sélecteurs écartent ces catégories).CategoriesError = { key } | { message }etcategoriesErrorTextpour la page et la fenêtre. Écarté : la clé dans l'ancien champ texte (la page ne saurait pas distinguer une clé d'un message).role="alert"), le bandeau de la page étant sous son voile. Écarté : un second affichage dansCategoryDetailPanel.logInfodès que le rattachement est écrit, avant les rechargements :Categories: orphan category reattached (<motif>), N categories back in the hierarchy(N = la catégorie et ses descendantes actives, rendu parreattachCategory). Ni nom ni identifiant.is_inputablede l'ancien parent n'est pas touché après un déplacement (hors du plan ;deactivateCategoryle restaure, le glisser-déposer non) : la case « Autoriser la saisie » du formulaire le règle.inactive_parentse lit « Son parent a été supprimé » (« Supprimer » désactive ; c'est le mot de l'utilisateur),missing_parent« Son parent est introuvable », l'option racine « Racine (niveau supérieur) », alignée sur « Aucun parent (niveau supérieur) » du formulaire.cb0e2d2) : après un rattachement écrit, un échec de la seconde vérification sautait le rechargement de l'arbre ; il passe dans unfinally, l'échec s'affiche toujours dans la fenêtre (test ajouté, M34). Écarté : recharger l'arbre avant la vérification (les boutons « Rattacher » redeviendraient actifs sur la liste périmée).useCategories(hierarchyCheck, une vérification qui finit après la fermeture ne rouvre rien) ; un parent choisi qui n'est plus proposé retombe sur la racine ; pas de gestion d'Échap, comme la fenêtre « Réinitialiser » de la page (#451 suit ce motif).displaysAsRootdans les deux constructeurs ;markAsParentpartagé par les trois écritures ;toSqlFlagpouris_inputable;buildTreeexporté pour le test de régression.Preuves
Test de régression (
type:bug) :categoryService.test.tsreproduit A → B → A parupdateCategory(A racine, B son enfant, A enregistrée sous B) : refuscycle,BEGINpuisROLLBACK, rien d'écrit, l'arbre (buildTree) montre toujours A et B. Sur des lignes déjà en boucle (5 ↔ 3 avec 8 sous 5, puis 6 → 4 → 9 → 6), le diagnostic trouve le plus petit membre,reattachCategoryse termine, casse la boucle (diagnostic vide ensuite) etbuildTreemontre de nouveau toute la branche. Réserve de la revue du plan :reattachCategorysur un membre d'une boucle de 2, puis de 3, se termine et casse la boucle (même fichier).Mutations : implémentation commitée avant la première ; une mutation à la fois, restaurée par
git checkout --,git status --shortvide après chacune ; vitest lancé parnode node_modules/vitest/vitest.mjs run --pool=threads <fichiers>soustimeout -s KILL 90etNODE_OPTIONS=--max-old-space-size=768(les mutations des gardes de visités rendent les parcours infinis).categoryHierarchy.tsself_parentretiréecategoryHierarchy.tscycle(parent dans la descendance) retiréecategoryHierarchy.tscycle(parent sans profondeur) retiréecategoryHierarchy.tstoo_deepretiréecategoryHierarchy.tsfitsUndersans le niveau de la catégoriecategoryHierarchy.tscategoryService.tsparent_has_transactionsretiréecategoryService.tsupdateCategory: le nouveau parent gardeis_inputablecategoryService.tsreattachCategory: le nouveau parent gardeis_inputablecategoryService.tscategoryService.tsgetDb()dans la transactioncategoryService.tsBEGIN(hors transaction)categoryService.tsgetCategoryDepthretiréecategoryService.tsInfinitycategoryHierarchy.tsdepthsOfretiréecategoryHierarchy.tscategoryHierarchy.tsfindOrphansretiréeRangeError: Invalid array lengthaprès ~5 s chacun)categoryHierarchy.tsidcategoryHierarchy.tsparentOptions.tsuseBudget.ts!c.parent_id)budgetService.tsuseCategories.tse.messageuseCategories.tsuseCategories.tsuseCategories.tsHierarchyCheckModal.tsxHierarchyCheckModal.tsxHierarchyCheckModal.tsxHierarchyCheckModal.tsxCategoriesPage.tsxCategoriesPage.tsxCategoriesPage.tsxuseCategories.tscategoryService.tscategoryService.tsHierarchyCheckModal.tsxcategoryHierarchy.tsid38 mutations, toutes rouges. M23 à M26 rejouées après
cb0e2d2; R1, R3, R4 et R5, de l'autorelecture, rejouées sur la tête8a9d990(R3 après l'ajout du test qui l'épingle). Aucun processus vitest resté actif, aucun fichier laissé dans l'arbre (le vidage mémoire de M15 est pris par apport, hors du dépôt).Suites : sonde rc=1 (
src/zz_tmp_probe.test.tsvolontairement rouge, supprimé) ;npm run buildvert ;npm test152 fichiers / 2458 tests verts (147 / 2375 sur la base). Rust : non concerné (aucun fichier Rust).Vérifications manuelles dues (prochaine release)
Sur un profil de test, application fermée pour les modifications SQLite (une boucle ne peut plus se créer par l'interface depuis #423) :
UPDATE categories SET parent_id = <id d'un enfant> WHERE id = <id de son parent>; relancer. La branche a disparu de l'arbre, du sélecteur de catégorie des Transactions, de la grille Budget et des rapports. La fenêtre compte 1 orpheline, nommée (le membre de plus petitid), motif « Prise dans une boucle de parenté », « Racine (niveau supérieur) » présélectionnée. « Rattacher » : la branche revient dans l'arbre, les sélecteurs, la grille Budget et les rapports, avec ses transactions, ses mots-clés et ses montants de budget ; la fenêtre dit « aucune ».Categories: orphan category reattached (cycle), N categories back in the hierarchy, sans nom de catégorie.UPDATE categories SET is_active = 0 WHERE id = <id d'un parent>(sans remonter ses enfants) ; relancer. L'enfant s'affiche au premier niveau dans la grille Budget et le rapport Budget vs réel, avec ses montants ; la fenêtre le liste « Son parent a été supprimé » ; « Rattacher » sous un parent valide le replace.À regarder
depthsOf(aucune profondeur dans ou sous une boucle) change ce quebuildParentOptionspropose dans ce cas ; épinglé par un test ajouté àparentOptions.test.ts, les tests existants inchangés.updateCategorytient maintenant le verrou dedb.tspour toute sa lecture-vérification-écriture (withTransaction) au lieu d'un seulexecute.getCategoryDepthcompte encore un parent inactif comme un niveau (comportement d'avant, chemin decreateCategoryseulement), alors que la validation et l'arbre le traitent comme absent : plus strict d'un niveau, laissé tel quel.🤖 Generated with Claude Code
Niveau : normal
Verdict : APPROVE
Autorelecture de l'auteur (worker /autopilot), pas une revue indépendante. Checklist de
/pr-reviewappliquée au diff complet de la PR contre sa base (issue-429-category-combobox-portal), à la profondeur du niveau normal.Niveau (Step 0bis,
REVIEW.mddemain)Aucune ligne du tableau Haut n'est remplie : le code de
src/**n'ajoute aucun appel réseau, ne change rien de ce qu'un appel envoie, n'ouvre aucune URL et ne touche aucun consentement (balayage du diff : nifetch, niinvoke, ni URL, niwindow.open) ; aucun fichier de licence, de profil, de migration, de manifeste, de lock, de workflow ni de configuration ; aucun fichier ajouté ou supprimé à la racine.docs/architecture.mdreste au moins normal ;src/i18n/locales/*.jsonet le code sont normaux ;CHANGELOG*.mdetdocs/guide-utilisateur.mdsont bas. La PR est donc normale.Ce qui a été rejoué
npm run buildvert ;npm test: 152 fichiers, 2458 tests verts, après une sonde rouge (rc=1).self_parent, les deux formes decycle,too_deep,parent_has_transactions), la racine toujours acceptée,is_inputabledu nouveau parent, la transaction (pas deBEGIN, lecture pargetDb()dans le rappel), le parent inchangé non vérifié, les quatre gardes de visités (parcours infinis : workers à court de mémoire ouRangeError), le plus petitidd'une boucle, la règle de racine du budget (grille et Budget vs réel), la clé traduite du hook et de la page, la ligne de Journal, les rechargements, la fenêtre (racine présélectionnée, parent choisi transmis, options de la ligne, choix périmé), le bouton (place, style, état pendant un enregistrement). Quatre mutations de relecture ajoutées (R1, R3, R4, R5).REVIEW.mdtouchées : Migrations, profils et données (lectures et écritures sous un seulwithTransaction, par son handle, prouvé par une base factice dontgetDb()lève pendant la transaction ;is_inputablelié partoSqlFlag; aucune migration ; seulecategoriesest écrite, queCOUNTERS_SQLne lit pas, donc pas denotifyDataChanged), Vie privée (la ligne de Journal ne porte qu'un code de motif et un nombre, ni nom, ni chemin), i18n et documentation (clés FR et EN, gardelocales.keys.test.tsverte, CHANGELOG des deux langues, guide §6 etdocs.categories,docs/architecture.md).type:bugprésent : A → B → A refusé parupdateCategory; boucles de 2 et de 3 membres trouvées par le diagnostic, cassées parreattachCategory, branche de nouveau visible dansbuildTree.Constats de l'autorelecture (tous corrigés dans la PR)
src/hooks/useCategories.ts:432— après un rattachement écrit, un échec de la seconde vérification sautait le rechargement de l'arbre : rechargement passé dans unfinally(cb0e2d2), test ajouté, mutation M34 rouge.docs/architecture.md:274— « Trois consommateurs » oubliait le budget, qui litdisplaysAsRoot: phrase corrigée (bad53fe).src/services/categoryService.ts:249— le nombre rendu parreattachCategory(repris dans la ligne de Journal) n'était pas épinglé aux seules descendantes actives : test complété (8a9d990), mutation R3 rouge.Remarques non bloquantes
src/utils/categoryHierarchy.ts:164— la racine est toujours acceptée : après la réparation d'une boucle de 3 membres ou plus avec des catégories dessous, une branche peut garder 4 niveaux (l'arbre l'affiche, la grille Budget s'arrête au troisième). Inatteignable depuis #423, écrit dans la PR.src/services/categoryService.ts:155—getCategoryDepthcompte encore un parent inactif comme un niveau (comportement d'avant, chemin decreateCategoryseulement) : plus strict d'un niveau que l'arbre, laissé tel quel.src/components/categories/HierarchyCheckModal.tsx:140— suggestion : les boutons « Rattacher » de chaque ligne ont le même nom accessible ; unaria-labelqui nomme la catégorie aiderait un lecteur d'écran (le combobox qui le précède est déjà nommé). Pas d'Échap ni de piège de focus, comme la fenêtre « Réinitialiser » de la page (#451 suit ce motif).Rien ne bloque. Label de l'issue laissé à
status:review: cette autorelecture n'est pas indépendante.CI lue par l'orchestrateur du run
/autopilotdu 2026-10-18 :check-frontendvert sur la tête8a9d990(run 582).categoryService.test.ts(19 tests), qui contient le test de régression A → B → A ;categoryHierarchy.test.ts(27) ;useCategories.test.ts(11) ;HierarchyCheckModal.render.test.tsx(9) etHierarchyCheckModal.wiring.test.ts(5) ;budgetService.test.ts(16) ;parentOptions.test.ts(15), qui reste vert après le déplacement dedepthsOfetsubtreeHeight.locales.keys.test.ts(8) est verte : les 28 clés ajoutées existent en français et en anglais.check-rustne s'applique pas à ce maillon (aucun chemin Rust touché).Niveau : normal
Verdict : APPROVE
Résumé. Le module pur
categoryHierarchy.tsest correct : chaque remontée et chaque descente garde ses visités, les quatre refus sont testés, la validation et l'écriture passent par un seulwithTransactionet son handle, et le diagnostic lit bien toutes les catégories. Aucun défaut bloquant. La revue relève un cas réel où la fenêtre perd les catégories qu'elle doit rattacher (après un glisser-déposer), cinq épingles manquantes sur du code correct, et une question d'intention suris_inputabledu nouveau parent.Niveau calculé sur les 24 fichiers : aucun ne remplit une ligne du tableau Haut. Le code livré n'ajoute ni appel réseau, ni URL, ni consentement (la ligne de Journal reste locale et ne nomme aucune catégorie), et aucun fichier n'est ajouté à la racine ni dans
src-tauri/.docs/architecture.mdet les locales sont normaux, les changelogs et le guide bas.Ce que la revue a exécuté
Clone détaché hors du dépôt sur la tête
8a9d990,npm ci:npm run build: rc=0 ;npm test: 152 fichiers, 2458 tests, rc=0 (les chiffres du corps de la PR) ;check-frontendrun 582 vert sur8a9d990(le journal montre le checkout de8a9d990lui-même, 152 fichiers, 2458 tests) ;check-rustnon déclenché, aucun fichier sous ses chemins.Mutations du relecteur
Une à la fois, suite complète, fichier restauré par
git checkout --, arbre propre après chacune.categoryHierarchy.tsuseCategories.tsSTART_REATTACHgarde le refus précédent dans la fenêtrecategoryHierarchy.tstoo_deepmesure le sous-arbre du nouveau parent au lieu de celui de la catégoriecategoryHierarchy.tscategoryService.tsparent_has_transactionscompte les transactions de la catégorie déplacéecategoryService.tsinTransactionavale l'erreur après leROLLBACKcategoryService.tscategoryHierarchy.tsuseCategories.tsHierarchyCheckModal.tsxCategoriesPage.tsxstate.tree(racines seules) à la fenêtrecategoryService.tsupdateCategorymarque aussi un parent inchangécategoryHierarchy.tscyclesur les descendants du parent (ses ancêtres)categoryService.tsupdateCategoryvérifie le parent à chaque enregistrementcategoryHierarchy.tsdisplaysAsRootmontre en racine une catégorie qui est son propre parentDix rouges sur quinze. Les cinq survivantes portent sur du code correct : ce sont des épingles manquantes (suggestion 2), pas des défauts.
Suggestions (non bloquantes)
moveCategoryenvoieSET_TREE(useCategories.ts:383), dont le reducer remplacestate.categoriesparflattenTreeToCategories(tree)(:149) : seulement ce qu'une racine atteint, donc aucun membre d'une boucle, et rien ne recharge la liste quand l'écriture réussit. La page passe cette liste à la fenêtre (CategoriesPage.tsx:160). Sonde sur la copie (lignes 1, 10 sous 1, 2, et 3 ↔ 5) : avant,state.categoriesvaut [1, 2, 3, 5, 10] ; aprèsmoveCategory(2, null, 0)puischeckHierarchy(), [1, 2, 10], alors que le diagnostic rend toujours{ id: 3, reason: "cycle" }. La ligne de 3 s'affiche alors « — » (nameOf, et l'aria-labeldevient « Nouveau parent de — »), et ses parents proposés sont [1, 2, 10] au lieu de [1, 2] : 10, de niveau 1, est offert alors que 5 pend sous 3, et le service le refuse entoo_deep. Aucune donnée en jeu (le service revalide, la racine choisie par défaut répare), mais dans ce cas, atteignable depuis l'interface, la fenêtre ne donne plus le nom de l'orpheline. Correctif d'une ligne, à poser en tête de pile : recharger les catégories danscheckHierarchy(par exempleawait loadCategories()avant le diagnostic), avec un test du hook « glisser-déposer, puis vérifier » qui épingle aussi RM11.idplus petit sous une boucle.findOrphans([r(2, 5), r(5, 8), r(8, 5), r(1, null)])doit rendre[{ id: 5, reason: "cycle" }]; sous la mutation il rend 2, dont le rattachement laisse 5 ↔ 8 cachés.updateCategory(1, form(3))doit passer ; sous la mutation, refustoo_deep.categories: []).is_inputabledu nouveau parent. Avant cette PR,updateCategoryne touchait pas le nouveau parent ; il passe désormais àis_inputable = 0, au formulaire comme dans la fenêtre (markAsParent), et le refusparent_has_transactionsne protège que les transactions. Sonde surbuildBudgetYearRows: une racine P avec 100 de budget à son nom et une feuille C à 40 ; une fois C sous P et P non saisissable, la grille montre P en sous-total de 40 et les 100 de P en sortent (la ligne reste dansbudget_entries). De même, les mots-clés actifs de P continuent de classer des transactions vers P (categorizeDescriptionetcategorizeBatchlisent tous les mots-clés actifs, sans regarderis_inputable), et le Budget vs réel ne montre la ligne « (direct) » d'un parent que s'il est saisissable. C'est le comportement decreateCategorydepuis toujours, que cette PR étend au changement de parent. À trancher par Max : élargir le refus (montants de budget, mots-clés actifs), ou l'accepter et ouvrir une issue de suivi.Vérifié sans constat
updateCategoryetreattachCategorylisent et écrivent par le handle dewithTransaction, avecBEGIN/COMMIT/ROLLBACKcomme les autres services et aucungetDb()dans le rappel ; le faux decategoryService.test.tslève surgetDb()pendant une transaction.toSqlFlag,0littéral dansmarkAsParent) ; aucune écriture d'une table lue parCOUNTERS_SQL, seulecategoriesest écrite.depthsOf,levelsBelowetfindOrphansse terminent sur toute boucle (ensemble de visités, chaque tour ajoute au moins une catégorie) ; une boucle qui passe par une catégorie désactivée ressort eninactive_parent, comme l'arbre l'affiche.buildParentOptionsrend la même liste qu'avant dans un profil sans boucle :depth + 1 + height <= 2équivaut à l'anciendepth <= 1 - height.getAllActiveCategories; une catégorie à parent désactivé passe au premier niveau sans double compte, le seau du parent absent n'étant jamais parcouru.common.closeexiste ;docs.categorieset le guide suivent (une fonctionnalité, une étape, deux astuces) ; CHANGELOG bilingue (Ajouté, Corrigé) ;architecture.md: 20 utilitaires recomptés danssrc/utils.pull_refclosesde cette PR, et les commits portentResolves #393: #393 se ferme au merge quel que soit le chemin, ce que la PR veut (ses vérifications manuelles visent la prochaine release).Actions faites
/tmp/pr472-*supprimés en fin de revue.status:review→status:approved.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.