Skip to content

Commit 694a735

Browse files
committed
feat(bcasl): alignement de la logique sur la GUI et exécution séquentielle par phase
- Implémentation de l'exécution strictement séquentielle par phase dans le moteur BCASL. - Support de l'activation/désactivation des sections (phases) depuis la GUI et persistance dans bcasl.yml. - Suppression complète de la logique de priorité manuelle et du mode expert (l'ordre visuel prime). - Suppression du parallélisme interdit dans l'exécution des plugins. - Mise à jour de la GUI pour refléter ces simplifications (retrait des spinbox de priorité et avertissements).
1 parent ec55506 commit 694a735

4 files changed

Lines changed: 128 additions & 513 deletions

File tree

Ui/Gui/Dialogs/BcaslDialog.py

Lines changed: 42 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@ def _apply_themed_icon(widget: QPushButton, icon_name: str, size: int = 18) -> N
116116
# Constantes
117117
# ---------------------------------------------------------------------------
118118

119-
# phase_score → (display_name, min_priority, max_priority)
120-
SECTION_PHASES: dict[int, tuple[str, int, int]] = {
121-
0: ("Nettoyage", 0, 9),
122-
10: ("Validation", 10, 19),
123-
20: ("Préparation", 20, 29),
124-
30: ("Conformité", 30, 39),
125-
40: ("Linting", 40, 49),
126-
50: ("Obfuscation", 50, 59),
127-
100: ("Défaut", 60, 199),
119+
# phase_score → (display_name, description)
120+
SECTION_PHASES: dict[int, tuple[str, str]] = {
121+
0: ("Nettoyage", "Nettoyage et hygiène du workspace"),
122+
10: ("Validation", "Validation et vérification des prérequis"),
123+
20: ("Préparation", "Préparation et génération des ressources"),
124+
30: ("Conformité", "Conformité et injection de headers"),
125+
40: ("Linting", "Linting, formatage et vérification de type"),
126+
50: ("Obfuscation", "Obfuscation, protection et transpilation"),
127+
100: ("Défaut", "Autres actions (phase par défaut)"),
128128
}
129129

130130
# Tag → score de phase
@@ -141,16 +141,16 @@ def _phase_score_for_tags(tags: list[str]) -> int:
141141
return min(scores) if scores else 100
142142

143143

144-
def _section_for_phase(score: int) -> tuple[int, str, int, int]:
145-
"""Retourne (key, name, min_prio, max_prio) pour un score de phase."""
144+
def _section_for_phase(score: int) -> tuple[int, str]:
145+
"""Retourne (key, name) pour un score de phase."""
146146
# Trouver la section dont le score correspond
147147
for key in sorted(SECTION_PHASES):
148148
if score == key:
149-
name, lo, hi = SECTION_PHASES[key]
150-
return key, name, lo, hi
149+
name, _ = SECTION_PHASES[key]
150+
return key, name
151151
# Section par défaut
152-
name, lo, hi = SECTION_PHASES[100]
153-
return 100, name, lo, hi
152+
name, _ = SECTION_PHASES[100]
153+
return 100, name
154154

155155

156156
# ---------------------------------------------------------------------------
@@ -204,25 +204,17 @@ class _PluginRow(QFrame):
204204
sig_move_up = Signal(str) # plugin_id
205205
sig_move_down = Signal(str)
206206
sig_enabled = Signal(str, bool)
207-
sig_priority = Signal(str, int)
208207

209208
def __init__(
210209
self,
211210
pid: str,
212211
name: str,
213-
priority: int,
214212
enabled: bool,
215-
min_prio: int,
216-
max_prio: int,
217-
expert_ref: "list[bool]",
218213
config: Optional[dict] = None,
219214
parent: Optional[QWidget] = None,
220215
) -> None:
221216
super().__init__(parent)
222217
self.pid = pid
223-
self._min = min_prio
224-
self._max = max_prio
225-
self._expert_ref = expert_ref # [bool] mutable reference
226218
self.config: dict[str, Any] = dict(config or {})
227219
self._on_save_cb = None
228220

@@ -240,28 +232,11 @@ def __init__(
240232
self.chk.toggled.connect(lambda v: self.sig_enabled.emit(self.pid, v))
241233
row.addWidget(self.chk)
242234

243-
# Icône warning
244-
self.lbl_warn = QLabel("⚠️")
245-
self.lbl_warn.setFixedWidth(24)
246-
self.lbl_warn.setVisible(False)
247-
row.addWidget(self.lbl_warn)
248-
249235
# Nom plugin
250236
self.lbl_name = QLabel(name or pid)
251237
self.lbl_name.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
252238
row.addWidget(self.lbl_name)
253239

254-
# Label "priority"
255-
row.addWidget(QLabel("priority"))
256-
257-
# Spinbox priorité
258-
self.spin = QSpinBox()
259-
self.spin.setRange(0, 999)
260-
self.spin.setValue(priority)
261-
self.spin.setFixedWidth(70)
262-
self.spin.valueChanged.connect(self._on_priority_changed)
263-
row.addWidget(self.spin)
264-
265240
# Boutons ↑ ↓
266241
self.btn_up = QPushButton()
267242
self.btn_up.setFixedWidth(32)
@@ -277,62 +252,22 @@ def __init__(
277252
self.btn_down.clicked.connect(lambda: self.sig_move_down.emit(self.pid))
278253
row.addWidget(self.btn_down)
279254

280-
self._refresh_warning()
255+
self.setStyleSheet("QFrame#PluginRow { background: transparent; border: none; }")
281256

282257
# ------------------------------------------------------------------
283258

284-
def _on_priority_changed(self, val: int) -> None:
285-
self._refresh_warning()
286-
self.sig_priority.emit(self.pid, val)
287-
288-
def _refresh_warning(self) -> None:
289-
"""Met à jour l'indicateur ⚠️ et les styles selon expert mode et valeur."""
290-
expert = bool(self._expert_ref[0]) if self._expert_ref else False
291-
val = self.spin.value()
292-
out_of_range = not (self._min <= val <= self._max)
293-
294-
if expert or not out_of_range:
295-
self.lbl_warn.setVisible(False)
296-
# Utilise un fond transparent pour laisser voir le fond de la section
297-
self.setStyleSheet("QFrame#PluginRow { background: transparent; border: none; }")
298-
self.spin.setStyleSheet("")
299-
else:
300-
self.lbl_warn.setVisible(True)
301-
self.lbl_warn.setToolTip(
302-
f"⚠️ Priorité {val} hors de la plage recommandée ({self._min}-{self._max}).\n"
303-
"L'ordre d'exécution peut être inattendu.\n"
304-
"Utilisez le mode Expert pour désactiver les avertissements."
305-
)
306-
colors = _get_bcasl_colors()
307-
self.setStyleSheet(f"QFrame#PluginRow {{ background: {colors['warn_bg']}; border: 1px solid {colors['warn_border']}; border-radius: 4px; }}")
308-
self.spin.setStyleSheet(f"QSpinBox {{ border: 2px solid {colors['warn_border']}; }}")
309-
310-
def refresh_expert(self) -> None:
311-
"""Appelé quand expert mode change."""
312-
expert = bool(self._expert_ref[0]) if self._expert_ref else False
313-
if expert:
314-
self.spin.setRange(0, 999)
315-
else:
316-
self.spin.setRange(0, 999) # toujours libre en saisie, warning géré visuellement
317-
self._refresh_warning()
318-
319259
# ------------------------------------------------------------------
320260
# Accesseurs
321261

322262
@property
323263
def is_enabled(self) -> bool:
324264
return self.chk.isChecked()
325265

326-
@property
327-
def priority_value(self) -> int:
328-
return self.spin.value()
329-
330266
def snapshot(self) -> dict[str, Any]:
331-
return {"pid": self.pid, "enabled": self.is_enabled, "priority": self.priority_value}
267+
return {"pid": self.pid, "enabled": self.is_enabled}
332268

333269
def restore(self, snap: dict[str, Any]) -> None:
334270
self.chk.setChecked(bool(snap.get("enabled", True)))
335-
self.spin.setValue(int(snap.get("priority", 0)))
336271

337272

338273
# ---------------------------------------------------------------------------
@@ -348,21 +283,15 @@ def __init__(
348283
self,
349284
phase_key: int,
350285
name: str,
351-
min_prio: int,
352-
max_prio: int,
353-
expert_ref: "list[bool]",
354286
parent: Optional[QWidget] = None,
355287
) -> None:
356288
super().__init__(parent)
357289
self.phase_key = phase_key
358290
self._name = name
359-
self._min = min_prio
360-
self._max = max_prio
361-
self._expert_ref = expert_ref
362291
self._rows: list[_PluginRow] = []
363292

364293
# Titre natif du GroupBox
365-
self.setTitle(f"{name} ({min_prio}{max_prio})")
294+
self.setTitle(name)
366295

367296
# Rendre le GroupBox collapsible via la checkbox native
368297
self.setCheckable(True)
@@ -388,7 +317,6 @@ def add_row(self, row: _PluginRow) -> None:
388317
row.sig_move_up.connect(self._on_move_up)
389318
row.sig_move_down.connect(self._on_move_down)
390319
row.sig_enabled.connect(lambda _pid, _v: self.sig_changed.emit())
391-
row.sig_priority.connect(lambda _pid, _v: self.sig_changed.emit())
392320
# S'assurer que la visibilité suit l'état actuel du toggle
393321
row.setVisible(self.isChecked())
394322
# Mettre à jour l'état des boutons ↑/↓ pour toute la section
@@ -496,9 +424,6 @@ def __init__(
496424
except Exception:
497425
self._ark_cfg = {}
498426

499-
# Expert mode : liste mutable partagée avec les rows
500-
self._expert: list[bool] = [False]
501-
502427
# Undo / Redo stacks
503428
self._undo_stack: list[Any] = []
504429
self._redo_stack: list[Any] = []
@@ -575,10 +500,6 @@ def _build_ui(self) -> None:
575500

576501
# Barre du bas
577502
bottom = QHBoxLayout()
578-
self._chk_expert = QCheckBox(self._gui.tr("Mode Expert (priorités libres)", "Expert mode (any priority)"))
579-
self._chk_expert.setChecked(False)
580-
self._chk_expert.toggled.connect(self._on_expert_toggled)
581-
bottom.addWidget(self._chk_expert)
582503
bottom.addStretch(1)
583504

584505
btn_cancel = QPushButton(self._gui.tr("Annuler", "Cancel"))
@@ -610,6 +531,11 @@ def _populate_sections(self) -> None:
610531
plugins_raw = self._cfg.get("plugins", {}) if isinstance(self._cfg, dict) else {}
611532
plugin_list = _read_plugin_list(plugins_raw)
612533

534+
# Charger l'état des phases
535+
phases_cfg = self._cfg.get("phases", {})
536+
if not isinstance(phases_cfg, dict):
537+
phases_cfg = {}
538+
613539
# Construire un mapping pid → entry
614540
pid_entry: dict[str, dict[str, Any]] = {}
615541
for entry in plugin_list:
@@ -629,7 +555,7 @@ def _populate_sections(self) -> None:
629555
score = _phase_score_for_tags(list(tags))
630556
phase_groups.setdefault(score, []).append((pid, entry))
631557

632-
# Trier chaque groupe par priorité
558+
# Trier chaque groupe par priorité (ordre initial)
633559
for score in phase_groups:
634560
phase_groups[score].sort(key=lambda x: int(x[1].get("priority", 0)))
635561

@@ -640,10 +566,15 @@ def _populate_sections(self) -> None:
640566
for score in sorted(SECTION_PHASES.keys()):
641567
if score not in phase_groups:
642568
continue
643-
key_name, lo, hi = SECTION_PHASES[score]
644-
section = _SectionWidget(score, key_name, lo, hi, self._expert)
569+
key_name, desc = SECTION_PHASES[score]
570+
section = _SectionWidget(score, key_name)
571+
section.setToolTip(desc)
645572
section.sig_changed.connect(self._on_any_change)
646573

574+
# Appliquer l'état d'activation de la phase
575+
if key_name in phases_cfg:
576+
section.setChecked(bool(phases_cfg[key_name]))
577+
647578
for pid, entry in phase_groups[score]:
648579
meta = self._meta_map.get(pid, {})
649580
display_name = meta.get("name") or pid
@@ -652,11 +583,7 @@ def _populate_sections(self) -> None:
652583
row = _PluginRow(
653584
pid=pid,
654585
name=label,
655-
priority=int(entry.get("priority", 0)),
656586
enabled=bool(entry.get("enabled", True)),
657-
min_prio=lo,
658-
max_prio=hi,
659-
expert_ref=self._expert,
660587
config=dict(entry.get("config", {})),
661588
)
662589
section.add_row(row)
@@ -813,21 +740,30 @@ def _do_save(self) -> None:
813740
f"Impossible de mettre à jour ark.yml: {e}"
814741
)
815742

816-
# 2) Construire la liste ordonnée de plugins pour bcasl.yml
743+
# 2) Construire la liste ordonnée de plugins pour bcasl.yml et l'état des phases
817744
ordered: list[dict[str, Any]] = []
745+
phases_state: dict[str, bool] = {}
746+
747+
# Le backend utilise rec.priority pour ordonner dans une phase.
748+
# On assigne un index global incrémental pour refléter l'ordre visuel.
749+
current_prio = 0
750+
818751
for section in self._sections:
752+
phases_state[section._name] = section.isChecked()
819753
for row in section.rows:
820754
cfg_for_row = plugin_configs.get(row.pid, row.config or {})
821755
ordered.append({
822756
"name": row.pid,
823757
"enabled": row.is_enabled,
824-
"priority": row.priority_value,
758+
"priority": current_prio,
825759
"config": cfg_for_row,
826760
})
761+
current_prio += 1
827762

828763
# Construire la config de sortie pour bcasl.yml
829764
cfg_out: dict[str, Any] = dict(self._cfg) if isinstance(self._cfg, dict) else {}
830765
cfg_out["plugins"] = _plugins_list_to_yaml(ordered)
766+
cfg_out["phases"] = phases_state
831767
# plugin_order maintient la compatibilité avec l'ancien loader
832768
cfg_out["plugin_order"] = [e["name"] for e in ordered]
833769

bcasl/Loader.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,17 @@ def _read_yml(p: Path) -> dict[str, Any]:
440440
except Exception:
441441
pass
442442

443+
# Phases par défaut (toutes activées)
444+
default_phases = {
445+
"Nettoyage": True,
446+
"Validation": True,
447+
"Préparation": True,
448+
"Conformité": True,
449+
"Linting": True,
450+
"Obfuscation": True,
451+
"Défaut": True,
452+
}
453+
443454
default_cfg = {
444455
"file_patterns": file_patterns,
445456
"exclude_patterns": exclude_patterns,
@@ -449,6 +460,7 @@ def _read_yml(p: Path) -> dict[str, Any]:
449460
"plugin_parallelism": 0,
450461
"iter_files_cache": True,
451462
},
463+
"phases": default_phases,
452464
"plugins": detected_plugins,
453465
"plugin_order": plugin_order,
454466
}

0 commit comments

Comments
 (0)