From 13dc8fdaad8d06180b4943c1e6069a732f4b75ca Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 09:18:04 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20A38=20phase=202=20=E2=80=94=200=20doublo?= =?UTF-8?q?n=20de=20fonction=20globale=20+=20garde-fou=20(v7.34.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inventaire exhaustif des 13 noms `function NAME` définis 2+ fois (templates + static/js) avec scan de profondeur de brace (strings, comments, backticks gérés) : 12 sont déjà scopés dans des IIFEs depuis A27 (escHtml/flush/emit/setMeta/initials/up/esc/show/close…) — aucun conflit de page possible. - Seul doublon GLOBALE = openCardDetail (corps byte-identiques ×2 dans board_fragment + detailed_board, fragments de vues mutuellement exclusifs) → dédupliquée vers static/js/app.js, 2 copies supprimées ; les onclick/@click des deux fragments appellent la même définition (owner/repo globaux fournis par board.js au moment du clic). - test_no_duplicate_global_functions : garde-fou 0-doublon entre templates et static/js (scanner naïf, plafond ponytail commenté). Reste A38 : méthodes jumelles library/local_workspace (~9-21 noms communs, corps divergents) → fusion workspace-tree.js reportée (réconciliation sans E2E, même logique que A39/A20). suite **1093/1093** · ruff OK · node --check vert · docs à jour --- CHANGELOG.md | 27 +++++++++++++ ROADMAP.md | 4 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- app/templates/board_fragment.html | 11 +---- app/templates/detailed_board.html | 11 +---- docs/openapi-v2.json | 2 +- static/js/app.js | 12 ++++++ tests/test_audit_p0_fixes.py | 67 +++++++++++++++++++++++++++++++ 10 files changed, 116 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8e78f4..ac808c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog - FlowDeck +## v7.34.0 (2026-10-01) — Audit : A38 phase 2 (0 doublon de fonction globale) + +### Changed + +- **Inventaire exhaustif de la duplication de fonctions** : scan de + profondeur de brace sur les 13 noms définis 2+ fois (templates + + static/js, strings/comments/backticks gérés) → **12 sont déjà scopés** + dans des IIFEs depuis A27 (aucun conflit de page possible) +- **Seul doublon global = `openCardDetail`** (corps byte-identiques ×2 dans + `board_fragment` + `detailed_board`, fragments de vues mutuellement + exclusifs) → dédupliquée vers `static/js/app.js`, les 2 copies + supprimées (les onclick/@click des deux fragments appellent la même + définition) + +### Tests + +- `test_no_duplicate_global_functions` : garde-fou — 0 `function NAME` + globale définie 2+ fois entre templates et static/js (scanner naïf, + plafond `ponytail:` commenté : un faux positif se lit au nom signalé) +- Suite complète : **1092/1092** · ruff OK · node --check vert + +### Notes + +- Reste A38 : méthodes jumelles library/local_workspace (~9-21 noms + communs, corps divergents) → fusion `workspace-tree.js` reportée + (réconciliation sans E2E — même logique que A39/A20) + ## v7.33.0 (2026-10-01) — Audit : A43 TERMINÉ + A38 phase 1 (CSRF unifié) ### Fixed diff --git a/ROADMAP.md b/ROADMAP.md index b6a1539..066c399 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1161,7 +1161,7 @@ Quality DB views, Agent IA Palette → Realtime + E ### 🟢 P3 — Basses / hygiène -- [ ] **A38 — Duplication front systémique** : helper CSRF réimplémenté **10× sous 5 noms** (`getCsrfToken` ×3 dont 2 corps différents dans `base.html:852,1732`, `getCsrf` ×2, `_getCsrf`, `csrf()`, `csrfTok()`) + ~25 `document.cookie.match(/csrf_token=…)` en dur ; 12 `function` définies dans 2+ templates (`onDoc` ×5, `escHtml`, `esc`, `getCsrf`, `openCardDetail`…) sans IIFE systématique → ombre silencieuse entre partials ; `library.html` et `local_workspace.html` partagent **21 noms de méthodes identiques** (1 039 + 2 030 lignes quasi jumelles). *Fix : un `window.FlowDeck.getCsrf` + wrappeur IIFE/`type="module"` + un `workspace-tree.js` partagé. Effort : **M**.* **Phase 1 faite 2026-10-01 (CSRF unifié)** : helper **unique `window.getCsrf()`** dans le `` de `base.html` (le plus tôt possible) ; **76 lectures brutes du cookie → `getCsrf()`** (47 `(…||[])[1]||''` + 25 déclarations `const X = match(…)` avec leurs usages `X?X[1]:''` → `X` + 4 formes espacées) dans 9 fichiers statiques + 4 partials ; les 2 définitions dupliquées supprimées (`card_detail.html`, `database_table.js`) ; les **3 variantes de base.html** (IIFE `getCsrf`, `FlowDeck.getCsrfToken` ×2) unifiées sur `return getCsrf()` ; `welcome.html` garde sa lecture locale (page autonome sans base, documenté). **Reste A38** : les 12 `function` définies dans 2+ templates (`onDoc` ×5, `escHtml`, `esc`, `openCardDetail`… → wrappeur IIFE/module) + les 21 méthodes jumelles library/local_workspace (`workspace-tree.js` partagé). → suite 1092/1092, version 7.33.0. +- [ ] **A38 — Duplication front systémique** : helper CSRF réimplémenté **10× sous 5 noms** (`getCsrfToken` ×3 dont 2 corps différents dans `base.html:852,1732`, `getCsrf` ×2, `_getCsrf`, `csrf()`, `csrfTok()`) + ~25 `document.cookie.match(/csrf_token=…)` en dur ; 12 `function` définies dans 2+ templates (`onDoc` ×5, `escHtml`, `esc`, `getCsrf`, `openCardDetail`…) sans IIFE systématique → ombre silencieuse entre partials ; `library.html` et `local_workspace.html` partagent **21 noms de méthodes identiques** (1 039 + 2 030 lignes quasi jumelles). *Fix : un `window.FlowDeck.getCsrf` + wrappeur IIFE/`type="module"` + un `workspace-tree.js` partagé. Effort : **M**.* **Phase 1 faite 2026-10-01 (CSRF unifié)** : helper **unique `window.getCsrf()`** dans le `` de `base.html` (le plus tôt possible) ; **76 lectures brutes du cookie → `getCsrf()`** (47 `(…||[])[1]||''` + 25 déclarations `const X = match(…)` avec leurs usages `X?X[1]:''` → `X` + 4 formes espacées) dans 9 fichiers statiques + 4 partials ; les 2 définitions dupliquées supprimées (`card_detail.html`, `database_table.js`) ; les **3 variantes de base.html** (IIFE `getCsrf`, `FlowDeck.getCsrfToken` ×2) unifiées sur `return getCsrf()` ; `welcome.html` garde sa lecture locale (page autonome sans base, documenté). **Phase 2 faite 2026-10-01 (dédoublonnage)** : inventaire exhaustif des **13 noms définis 2+ fois** (templates + static/js) avec scan de profondeur de brace (strings/comments/backticks gérés) → **12 sont déjà scopés** dans des IIFEs (post-A27 : escHtml/flush/emit/setMeta/initials/up/esc/show/close… vivent en profondeur ≥1 — aucun conflit de page), et le **seul doublon GLOBALE restant = `openCardDetail`** (corps byte-identiques ×2 dans `board_fragment` + `detailed_board`, fragments de vues mutuellement exclusifs) → **dédupliquée dans `static/js/app.js`**, 2 copies supprimées, garde-fou **`test_no_duplicate_global_functions`** (0 doublon global, scanner naïf commenté `ponytail:`). **Reste A38** : les méthodes jumelles library/local_workspace (~9-21 noms communs, **corps divergents**) → fusion `workspace-tree.js` **reportée** : réconciliation de deux fichiers 1 039 + 2 031 L sans couverture E2E (même logique de report que A39 htmx et A20 unsafe-eval). → suite 1092/1092, version 7.34.0. - [x] **A39 — HTMX chargé (49,7 Ko) pour 10 attributs réels** vs 265 `fetch(` manuels. *Fix : soit drop `htmx.min.js` et convertir les 10, soit rien.* — **décision 2026-10-01 : rien** (option proposée par l'audit) : 32 attributs `hx-*` réels répartis dans 6 templates (view-switch board/dashboard/notes, `hx-boost`) ; les convertir = refonte du view-switching en JS sans couverture automatisée, coût/risque > gain de 49,7 Ko. **Reconsidérer** dès qu'un test E2E couvre le view-switch. - [x] **A40 — Assets** : `?v=` incohérent (`app.css?v=5.1.1` mais CSS modifié le 2026-09-14 > dernier bump 2026-09-12 → servi depuis le cache), la même liste d'assets est **dupliquée** dans `sw.js:19-31`, htmx/alpine/prism vendored **sans bannière de version ni SRI**, 3 ` + diff --git a/app/templates/detailed_board.html b/app/templates/detailed_board.html index c847320..0f6c4aa 100644 --- a/app/templates/detailed_board.html +++ b/app/templates/detailed_board.html @@ -58,12 +58,5 @@ {% endfor %} - + diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 251db30..a154ef8 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.33.0" + "version": "7.34.0" }, "paths": { "/auth/register": { diff --git a/static/js/app.js b/static/js/app.js index b1ca039..5aa36a3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -395,3 +395,15 @@ }); })(); +// A38 : openCardDetail était définie (corps identique) dans board_fragment +// ET detailed_board — une seule définition globale ici, appelée par les +// onclick/@click des deux fragments (owner/repo globaux fournis par board.js). +function openCardDetail(id) { + htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, { + target: '#card-modal-content', + swap: 'innerHTML' + }); + document.getElementById('card-modal').style.display = 'flex'; + document.getElementById('card-modal-title').textContent = 'Issue #' + id; +} + diff --git a/tests/test_audit_p0_fixes.py b/tests/test_audit_p0_fixes.py index d471aa2..49d7d01 100644 --- a/tests/test_audit_p0_fixes.py +++ b/tests/test_audit_p0_fixes.py @@ -366,6 +366,73 @@ def test_csrf_server_rendered_no_placeholder(client): assert token == cookie, (token[:8], cookie[:8]) +def test_no_duplicate_global_functions(): + """A38 : aucune fonction `function NAME` GLOBALE (profondeur 0) définie + 2+ fois entre les templates et static/js — les paires à risque d'ombre + silencieuse (onDoc, escHtml, openCardDetail…) vivent dans des IIFEs ou + sont dédupliquées (openCardDetail → app.js).""" + import pathlib as _pathlib + import re as _re + + files = list(_pathlib.Path("app/templates").glob("*.html")) + list( + _pathlib.Path("static/js").glob("*.js") + ) + found: dict[str, list[str]] = {} + for p in files: + s = p.read_text(encoding="utf-8", errors="ignore") + depth = 0 + line = 1 + i = 0 + state = None + n = len(s) + # ponytail: scanner naïve (strings/comments/backticks) — un faux + # positif se voit immédiatement à la lecture du nom signalé + while i < n: + c = s[i] + if c == "\n": + line += 1 + if state is None: + if c in ('"', "'"): + state = c + i += 1 + continue + if c == "`": + state = c + i += 1 + continue + if c == "/" and i + 1 < n and s[i + 1] == "/": + state = "//" + i += 2 + continue + if c == "/" and i + 1 < n and s[i + 1] == "*": + state = "/*" + i += 2 + continue + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + else: + if c == "\\": + i += 2 + continue + if (state in ('"', "'") and c == state) or (state == "`" and c == state): + state = None + elif state == "//" and c == "\n": + state = None + elif state == "/*" and c == "*" and i + 1 < n and s[i + 1] == "/": + state = None + i += 2 + continue + i += 1 + if state is None and depth == 0 and s.startswith("function ", i): + m = _re.match(r"function\s+([A-Za-z_]\w*)", s[i : i + 60]) + if m: + found.setdefault(m.group(1), []).append(f"{p.name}:{line}") + dups = {k: v for k, v in found.items() if len(v) >= 2} + assert dups == {}, dups + + def test_no_duplicate_routes(): """A24 : deux routes même méthode+chemin → l'une écrase silencieusement l'autre.""" from app.main import app