v0.4.0 MIAM choisit le modèle : banc et choix.json retirés, type + seuil
Applique docs/protocole-agents.md de MIAM (PH2-E3) : - banc/ et /miam:banc supprimés : MIAM teste les presets lui-même - miam:delegue passe type + seuil, ou preset ; MIAM peut donc remplacer le modèle chargé s'il est sous le seuil - modeles en repli si le contexte est dépassé ; preset_defaut seulement sans test ni modèle chargé - MIAM 2.5.1 minimum Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f59453b84
commit
5363b86a16
15 files changed
+50
-1515
No files matched your search
+1
-13
@@ -12,24 +12,12 @@
|
||||
# bundles MCP décompressés
|
||||
.mcpb-cache/
|
||||
|
||||
# --- Python (banc d'essai) --------------------------------------------------------------------------------------
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
# résultats d'un banc lancé à la main dans le dépôt (le skill écrit dans le dossier de données du plugin)
|
||||
plugins/miam/banc/resultats/
|
||||
plugins/miam/banc/rapport.md
|
||||
plugins/miam/banc/choix.json
|
||||
|
||||
# --- Node / TypeScript (tests du mod) ---------------------------------------------------------------------------
|
||||
node_modules/
|
||||
*.tsbuildinfo
|
||||
|
||||
# --- Journaux, sauf les données fixes du banc -------------------------------------------------------------------
|
||||
# --- Journaux ---------------------------------------------------------------------------------------------------
|
||||
*.log
|
||||
!plugins/miam/banc/fichiers/*.log
|
||||
|
||||
# --- Système et éditeurs ----------------------------------------------------------------------------------------
|
||||
.DS_Store
|
||||
|
||||
@@ -23,9 +23,9 @@ Ce dépôt est le marketplace A2M Systèmes. Actuellement, il contient le plugin
|
||||
| `miam` | Délègue des tâches à un modèle local géré par MIAM (Mon IA Manager, orchestrateur llama.cpp), via son serveur MCP. | [plugins/miam/README.md](plugins/miam/README.md) |
|
||||
|
||||
Le plugin `miam` fournit :
|
||||
- le serveur MCP `miam` (outils `etat`, `presets`, `demarrer`, `deleguer`, `suivre`…) ;
|
||||
- le subagent `miam:delegue`, qui démarre le bon preset, délègue, vérifie le résultat et rend un compte rendu court ;
|
||||
- la skill `deleguer` (quand et comment déléguer) et la skill `/miam:banc` (banc d'essai des presets) ;
|
||||
- le serveur MCP `miam` (outils `etat`, `modeles`, `presets`, `demarrer`, `deleguer`, `suivre`…) ;
|
||||
- le subagent `miam:delegue`, qui délègue en laissant MIAM choisir le modèle, vérifie le résultat et rend un compte rendu court ;
|
||||
- la skill `deleguer` (quand et comment déléguer) ;
|
||||
- un compteur de tokens économisés (notification, ligne d'état, `/miam-economies`), net du pilotage et des
|
||||
corrections, sans coût en tokens.
|
||||
|
||||
@@ -119,14 +119,6 @@ plugins/
|
||||
skills/
|
||||
deleguer/ # Skill pour déclencher la délégation
|
||||
SKILL.md
|
||||
banc/ # Skill pour le banc d'essai
|
||||
SKILL.md
|
||||
banc/ # Composants du banc d'essai
|
||||
banc_miam.py
|
||||
mcp_http.py
|
||||
taches.py
|
||||
verifier_controles.py
|
||||
fichiers/
|
||||
LICENSE # Licence du plugin (MIT)
|
||||
README.md # Documentation du plugin
|
||||
README.md # Ce fichier
|
||||
|
||||
@@ -120,7 +120,7 @@ rien (coût 0) n'est pas envoyé.
|
||||
|
||||
**Gain net** = somme des `estimation.evites` − somme des `cout` de `pilotage` − somme des `cout` de `correction`.
|
||||
Le taux de reprise d'une délégation (`cout` de ses corrections / ses `evites`) mesure la qualité réelle du modèle
|
||||
local sur la tâche : c'est la donnée la plus utile pour classer les presets avec le banc.
|
||||
local sur la tâche : c'est la donnée la plus utile pour classer les presets avec les tests de MIAM.
|
||||
|
||||
**Calcul du coût (fonction `cout`) :**
|
||||
`input_tokens + cache_creation_input_tokens + output_tokens + Math.round(cache_read_input_tokens / 10)`.
|
||||
@@ -190,7 +190,7 @@ tous les ordres.
|
||||
`evites` (somme de `estimation.evites`), `pilotage` (somme des `cout` de `pilotage`), `corrections` (somme des
|
||||
`cout` de `correction`), `net = evites - pilotage - corrections`, et `taux_reprise = corrections / evites`
|
||||
(`null` si `evites` ≤ 0). Fichier absent : section à zéro. La jointure avec `delegations.jsonl` par `demande_id` permet ensuite le détail par preset ou par
|
||||
format (utile au choix des presets par le banc).
|
||||
format (utile au choix du modèle par MIAM).
|
||||
5. Tests : unitaires sur la validation (chaque champ requis, types, lot vide, 501 événements, `evites` négatif accepté)
|
||||
et sur le calcul du bilan (doublons, pilotage reçu avant sa délégation) ; intégration HTTP réelle comme les tests
|
||||
MCP existants (401 sans jeton, 200 avec `recus`/`doublons`, renvoi du même lot → tout en doublons, 422, 413).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "miam",
|
||||
"displayName": "MIAM",
|
||||
"version": "0.3.0",
|
||||
"description": "Délégation de tâches à un modèle local géré par MIAM (Mon IA Manager) : subagent, règles de délégation, banc d'essai des presets",
|
||||
"version": "0.4.0",
|
||||
"description": "Délégation de tâches à un modèle local géré par MIAM (Mon IA Manager) : subagent, règles de délégation, compteur de tokens économisés",
|
||||
"author": {
|
||||
"name": "Ange-Marie MAURIN"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"preset_defaut": {
|
||||
"type": "string",
|
||||
"title": "Preset MIAM par défaut",
|
||||
"description": "Preset démarré quand aucun modèle n'est chargé et que le banc n'a pas encore de résultats (id ou nom, voir l'outil presets)",
|
||||
"description": "Preset démarré quand aucun modèle n'est chargé et que MIAM n'a aucun test pour le type de la tâche (id ou nom, voir l'outil modeles)",
|
||||
"default": "pre-16312f8b"
|
||||
}
|
||||
}
|
||||
|
||||
+8
-14
@@ -8,10 +8,9 @@ Plugin Claude Code pour déléguer des tâches à un modèle local géré par MI
|
||||
- Serveur MCP `miam` (http://127.0.0.1:{port}/mcp, jeton Bearer) : point d'entrée pour la communication avec le serveur MCP de MIAM.
|
||||
- Subagent `miam:delegue` : agent dédié qui pilote la délégation des tâches vers MIAM.
|
||||
- Skill `deleguer` : règles, pour l'agent principal, sur quand et comment déléguer une tâche à MIAM.
|
||||
- Skill `/miam:banc` : banc d'essai des presets de MIAM pour évaluer les performances.
|
||||
|
||||
## Prérequis
|
||||
MIAM 2.5.0 ou plus lancé avec son serveur MCP démarré (interrupteur du dashboard, menu « Serveur MCP » du tray, ou `--auto-start-mcp`) ; Python 3.12 pour le banc.
|
||||
MIAM 2.5.1 ou plus (`type`, `seuil` et l'outil `modeles`, champs `offload` à partir de 2.5.2) lancé avec son serveur MCP démarré (interrupteur du dashboard, menu « Serveur MCP » du tray, ou `--auto-start-mcp`).
|
||||
|
||||
## Installation
|
||||
Le plugin fait partie de la marketplace `a2m-plugins` (voir le [README du dépôt](../../README.md#installation) pour
|
||||
@@ -38,10 +37,12 @@ Puis : à l'activation, Claude Code demande les réglages (voir section suivante
|
||||
| jeton | (vide) | Jeton Bearer du serveur MCP de MIAM, rangé dans le coffre d'identifiants. |
|
||||
| port | 8090 | Port du serveur MCP de MIAM sur 127.0.0.1. |
|
||||
| modele_subagent | haiku | Modèle Claude qui pilote MIAM pour l'agent principal (rédige la consigne, attend, vérifie). |
|
||||
| preset_defaut | pre-16312f8b | Preset MIAM par défaut, démarré quand aucun modèle n'est chargé et que le banc n'a pas encore de résultats. |
|
||||
| preset_defaut | pre-16312f8b | Preset MIAM par défaut, démarré quand aucun modèle n'est chargé et que MIAM n'a aucun test pour le type de la tâche. |
|
||||
|
||||
## Utilisation
|
||||
L'agent principal lance le subagent miam:delegue ; celui-ci vérifie que MIAM répond (etat), démarre si aucun modèle n'est chargé le meilleur preset de la catégorie de la tâche d'après choix.json du banc (sinon le preset par défaut), ne remplace jamais un modèle déjà chargé sans demande, délègue, vérifie, et rend un compte rendu court.
|
||||
L'agent principal lance le subagent miam:delegue ; celui-ci vérifie que MIAM répond (etat), classe la tâche, délègue avec `type` (`code`, `texte`, `rapide`) et `seuil` (0,8 par défaut), vérifie, et rend un compte rendu court avec le choix de MIAM.
|
||||
|
||||
MIAM choisit le modèle d'après ses tests de qualité : il garde le modèle chargé s'il atteint le seuil, sinon il charge le meilleur preset, quitte à remplacer le modèle chargé (20 à 40 s) ; si aucun n'atteint le seuil, il prend le meilleur et le signale. L'agent principal peut imposer un preset ou demander de garder le modèle chargé. Contrat : `docs/protocole-agents.md` du dépôt MIAM.
|
||||
|
||||
## Tokens économisés
|
||||
|
||||
@@ -63,8 +64,8 @@ MIAM ne répond pas) : contrat et modèle de données dans [docs/token-summary-m
|
||||
moins qu'un token d'Opus, ce que le net ne pondère pas ; une reprise hors du fichier `sortie` n'est pas vue (d'où
|
||||
l'intérêt de faire écrire les résultats par MIAM avec `sortie`).
|
||||
|
||||
## Banc d'essai
|
||||
Tâches fixes reprises de llm-banc (catégories code, texte, rapide ; pas de vision), lancé par /miam:banc dans une fenêtre PowerShell externe, résultats rapport.md et choix.json dans le dossier de données du plugin, critère de choix (meilleure note par catégorie ; à moins de 0,02 d'écart, la vitesse en tok/s départage). Le script lit le jeton dans %LOCALAPPDATA%\MIAM\config.json ou la variable MIAM_JETON.
|
||||
## Tests des modèles
|
||||
MIAM teste lui-même les presets, en travaux de fond (page Benchmarks) : le plugin n'a plus de banc. L'ancien dossier de données du plugin (`banc/`, `rapport.md`, `choix.json`) n'est plus lu.
|
||||
|
||||
## Structure du plugin
|
||||
```text
|
||||
@@ -75,18 +76,11 @@ agents/
|
||||
delegue.md # subagent miam:delegue
|
||||
skills/
|
||||
deleguer/SKILL.md # quand et comment déléguer
|
||||
banc/SKILL.md # /miam:banc
|
||||
hooks/
|
||||
hooks.json
|
||||
register.ts # compteur de tokens économisés (ligne d'état, notifications, /miam-economies)
|
||||
economies.ts # calcul pur, testé
|
||||
*.test.ts # 35 tests : claude plugin test plugins/miam
|
||||
banc/
|
||||
banc_miam.py # banc d'essai des presets via MIAM
|
||||
mcp_http.py # client MCP minimal
|
||||
taches.py # tâches et contrôles (repris de llm-banc)
|
||||
verifier_controles.py
|
||||
fichiers/ # données fixes du banc
|
||||
LICENSE
|
||||
README.md
|
||||
```
|
||||
@@ -100,4 +94,4 @@ README.md
|
||||
|
||||
## Licence
|
||||
|
||||
MIT, voir [LICENSE](LICENSE). Les fichiers du banc (`banc/`) reprennent ceux de llm-banc, du même auteur.
|
||||
MIT, voir [LICENSE](LICENSE).
|
||||
@@ -19,21 +19,20 @@ Appelle `etat`. Si l'outil est absent ou échoue (connexion refusée, 401, serve
|
||||
Reconnect sur `plugin:miam:miam`. Sur 401 : jeton à ressaisir dans `/plugin` → miam → configurer. » Ne tente pas de
|
||||
faire la tâche à la place du modèle local.
|
||||
|
||||
## 2. Un modèle est-il chargé ?
|
||||
## 2. Quel modèle ?
|
||||
|
||||
- **Un modèle est chargé** : utilise-le. Ne le remplace **jamais** (`remplacer=true`) sans que l'agent principal l'ait
|
||||
demandé explicitement.
|
||||
- **Aucun modèle** : choisis le preset dans `${CLAUDE_PLUGIN_DATA}/banc/choix.json` (lis-le avec Read) : entrée
|
||||
`categories.<catégorie>.preset`, catégorie selon la tâche :
|
||||
- `code` : fonction, tests, script, SVG ;
|
||||
- `texte` : traduction, résumé court, notes de version, docstrings, commit, JSON à rédiger ;
|
||||
- `rapide` : lire beaucoup pour rendre peu (log, sortie de tests, questions sur un long document).
|
||||
Sans ce fichier (banc pas encore lancé), prends le preset par défaut `${user_config.preset_defaut}`.
|
||||
Appelle `demarrer(preset=…)`. Le chargement prend 20 à 40 s : si la réponse est « en file » (`ok` absent ou nul,
|
||||
avec `demande_id`), appelle `suivre(demande_id, attente_s=45)` jusqu'à la fin. Un refus (Unreal ouvert, preset
|
||||
inconnu) : rends-le tel quel.
|
||||
- Si `choix.json` montre que le modèle chargé est faible pour la catégorie (note < `seuil`), délègue quand même et
|
||||
signale-le dans le compte rendu, avec le preset conseillé.
|
||||
MIAM choisit le modèle d'après ses propres tests de qualité. Ne choisis pas à sa place, et n'appelle pas `demarrer` avant
|
||||
de déléguer. Classe la tâche :
|
||||
- `code` : fonction, tests, script, SVG ;
|
||||
- `texte` : traduction, résumé court, notes de version, docstrings, commit, JSON à rédiger ;
|
||||
- `rapide` : lire beaucoup pour rendre peu (log, sortie de tests, questions sur un long document).
|
||||
|
||||
Puis, selon le brief :
|
||||
- **cas général** : `type` + `seuil` (celui du brief, sinon `0.8`). MIAM garde le modèle chargé s'il atteint le seuil,
|
||||
sinon il charge le meilleur preset, y compris s'il faut remplacer le modèle chargé ;
|
||||
- **preset imposé** : `preset` (id ou nom), prioritaire sur `type` ;
|
||||
- **« garder le modèle chargé »** : ni `type` ni `preset`. Si la catégorie figure dans `sous_seuil` de l'instance
|
||||
(`etat`), délègue quand même et signale-le dans le compte rendu.
|
||||
|
||||
## 3. Déléguer
|
||||
|
||||
@@ -41,6 +40,7 @@ faire la tâche à la place du modèle local.
|
||||
ce qu'il ne faut pas faire), les cas limites. Une consigne vague donne un résultat brouillon.
|
||||
|
||||
Appelle `deleguer` :
|
||||
- `type`, `seuil` ou `preset` : voir la section 2 ;
|
||||
- `consigne` ; `fichiers` : chemins **absolus** (ou relatifs avec `racine` = dossier du projet), `chemin:debut-fin`
|
||||
pour une partie ; MIAM les lit, toi non ;
|
||||
- `format` : `json` pour des faits extraits (validité vérifiée par MIAM, `json_valide`), `code` pour du code (balises
|
||||
@@ -49,7 +49,16 @@ Appelle `deleguer` :
|
||||
si l'agent principal l'a dit ;
|
||||
- `reflexion` : `off` par défaut ; `bas`/`moyen`/`haut` seulement si la tâche demande du raisonnement.
|
||||
|
||||
Réponse « en file » : `suivre(demande_id, attente_s=45)` jusqu'au résultat. Pour abandonner : `annuler(demande_id)`.
|
||||
Réponse « en file » (`ok` absent ou nul, avec `demande_id`) : `suivre(demande_id, attente_s=45)` jusqu'au résultat. Pour
|
||||
abandonner : `annuler(demande_id)`. Un changement de modèle prend 20 à 40 s de plus : `choix.demarrage` est le
|
||||
`demande_id` du chargement, la délégation passe juste après ; si elle échoue, `suivre(choix.demarrage)` en donne la cause.
|
||||
|
||||
Refus de `deleguer` :
|
||||
- `aucun preset testé pour « … »` (aucun modèle chargé, MIAM n'a pas de note pour ce type) : `demarrer(preset=
|
||||
"${user_config.preset_defaut}")`, `suivre` jusqu'à la fin, puis refais le même `deleguer` ;
|
||||
- contexte dépassé : `modeles`, puis refais le `deleguer` avec `preset` = le mieux noté dont le `contexte` suffit, sans
|
||||
`offload` si possible (une partie du modèle tourne alors sur le CPU, donc lentement) ;
|
||||
- autre refus (Unreal ouvert, `seuil` hors limites, preset inconnu) : rends-le tel quel.
|
||||
|
||||
## 4. Vérifier
|
||||
|
||||
@@ -67,6 +76,7 @@ rends l'échec, n'essaie pas de corriger toi-même un gros résultat.
|
||||
|
||||
Court, pour l'agent principal :
|
||||
- statut (réussi / raté / partiel) et vérifications faites (avec leur résultat) ;
|
||||
- modèle utilisé, durée, tokens lus/écrits (champs de la réponse de `deleguer`) ;
|
||||
- modèle utilisé, durée, tokens lus/écrits (champs de la réponse de `deleguer`) ; `choix.raison`, et
|
||||
`choix.avertissement` s'il existe, tel quel ;
|
||||
- le résultat : en entier s'il est court, sinon le chemin de `sortie` et ce qu'il contient ;
|
||||
- limites ou points à relire.
|
||||
@@ -1,184 +0,0 @@
|
||||
"""Banc d'essai des presets de MIAM : les tâches fixes de llm-banc, passées par la vraie chaîne de délégation.
|
||||
|
||||
Usage (dans une fenêtre PowerShell à part : plusieurs minutes par preset) :
|
||||
python banc_miam.py --sortie <dossier> tous les presets validés
|
||||
python banc_miam.py --sortie <dossier> pre-16312f8b ... seulement ceux-là (id ou nom)
|
||||
python banc_miam.py --sortie <dossier> --categorie code une catégorie (code, texte, rapide)
|
||||
|
||||
Pour chaque preset : `demarrer(remplacer=True)`, puis chaque tâche par `deleguer` (fichiers passés par chemin, lus par
|
||||
MIAM), notée par les contrôles de taches.py. Pas de vision : `deleguer` ne transmet que du texte. MIAM applique ses
|
||||
règles du poste à chaque démarrage (ComfyUI vidé, refus si Unreal est ouvert). Le modèle chargé au départ est remis à
|
||||
la fin (sauf --sans-restaurer).
|
||||
|
||||
Écrit : <sortie>/<date-heure>/<preset>.json (+ reponses/), puis, à partir du dernier essai de chaque preset,
|
||||
<sortie>/rapport.md et <sortie>/choix.json (meilleur preset par catégorie, lu par le subagent miam:delegue).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_http import ClientMcp, ErreurMcp, jeton_miam
|
||||
from taches import FICHIERS, TACHES, Tache
|
||||
|
||||
CATEGORIES = ("code", "texte", "rapide")
|
||||
MAX_TOKENS = 3000
|
||||
SEUIL = 0.8 # note à partir de laquelle une tâche est réussie
|
||||
EGALITE = 0.02 # écart de note sous lequel la vitesse départage
|
||||
LU, ECRIT = 1, 5 # unités de coût de Claude (cf. llm-banc/rapport.py)
|
||||
|
||||
|
||||
def attendre(client: ClientMcp, reponse: dict) -> dict:
|
||||
"""Suit une demande « en file » jusqu'à sa fin (ok vrai ou faux)."""
|
||||
while reponse.get("ok") is None and reponse.get("demande_id"):
|
||||
reponse = client.outil("suivre", demande_id=reponse["demande_id"], attente_s=45)
|
||||
return reponse
|
||||
|
||||
|
||||
def essayer(client: ClientMcp, tache: Tache) -> tuple[dict, str]:
|
||||
fichiers = [str(FICHIERS / tache.fichier)] if tache.fichier else None
|
||||
r = attendre(client, client.outil("deleguer", consigne=tache.consigne, fichiers=fichiers, format="texte",
|
||||
max_tokens=MAX_TOKENS))
|
||||
base = {"tache": tache.id, "categorie": tache.categorie, "nature": tache.nature, "a_juger": tache.a_juger}
|
||||
if not r.get("ok"):
|
||||
return base | {"note": 0.0, "detail": f"erreur : {r.get('erreur') or r.get('raison') or r}"}, ""
|
||||
texte = r.get("reponse") or ""
|
||||
note, detail = tache.controle(texte) if texte.strip() else (0.0, "aucune réponse")
|
||||
duree, sortie = r.get("duree_s") or 0, r.get("tokens_sortie") or 0
|
||||
return base | {"note": round(note, 3), "detail": detail, "duree_s": duree,
|
||||
"tokens_entree": r.get("tokens_entree"), "tokens_sortie": sortie,
|
||||
"tok_s": round(sortie / duree, 1) if duree else None, # prompt compris : sous-estime l'écriture
|
||||
"limite_atteinte": bool(r.get("tronque"))}, texte
|
||||
|
||||
|
||||
def gain(t: dict) -> tuple[float, float]:
|
||||
"""(coût sans déléguer, coût en déléguant) ; une tâche ratée est refaite par Claude (cf. llm-banc)."""
|
||||
entree, sortie = t.get("tokens_entree") or 0, t.get("tokens_sortie") or 0
|
||||
if t["nature"] == "lecture":
|
||||
sans, avec = entree * LU + sortie * ECRIT, 50 * ECRIT + sortie * LU
|
||||
else:
|
||||
sans, avec = sortie * ECRIT, entree * ECRIT + sortie * LU
|
||||
return sans, avec + (sans if t["note"] < SEUIL else 0)
|
||||
|
||||
|
||||
def resume(r: dict) -> dict:
|
||||
notes = {c: [t["note"] for t in r["taches"] if t["categorie"] == c] for c in CATEGORIES}
|
||||
vitesses = [t["tok_s"] for t in r["taches"] if t.get("tok_s")]
|
||||
couts = [gain(t) for t in r["taches"]]
|
||||
sans, avec = sum(c[0] for c in couts), sum(c[1] for c in couts)
|
||||
toutes = [n for ns in notes.values() for n in ns]
|
||||
return {"nom": r["preset"], "modele": r.get("modele"), "date": r["date"],
|
||||
"notes": {c: round(sum(ns) / len(ns), 3) for c, ns in notes.items() if ns},
|
||||
"moyenne": round(sum(toutes) / max(len(toutes), 1), 3),
|
||||
"reussies": f"{sum(n >= SEUIL for n in toutes)}/{len(toutes)}",
|
||||
"tok_s": round(sum(vitesses) / len(vitesses), 1) if vitesses else None,
|
||||
"vram_mo": r.get("vram_mo"), "chargement_s": r.get("chargement_s"),
|
||||
"gain": round((sans - avec) / sans, 2) if sans else None}
|
||||
|
||||
|
||||
def derniers(sortie: Path) -> dict[str, dict]:
|
||||
"""Dernier essai de chaque preset, tous dossiers d'essai confondus."""
|
||||
retenus: dict[str, dict] = {}
|
||||
for f in sorted(sortie.glob("*/*.json")):
|
||||
r = json.loads(f.read_text(encoding="utf-8"))
|
||||
if "erreur" not in r and r.get("taches") and r["date"] >= retenus.get(r["id"], {}).get("date", ""):
|
||||
retenus[r["id"]] = r
|
||||
return retenus
|
||||
|
||||
|
||||
def choisir(resumes: dict[str, dict]) -> dict:
|
||||
categories = {}
|
||||
for c in CATEGORIES:
|
||||
candidats = [(i, s) for i, s in resumes.items() if c in s["notes"]]
|
||||
if not candidats:
|
||||
continue
|
||||
meilleure = max(s["notes"][c] for _, s in candidats)
|
||||
proches = [(i, s) for i, s in candidats if s["notes"][c] >= meilleure - EGALITE]
|
||||
i, s = max(proches, key=lambda p: p[1]["tok_s"] or 0)
|
||||
categories[c] = {"preset": i, "nom": s["nom"], "note": s["notes"][c], "tok_s": s["tok_s"]}
|
||||
return {"date": datetime.datetime.now().isoformat(timespec="seconds"), "seuil": SEUIL, "categories": categories,
|
||||
"presets": resumes}
|
||||
|
||||
|
||||
def rapport(choix: dict) -> str:
|
||||
lignes = ["# Banc d'essai des presets de MIAM\n",
|
||||
("Tâches fixes de llm-banc passées par `deleguer` (sans vision). Note de 0 à 1 ; dernier essai de chaque "
|
||||
"preset. tok/s : tokens écrits / durée de l'appel (lecture du prompt comprise).\n"),
|
||||
"## Choix par catégorie\n", "| Catégorie | Preset | Note | tok/s |", "|---|---|---|---|"]
|
||||
lignes += [f"| {c} | {v['nom']} (`{v['preset']}`) | {v['note']:.2f} | {v['tok_s']} |"
|
||||
for c, v in choix["categories"].items()]
|
||||
lignes += ["\n## Presets\n", "| Preset | " + " | ".join(CATEGORIES) + " | Moyenne | Réussies | tok/s | VRAM | "
|
||||
"Chargement | Gain | Essai |", "|---|" + "---|" * (len(CATEGORIES) + 7)]
|
||||
for i, s in choix["presets"].items():
|
||||
notes = [f"{s['notes'][c]:.2f}" if c in s["notes"] else "—" for c in CATEGORIES]
|
||||
vram = f"{s['vram_mo'] / 1024:.1f} Go" if s.get("vram_mo") else "?"
|
||||
gain_ = f"{s['gain']:+.0%}" if s.get("gain") is not None else "?"
|
||||
lignes.append(f"| {s['nom']} (`{i}`) | " + " | ".join(notes) + f" | **{s['moyenne']:.2f}** | {s['reussies']} "
|
||||
f"| {s['tok_s']} | {vram} | {s.get('chargement_s') or '—'} s | {gain_} | {s['date'][:16]} |")
|
||||
return "\n".join(lignes) + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parseur = argparse.ArgumentParser(description="Banc d'essai des presets de MIAM")
|
||||
parseur.add_argument("presets", nargs="*", help="id ou nom (défaut : tous les presets validés)")
|
||||
parseur.add_argument("--sortie", required=True, type=Path, help="dossier des résultats")
|
||||
parseur.add_argument("--categorie", choices=CATEGORIES)
|
||||
parseur.add_argument("--port", type=int, default=int(os.environ.get("MIAM_PORT", "8090")))
|
||||
parseur.add_argument("--sans-restaurer", action="store_true", help="laisser chargé le dernier preset essayé")
|
||||
options = parseur.parse_args()
|
||||
|
||||
client = ClientMcp(f"http://127.0.0.1:{options.port}/mcp", jeton_miam())
|
||||
client.ouvrir()
|
||||
valides = client.outil("presets")["presets"]
|
||||
initial = client.outil("etat").get("instances") or []
|
||||
initial_id = next((p["id"] for p in valides if initial and p["preset"] == initial[0]["preset"]), None)
|
||||
demandes = [d.lower() for d in options.presets]
|
||||
choisis = [p for p in valides if not demandes or p["id"].lower() in demandes or p["preset"].lower() in demandes]
|
||||
if demandes and len(choisis) < len(demandes):
|
||||
raise SystemExit(f"presets validés inconnus parmi : {', '.join(options.presets)}")
|
||||
categories = {options.categorie} if options.categorie else set(CATEGORIES)
|
||||
taches = [t for t in TACHES if t.categorie in categories]
|
||||
|
||||
essai = options.sortie / datetime.datetime.now().strftime("%Y-%m-%d_%H%M")
|
||||
(essai / "reponses").mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
for p in choisis:
|
||||
print(f"\n=== {p['preset']} ({p['id']}) : {len(taches)} tâche(s)", flush=True)
|
||||
resultat = {"id": p["id"], "preset": p["preset"], "modele": p["modele"],
|
||||
"date": datetime.datetime.now().isoformat(timespec="seconds"), "taches": []}
|
||||
fichier = essai / f"{p['id']}.json"
|
||||
demarrage = attendre(client, client.outil("demarrer", preset=p["id"], remplacer=True))
|
||||
if not demarrage.get("ok"):
|
||||
print(f"✘ {demarrage.get('erreur') or demarrage.get('raison')}", flush=True)
|
||||
resultat["erreur"] = str(demarrage.get("erreur") or demarrage.get("raison"))
|
||||
fichier.write_text(json.dumps(resultat, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
continue
|
||||
vram = (client.outil("etat").get("memoire_graphique") or {}).get("utilisee_mo")
|
||||
resultat |= {"chargement_s": demarrage.get("chargement_s"), "vram_mo": vram}
|
||||
print(f"prêt ({demarrage.get('chargement_s', 0)} s), mémoire graphique : {vram} Mo", flush=True)
|
||||
for tache in taches:
|
||||
try:
|
||||
note, texte = essayer(client, tache)
|
||||
except ErreurMcp as e:
|
||||
note, texte = {"tache": tache.id, "categorie": tache.categorie, "nature": tache.nature,
|
||||
"note": 0.0, "detail": f"erreur : {e}"}, ""
|
||||
print(f" {tache.id:<16} note {note['note']:.2f} {note['detail']}", flush=True)
|
||||
(essai / "reponses" / f"{p['id']}__{tache.id}.txt").write_text(texte, encoding="utf-8")
|
||||
resultat["taches"].append(note)
|
||||
fichier.write_text(json.dumps(resultat, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
finally:
|
||||
if initial_id and not options.sans_restaurer and choisis and choisis[-1]["id"] != initial_id:
|
||||
print(f"\nremise du preset de départ : {initial_id}", flush=True)
|
||||
attendre(client, client.outil("demarrer", preset=initial_id, remplacer=True))
|
||||
choix = choisir({i: resume(r) for i, r in derniers(options.sortie).items()})
|
||||
(options.sortie / "choix.json").write_text(json.dumps(choix, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
(options.sortie / "rapport.md").write_text(rapport(choix), encoding="utf-8")
|
||||
print(f"\nfini : {essai}\nrapport : {options.sortie / 'rapport.md'}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,165 +0,0 @@
|
||||
## 9. Médias autonomes (proposé le 2026-10-21)
|
||||
|
||||
Les médias des contenus **sans repo** : articles du blog et fiches « page » (4.4). Les projets avec repo gardent les
|
||||
leurs dans leur repo (images du README récupérées au tag, fichiers joints aux releases) ; rien ne change pour eux.
|
||||
|
||||
### 9.1 Où vivent les médias
|
||||
|
||||
| Lieu | Rôle |
|
||||
|---|---|
|
||||
| **NAS** : repo Gitea privé `ange/apc-mediastorage`, fichiers en **Git LFS** | **Source de vérité** : chaque fichier une seule fois, historique complet (qui, quand, versions précédentes). Jamais nettoyé. Accès par l'API Gitea, **sans clone** |
|
||||
| **IONOS** : `~/www/media/{prod,preprod}/` | **Diffusion** de l'état courant ; reconstructible depuis le NAS. À côté de `~/www/prod` et `~/www/preprod`, jamais dedans : jamais touché par le déploiement (qui n'écrit que dans `www/<env>`) ni par la bascule |
|
||||
| **Repo du site** | **Uniquement les `.md`**, avec des adresses `media:` ; jamais de média ni de fiche |
|
||||
|
||||
Le repo `apc-mediastorage` est privé, mais ses médias peuvent être publics : le plafond « repo privé ⇒ au plus
|
||||
`members` » (3.2.1) ne s'applique qu'aux repos de projets.
|
||||
|
||||
Création (`a2m media init`, une seule fois) : la chaîne d'outils crée le repo **par l'API Gitea**, **privé**, vérifie
|
||||
que LFS y est actif (sinon : arrêt, rien n'est envoyé), et y commite un `README.md` comme **seul fichier**. Aucun
|
||||
`.gitattributes` (voir ci-dessous) ; le repo n'est jamais cloné, ni à la création ni ensuite. Un repo déjà existant
|
||||
n'est pas recréé : la commande vérifie seulement qu'il est privé et que LFS y est actif.
|
||||
|
||||
Écriture dans `apc-mediastorage` (vérifiée avec Gitea 28.2 le 2026-10-21) : le fichier est envoyé par l'**API LFS**
|
||||
(« batch »), puis la chaîne d'outils commite elle-même son **pointeur** par l'API « contents ». Le repo n'a **pas de
|
||||
`.gitattributes` LFS** : avec une règle LFS, Gitea emballerait le pointeur une seconde fois. Gitea reconnaît les
|
||||
pointeurs et sert les fichiers (`/media/<chemin>`) ; un fichier déjà présent (même `sha256`) n'est pas renvoyé. Le
|
||||
repo ne se clone pas : un clone montrerait les pointeurs ; toute exportation passe par la chaîne d'outils.
|
||||
|
||||
Sur IONOS, dans `~/www/media/<env>/` :
|
||||
|
||||
| Dossier | Servi par | Adresse (production / préproduction) |
|
||||
|---|---|---|
|
||||
| `public/` | Apache, en statique : racine web du sous-domaine | `https://media.a2msystemes.org/<chemin>` / `https://media-preprod.a2msystemes.org/<chemin>` |
|
||||
| `private/` | Laravel, après contrôle des droits (« Require all denied » pour Apache) | `https://a2msystemes.org/private/media/<chemin>` / `https://preprod.a2msystemes.org/private/media/<chemin>` |
|
||||
| `.incoming/` | Personne : envois en cours | — |
|
||||
|
||||
Comme pour le site, chaque domaine pointe sur un dossier `…/public`, jamais plus haut.
|
||||
|
||||
### 9.2 Rangement : un contenu, ses propres médias
|
||||
|
||||
- **Dossier d'un contenu** : `blog/<translationKey>/` pour un article (ses versions FR et EN partagent les mêmes
|
||||
médias), `pages/<id>/` pour une fiche « page ».
|
||||
- Un contenu ne cite **que les médias de son dossier** : aucun partage entre contenus. Un fichier utile à deux
|
||||
contenus est **dupliqué** dans chaque dossier.
|
||||
- **Sur IONOS**, le chemin d'un média est toujours `<dossier du contenu>/<chemin relatif>` : une copie réelle par
|
||||
contenu.
|
||||
- **Sur le NAS**, chaque fichier n'existe **qu'une fois**, dans le dossier du premier contenu qui l'a ajouté. Un autre
|
||||
contenu le **référence** dans sa fiche (`from` + `sha256` figé) ; aucun fichier n'est créé.
|
||||
- **Fiche `media.toml`** dans chaque dossier de contenu ([schemas/media-v2.schema.json](schemas/media-v2.schema.json)) :
|
||||
une entrée par média (fichier propre ou référence), avec sa visibilité, son `sha256`, `keep` (jamais nettoyé) et une
|
||||
description FR/EN (texte alternatif).
|
||||
- **Valeurs à l'ajout d'un média** (décision `upload` ou `reference`, §9.4) :
|
||||
- visibilité : celle du **contenu qui l'ajoute** (identique dans ses versions FR et EN, règle 3.2.4) ; modifiable
|
||||
ensuite (§9.7) ;
|
||||
- description : `fr` et `en` reprennent le **texte alternatif** de l'image dans la version FR et dans la version EN
|
||||
du contenu, quand il existe et n'est pas vide ; une langue sans texte alternatif est omise, et la description
|
||||
entière si aucune n'en a. Rien n'est demandé à l'utilisateur.
|
||||
- **Noms lisibles et normalisés** : minuscules, accents retirés, tout autre caractère que `[a-z0-9_.-]` remplacé par
|
||||
`-` ; sous-dossiers conservés. Jamais de sha256 ni d'identifiant dans un nom. Deux fichiers différents qui donnent
|
||||
le même nom : refus.
|
||||
|
||||
### 9.3 Détection dans le Markdown
|
||||
|
||||
Implémentation de référence : [tests/medias.py](tests/medias.py) ; exemples : [exemples/markdown/](exemples/markdown/).
|
||||
|
||||
- Analyse par un **analyseur CommonMark et un analyseur HTML**, jamais par recherche dans le texte brut : un lien
|
||||
n'existe que là où l'analyseur en voit un. Le code (en ligne, bloc délimité, bloc indenté) ne contient jamais de lien.
|
||||
- Liens examinés : images et liens Markdown (y compris les liens de référence), attributs HTML `img[src]`,
|
||||
`video[src|poster]`, `source[src]`, `audio[src]`, `a[href]`, `iframe[src]`, champ d'en-tête `cover` (seul nom d'une
|
||||
couverture, §4.5 ; ni `image` ni `coverImage`).
|
||||
- La réécriture ne remplace **que le texte de l'adresse**, sans reformater le reste ; la relancer ne change rien.
|
||||
- Adresse d'un média : **`media:<dossier du contenu>/<chemin>`**, sans version ni environnement. Le site la traduit au
|
||||
build : `https://media.a2msystemes.org/<chemin>` (public) ou `/private/media/<chemin>` (privé), selon l'environnement.
|
||||
|
||||
### 9.4 Décisions
|
||||
|
||||
Chaque lien reçoit une décision (codes repris par les exemples) :
|
||||
|
||||
| Décision | Cas | Effet |
|
||||
|---|---|---|
|
||||
| `upload` | Chemin **relatif**, fichier existant, **dans** le dossier du contenu, contenu inconnu du NAS | Envoi au NAS, lien réécrit en `media:` |
|
||||
| `reference` | Idem, mais un fichier de même `sha256` existe déjà sur le NAS | Référence dans la fiche, aucun envoi ; lien réécrit |
|
||||
| `unchanged` | Même nom et même `sha256` qu'un média du contenu ; ou notre propre adresse d'un média du contenu | Rien à envoyer ; lien réécrit |
|
||||
| `replace` | Même nom qu'un média du contenu, `sha256` différent | **Remplacement**, après confirmation ; lien réécrit |
|
||||
| `duplicate` | **Notre propre adresse** (9.1) d'un média d'un autre contenu | Proposition de dupliquer (référence) ; avec `confirm = make-public` si le média est moins visible que le contenu (confirmation explicite, **non** par défaut) ; refus = erreur |
|
||||
| `keep` | Adresse `media:` de son propre dossier, connue, assez visible | Inchangée (`available` : arrivée sur IONOS ou non) |
|
||||
| `ignore` | `external` (tout protocole : `https:`, `mailto:`, `tel:`, `data:`… et `//…`, toute `iframe` externe), `site-route` (`/…`), `anchor` (`#…`), `page-link` (`.md`), `not-media` (chemin local absent sans extension de média) | Inchangé |
|
||||
| `error` | `missing-file`, `absolute-path` (`C:\`, `C:/`), `network-path` (`\…`), `outside-folder` (`../`), `foreign-media` (`media:` d'un autre contenu), `local-iframe`, `visibility`, `unknown-media`, `name-collision`, `local-link` (build) | Rien n'est envoyé ni publié ; le message dit **quoi faire** (ex. « copiez le fichier dans le dossier du contenu : il ne sera pas dupliqué sur le NAS ») |
|
||||
|
||||
### 9.5 Versions et remplacement
|
||||
|
||||
- Les **versions** vivent sur le **NAS** (historique git et LFS) ; une ancienne version peut être renvoyée
|
||||
(`a2m media restore`). **IONOS ne garde que le fichier courant**.
|
||||
- **Remplacer** un média = même nom, `sha256` différent : nouveau commit sur le NAS (l'ancien fichier reste dans
|
||||
l'historique), puis remplacement **atomique** sur IONOS (réception dans `.incoming/`, vérification, renommage).
|
||||
- Une **référence** est figée par son `sha256` : si le contenu d'origine remplace son fichier, la référence garde
|
||||
l'ancien contenu (retrouvé sur le NAS par son `sha256`). Remplacer une référence en fait un fichier propre.
|
||||
|
||||
### 9.6 Transfert, intégrité, contenus en attente
|
||||
|
||||
- Le transfert vers IONOS est **asynchrone** : file persistante sur le poste, worker en arrière-plan (démarrage
|
||||
immédiat par défaut, ou à une heure donnée), envoi **par morceaux avec reprise**, débit limitable. Protocole :
|
||||
[INTERFACE-MEDIAS.md](INTERFACE-MEDIAS.md).
|
||||
- **Intégrité** : le `sha256` (= identifiant LFS) est vérifié par IONOS **avant** que le fichier soit servi ; un fichier
|
||||
non conforme est rejeté. Revérification périodique. Un fichier dont IONOS a déjà le `sha256` y est **copié sur
|
||||
place**, sans transfert.
|
||||
- **La publication n'attend jamais les médias** : l'adresse finale d'un média est connue d'avance. État d'un contenu
|
||||
au build :
|
||||
|
||||
| État | Condition | Rendu |
|
||||
|---|---|---|
|
||||
| `complete` | Tous ses médias sont disponibles | Normal |
|
||||
| `pending` | Au moins un média pas encore arrivé | Publié à son adresse, **accessible par le lien** ; bandeau « Article incomplet : médias en cours de mise en ligne » (jusqu'au déploiement suivant) et emplacements réservés (chacun remplacé par son média dès l'arrivée, sans redéploiement) ; **absent** des listes, pages de tags, sitemap, recherche et flux ; `noindex` ; **pas annoncé** |
|
||||
| `ignored` | Média inconnu, d'un autre contenu ou trop peu visible | Contenu non publié, avertissement |
|
||||
| `refused` | Un lien local subsiste | **Build en échec** |
|
||||
|
||||
Un contenu `pending` devenu complet entre dans les listes et les flux, et il est annoncé, au **déploiement suivant**.
|
||||
|
||||
- **Par version** : `complete` et `pending` se calculent pour chaque version d'un article, qui cite ses propres
|
||||
médias ; la version FR peut être publiée normalement et la version EN avec le bandeau.
|
||||
- **Pour le contenu entier** : `ignored` écarte toutes ses versions (jamais un article publié dans une seule langue par
|
||||
accident) ; `refused` fait échouer le build.
|
||||
- Un média en cours de **remplacement** reste disponible tant que l'ancien fichier est servi (`served_sha256`,
|
||||
[INTERFACE-MEDIAS.md](INTERFACE-MEDIAS.md) §4).
|
||||
|
||||
### 9.7 Visibilité, cache, référencement
|
||||
|
||||
- Visibilité **par média** : `public`, `members` ou `groups` (`unlisted` n'a pas de sens : un média n'est jamais
|
||||
listé). Un média doit être **au moins aussi visible** que le contenu qui le cite. Changer la visibilité déplace le
|
||||
fichier entre `public/` et `private/`.
|
||||
- **Changer la visibilité d'un média** (à la demande de l'utilisateur, par la chaîne d'outils) :
|
||||
1. **Contrôle** : refus (erreur `visibility`, rien n'est modifié) si le média deviendrait moins visible qu'une
|
||||
version du contenu qui le cite ; le message dit quoi faire (rendre le contenu moins visible d'abord, ou retirer le
|
||||
lien). Même règle qu'à l'ajout.
|
||||
2. **NAS d'abord** (source de vérité) : la chaîne d'outils modifie l'entrée dans `media.toml` et la commite par l'API
|
||||
« contents ». Le fichier ne bouge pas sur le NAS.
|
||||
3. **Puis le site**, par la **file du worker** (§9.6), dans chaque environnement où le média existe :
|
||||
`POST /api/media/visibility` ([INTERFACE-MEDIAS.md](INTERFACE-MEDIAS.md) §5). Site injoignable : la demande reste
|
||||
dans la file (après un envoi du même média encore en attente) et repart plus tard ; en attendant, le site
|
||||
sert le média avec son ancienne visibilité.
|
||||
- Une **référence** (`from`) a sa propre visibilité, dans la fiche de son dossier : changer celle du fichier
|
||||
d'origine ne la modifie pas, et inversement.
|
||||
- **Adresse fixe**, sans paramètre de version : `Cache-Control: no-cache` et `ETag` (un remplacement est visible
|
||||
immédiatement, chaque média garde une adresse unique). Médias privés : `X-Robots-Tag: noindex`. Le sous-domaine des
|
||||
médias n'est pas bloqué par `robots.txt`.
|
||||
|
||||
### 9.8 Usage et nettoyage
|
||||
|
||||
- À chaque déploiement, le script relève toutes les adresses `media:` des contenus (y compris ceux en attente) et les
|
||||
transmet au site : c'est l'**usage** de chaque média. Un contenu **retiré** (`status = unpublished`, §4.5 et §8.2)
|
||||
ne cite plus rien : sa page de retrait n'affiche aucun média, qui deviennent orphelins. S'il est republié, la chaîne
|
||||
d'outils renvoie depuis le NAS ceux qui ont été nettoyés d'IONOS (le contenu est `pending` en attendant).
|
||||
- **Nettoyage** des fichiers **d'IONOS** qui ne sont plus cités (le NAS n'est jamais nettoyé) : un fichier non cité
|
||||
devient **orphelin** (avec sa date), puis il est supprimé après un **délai de grâce** réglable dans `/admin` (30 jours
|
||||
par défaut, par environnement). `keep` protège un média : transmis au site à l'envoi et à la copie, et modifié par le
|
||||
même circuit qu'un changement de visibilité (§9.7 : NAS d'abord, puis le site par la file du worker). Exécuté par le
|
||||
site, déclenché à la demande
|
||||
(`a2m media cleanup`) ou au déploiement (`a2m deploy --cleanup`) ; simulation par défaut.
|
||||
|
||||
### 9.9 Circuit de rédaction
|
||||
|
||||
- Les brouillons vivent **hors des repos** (dossier de brouillons de la chaîne d'outils). La chaîne d'outils détecte
|
||||
les médias, les envoie au NAS, réécrit les liens, puis copie **les seuls `.md`** dans le repo du site.
|
||||
- Garde-fous côté site : les dossiers de contenus ne versionnent que les `.md` (`.gitignore` en liste blanche), et le
|
||||
build refuse un lien local (`local-link`). L'en-tête d'un article n'a pas de champ de brouillon (§4.5).
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
(.venv) PS E:\0000WorkTechnique\0002dev\light_preset_manager> a2m release publish 0.6.0 --dev --asset "file=.\dist\light_preset_manager-0.6.0.zip;role=download" --deploy preprod
|
||||
Contrat : apc-spec v3.0.1 (dernier tag publié)
|
||||
|
||||
light_preset_manager 0.6.0-dev.1 (dev) → ange/light_preset_manager, visibilité members, branche main
|
||||
Publier sur Gitea ? [o/n] (o):
|
||||
✔ tag dev placé sur 68e235d7de et poussé
|
||||
✔ light_preset_manager-0.6.0.zip envoyé
|
||||
✔ publication.json envoyé
|
||||
✔ Release publiée : https://a2msystemes-git.duckdns.org/ange/light_preset_manager/releases/tag/dev
|
||||
Déploiement preprod — la phrase de passe de la clé SSH peut être demandée.
|
||||
Container a2msystemes-deploy-run-636568cea4c5 Creating
|
||||
Container a2msystemes-deploy-run-636568cea4c5 Created
|
||||
|
||||
▶ Configuration (preprod) 16:02:58
|
||||
Serveur : u57217383@home315313559.1and1-data.host:~/www/preprod
|
||||
Adresse : https://preprod.a2msystemes.org
|
||||
|
||||
▶ Dépôt git 16:02:58
|
||||
Commit : ec2db97 — Contrat apc-spec : passage à v3.0.1
|
||||
Branche : main
|
||||
✔ Arbre propre
|
||||
|
||||
▶ Connexion SSH 16:03:05
|
||||
Phrase de passe de la clé a2m_ionos :
|
||||
Enter passphrase for /root/.ssh/deploy_key:
|
||||
Identity added: /root/.ssh/deploy_key (a2m-ionos depuis WARMACHINE-A2M)
|
||||
✔ Serveur joignable, dossiers et .env présents
|
||||
|
||||
▶ Export du commit ec2db97 16:03:17
|
||||
✔ Fichiers exportés dans /build
|
||||
|
||||
▶ Projets publiés (Gitea) 16:03:20
|
||||
4 projet(s) sur Gitea → 2 sur le site public, 0 retiré(s), 2 addon(s) dans le catalogue Blender
|
||||
a2m_probe_public (public, active) : version 0.1.0
|
||||
slice_wrap (public, active) : version 1.4.2
|
||||
0 fichier(s) téléchargé(s) depuis Gitea (les autres venaient du cache)
|
||||
Changements depuis le déploiement précédent : 2
|
||||
• nouveau projet : light_preset_manager — public [annoncé]
|
||||
• nouvelle version : light_preset_manager 0.6.0-dev.1 — members
|
||||
✔ Projets synchronisés
|
||||
|
||||
▶ Build du site (Analog) 16:03:29
|
||||
|
||||
added 668 packages in 10s
|
||||
|
||||
> a2m-site@0.0.0 build
|
||||
> vite build && node scripts/clean-sitemap.mjs && node scripts/rss.mjs && pagefind --site dist/analog/public
|
||||
|
||||
vite v8.3.4 building client environment for production...
|
||||
✓ 385 modules transformed.
|
||||
computing gzip size...
|
||||
dist/client/index.html 2.34 kB │ gzip: 1.15 kB
|
||||
dist/client/assets/index-tfsG3tOf.css 2.92 kB │ gzip: 1.18 kB
|
||||
dist/client/assets/_id_.page-BiFD4vmf.js 0.04 kB │ gzip: 0.06 kB
|
||||
dist/client/assets/_tag_.page-C78GKh1A.js 0.04 kB │ gzip: 0.06 kB
|
||||
dist/client/assets/_tag_.page-ul3g5iGa.js 0.04 kB │ gzip: 0.06 kB
|
||||
dist/client/assets/_slug_.page-CwCGAHT2.js 0.04 kB │ gzip: 0.06 kB
|
||||
dist/client/assets/index.page-CS5uSO-_.js 0.10 kB │ gzip: 0.12 kB
|
||||
dist/client/assets/index.page-DtDOTxQs.js 0.11 kB │ gzip: 0.13 kB
|
||||
dist/client/assets/about.page-DfMG4ZaU.js 0.11 kB │ gzip: 0.13 kB
|
||||
dist/client/assets/index.page-E1EPiEXA.js 0.11 kB │ gzip: 0.13 kB
|
||||
dist/client/assets/contact.page-x2KeEPA0.js 0.12 kB │ gzip: 0.13 kB
|
||||
dist/client/assets/search.page-Cf4mS9zG.js 0.12 kB │ gzip: 0.13 kB
|
||||
dist/client/assets/login.page-vwQEAs8x.js 0.14 kB │ gzip: 0.15 kB
|
||||
dist/client/assets/invitation.page-GM_hviQD.js 0.14 kB │ gzip: 0.15 kB
|
||||
dist/client/assets/blender-addons.page-BX7p4LXE.js 0.14 kB │ gzip: 0.15 kB
|
||||
dist/client/assets/_...not-found_.page-T4aDkZfz.js 0.14 kB │ gzip: 0.16 kB
|
||||
dist/client/assets/request-access.page-OVqCRDnG.js 0.15 kB │ gzip: 0.15 kB
|
||||
dist/client/assets/reset-password.page-Dtr4wPxF.js 0.15 kB │ gzip: 0.16 kB
|
||||
dist/client/assets/forgot-password.page-NHopf1t6.js 0.16 kB │ gzip: 0.17 kB
|
||||
dist/client/assets/2026-10-17-opening-COXeaVIs.js 0.51 kB │ gzip: 0.37 kB
|
||||
dist/client/assets/private-page-g33is5sA.js 0.51 kB │ gzip: 0.36 kB
|
||||
dist/client/assets/2026-10-17-ouverture-Ccgu8oq2.js 0.55 kB │ gzip: 0.39 kB
|
||||
dist/client/assets/content-types-CKWdUnQo.js 0.60 kB │ gzip: 0.37 kB
|
||||
dist/client/assets/a2m-logo.component-B7g-8HJ-.js 0.84 kB │ gzip: 0.52 kB
|
||||
dist/client/assets/_tag_.page-Cu3xgJqj.js 1.27 kB │ gzip: 0.72 kB
|
||||
dist/client/assets/tag-links.component-Dqf80SLl.js 1.32 kB │ gzip: 0.73 kB
|
||||
dist/client/assets/_tag_.page-C6Rw5J1O.js 1.44 kB │ gzip: 0.78 kB
|
||||
dist/client/assets/index.page-DLuhHome.js 1.49 kB │ gzip: 0.84 kB
|
||||
dist/client/assets/index.page-DSPqEf6p.js 1.61 kB │ gzip: 0.79 kB
|
||||
dist/client/assets/turnstile.component-DmoFGgsh.js 1.73 kB │ gzip: 0.91 kB
|
||||
dist/client/assets/index.page-BMlGtaCa.js 2.16 kB │ gzip: 1.00 kB
|
||||
dist/client/assets/_slug_.page-DIMMZGOu.js 2.23 kB │ gzip: 1.13 kB
|
||||
dist/client/assets/post-list.component-Dx5-l1_N.js 2.24 kB │ gzip: 1.01 kB
|
||||
dist/client/assets/index.page-BymlGbnh.js 2.29 kB │ gzip: 1.12 kB
|
||||
dist/client/assets/projet-card.component-CElweaIh.js 2.75 kB │ gzip: 1.21 kB
|
||||
dist/client/assets/_...not-found_.page-Dqakym9Z.js 2.89 kB │ gzip: 1.38 kB
|
||||
dist/client/assets/_common_module-chunk-h6ypKhQY.js 2.92 kB │ gzip: 1.06 kB
|
||||
dist/client/assets/mentions-legales.page-wyCgNBH5.js 3.31 kB │ gzip: 1.62 kB
|
||||
dist/client/assets/legal-notice.page-DqNC6Dcv.js 3.33 kB │ gzip: 1.62 kB
|
||||
dist/client/assets/a-propos.page-CASZ6S2i.js 3.87 kB │ gzip: 1.81 kB
|
||||
dist/client/assets/recherche.page-eXrBMQri.js 3.94 kB │ gzip: 1.82 kB
|
||||
dist/client/assets/mot-de-passe-oublie.page-CV_9XzoQ.js 4.65 kB │ gzip: 1.91 kB
|
||||
dist/client/assets/blender-addons.page-Cr_MhXe_.js 6.49 kB │ gzip: 2.53 kB
|
||||
dist/client/assets/reinitialisation.page-CgELz1N6.js 6.54 kB │ gzip: 2.39 kB
|
||||
dist/client/assets/demande-acces.page-C12RNPUd.js 7.11 kB │ gzip: 2.64 kB
|
||||
dist/client/assets/contact.page-BjBS0zgJ.js 7.32 kB │ gzip: 2.68 kB
|
||||
dist/client/assets/invitation.page-Jyg2M_5I.js 8.82 kB │ gzip: 3.08 kB
|
||||
dist/client/assets/i18n-Bxb5dOM4.js 9.04 kB │ gzip: 3.59 kB
|
||||
dist/client/assets/privacy.page-CleTh8fE.js 9.69 kB │ gzip: 3.95 kB
|
||||
dist/client/assets/confidentialite.page-Dy-XOmo4.js 10.43 kB │ gzip: 4.21 kB
|
||||
dist/client/assets/connexion.page-CqJuvRvc.js 10.79 kB │ gzip: 3.64 kB
|
||||
dist/client/assets/two-factor-setup.component-OW1SHATp.js 12.69 kB │ gzip: 3.61 kB
|
||||
dist/client/assets/_id_.page-Bk_3d75e.js 12.96 kB │ gzip: 4.13 kB
|
||||
dist/client/assets/compte.page-DoqddlkC.js 15.29 kB │ gzip: 4.64 kB
|
||||
dist/client/assets/projets-DRoUJUHQ.js 31.87 kB │ gzip: 11.17 kB
|
||||
dist/client/assets/_router-chunk-jpVp88a1.js 293.95 kB │ gzip: 90.58 kB
|
||||
dist/client/assets/index-DmSuz30L.js 362.89 kB │ gzip: 104.72 kB
|
||||
|
||||
✓ built in 3.73s
|
||||
vite v8.3.4 building ssr environment for production...
|
||||
✓ 117 modules transformed.
|
||||
computing gzip size...
|
||||
dist/ssr/assets/_id_.page-suOqbZ-i.js 0.05 kB │ gzip: 0.07 kB
|
||||
dist/ssr/assets/_tag_.page-BswG67Wm.js 0.05 kB │ gzip: 0.07 kB
|
||||
dist/ssr/assets/_tag_.page-Cy8X2rEW.js 0.05 kB │ gzip: 0.07 kB
|
||||
dist/ssr/assets/_slug_.page-D4AUm7l0.js 0.05 kB │ gzip: 0.07 kB
|
||||
dist/ssr/assets/index.page-BGinqpfB.js 0.19 kB │ gzip: 0.16 kB
|
||||
dist/ssr/assets/index.page-BFrjNVmW.js 0.20 kB │ gzip: 0.17 kB
|
||||
dist/ssr/assets/about.page-CLZCafNJ.js 0.20 kB │ gzip: 0.18 kB
|
||||
dist/ssr/assets/search.page-DiRdMLCy.js 0.21 kB │ gzip: 0.18 kB
|
||||
dist/ssr/assets/contact.page-C3U_GqKN.js 0.21 kB │ gzip: 0.17 kB
|
||||
dist/ssr/assets/index.page-B_wk4wT1.js 0.21 kB │ gzip: 0.18 kB
|
||||
dist/ssr/assets/login.page-BPWSzZb1.js 0.24 kB │ gzip: 0.20 kB
|
||||
dist/ssr/assets/invitation.page-DHcT2hsz.js 0.25 kB │ gzip: 0.20 kB
|
||||
dist/ssr/assets/blender-addons.page-DsBOI6pQ.js 0.25 kB │ gzip: 0.19 kB
|
||||
dist/ssr/assets/_...not-found_.page-T4k2y208.js 0.25 kB │ gzip: 0.21 kB
|
||||
dist/ssr/assets/request-access.page-D0mYV7Cz.js 0.26 kB │ gzip: 0.21 kB
|
||||
dist/ssr/assets/reset-password.page-CDz54Z8T.js 0.27 kB │ gzip: 0.21 kB
|
||||
dist/ssr/assets/forgot-password.page-CNEJdVPn.js 0.29 kB │ gzip: 0.23 kB
|
||||
dist/ssr/assets/2026-10-17-opening-BZa7Hkso.js 0.67 kB │ gzip: 0.45 kB
|
||||
dist/ssr/assets/2026-10-17-ouverture-DjDzIEYl.js 0.72 kB │ gzip: 0.46 kB
|
||||
dist/ssr/assets/private-page-BscbDqj9.js 1.22 kB │ gzip: 0.70 kB
|
||||
dist/ssr/assets/content-types-BiGRZjTL.js 1.54 kB │ gzip: 0.85 kB
|
||||
dist/ssr/assets/a2m-logo.component-nzqLmVmk.js 2.16 kB │ gzip: 1.00 kB
|
||||
dist/ssr/assets/tag-links.component-OXscgeUN.js 2.70 kB │ gzip: 1.14 kB
|
||||
dist/ssr/assets/_tag_.page-Bn8XN-cA.js 2.75 kB │ gzip: 1.09 kB
|
||||
dist/ssr/assets/index.page-DSAWJlxw.js 2.84 kB │ gzip: 1.06 kB
|
||||
dist/ssr/assets/index.page-hU2clTxq.js 3.23 kB │ gzip: 1.20 kB
|
||||
dist/ssr/assets/_tag_.page-BaPJqA_c.js 3.30 kB │ gzip: 1.19 kB
|
||||
dist/ssr/assets/turnstile.component-C9cEwfb9.js 3.78 kB │ gzip: 1.68 kB
|
||||
dist/ssr/assets/post-list.component-BHhoD_Dr.js 4.27 kB │ gzip: 1.48 kB
|
||||
dist/ssr/assets/_slug_.page-BV_DBwXS.js 4.55 kB │ gzip: 1.58 kB
|
||||
dist/ssr/assets/index.page-BmwnnbTw.js 4.72 kB │ gzip: 1.54 kB
|
||||
dist/ssr/assets/mentions-legales.page-BnJUKYcZ.js 5.08 kB │ gzip: 1.85 kB
|
||||
dist/ssr/assets/legal-notice.page-Bwta7eN9.js 5.14 kB │ gzip: 1.85 kB
|
||||
dist/ssr/assets/index.page-BxALBSO8.js 5.31 kB │ gzip: 1.48 kB
|
||||
dist/ssr/assets/projet-card.component-5vJnk0hk.js 5.42 kB │ gzip: 1.68 kB
|
||||
dist/ssr/assets/_common_module-chunk-BSBE_0o6.js 5.76 kB │ gzip: 1.71 kB
|
||||
dist/ssr/assets/_...not-found_.page-CQ7H5Qb5.js 6.32 kB │ gzip: 2.26 kB
|
||||
dist/ssr/assets/a-propos.page-DRTW7xgX.js 6.91 kB │ gzip: 2.52 kB
|
||||
dist/ssr/assets/recherche.page-DOV1cBdN.js 7.77 kB │ gzip: 2.77 kB
|
||||
dist/ssr/assets/mot-de-passe-oublie.page-9lBN9y7G.js 7.98 kB │ gzip: 2.54 kB
|
||||
dist/ssr/assets/blender-addons.page-CohBx8y_.js 11.79 kB │ gzip: 3.32 kB
|
||||
dist/ssr/assets/i18n-BXYrWBKp.js 12.38 kB │ gzip: 4.49 kB
|
||||
dist/ssr/assets/reinitialisation.page-gqn--W9W.js 12.56 kB │ gzip: 3.24 kB
|
||||
dist/ssr/assets/privacy.page-DSf6t9B2.js 13.55 kB │ gzip: 4.26 kB
|
||||
dist/ssr/assets/demande-acces.page-uvzUvGbD.js 14.01 kB │ gzip: 3.52 kB
|
||||
dist/ssr/assets/contact.page-D2o9HWcm.js 14.08 kB │ gzip: 3.48 kB
|
||||
dist/ssr/assets/confidentialite.page-Bb8poMBs.js 14.26 kB │ gzip: 4.52 kB
|
||||
dist/ssr/assets/invitation.page-BXBQsf6_.js 16.64 kB │ gzip: 4.03 kB
|
||||
dist/ssr/assets/xhr2-D9DVWp6g.js 20.67 kB │ gzip: 4.75 kB
|
||||
dist/ssr/assets/connexion.page-CNF113yJ.js 22.16 kB │ gzip: 4.94 kB
|
||||
dist/ssr/assets/two-factor-setup.component-ikkTKlnR.js 22.57 kB │ gzip: 5.06 kB
|
||||
dist/ssr/assets/_id_.page-D-LDsjo0.js 27.40 kB │ gzip: 5.53 kB
|
||||
dist/ssr/assets/compte.page-aXeGoqTM.js 31.66 kB │ gzip: 6.09 kB
|
||||
dist/ssr/assets/projets-DGDnIuvC.js 34.43 kB │ gzip: 11.81 kB
|
||||
dist/ssr/main.server.js 586.23 kB │ gzip: 129.77 kB
|
||||
dist/ssr/assets/_router-chunk-BLHm19oG.js 597.09 kB │ gzip: 131.83 kB
|
||||
|
||||
✓ built in 2.48s
|
||||
Prerendering static pages...
|
||||
Building Sitemap...
|
||||
Writing sitemap at /build/site/dist/analog/public/sitemap.xml
|
||||
|
||||
|
||||
The '@analogjs/platform' server has been successfully built.
|
||||
sitemap : 14 page(s) retirée(s) (erreurs, comptes, projets non listés ou retirés)
|
||||
rss fr : 1 article(s), 3 entrée(s) de projets
|
||||
rss en : 1 article(s), 3 entrée(s) de projets
|
||||
|
||||
Running Pagefind v1.5.2 (Extended)
|
||||
Running from: "/build/site"
|
||||
Source: "dist/analog/public"
|
||||
Output: "dist/analog/public/pagefind"
|
||||
|
||||
[Walking source directory]
|
||||
Found 49 files matching **/*.{html}
|
||||
|
||||
[Parsing files]
|
||||
Found a data-pagefind-body element on the site.
|
||||
↳ Ignoring pages without this tag.
|
||||
|
||||
[Reading languages]
|
||||
Discovered 2 languages: en, fr
|
||||
|
||||
[Building search indexes]
|
||||
Total:
|
||||
Indexed 2 languages
|
||||
Indexed 16 pages
|
||||
Indexed 1984 words
|
||||
Indexed 0 filters
|
||||
Indexed 0 sorts
|
||||
|
||||
Finished in 0.054 seconds
|
||||
✔ Site construit (163 fichiers)
|
||||
|
||||
▶ Laravel : composer install --no-dev 16:03:49
|
||||
Installing dependencies from lock file
|
||||
Verifying lock file contents can be installed on current platform.
|
||||
Package operations: 111 installs, 0 updates, 0 removals
|
||||
- Installing voku/portable-ascii (2.1.1): Extracting archive
|
||||
- Installing symfony/polyfill-php80 (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-mbstring (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-ctype (v1.37.0): Extracting archive
|
||||
- Installing phpoption/phpoption (1.10.0): Extracting archive
|
||||
- Installing graham-campbell/result-type (v1.2.0): Extracting archive
|
||||
- Installing vlucas/phpdotenv (v5.7.0): Extracting archive
|
||||
- Installing symfony/css-selector (v8.1.6): Extracting archive
|
||||
- Installing tijsverkoyen/css-to-inline-styles (v2.4.0): Extracting archive
|
||||
- Installing symfony/var-dumper (v8.1.9): Extracting archive
|
||||
- Installing symfony/polyfill-uuid (v1.43.0): Extracting archive
|
||||
- Installing symfony/uid (v8.1.9): Extracting archive
|
||||
- Installing symfony/deprecation-contracts (v3.7.1): Extracting archive
|
||||
- Installing symfony/routing (v8.1.9): Extracting archive
|
||||
- Installing symfony/process (v8.1.9): Extracting archive
|
||||
- Installing symfony/polyfill-php86 (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-php85 (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-php84 (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-intl-normalizer (v1.43.0): Extracting archive
|
||||
- Installing symfony/polyfill-intl-idn (v1.43.0): Extracting archive
|
||||
- Installing symfony/mime (v8.1.7): Extracting archive
|
||||
- Installing psr/container (2.0.2): Extracting archive
|
||||
- Installing symfony/service-contracts (v3.7.3): Extracting archive
|
||||
- Installing psr/event-dispatcher (1.0.0): Extracting archive
|
||||
- Installing symfony/event-dispatcher-contracts (v3.7.1): Extracting archive
|
||||
- Installing symfony/event-dispatcher (v8.1.5): Extracting archive
|
||||
- Installing psr/log (3.0.2): Extracting archive
|
||||
- Installing doctrine/lexer (3.0.3): Extracting archive
|
||||
- Installing egulias/email-validator (4.0.4): Extracting archive
|
||||
- Installing symfony/mailer (v8.1.7): Extracting archive
|
||||
- Installing symfony/http-foundation (v8.1.9): Extracting archive
|
||||
- Installing symfony/error-handler (v8.1.8): Extracting archive
|
||||
- Installing symfony/http-kernel (v8.1.9): Extracting archive
|
||||
- Installing symfony/finder (v8.1.8): Extracting archive
|
||||
- Installing symfony/polyfill-intl-grapheme (v1.43.0): Extracting archive
|
||||
- Installing symfony/string (v8.1.9): Extracting archive
|
||||
- Installing symfony/console (v8.1.9): Extracting archive
|
||||
- Installing ramsey/collection (2.1.1): Extracting archive
|
||||
- Installing brick/math (1.0.0): Extracting archive
|
||||
- Installing ramsey/uuid (4.9.4): Extracting archive
|
||||
- Installing psr/simple-cache (3.0.0): Extracting archive
|
||||
- Installing psr/http-message (2.0): Extracting archive
|
||||
- Installing nunomaduro/termwind (v2.4.0): Extracting archive
|
||||
- Installing symfony/translation-contracts (v3.7.1): Extracting archive
|
||||
- Installing symfony/translation (v8.1.5): Extracting archive
|
||||
- Installing psr/clock (1.0.0): Extracting archive
|
||||
- Installing symfony/clock (v8.1.9): Extracting archive
|
||||
- Installing carbonphp/carbon-doctrine-types (3.2.1): Extracting archive
|
||||
- Installing nesbot/carbon (3.14.2): Extracting archive
|
||||
- Installing monolog/monolog (3.12.1): Extracting archive
|
||||
- Installing psr/http-factory (1.1.0): Extracting archive
|
||||
- Installing league/uri-interfaces (7.9.0): Extracting archive
|
||||
- Installing league/uri (7.9.0): Extracting archive
|
||||
- Installing league/mime-type-detection (1.17.0): Extracting archive
|
||||
- Installing league/flysystem-local (3.35.3): Extracting archive
|
||||
- Installing league/flysystem (3.36.0): Extracting archive
|
||||
- Installing nette/utils (v4.1.5): Extracting archive
|
||||
- Installing nette/schema (v1.3.6): Extracting archive
|
||||
- Installing dflydev/dot-access-data (v3.0.3): Extracting archive
|
||||
- Installing league/config (v1.2.0): Extracting archive
|
||||
- Installing league/commonmark (2.10.3): Extracting archive
|
||||
- Installing laravel/serializable-closure (v2.1.0): Extracting archive
|
||||
- Installing laravel/prompts (v0.3.26): Extracting archive
|
||||
- Installing guzzlehttp/uri-template (v2.0.1): Extracting archive
|
||||
- Installing symfony/polyfill-php82 (v1.43.0): Extracting archive
|
||||
- Installing guzzlehttp/psr7 (3.1.0): Extracting archive
|
||||
- Installing guzzlehttp/promises (3.0.2): Extracting archive
|
||||
- Installing psr/http-client (1.0.3): Extracting archive
|
||||
- Installing guzzlehttp/guzzle (8.2.0): Extracting archive
|
||||
- Installing fruitcake/php-cors (v1.4.0): Extracting archive
|
||||
- Installing dragonmantank/cron-expression (v3.6.0): Extracting archive
|
||||
- Installing doctrine/inflector (2.1.0): Extracting archive
|
||||
- Installing laravel/framework (v13.37.0): Extracting archive
|
||||
- Installing anourvalar/eloquent-serialize (1.3.12): Extracting archive
|
||||
- Installing blade-ui-kit/blade-icons (1.10.1): Extracting archive
|
||||
- Installing blade-ui-kit/blade-heroicons (2.7.0): Extracting archive
|
||||
- Installing chillerlan/php-settings-container (3.3.0): Extracting archive
|
||||
- Installing composer/pcre (3.4.0): Extracting archive
|
||||
- Installing composer/class-map-generator (1.8.0): Extracting archive
|
||||
- Installing danharrin/date-format-converter (v0.3.1): Extracting archive
|
||||
- Installing danharrin/livewire-rate-limiting (v2.3.0): Extracting archive
|
||||
- Installing paragonie/constant_time_encoding (v3.1.3): Extracting archive
|
||||
- Installing pragmarx/google2fa (v9.1.0): Extracting archive
|
||||
- Installing pragmarx/google2fa-qrcode (v4.0.0): Extracting archive
|
||||
- Installing symfony/html-sanitizer (v8.1.8): Extracting archive
|
||||
- Installing spatie/laravel-package-tools (1.93.3): Extracting archive
|
||||
- Installing spatie/invade (2.2.0): Extracting archive
|
||||
- Installing ryangjchandler/blade-capture-directive (v1.1.1): Extracting archive
|
||||
- Installing nette/php-generator (v4.2.2): Extracting archive
|
||||
- Installing livewire/livewire (v4.4.7): Extracting archive
|
||||
- Installing league/uri-components (7.9.0): Extracting archive
|
||||
- Installing kirschbaum-development/eloquent-power-joins (4.3.5): Extracting archive
|
||||
- Installing filament/support (v5.10.3): Extracting archive
|
||||
- Installing openspout/openspout (v4.32.0): Extracting archive
|
||||
- Installing league/csv (9.28.0): Extracting archive
|
||||
- Installing filament/actions (v5.10.3): Extracting archive
|
||||
- Installing filament/notifications (v5.10.3): Extracting archive
|
||||
- Installing filament/schemas (v5.10.3): Extracting archive
|
||||
- Installing filament/infolists (v5.10.3): Extracting archive
|
||||
- Installing spatie/shiki-php (2.4.0): Extracting archive
|
||||
- Installing scrivo/highlight.php (v9.18.1.10): Extracting archive
|
||||
- Installing ueberdosis/tiptap-php (2.2.1): Extracting archive
|
||||
- Installing filament/forms (v5.10.3): Extracting archive
|
||||
- Installing filament/widgets (v5.10.3): Extracting archive
|
||||
- Installing filament/query-builder (v5.10.3): Extracting archive
|
||||
- Installing filament/tables (v5.10.3): Extracting archive
|
||||
- Installing chillerlan/php-qrcode (5.0.5): Extracting archive
|
||||
- Installing filament/filament (v5.10.3): Extracting archive
|
||||
- Installing nikic/php-parser (v5.9.0): Extracting archive
|
||||
- Installing psy/psysh (v0.12.24): Extracting archive
|
||||
- Installing laravel/tinker (v3.0.2): Extracting archive
|
||||
Generating optimized autoload files
|
||||
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
|
||||
> @php artisan package:discover --ansi
|
||||
|
||||
INFO Discovering packages.
|
||||
|
||||
anourvalar/eloquent-serialize ......................................... DONE
|
||||
blade-ui-kit/blade-heroicons .......................................... DONE
|
||||
blade-ui-kit/blade-icons .............................................. DONE
|
||||
filament/actions ...................................................... DONE
|
||||
filament/filament ..................................................... DONE
|
||||
filament/forms ........................................................ DONE
|
||||
filament/infolists .................................................... DONE
|
||||
filament/notifications ................................................ DONE
|
||||
filament/query-builder ................................................ DONE
|
||||
filament/schemas ...................................................... DONE
|
||||
filament/support ...................................................... DONE
|
||||
filament/tables ....................................................... DONE
|
||||
filament/widgets ...................................................... DONE
|
||||
kirschbaum-development/eloquent-power-joins ........................... DONE
|
||||
laravel/tinker ........................................................ DONE
|
||||
livewire/livewire ..................................................... DONE
|
||||
nesbot/carbon ......................................................... DONE
|
||||
nunomaduro/termwind ................................................... DONE
|
||||
ryangjchandler/blade-capture-directive ................................ DONE
|
||||
|
||||
> @php artisan filament:upgrade
|
||||
⇂ /build/public/js/filament/forms/components/builder.js
|
||||
⇂ /build/public/js/filament/forms/components/checkbox-list.js
|
||||
⇂ /build/public/js/filament/forms/components/code-editor.js
|
||||
⇂ /build/public/js/filament/forms/components/color-picker.js
|
||||
⇂ /build/public/js/filament/forms/components/date-time-picker.js
|
||||
⇂ /build/public/js/filament/forms/components/file-upload.js
|
||||
⇂ /build/public/js/filament/forms/components/key-value.js
|
||||
⇂ /build/public/js/filament/forms/components/markdown-editor.js
|
||||
⇂ /build/public/js/filament/forms/components/repeater.js
|
||||
⇂ /build/public/js/filament/forms/components/rich-editor.js
|
||||
⇂ /build/public/js/filament/forms/components/select.js
|
||||
⇂ /build/public/js/filament/forms/components/slider.js
|
||||
⇂ /build/public/js/filament/forms/components/tags-input.js
|
||||
⇂ /build/public/js/filament/forms/components/textarea.js
|
||||
⇂ /build/public/js/filament/forms/components/text-input/password-reveal.js
|
||||
⇂ /build/public/js/filament/forms/components/toggle-buttons.js
|
||||
⇂ /build/public/js/filament/schemas/components/actions.js
|
||||
⇂ /build/public/js/filament/schemas/components/tabs.js
|
||||
⇂ /build/public/js/filament/schemas/components/wizard.js
|
||||
⇂ /build/public/js/filament/tables/components/columns/checkbox.js
|
||||
⇂ /build/public/js/filament/tables/components/columns/select.js
|
||||
⇂ /build/public/js/filament/tables/components/columns/text-input.js
|
||||
⇂ /build/public/js/filament/tables/components/columns/toggle.js
|
||||
⇂ /build/public/js/filament/widgets/components/chart.js
|
||||
⇂ /build/public/js/filament/widgets/components/stats-overview/stat/chart.js
|
||||
⇂ /build/public/fonts/filament/filament/inter/index.css
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-SP7Z6XGK.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-DR6K5BQD.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-A2J6H3EX.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-greek-wght-normal-ZABHMKQG.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-ZIT2UBIY.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-latin-wght-normal-DIHYUR35.woff2
|
||||
⇂ /build/public/fonts/filament/filament/inter/inter-vietnamese-wght-normal-VWEHJHBA.woff2
|
||||
⇂ /build/public/js/filament/actions/actions.js
|
||||
⇂ /build/public/js/filament/filament/app.js
|
||||
⇂ /build/public/js/filament/filament/echo.js
|
||||
⇂ /build/public/js/filament/notifications/notifications.js
|
||||
⇂ /build/public/js/filament/schemas/schemas.js
|
||||
⇂ /build/public/js/filament/support/support.js
|
||||
⇂ /build/public/js/filament/tables/tables.js
|
||||
⇂ /build/public/css/filament/filament/app.css
|
||||
|
||||
INFO Successfully published assets!
|
||||
|
||||
INFO Configuration cache cleared successfully.
|
||||
|
||||
INFO Route cache cleared successfully.
|
||||
|
||||
INFO Compiled views cleared successfully.
|
||||
|
||||
INFO Successfully upgraded!
|
||||
|
||||
73 packages you are using are looking for funding.
|
||||
Use the `composer fund` command to find out more!
|
||||
✔ Dépendances installées
|
||||
✔ Fichiers de l'administration ajoutés au site
|
||||
|
||||
▶ Envoi vers ~/www/preprod 16:03:51
|
||||
api/ → ~/www/preprod/api
|
||||
Number of created files: 0
|
||||
Number of deleted files: 0
|
||||
Number of regular files transferred: 0
|
||||
Total transferred file size: 0 bytes
|
||||
sent 678,674 bytes received 3,604 bytes 3,272.32 bytes/sec
|
||||
Durée : 3 min 28 s
|
||||
site → ~/www/preprod/public
|
||||
Number of created files: 0
|
||||
Number of deleted files: 0
|
||||
Number of regular files transferred: 0
|
||||
Total transferred file size: 0 bytes
|
||||
sent 12,481 bytes received 104 bytes 2,288.18 bytes/sec
|
||||
Durée : 5 s
|
||||
✔ Envoi terminé
|
||||
|
||||
▶ Laravel sur le serveur (migrations, caches) 16:07:24
|
||||
|
||||
INFO Nothing to migrate.
|
||||
|
||||
|
||||
INFO Caching framework bootstrap, configuration, and metadata.
|
||||
|
||||
config ........................................................ 48.31ms DONE
|
||||
events ......................................................... 6.76ms DONE
|
||||
routes ........................................................ 52.46ms DONE
|
||||
views ........................................................ 604.58ms DONE
|
||||
blade-icons .................................................... 7.27ms DONE
|
||||
filament ...................................................... 10.03ms DONE
|
||||
|
||||
✔ Migrations et caches à jour
|
||||
|
||||
▶ Enregistrement des projets publiés 16:07:26
|
||||
Déploiement n° 20 enregistré, 2 événement(s)
|
||||
|
||||
▶ Contrôle de santé : https://preprod.a2msystemes.org/api/health 16:07:27
|
||||
HTTP 200 : {"status":"ok","database":"ok"}
|
||||
✔ Le site répond
|
||||
|
||||
▶ Déployé : ec2db97 en preprod (https://preprod.a2msystemes.org), en 4 min 29 s 16:07:27
|
||||
✔ preprod : déployé
|
||||
</pasted_content id="e9e5">
|
||||
@@ -1,45 +0,0 @@
|
||||
============================= test session starts =============================
|
||||
platform win32 -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0
|
||||
rootdir: E:\0000WorkTechnique\0002dev\apc-toolchain
|
||||
configfile: pyproject.toml
|
||||
plugins: xdist-3.8.0
|
||||
8 workers [214 items]
|
||||
|
||||
........................................................................ [ 33%]
|
||||
...................F..........................s......................... [ 67%]
|
||||
.................................F..................F.................s. [100%]
|
||||
=================================== FAILURES ===================================
|
||||
__________________________ test_upload_in_chunks_then_available __________________________
|
||||
[gw2] win32 -- Python 3.12.3 E:\0000WorkTechnique\0002dev\apc-toolchain\.venv\Scripts\python.exe
|
||||
|
||||
site = <test_media_worker.FakeSite object at 0x000001B2F6A83050>
|
||||
|
||||
def test_upload_in_chunks_then_available(site, nas, tmp_path):
|
||||
_send()
|
||||
worker, events = _worker(site, nas, tmp_path)
|
||||
report = worker.run()
|
||||
> assert site.patches == 5 and report.transient == {}
|
||||
E assert 6 == 5
|
||||
|
||||
tests\test_media_worker.py:178: AssertionError
|
||||
______________________________ test_slug_changed_removes_the_old_version ______________________________
|
||||
[gw5] win32 -- Python 3.12.3 E:\0000WorkTechnique\0002dev\apc-toolchain\.venv\Scripts\python.exe
|
||||
|
||||
def test_slug_changed_removes_the_old_version(drafts, nas, repos):
|
||||
> assert sorted(p.name for p in blog_fr.iterdir()) == ["ouverture.md"]
|
||||
E AssertionError: assert ['2026-10-17-ouverture-du-site.md', 'ouverture.md'] == ['ouverture.md']
|
||||
|
||||
tests\test_blog_publish.py:142: AssertionError
|
||||
_______________________________ test_cleanup_simulates_then_deletes_on_request _______________________________
|
||||
[gw1] win32 -- Python 3.12.3 E:\0000WorkTechnique\0002dev\apc-toolchain\.venv\Scripts\python.exe
|
||||
|
||||
def test_cleanup_simulates_then_deletes_on_request(site, monkeypatch):
|
||||
> assert "1 fichier(s) seraient supprimés" in output and site.cleanups == [True]
|
||||
E AssertionError: assert '1 fichier(s) seraient supprimés' in 'IONOS preprod : 2 média(s) orphelin(s)'
|
||||
|
||||
tests\test_media_worker.py:301: AssertionError
|
||||
=========================== short test summary info ===========================
|
||||
FAILED tests/test_media_worker.py::test_upload_in_chunks_then_available - assert 6 == 5
|
||||
FAILED tests/test_blog_publish.py::test_slug_changed_removes_the_old_version - AssertionError: assert ['2026-10-17-ouv...
|
||||
FAILED tests/test_media_worker.py::test_cleanup_simulates_then_deletes_on_request - AssertionError: assert '1 fichi...
|
||||
============= 3 failed, 209 passed, 2 skipped, 1 warning in 131.42s (0:02:11) =============
|
||||
@@ -1,92 +0,0 @@
|
||||
"""Client MCP minimal (HTTP « streamable », bibliothèque standard seulement) pour le serveur de MIAM.
|
||||
|
||||
Juste ce qu'il faut au banc : initialize, puis tools/call. Le serveur répond en JSON ou en flux SSE ; l'en-tête
|
||||
`Mcp-Session-Id` éventuel est renvoyé à chaque requête.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ErreurMcp(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def jeton_miam() -> str:
|
||||
"""Jeton : variable MIAM_JETON, sinon la section `mcp` de la config de MIAM (même poste, même compte)."""
|
||||
if os.environ.get("MIAM_JETON"):
|
||||
return os.environ["MIAM_JETON"]
|
||||
config = Path(os.environ.get("LOCALAPPDATA", "")) / "MIAM" / "config.json"
|
||||
try:
|
||||
jeton = json.loads(config.read_text(encoding="utf-8"))["mcp"]["token"]
|
||||
except (OSError, ValueError, KeyError) as e:
|
||||
raise ErreurMcp(f"jeton introuvable (MIAM_JETON ou {config}) : {e}") from e
|
||||
if not jeton:
|
||||
raise ErreurMcp(f"jeton vide dans {config} : démarrer une fois le serveur MCP de MIAM")
|
||||
return jeton
|
||||
|
||||
|
||||
class ClientMcp:
|
||||
def __init__(self, url: str, jeton: str, timeout: float = 120) -> None:
|
||||
self.url, self.jeton, self.timeout = url, jeton, timeout
|
||||
self.session: str | None = None
|
||||
self._id = 0
|
||||
|
||||
def _envoyer(self, message: dict) -> dict | None:
|
||||
entetes = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
|
||||
"Authorization": f"Bearer {self.jeton}"}
|
||||
if self.session:
|
||||
entetes["Mcp-Session-Id"] = self.session
|
||||
requete = urllib.request.Request(self.url, data=json.dumps(message).encode(), headers=entetes, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(requete, timeout=self.timeout) as reponse:
|
||||
self.session = reponse.headers.get("Mcp-Session-Id") or self.session
|
||||
type_ = reponse.headers.get("Content-Type", "")
|
||||
corps = reponse.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
raise ErreurMcp("401 : jeton refusé par MIAM") from e
|
||||
raise ErreurMcp(f"HTTP {e.code} : {e.read()[:300]!r}") from e
|
||||
except urllib.error.URLError as e:
|
||||
raise ErreurMcp(f"MIAM injoignable sur {self.url} (lancé, serveur MCP démarré ?) : {e.reason}") from e
|
||||
if "id" not in message or not corps.strip():
|
||||
return None # notification : 202 sans corps
|
||||
if "text/event-stream" in type_:
|
||||
for ligne in corps.splitlines():
|
||||
if ligne.startswith("data:"):
|
||||
donnees = json.loads(ligne[5:].strip())
|
||||
if donnees.get("id") == message["id"]:
|
||||
return donnees
|
||||
raise ErreurMcp("flux SSE sans réponse à la requête")
|
||||
return json.loads(corps)
|
||||
|
||||
def _appel(self, methode: str, params: dict) -> dict:
|
||||
self._id += 1
|
||||
reponse = self._envoyer({"jsonrpc": "2.0", "id": self._id, "method": methode, "params": params})
|
||||
if reponse is None or "error" in reponse:
|
||||
raise ErreurMcp(f"{methode} : {(reponse or {}).get('error')}")
|
||||
return reponse["result"]
|
||||
|
||||
def ouvrir(self) -> None:
|
||||
self._appel("initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
|
||||
"clientInfo": {"name": "miam-banc", "version": "0.1.0"}})
|
||||
self._envoyer({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
||||
|
||||
def outil(self, nom: str, **arguments) -> dict:
|
||||
"""Appelle un outil de MIAM ; renvoie son résultat JSON (dict)."""
|
||||
resultat = self._appel("tools/call", {"name": nom, "arguments": arguments})
|
||||
if resultat.get("structuredContent"):
|
||||
contenu = resultat["structuredContent"]
|
||||
return contenu.get("result", contenu) if set(contenu) == {"result"} else contenu
|
||||
texte = "".join(c.get("text", "") for c in resultat.get("content", []) if c.get("type") == "text")
|
||||
if resultat.get("isError"):
|
||||
raise ErreurMcp(f"{nom} : {texte[:300]}")
|
||||
try:
|
||||
return json.loads(texte)
|
||||
except ValueError:
|
||||
return {"ok": False, "erreur": f"réponse non JSON : {texte[:300]}"}
|
||||
@@ -1,372 +0,0 @@
|
||||
"""Tâches fixes du banc d'essai et leurs contrôles (note de 0 à 1).
|
||||
|
||||
Catégories : code, texte, rapide (lire beaucoup pour rendre peu), vision. Chaque tâche a :
|
||||
- `consigne` (et éventuellement `fichier` : document ou image joint) ;
|
||||
- `nature` : `lecture` (le modèle lit un long document à ma place) ou `ecriture` (il rédige) — sert au calcul du gain ;
|
||||
- `controle(reponse) -> (note, détail)` ; `a_juger` : une partie se juge à la lecture (SVG, notes de version).
|
||||
Les fichiers de `fichiers/` et les références ci-dessous ne changent jamais : c'est ce qui rend les essais comparables.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
FICHIERS = Path(__file__).parent / "fichiers"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tache:
|
||||
id: str
|
||||
categorie: str
|
||||
nature: str # lecture | ecriture
|
||||
consigne: str
|
||||
controle: Callable[[str], tuple[float, str]]
|
||||
fichier: str | None = None # document (texte ajouté à la consigne) ou image (.png)
|
||||
a_juger: str = "" # ce qu'il faut regarder à la main
|
||||
|
||||
@property
|
||||
def image(self) -> Path | None:
|
||||
return FICHIERS / self.fichier if self.fichier and self.fichier.endswith(".png") else None
|
||||
|
||||
def message(self) -> str:
|
||||
if self.fichier and not self.fichier.endswith(".png"):
|
||||
return self.consigne + "\n\n" + (FICHIERS / self.fichier).read_text(encoding="utf-8")
|
||||
return self.consigne
|
||||
|
||||
|
||||
# --- outils de contrôle ----------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def bloc(texte: str, langue: str = r"\w*") -> str:
|
||||
"""Contenu du premier bloc de code (de cette langue s'il y en a un), sinon le texte entier."""
|
||||
m = re.search(rf"```(?:{langue})\s*\n(.*?)```", texte, re.S) or re.search(r"```\w*\s*\n(.*?)```", texte, re.S)
|
||||
return (m.group(1) if m else texte).strip()
|
||||
|
||||
|
||||
def lire_json(texte: str) -> dict | list | None:
|
||||
brut = bloc(texte, "json")
|
||||
m = re.search(r"[\[{].*[\]}]", brut, re.S)
|
||||
try:
|
||||
return json.loads(m.group(0) if m else brut)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def executer(langage: str, code: str, essais: str) -> tuple[float, str]:
|
||||
"""Lance le code du modèle et des assertions dans un processus à part (10 s au plus) ; note = assertions justes."""
|
||||
lignes = [l for l in essais.strip().splitlines() if l.strip()]
|
||||
if langage == "python":
|
||||
aide = ("\ndef _leve(f, erreur):\n try:\n f()\n except erreur:\n return True\n"
|
||||
" except Exception:\n return False\n return False\n")
|
||||
corps = code + aide + "\n_ok = 0\n" + "".join(
|
||||
f"try:\n assert {l}\n _ok += 1\nexcept Exception:\n pass\n" for l in lignes) + "print(_ok)\n"
|
||||
commande, suffixe = [sys.executable, "-I"], ".py"
|
||||
else:
|
||||
corps = code + "\nlet _ok = 0;\n" + "".join(
|
||||
f"try {{ if ({l}) _ok++; }} catch (e) {{}}\n" for l in lignes) + "console.log(_ok);\n"
|
||||
commande, suffixe = ["node"], ".js"
|
||||
with tempfile.TemporaryDirectory() as dossier:
|
||||
script = Path(dossier) / f"essai{suffixe}"
|
||||
script.write_text(corps, encoding="utf-8")
|
||||
try:
|
||||
sortie = subprocess.run(commande + [str(script)], capture_output=True, text=True, timeout=10, cwd=dossier)
|
||||
except subprocess.TimeoutExpired:
|
||||
return 0.0, "trop long (boucle ?)"
|
||||
try:
|
||||
justes = int(sortie.stdout.strip().splitlines()[-1])
|
||||
except (ValueError, IndexError):
|
||||
return 0.0, "code inutilisable : " + (sortie.stderr.strip().splitlines() or ["?"])[-1][:120]
|
||||
return justes / len(lignes), f"{justes}/{len(lignes)} cas justes"
|
||||
|
||||
|
||||
def chrf(hypothese: str, reference: str, n: int = 6, beta: float = 2.0) -> float:
|
||||
"""chrF (n-grammes de caractères, 1 à 6) : mesure classique de la proximité d'une traduction avec la référence."""
|
||||
def grammes(texte, k):
|
||||
t = re.sub(r"\s+", " ", texte.strip())
|
||||
return Counter(t[i:i + k] for i in range(len(t) - k + 1))
|
||||
precisions, rappels = [], []
|
||||
for k in range(1, n + 1):
|
||||
h, r = grammes(hypothese, k), grammes(reference, k)
|
||||
commun = sum((h & r).values())
|
||||
precisions.append(commun / max(sum(h.values()), 1))
|
||||
rappels.append(commun / max(sum(r.values()), 1))
|
||||
p, r = sum(precisions) / n, sum(rappels) / n
|
||||
return 0.0 if p + r == 0 else (1 + beta ** 2) * p * r / (beta ** 2 * p + r)
|
||||
|
||||
|
||||
MOTS_FR = {"le", "la", "les", "des", "du", "un", "une", "et", "est", "pour", "dans", "sur", "pas", "que", "qui", "au"}
|
||||
MOTS_EN = {"the", "and", "is", "for", "in", "on", "not", "that", "which", "to", "of", "with", "are"}
|
||||
|
||||
|
||||
def en_francais(texte: str) -> bool:
|
||||
mots = re.findall(r"[a-zàâçéèêëîïôûùüÿœ']+", texte.lower())
|
||||
return sum(m in MOTS_FR for m in mots) > sum(m in MOTS_EN for m in mots)
|
||||
|
||||
|
||||
def mots_cles(texte: str, groupes: list[list[str]]) -> tuple[float, list[str]]:
|
||||
"""Part des groupes dont au moins un mot apparaît (sans tenir compte de la casse)."""
|
||||
bas = texte.lower()
|
||||
manquants = [g[0] for g in groupes if not any(m in bas for m in g)]
|
||||
return 1 - len(manquants) / len(groupes), manquants
|
||||
|
||||
|
||||
# --- code -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def tache_python(id_, consigne, essais):
|
||||
return Tache(id_, "code", "ecriture", consigne + "\nRéponds seulement avec le code, dans un bloc ```python.",
|
||||
lambda r: executer("python", bloc(r, "python"), essais))
|
||||
|
||||
|
||||
CODE = [
|
||||
tache_python("code-slug", """Écris une fonction Python `slug(date: str, title: str) -> str`, sans dépendance externe,
|
||||
qui renvoie `<date>-<titre normalisé>` : accents retirés (unicodedata NFKD, puis ASCII), minuscules, toute suite de
|
||||
caractères hors [a-z0-9] remplacée par un seul « - », tirets retirés au début et à la fin ; si le titre normalisé est
|
||||
vide, renvoyer seulement la date.""", """
|
||||
slug("2026-10-17", "Ouverture du site") == "2026-10-17-ouverture-du-site"
|
||||
slug("2026-10-17", "Éditeur 3D : l'essai_final !") == "2026-10-17-editeur-3d-l-essai-final"
|
||||
slug("2026-10-17", "!!!") == "2026-10-17"
|
||||
slug("2026-01-02", " Ça marche -- vraiment ") == "2026-01-02-ca-marche-vraiment"
|
||||
"""),
|
||||
tache_python("code-version", """Écris une fonction Python `bump(version: str, part: str) -> str` pour des numéros
|
||||
X.Y.Z : part vaut "major", "minor" ou "patch" ; major remet minor et patch à 0, minor remet patch à 0. Une version mal
|
||||
formée (autre chose que trois entiers séparés par des points) lève ValueError.""", """
|
||||
bump("1.4.2", "patch") == "1.4.3"
|
||||
bump("1.4.2", "minor") == "1.5.0"
|
||||
bump("1.4.2", "major") == "2.0.0"
|
||||
bump("0.9.9", "patch") == "0.9.10"
|
||||
_leve(lambda: bump("1.2", "patch"), ValueError)
|
||||
_leve(lambda: bump("1.2.x", "minor"), ValueError)
|
||||
"""),
|
||||
tache_python("code-visibilite", """Écris une fonction Python `more_visible(a: dict, b: dict) -> bool` : vrai si la
|
||||
visibilité `a` est plus ouverte que `b`. Une visibilité est {"level": niveau} avec niveau parmi, du plus restreint au
|
||||
plus ouvert : "groups", "members", "unlisted", "public" ; pour "groups", elle a aussi "groups" (liste de noms). À
|
||||
niveau égal "groups", `a` est plus ouverte si l'un de ses groupes n'est pas dans ceux de `b`. Sinon, à niveau égal :
|
||||
faux.""", """
|
||||
more_visible({"level": "public"}, {"level": "members"}) is True
|
||||
more_visible({"level": "members"}, {"level": "public"}) is False
|
||||
more_visible({"level": "unlisted"}, {"level": "unlisted"}) is False
|
||||
more_visible({"level": "groups", "groups": ["a"]}, {"level": "groups", "groups": ["a", "b"]}) is False
|
||||
more_visible({"level": "groups", "groups": ["a", "c"]}, {"level": "groups", "groups": ["a", "b"]}) is True
|
||||
"""),
|
||||
tache_python("code-tus", """Écris une fonction Python `parse_metadata(header: str) -> dict` qui lit un en-tête tus
|
||||
`Upload-Metadata` : des paires « clé valeur » séparées par des virgules, la valeur encodée en base64 (une clé peut
|
||||
n'avoir aucune valeur : elle vaut alors ""). Les espaces autour des paires sont ignorés.""", """
|
||||
parse_metadata("path YmxvZy9hLnBuZw==,mode Y3JlYXRl") == {"path": "blog/a.png", "mode": "create"}
|
||||
parse_metadata("keep dHJ1ZQ==, vide") == {"keep": "true", "vide": ""}
|
||||
parse_metadata("") == {}
|
||||
"""),
|
||||
Tache("code-js", "code", "ecriture", """Écris une fonction JavaScript `formatBytes(n)` qui rend une taille lisible en
|
||||
français avec une décimale au plus, sans zéro inutile : moins de 1024 → « 512 o » ; puis Kio, Mio, Gio (base 1024) ;
|
||||
la virgule comme séparateur décimal (« 1,5 Kio »). Réponds seulement avec le code, dans un bloc ```javascript.""",
|
||||
lambda r: executer("javascript", bloc(r, "javascript|js"), """
|
||||
formatBytes(512) === "512 o"
|
||||
formatBytes(1536) === "1,5 Kio"
|
||||
formatBytes(1048576) === "1 Mio"
|
||||
formatBytes(3221225472) === "3 Gio"
|
||||
""")),
|
||||
]
|
||||
|
||||
|
||||
def controle_svg(r: str, formes: int) -> tuple[float, str]:
|
||||
try:
|
||||
racine = ET.fromstring(bloc(r, "svg|xml"))
|
||||
except ET.ParseError as e:
|
||||
return 0.0, f"SVG invalide ({e})"
|
||||
attendus = {"viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "stroke-width": "2"}
|
||||
justes = sum(racine.get(k) == v for k, v in attendus.items())
|
||||
elements = [e for e in racine.iter() if e.tag.split("}")[-1] in ("path", "line", "polyline", "circle", "rect")]
|
||||
note = (justes / len(attendus)) * 0.7 + (0.3 if len(elements) >= formes else 0)
|
||||
return note, f"valide, {justes}/{len(attendus)} attributs, {len(elements)} tracés (dessin à regarder)"
|
||||
|
||||
|
||||
SVG_CONSIGNE = """Écris une icône SVG 24×24, en traits seulement : viewBox="0 0 24 24", fill="none",
|
||||
stroke="currentColor", stroke-width="2", stroke-linecap="round", stroke-linejoin="round". Réponds seulement avec le SVG,
|
||||
dans un bloc ```svg. Icône : """
|
||||
|
||||
CODE += [
|
||||
Tache("svg-nuage", "code", "ecriture", SVG_CONSIGNE + "un nuage avec une flèche vers le haut (envoi).",
|
||||
lambda r: controle_svg(r, 2), a_juger="le dessin est-il un nuage avec une flèche vers le haut ?"),
|
||||
Tache("svg-dossier", "code", "ecriture", SVG_CONSIGNE + "un dossier ouvert avec un signe « + ».",
|
||||
lambda r: controle_svg(r, 2), a_juger="le dessin est-il un dossier avec un « + » ?"),
|
||||
]
|
||||
|
||||
# --- texte ----------------------------------------------------------------------------------------------------------
|
||||
|
||||
FR_SOURCE = """Les médias des contenus **sans repo** (articles du blog, fiches) vivent sur le NAS, dans le repo Gitea privé
|
||||
`apc-mediastorage`, en Git LFS : c'est la source de vérité, jamais nettoyée. IONOS ne fait que la diffusion de l'état
|
||||
courant, reconstructible depuis le NAS. La chaîne d'outils envoie les fichiers par l'API LFS, puis commite elle-même
|
||||
leurs pointeurs et la fiche `media.toml` par l'API « contents » : le repo n'est jamais cloné."""
|
||||
EN_REFERENCE = """The media of content **without a repo** (blog posts, pages) live on the NAS, in the private Gitea repo
|
||||
`apc-mediastorage`, in Git LFS: it is the source of truth, never cleaned up. IONOS only serves the current state, which
|
||||
can be rebuilt from the NAS. The toolchain sends the files through the LFS API, then commits their pointers and the
|
||||
`media.toml` record itself through the "contents" API: the repo is never cloned."""
|
||||
EN_SOURCE = """When a release is published, the toolchain first checks everything locally: clean working tree, default
|
||||
branch, changelog and contract. Only then does it commit the version, create the tag and push both at once. The release
|
||||
is created as a **draft**, so the website never sees it half-uploaded; it is published once every file is attached."""
|
||||
FR_REFERENCE = """Quand une version est publiée, la chaîne d'outils vérifie d'abord tout en local : arbre propre, branche
|
||||
par défaut, journal des modifications et contrat. Ensuite seulement, elle commite la version, crée le tag et pousse les
|
||||
deux en même temps. La release est créée en **brouillon**, pour que le site ne la voie jamais à moitié envoyée ; elle
|
||||
est publiée une fois tous les fichiers joints."""
|
||||
|
||||
|
||||
def controle_traduction(r: str, reference: str, termes: list[str], vers_fr: bool) -> tuple[float, str]:
|
||||
score = chrf(r, reference)
|
||||
gardes = [t for t in termes if t in r]
|
||||
langue_ok = en_francais(r) == vers_fr
|
||||
note = min(1.0, score / 0.65) * 0.6 + 0.3 * len(gardes) / len(termes) + (0.1 if langue_ok else 0)
|
||||
return min(note, 1.0), f"chrF {score:.2f}, termes {len(gardes)}/{len(termes)}, langue {'ok' if langue_ok else 'FAUSSE'}"
|
||||
|
||||
|
||||
def controle_notes(r: str) -> tuple[float, str]:
|
||||
titres = re.findall(r"^###\s+(.+)$", r, re.M)
|
||||
puces = re.findall(r"^\s*[-*]\s+(.+)$", r, re.M)
|
||||
if not titres or not puces:
|
||||
return 0.0, "ni titres ni puces"
|
||||
copiees = sum(1 for p in puces if "SPEC" in p or "lot 1" in p.lower() or "Lot 2" in p)
|
||||
points = [all(t.strip() in ("Ajouté", "Modifié", "Corrigé") for t in titres) and bool(titres),
|
||||
3 <= len(puces) <= 10, en_francais(r), copiees == 0]
|
||||
return sum(points) / len(points), (f"{len(titres)} titres, {len(puces)} puces, {copiees} détail(s) interne(s) "
|
||||
"recopié(s) (exactitude à juger)")
|
||||
|
||||
|
||||
TEXTE = [
|
||||
Tache("trad-fr-en", "texte", "ecriture", "Traduis en anglais, sans rien ajouter ni commenter, en gardant le "
|
||||
"Markdown et les termes entre ` ` :\n\n" + FR_SOURCE,
|
||||
lambda r: controle_traduction(r, EN_REFERENCE, ["`apc-mediastorage`", "`media.toml`", "**", "LFS"], False)),
|
||||
Tache("trad-en-fr", "texte", "ecriture", "Traduis en français, sans rien ajouter ni commenter, en gardant le "
|
||||
"Markdown :\n\n" + EN_SOURCE,
|
||||
lambda r: controle_traduction(r, FR_REFERENCE, ["**", "tag", "commit"], True)),
|
||||
Tache("notes-version", "texte", "ecriture", """Voici les sujets des derniers commits d'un outil en ligne de commande
|
||||
(a2m). Rédige la section « Non publié » d'un CHANGELOG en français : titres ### Ajouté, ### Modifié, ### Corrigé (seulement
|
||||
ceux qui servent), une puce courte par changement utile à un utilisateur, sans détail interne ni invention. Réponds
|
||||
seulement avec la section Markdown, sans titre de niveau 2.
|
||||
|
||||
- Médias : worker lancé de lui-même (verrou, --no-send), a2m media cleanup, a2m deploy --cleanup, détail des erreurs du site
|
||||
- apc-spec v3.3.0 : keep (envoi, copie, a2m media keep), served_sha256, médias renvoyés à la republication d'un article retiré
|
||||
- Lot 2 des médias : file SQLite, a2m media worker (tus, copie sur place, reprise), jetons d'envoi, état IONOS
|
||||
- a2m media status et media visibility ; SPEC : lot 1 codé
|
||||
- a2m blog new et drafts_path dans a2m config (brouillons d'articles hors des repos)
|
||||
- Option --deploy validée dès la lecture""", controle_notes,
|
||||
a_juger="chaque puce correspond-elle à un vrai changement, sans invention ?"),
|
||||
Tache("resume-court", "texte", "lecture", """Résume ce texte en trois phrases au plus, en français, pour quelqu'un
|
||||
qui ne connaît pas le projet. Réponds seulement avec le résumé.""", lambda r: _controle_resume_court(r),
|
||||
fichier="contrat-medias.md"),
|
||||
Tache("consigne-json", "texte", "ecriture", """Réponds UNIQUEMENT par un objet JSON (sans texte autour) décrivant
|
||||
trois tâches pour publier un article de blog, avec les clés : "taches" (liste d'objets ayant "titre" (texte en
|
||||
français), "duree_minutes" (entier) et "ordre" (entier de 1 à 3)).""", lambda r: _controle_json(r)),
|
||||
]
|
||||
|
||||
|
||||
def _controle_resume_court(r: str) -> tuple[float, str]:
|
||||
phrases = [p for p in re.split(r"(?<=[.!?])\s+", r.strip()) if p]
|
||||
couverture, manquants = mots_cles(r, [["nas"], ["ionos"], ["lfs", "gitea"], ["nettoy", "orphelin"]])
|
||||
points = [len(phrases) <= 3, en_francais(r)]
|
||||
note = couverture * (0.5 + 0.25 * sum(points)) # le contenu d'abord ; la forme module
|
||||
return note, f"{len(phrases)} phrase(s), notions manquantes : {', '.join(manquants) or 'aucune'}"
|
||||
|
||||
|
||||
def _controle_json(r: str) -> tuple[float, str]:
|
||||
d = lire_json(r)
|
||||
if not isinstance(d, dict) or not isinstance(d.get("taches"), list):
|
||||
return 0.0, "JSON absent ou mal formé"
|
||||
taches = d["taches"]
|
||||
bonnes = [t for t in taches if isinstance(t, dict) and isinstance(t.get("titre"), str) and en_francais(t["titre"] + " le")
|
||||
and isinstance(t.get("duree_minutes"), int) and t.get("ordre") in (1, 2, 3)]
|
||||
texte_autour = not r.strip().startswith(("{", "```"))
|
||||
note = (len(bonnes) / 3 if len(taches) == 3 else 0.3) - (0.2 if texte_autour else 0)
|
||||
return max(note, 0.0), f"{len(taches)} tâche(s), {len(bonnes)} conforme(s)" + (", texte autour" if texte_autour else "")
|
||||
|
||||
|
||||
# --- rapide : lire beaucoup pour rendre peu --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def controle_faits(r: str, attendus: dict) -> tuple[float, str]:
|
||||
d = lire_json(r)
|
||||
if not isinstance(d, dict):
|
||||
return 0.0, "JSON absent ou mal formé"
|
||||
faux = [k for k, v in attendus.items() if str(d.get(k)).strip().lower() != str(v).lower()]
|
||||
return 1 - len(faux) / len(attendus), f"{len(attendus) - len(faux)}/{len(attendus)} faits exacts" + (
|
||||
f" (faux : {', '.join(faux)})" if faux else "")
|
||||
|
||||
|
||||
def controle_pytest(r: str) -> tuple[float, str]:
|
||||
d = lire_json(r)
|
||||
if not isinstance(d, dict):
|
||||
return 0.0, "JSON absent ou mal formé"
|
||||
attendus = {"test_upload_in_chunks_then_available", "test_slug_changed_removes_the_old_version",
|
||||
"test_cleanup_simulates_then_deletes_on_request"}
|
||||
trouves = {str(t).split("::")[-1].split(" ")[0] for t in d.get("echecs") or []}
|
||||
points = [trouves == attendus, d.get("reussis") == 209, d.get("ignores") == 2, d.get("fichiers") ==
|
||||
sorted(["tests/test_blog_publish.py", "tests/test_media_worker.py"])
|
||||
or sorted(d.get("fichiers") or []) == ["tests/test_blog_publish.py", "tests/test_media_worker.py"]]
|
||||
return sum(points) / len(points), f"{sum(points)}/4 points (échecs trouvés : {len(trouves & attendus)}/3)"
|
||||
|
||||
|
||||
def controle_questions(r: str) -> tuple[float, str]:
|
||||
d = lire_json(r)
|
||||
if not isinstance(d, dict):
|
||||
return 0.0, "JSON absent ou mal formé"
|
||||
attendus = {"q1": ["30"], "q2": ["apc-mediastorage"], "q3": ["media-preprod.a2msystemes.org"],
|
||||
"q4": ["nas"], "q5": [".incoming"]}
|
||||
justes = [k for k, mots in attendus.items() if any(m in str(d.get(k, "")).lower() for m in mots)]
|
||||
return len(justes) / len(attendus), f"{len(justes)}/{len(attendus)} réponses justes"
|
||||
|
||||
|
||||
RAPIDE = [
|
||||
Tache("log-deploiement", "rapide", "lecture", """Voici le log d'un déploiement de site. Réponds UNIQUEMENT par un objet
|
||||
JSON avec ces clés : cible (preprod ou prod), commit (7 caractères), duree_totale (texte exact du log),
|
||||
numero_deploiement (entier), sante_ok (booléen), fichiers_api_transferes (entier).""",
|
||||
lambda r: controle_faits(r, {"cible": "preprod", "commit": "ec2db97", "duree_totale": "4 min 29 s",
|
||||
"numero_deploiement": 20, "sante_ok": True, "fichiers_api_transferes": 0}),
|
||||
fichier="deploiement.log"),
|
||||
Tache("log-tests", "rapide", "lecture", """Voici la sortie d'une suite de tests. Réponds UNIQUEMENT par un objet JSON
|
||||
avec : echecs (liste des noms des tests en échec, sans le chemin), reussis (entier), ignores (entier), fichiers (liste
|
||||
triée des fichiers de test qui ont au moins un échec, chemins avec des « / »).""", controle_pytest,
|
||||
fichier="pytest.log"),
|
||||
Tache("questions-doc", "rapide", "lecture", """Lis ce document et réponds UNIQUEMENT par un objet JSON avec les clés
|
||||
q1 à q5 (réponses courtes) : q1 = délai de grâce par défaut du nettoyage, en jours ; q2 = nom du repo Gitea des médias ;
|
||||
q3 = adresse (domaine) des médias publics de préproduction ; q4 = lieu où se trouve la source de vérité des médias
|
||||
(un mot) ; q5 = dossier d'IONOS où arrivent les envois en cours.""", controle_questions, fichier="contrat-medias.md"),
|
||||
]
|
||||
|
||||
# --- vision -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def controle_alt(r: str) -> tuple[float, str]:
|
||||
couverture, manquants = mots_cles(r, [["tête", "tete", "head"], ["mannequin", "buste", "sculpt", "modèle", "3d"],
|
||||
["dos", "arrière", "derrière", "back"], ["gris", "grey", "gray"]])
|
||||
court = len(r.strip()) <= 125
|
||||
return couverture * 0.8 + (0.2 if court else 0), f"{len(r.strip())} caractères, manque : {', '.join(manquants) or 'rien'}"
|
||||
|
||||
|
||||
def controle_lecture_image(r: str) -> tuple[float, str]:
|
||||
attendus = ["a2m media worker --once", "erreur 409", "le nas et le site divergent", "3 livraison"]
|
||||
bas = r.lower()
|
||||
justes = [a for a in attendus if a in bas]
|
||||
return len(justes) / len(attendus), f"{len(justes)}/{len(attendus)} passages lus"
|
||||
|
||||
|
||||
VISION = [
|
||||
Tache("vision-alt", "vision", "lecture", "Écris le texte alternatif (attribut alt) de cette image pour un site web, "
|
||||
"en français, en une phrase de 125 caractères au plus. Réponds seulement avec la phrase.", controle_alt,
|
||||
fichier="vision-mannequin.png"),
|
||||
Tache("vision-texte", "vision", "lecture", "Recopie exactement le texte de cette capture d'écran, ligne par ligne, "
|
||||
"sans rien ajouter.", controle_lecture_image, fichier="vision-texte.png"),
|
||||
Tache("vision-formes", "vision", "lecture", "Compte les formes de cette image. Réponds UNIQUEMENT par un objet JSON : "
|
||||
'{"cercles_rouges": n, "carres_bleus": n, "triangles_verts": n}.',
|
||||
lambda r: controle_faits(r, {"cercles_rouges": 3, "carres_bleus": 2, "triangles_verts": 1}),
|
||||
fichier="vision-formes.png"),
|
||||
]
|
||||
|
||||
TACHES = CODE + TEXTE + RAPIDE + VISION
|
||||
CATEGORIES = ("code", "texte", "rapide", "vision")
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Contrôle du banc lui-même : une réponse parfaite doit avoir 1, une réponse fausse une note basse.
|
||||
|
||||
Usage : python verifier_controles.py (à relancer après toute modification de taches.py).
|
||||
"""
|
||||
|
||||
from taches import EN_REFERENCE, FR_REFERENCE, TACHES
|
||||
|
||||
PARFAITES = {
|
||||
"code-slug": '''```python
|
||||
import re, unicodedata
|
||||
def slug(date, title):
|
||||
t = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode().lower()
|
||||
t = re.sub(r"[^a-z0-9]+", "-", t).strip("-")
|
||||
return f"{date}-{t}" if t else date
|
||||
```''',
|
||||
"code-version": '''```python
|
||||
def bump(version, part):
|
||||
morceaux = version.split(".")
|
||||
if len(morceaux) != 3 or not all(m.isdigit() for m in morceaux):
|
||||
raise ValueError(version)
|
||||
a, b, c = map(int, morceaux)
|
||||
return {"major": f"{a + 1}.0.0", "minor": f"{a}.{b + 1}.0", "patch": f"{a}.{b}.{c + 1}"}[part]
|
||||
```''',
|
||||
"code-visibilite": '''```python
|
||||
RANG = {"groups": 1, "members": 2, "unlisted": 3, "public": 4}
|
||||
def more_visible(a, b):
|
||||
if RANG[a["level"]] != RANG[b["level"]]:
|
||||
return RANG[a["level"]] > RANG[b["level"]]
|
||||
if a["level"] == "groups":
|
||||
return not set(a.get("groups", [])) <= set(b.get("groups", []))
|
||||
return False
|
||||
```''',
|
||||
"code-tus": '''```python
|
||||
import base64
|
||||
def parse_metadata(header):
|
||||
resultat = {}
|
||||
for paire in filter(None, (p.strip() for p in header.split(","))):
|
||||
cle, _, valeur = paire.partition(" ")
|
||||
resultat[cle] = base64.b64decode(valeur).decode() if valeur else ""
|
||||
return resultat
|
||||
```''',
|
||||
"code-js": '''```javascript
|
||||
function formatBytes(n) {
|
||||
const unites = ["o", "Kio", "Mio", "Gio"];
|
||||
let i = 0;
|
||||
while (n >= 1024 && i < unites.length - 1) { n /= 1024; i++; }
|
||||
const v = Math.round(n * 10) / 10;
|
||||
return String(v).replace(".", ",") + " " + unites[i];
|
||||
}
|
||||
```''',
|
||||
"svg-nuage": '''```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
||||
<path d="M12 18v-7"/><path d="M9 14l3-3 3 3"/></svg>
|
||||
```''',
|
||||
"svg-dossier": '''```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<line x1="12" y1="10" x2="12" y2="16"/><line x1="9" y1="13" x2="15" y2="13"/></svg>
|
||||
```''',
|
||||
"trad-fr-en": EN_REFERENCE,
|
||||
"trad-en-fr": FR_REFERENCE,
|
||||
"notes-version": """### Ajouté
|
||||
- `a2m media cleanup` et `a2m deploy --cleanup` : nettoyage des médias qui ne sont plus utilisés.
|
||||
- `a2m media keep` : protéger un média du nettoyage.
|
||||
- `a2m media worker` : envoi des médias vers le site, avec reprise après une coupure.
|
||||
- `a2m media status` et `a2m media visibility`.
|
||||
- `a2m blog new` et le dossier des brouillons dans `a2m config`.
|
||||
|
||||
### Modifié
|
||||
- L'envoi des médias se lance de lui-même après une publication (`--no-send` pour l'éviter).
|
||||
- Les erreurs du site s'affichent en détail.
|
||||
- L'option `--deploy` est vérifiée dès la lecture de la commande.""",
|
||||
"resume-court": "Les médias des articles sont gardés sur le NAS dans un repo Gitea en LFS, qui fait foi et n'est "
|
||||
"jamais nettoyé. IONOS les diffuse et supprime ceux qui ne sont plus cités après un délai. La "
|
||||
"publication n'attend pas les médias, envoyés ensuite par morceaux.",
|
||||
"consigne-json": '{"taches": [{"titre": "Écrire le brouillon", "duree_minutes": 60, "ordre": 1}, {"titre": "Ajouter '
|
||||
'les images", "duree_minutes": 15, "ordre": 2}, {"titre": "Publier et vérifier le site", '
|
||||
'"duree_minutes": 10, "ordre": 3}]}',
|
||||
"log-deploiement": '{"cible": "preprod", "commit": "ec2db97", "duree_totale": "4 min 29 s", "numero_deploiement": '
|
||||
'20, "sante_ok": true, "fichiers_api_transferes": 0}',
|
||||
"log-tests": '{"echecs": ["test_upload_in_chunks_then_available", "test_slug_changed_removes_the_old_version", '
|
||||
'"test_cleanup_simulates_then_deletes_on_request"], "reussis": 209, "ignores": 2, "fichiers": '
|
||||
'["tests/test_blog_publish.py", "tests/test_media_worker.py"]}',
|
||||
"questions-doc": '{"q1": "30 jours", "q2": "apc-mediastorage", "q3": "media-preprod.a2msystemes.org", "q4": "le NAS", '
|
||||
'"q5": ".incoming/"}',
|
||||
"vision-alt": "Tête de mannequin lisse vue de dos, sur fond gris.",
|
||||
"vision-texte": "PS E:\\projets> a2m media worker --once\nErreur 409 : le NAS et le site divergent\n"
|
||||
"Fin : 3 livraison(s) encore en attente",
|
||||
"vision-formes": '{"cercles_rouges": 3, "carres_bleus": 2, "triangles_verts": 1}',
|
||||
}
|
||||
|
||||
FAUSSE = "Je ne sais pas."
|
||||
|
||||
|
||||
def main() -> None:
|
||||
erreurs = 0
|
||||
for tache in TACHES:
|
||||
bonne, detail = tache.controle(PARFAITES[tache.id])
|
||||
mauvaise, _ = tache.controle(FAUSSE)
|
||||
ok = bonne >= 0.99 and mauvaise <= 0.3
|
||||
erreurs += not ok
|
||||
print(f"{'ok ' if ok else 'KO '} {tache.id:<16} parfaite {bonne:.2f} ({detail}) ; fausse {mauvaise:.2f}")
|
||||
print(f"\n{len(TACHES)} tâches, {erreurs} contrôle(s) à revoir")
|
||||
raise SystemExit(1 if erreurs else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: banc
|
||||
description: Lance le banc d'essai des presets de MIAM (tâches fixes code/texte/rapide passées par deleguer) ou affiche son dernier rapport. Le résultat sert au subagent miam:delegue pour choisir le preset à démarrer.
|
||||
disable-model-invocation: true
|
||||
argument-hint: "[rapport | preset… | --categorie code|texte|rapide]"
|
||||
---
|
||||
|
||||
# Banc d'essai des presets de MIAM
|
||||
|
||||
Pour chaque preset validé de MIAM : `demarrer(remplacer=True)`, puis 15 tâches fixes (code, texte, rapide) envoyées par
|
||||
`deleguer` et notées automatiquement. Résultats dans `${CLAUDE_PLUGIN_DATA}/banc/` : `rapport.md`, `choix.json` (lu par
|
||||
`miam:delegue`), un dossier par essai. Plusieurs minutes par preset ; aucun token de Claude pendant l'essai.
|
||||
|
||||
Arguments reçus : `$ARGUMENTS`
|
||||
|
||||
## « rapport » (ou aucun argument et un rapport existe déjà)
|
||||
|
||||
Lis `${CLAUDE_PLUGIN_DATA}/banc/rapport.md` et montre le tableau « Choix par catégorie », la date du dernier essai, et
|
||||
propose de relancer le banc.
|
||||
|
||||
## Lancer le banc
|
||||
|
||||
1. Appelle `etat` (outil MIAM) : MIAM doit répondre. Une délégation en cours ? Préviens que le banc attendra son tour.
|
||||
2. Préviens l'utilisateur : le modèle chargé sera remplacé pendant l'essai, puis remis à la fin ; Unreal doit être fermé.
|
||||
3. Lance le banc dans une **fenêtre PowerShell externe** (jamais dans un onglet de l'application) :
|
||||
|
||||
```powershell
|
||||
Start-Process pwsh -ArgumentList '-NoExit', '-Command', "python '${CLAUDE_PLUGIN_ROOT}/banc/banc_miam.py' --sortie '${CLAUDE_PLUGIN_DATA}/banc' $ARGUMENTS"
|
||||
```
|
||||
|
||||
(les arguments sont des id ou noms de presets, ou `--categorie …` ; sans argument : tous les presets validés).
|
||||
Le script lit le jeton dans `%LOCALAPPDATA%\MIAM\config.json` (ou la variable `MIAM_JETON`) et le port dans
|
||||
`MIAM_PORT` (défaut 8090 ; le port réglé dans le plugin est `${user_config.port}`, à passer avec `--port` s'il
|
||||
diffère).
|
||||
4. Dis à l'utilisateur où lire le résultat (`${CLAUDE_PLUGIN_DATA}/banc/rapport.md`) et de relancer `/miam:banc rapport`
|
||||
une fois la fenêtre terminée. N'attends pas la fin en interrogeant l'état.
|
||||
@@ -21,16 +21,19 @@ délégation.
|
||||
## Comment
|
||||
|
||||
Lance le subagent `miam:delegue` (outil Agent, `subagent_type: "miam:delegue"`, `model: "${user_config.modele_subagent}"`).
|
||||
Il vérifie que MIAM répond, démarre au besoin le bon preset, délègue, attend, vérifie et rend un compte rendu court.
|
||||
Il vérifie que MIAM répond, indique le type de la tâche pour que MIAM choisisse le modèle, délègue, attend, vérifie et
|
||||
rend un compte rendu court.
|
||||
|
||||
Son brief doit se suffire à lui-même :
|
||||
- la tâche, précise (signature, langue, format exact, cas limites) ;
|
||||
- les fichiers en **chemins absolus** (MIAM les lit : ne les lis pas toi-même avant) ;
|
||||
- le format (`texte`, `code`, `json`) et le fichier de `sortie` éventuel (dire si on peut l'écraser) ;
|
||||
- comment vérifier (tests à lancer, clés JSON attendues) ;
|
||||
- s'il peut remplacer le modèle déjà chargé (par défaut : non).
|
||||
- si besoin : un seuil de qualité (0 à 1, défaut 0,8), un preset imposé, ou « garder le modèle chargé ». Par défaut,
|
||||
MIAM remplace le modèle chargé s'il est sous le seuil pour cette tâche (20 à 40 s).
|
||||
|
||||
Plusieurs tâches indépendantes : plusieurs subagents, MIAM les met en file (une délégation à la fois sur le GPU).
|
||||
Plusieurs tâches indépendantes : plusieurs subagents, MIAM les met en file (une délégation à la fois sur le GPU) ; des
|
||||
types différents peuvent recharger un modèle entre deux tâches.
|
||||
|
||||
## Ensuite
|
||||
|
||||
@@ -40,4 +43,4 @@ Relis le compte rendu : un résultat « raté » ou « partiel », tu le reprend
|
||||
MIAM injoignable (le compte rendu le dit) : le signaler à l'utilisateur (lancer MIAM et son serveur MCP, puis `/mcp` →
|
||||
Reconnect sur `plugin:miam:miam`) et faire la tâche toi-même si elle est urgente.
|
||||
|
||||
Bilan des délégations : outil `bilan` de MIAM. Meilleur preset par catégorie : `/miam:banc`.
|
||||
Bilan des délégations : outil `bilan` de MIAM. Notes et caractéristiques des presets : outil `modeles`.
|
||||
Reference in new issue
Block a user