fix: A38 phase 2 — 0 doublon de fonction globale + garde-fou (v7.34.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- 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
This commit is contained in:
2026-10-02 09:18:04 -04:00
parent 770fdc2b68
commit 13dc8fdaad
10 changed files with 116 additions and 24 deletions
+67
View File
@@ -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