Refactor rewrite prompt and update tests for narrative generation

- Updated the rewrite prompt to remove markdown elements such as titles, bullet points, and code fences, ensuring a cleaner narrative output.
- Enhanced the GenerationPipeline tests to verify that code fences and chapter titles are stripped before manuscript promotion.
- Adjusted NextLots tests to include new functionality for command runners and added tests for deduplication of headings and timeout handling.
- Introduced new documentation files detailing project context and execution plans for March 2026, outlining current project status and objectives.
- Added operational memory documents to summarize project state and decisions for resuming work on the ai-novel-engine.
This commit is contained in:
L'électron rare
2026-03-16 06:03:24 +01:00
parent 7f53fe3bcf
commit 5e63ceb782
26 changed files with 1334 additions and 200 deletions
+16
View File
@@ -1,4 +1,20 @@
__pycache__/
*.pyc
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
.eggs/
*.egg
automation/reports/
automation/state/
.env
.venv/
venv/
.idea/
.vscode/
*.swp
*.swo
.DS_Store
brouillons/*/meta.json
+16 -31
View File
@@ -20,7 +20,9 @@ v2 — développement en cours (open-source)
## Suivi
- backlog actif: [`TODO_ACTIVE.md`](./TODO_ACTIVE.md)
- etat livre: [`TODO_IMPLEMENTE.md`](./TODO_IMPLEMENTE.md)
- ordre d'execution recommande: [`docs/EXECUTION_PLAN_2026-03-08.md`](./docs/EXECUTION_PLAN_2026-03-08.md)
- contexte courant: [`docs/CONTEXTE_PROJET_2026-03-14.md`](./docs/CONTEXTE_PROJET_2026-03-14.md)
- memoire de reprise: [`docs/MEMOIRE_REPRISE_2026-03-14.md`](./docs/MEMOIRE_REPRISE_2026-03-14.md)
- ordre d'execution recommande: [`docs/EXECUTION_PLAN_2026-03-14.md`](./docs/EXECUTION_PLAN_2026-03-14.md)
- runbook local: [`docs/runbooks/LOCAL_GENERATION.md`](./docs/runbooks/LOCAL_GENERATION.md)
- comparatif modeles local: [`docs/MODEL_COMPARISON_2026-03-08.md`](./docs/MODEL_COMPARISON_2026-03-08.md)
@@ -34,7 +36,10 @@ python3 scripts/run_next_lots.py --lot full
Points clés:
- le manifeste versionné est `automation/next_lots.toml`
- `paths.ollama_runtime = "native"` garde le preflight Ollama natif avant smoke
- `paths.ollama_runtime = "openai_compatible"` saute ce preflight et envoie `ollama:*` vers `paths.ollama_openai_base_url`
- le driver réutilise les smokes existants au lieu de dupliquer le pipeline
- `tracking_sync` consolide maintenant les derniers verdicts connus par modele a partir de `automation/reports/*/run.json`
- les opérations sensibles restent semi-autos: en cas de switch Apple ou de restart runtime, le cycle prépare les commandes exactes puis s'arrête avec un état de reprise
- reprise:
@@ -85,13 +90,18 @@ Notes :
- `ANE_REPAIR_FALLBACK_MODEL` permet de forcer le modele du second passage `repair`
- le pipeline narratif reste entierement dans `ai-novel-engine`
- `mascarade` sert uniquement de runtime local et de couche OpenAI-compatible
- au 13 mars 2026 au soir, `:8100` et `:8201` sont remontes, et `http://127.0.0.1:11434/api/tags` repond de nouveau
- le vrai blocage restant n'est plus un service eteint mais `ollama` natif 0.17.7, qui echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal
- le rerun Apple comparable `automation/reports/apple_rerun_7oY51o` reste utile comme incident historique: il a ete bloque a `gate` sur `too_short` + `truncated_ending`, puis a casse sur l'ancien fallback `repair` vers Ollama
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` est `accepted` le 13 mars 2026 avec `323` mots et `repair_attempts=0`; la reference Apple locale est donc reconfirmee
- dernier cycle complet termine au 9 mars 2026 :
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted` de bout en bout sous garde-fou
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live, puis finit `quality_blocked` sur `outline_like`
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste `preflight_only`
- les baselines `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` sont en rerun automatise separe; ils ne sont plus la reference locale courante
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` est sorti du chemin critique; il reste archive comme piste experimentale
- les baselines `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` ont ete rejouees et finissent actuellement `quality_blocked` sur `truncated_ending`
- le smoke et `status` exposent maintenant `gate_v1.json`, `quality_blockers`, `failed_stage`, `repair_attempts` et `repair_models`
- le runtime Apple local ne sert qu'un `model_id` a la fois; un fallback `repair` vers un autre modele Apple exige donc un switch de service entre runs
- par defaut, le second passage `repair` reste maintenant sur le meme provider; `ANE_REPAIR_FALLBACK_MODEL` sert seulement a forcer un switch explicite
Smoke test local rapide :
@@ -104,38 +114,13 @@ Smoke test local rapide :
Le script cree un workspace temporaire, ecrit une intention de test, lance la vraie CLI publique, fait un warm-up automatique pour `apple-coreml`, puis affiche un resume humain des artefacts et du `meta.json`. En mode `apple-coreml`, il applique par defaut un timeout plus large (`ANE_TIMEOUT=900`) et des budgets de smoke plus courts pour eviter de faire exploser la latence locale. Pour les reruns qualitatifs de reference, fixer explicitement `--timeout 300` et des budgets `ANE_MAX_TOKENS_*` communs. Utiliser `--workspace`, `--chapter`, `--intention`, `--timeout`, `--approve` ou `--reject` si besoin.
## Etat auto-synchronise
## Etat auto-synchronise
<!-- AUTO-SYNC:ANE-README:START -->
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
- reference locale actuelle: aucun accepted, meilleur diagnostic: apple-coreml:qwen2.5-0.5b-instruct-onnx
- prochain lot utile: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
- reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16
- prochain lot utile: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
- lancer un cycle: `python3 scripts/run_next_lots.py --lot full`
- checkpoint manuel en attente: Le runtime Apple sert `qwen2.5-0.5b-instruct-onnx` au lieu de `stateful-mistral7b-instruct-int4-coreml`.
<!-- AUTO-SYNC:ANE-README:END -->
<!-- CHANTIER:AUDIT START -->
## Audit & Execution Plan (2026-03-10)
+42 -60
View File
@@ -1,74 +1,56 @@
# TODO actif - AI Novel Engine
Source de verite des taches restantes pour `ai-novel-engine`.
Backlog courant pour reprendre `ai-novel-engine` sans melanger le reste avec les lots deja livres.
Regle:
- cocher ici ce qui est fait puis deplacer le lot livre vers `TODO_IMPLEMENTE.md`
- ne suivre ici que le travail restant ou les blocages encore ouverts
- garder les dependances `mascarade` explicites
References:
## Deja implemente
- [x] P0 Pipeline chapitre `intention -> structure -> draft -> critique -> rewrite -> validation -> memoire`
- [x] P0 Normalisation de chapitre avec identifiant canonique `chapitre_XX` et detection des collisions legacy
- [x] P0 Provider OpenAI-compatible et branchement local via `mascarade`
- [x] P0 Budgets par etape (`ANE_MAX_TOKENS_*`)
- [x] P0 Parsing JSON tolerant pour les sorties locales imparfaites
- [x] P0 Second passage de reessai pour `critique` et `memory` si le JSON reste invalide apres parsing tolerant
- [x] P0 Garde-fou manuscrit dur avant promotion (`gate_v1.json`, heuristiques locales, verdict `quality_blocked`)
- [x] P0 Boucle `repair` automatique entre `gate` et `quality_blocked`, avec artefacts `repair_vN.md`, fallback modele et preservation de `draft_v2.md`
- [x] P0 Smoke script local `scripts/smoke_local_generation.sh`
- [x] P0 Timeout provider remonte maintenant en `ProviderError` et marque correctement `failed_stage` dans `meta.json`
- [x] P0 Warm-up Apple du smoke remonte maintenant une erreur lisible au lieu d'une stacktrace brute
- [x] P0 Prompts `draft_v1` et `rewrite_v1` durcis pour imposer une prose continue sans titres ni puces
- [x] P1 Flags CLI non interactifs `--approve` et `--reject`
- [x] P1 `status` enrichi avec les chapitres en echec, en attente et bloques par garde-fou
- [x] P1 Resume de smoke humain a partir du `meta.json`
- [x] P1 Contrat cross-repo et recovery documentes via les runbooks
- [x] P2 `docs/vision.md`, `docs/roadmap.md` et le runbook local ne sont plus des placeholders
- [x] P0 Revalidation sous garde-fou de `ollama:qwen2.5:1.5b` -> `quality_blocked`
- [x] P0 Revalidation sous garde-fou de `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
- [x] P0 Revalidation sous garde-fou de `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `provider_failed` en `rewrite`
- [x] P0 Revalidation sous garde-fou de `ollama:qwen2.5:7b` -> `provider_failed` par timeout en `draft`
- [x] P0 Comparatif local re-ecrit pour le protocole avec garde-fou dans `docs/MODEL_COMPARISON_2026-03-08.md`
- [x] P0 Revalidation reelle sous protocole `gate + repair` borne a `300s` par requete:
- `ollama:qwen2.5:1.5b` -> `failed` en `structure`
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `failed` en `rewrite`
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `failed` en `rewrite`
- `ollama:qwen2.5:7b` -> `failed` en `rewrite`
- contexte courant: [`docs/CONTEXTE_PROJET_2026-03-14.md`](./docs/CONTEXTE_PROJET_2026-03-14.md)
- memoire de reprise: [`docs/MEMOIRE_REPRISE_2026-03-14.md`](./docs/MEMOIRE_REPRISE_2026-03-14.md)
- plan courant: [`docs/EXECUTION_PLAN_2026-03-14.md`](./docs/EXECUTION_PLAN_2026-03-14.md)
- lots livres: [`TODO_IMPLEMENTE.md`](./TODO_IMPLEMENTE.md)
## Memoire de reprise
- reference locale reconfirmee: `apple-coreml:qwen3.5-4b-onnx-q4f16` sur `20260309T055457Z` puis `apple_rerun_preset_20260313T223555Z`
- meilleur candidat alternatif: `qwen2.5:7b`, mais il faut d'abord lui rendre un runtime stable
- baselines vitesse a ce jour:
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
- `ollama:qwen2.5:1.5b` -> `provider_failed`
- faits live du 14 mars:
- Apple warm-up direct sur `:8100` en `2.76s`
- requete prose Apple representative en `39.99s`
- `llama-server` a charge le blob `qwen2.5:1.5b` et a repondu en `0.31s`
- etat automatise clos; `stateful-mistral7b-instruct-int4-coreml` est hors chemin critique
- le suivi `tracking_sync` consolide maintenant les derniers resultats connus par modele au lieu de n'utiliser qu'un lot partiel recent
## Actif
- [ ] P0 Terminer le lot `baselines`, puis relancer `tracking_sync` sur un etat complet incluant `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b`
- [ ] P0 Faire passer `ollama:qwen2.5:7b` de `quality_blocked` a `accepted` en supprimant le residu `outline_like` apres `repair`
- [ ] P0 Faire terminer au moins un cycle `python3 scripts/run_next_lots.py --lot full` jusqu'a `tracking_sync` sans checkpoint manuel autre qu'un switch Apple explicite
- [ ] P1 Requalifier `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference Apple stable sur plusieurs cycles, pas sur un seul run accepte
- [ ] P1 Rendre la strategie de fallback `repair` consciente des modeles reellement servis: le runtime Apple n'expose qu'un `model_id` a la fois
- [ ] P1 Garder l'installation/staging Apple de `qwen2.5-0.5b-instruct-onnx`, `qwen3.5-4b-onnx-q4f16` et `stateful-mistral7b-instruct-int4-coreml` comme prerequis explicite
- [ ] P0 Industrialiser un chemin `llama.cpp` / `llama-server` reusable pour `qwen2.5:1.5b`
- [ ] P0 Tester le blob local `qwen2.5:7b` via le meme chemin alternatif
- [ ] P1 Rejouer `priority_models` puis `baselines` des que le backend alternatif est branche
- [ ] P1 Reprendre `rewrite` / `repair` uniquement sur les blockers qui survivent apres stabilisation runtime
## Bloque
- [ ] P1 `ollama:qwen2.5:7b` atteint maintenant `gate` et exerce `repair`, mais reste bloque sur `outline_like` apres deux passes
- [ ] P1 Le lot `baselines` exige encore un switch Apple explicite avant de pouvoir finir `apple-coreml:qwen2.5-0.5b-instruct-onnx`
- [ ] P1 `ollama:qwen2.5:1.5b` reste lent et reste a requalifier une fois `baselines` repris jusqu'au bout
- [ ] P1 `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste preflight-only sur cette machine: preflight froid `:8100` OK en 128 s, preflight chaud OK en 63 s, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes avec les budgets de smoke
- [ ] P1 Le host `ollama` natif 0.17.7 sur cette machine echoue sur `qwen2.5:1.5b` avec un crash Metal; la validation ANE reelle passe par un service Docker CPU expose sur `127.0.0.1:11435` et route via `mascarade`
- [ ] P1 Le runtime Apple local n'expose qu'un seul `model_id` a la fois; un fallback `repair` vers un autre modele Apple exige donc un switch de service entre deux runs, pas au milieu d'un smoke
- [ ] P0 `ollama` natif 0.17.7 sur macOS 26.3.1 / Apple M5 echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal / `HTTP 500`
- [ ] P1 Le runtime Apple local ne sert qu'un seul `model_id` a la fois; tout switch Apple reste une action manuelle ou semi-auto
- [ ] P1 `mascarade` route aujourd'hui `ollama:*` vers `/api/chat`, alors que `llama-server` expose surtout `/v1/chat/completions`; un pont runtime ou provider reste a creer
## Prochain ordre
- [ ] P0 Finir `python3 scripts/run_next_lots.py --lot baselines`, puis laisser `tracking_sync` recalculer les verdicts complets
- [ ] P0 Tuner `rewrite_v1` et la passe `repair` pour eliminer `outline_like` sur `ollama:qwen2.5:7b`
- [ ] P1 Rejouer ensuite `python3 scripts/run_next_lots.py --lot priority_models` pour verifier la stabilite de `apple-coreml:qwen3.5-4b-onnx-q4f16` et le sort de `ollama:qwen2.5:7b`
- [ ] P1 Garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse ou regressions tant qu'ils n'ont pas de verdict comparable au protocole courant
- [ ] P1 Verifier avant tout rerun Apple que `qwen2.5-0.5b-instruct-onnx`, `qwen3.5-4b-onnx-q4f16` et `stateful-mistral7b-instruct-int4-coreml` sont bien installes/stages et que le bon `model_id` est charge sur `:8201`
## Auto-sync
- [ ] P0 Choisir le mode d'integration `llama.cpp`: adapter provider Mascarade ou shim local
- [ ] P0 Valider `qwen2.5:1.5b` de bout en bout via ce nouveau chemin
- [ ] P1 Etendre le meme chemin a `qwen2.5:7b`
- [ ] P1 Relancer `python3 scripts/run_next_lots.py --lot priority_models`
- [ ] P1 Relancer ensuite `python3 scripts/run_next_lots.py --lot baselines`
- [ ] P1 Garder `automation/reports/apple_rerun_preset_20260313T223555Z` comme rerun de reference pour les comparaisons Apple futures
## Auto-sync
<!-- AUTO-SYNC:ANE-TODO-ACTIVE:START -->
- dernier cycle automatique: 2026-03-09T06:53:02+00:00
- modeles accepted: aucun
- modeles ayant atteint gate: apple-coreml:qwen2.5-0.5b-instruct-onnx, ollama:qwen2.5:1.5b
- quality_blocked: apple-coreml:qwen2.5-0.5b-instruct-onnx, ollama:qwen2.5:1.5b
- provider_failed: aucun
- prochain lot recommande: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
- checkpoint manuel en attente: Le runtime Apple sert `qwen2.5-0.5b-instruct-onnx` au lieu de `stateful-mistral7b-instruct-int4-coreml`.
- commande preparee: `bash scripts/prepare_runtime_step.sh --apple-model stateful-mistral7b-instruct-int4-coreml --resume-state /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json --ane-script /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/scripts/run_next_lots.py`
- reprise: `python3 scripts/run_next_lots.py --resume /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json`
- dernier cycle automatique: 2026-03-14T14:03:06+00:00
- modeles accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
- modeles ayant atteint gate: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
- quality_blocked: apple-coreml:qwen2.5-0.5b-instruct-onnx
- provider_failed: ollama:qwen2.5:7b, ollama:qwen2.5:1.5b
- prochain lot recommande: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
<!-- AUTO-SYNC:ANE-TODO-ACTIVE:END -->
+10 -2
View File
@@ -91,6 +91,15 @@ Regle:
- [x] `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live sur deux passes, puis finit `quality_blocked` avec `outline_like`
- [x] Le comparatif local, les TODOs, les README et les runbooks disposent maintenant d'un premier resultat `accepted` sous protocole courant
### Lot livre - 13 mars 2026 (reprise et durcissement prose)
- [x] `tracking_sync` consolide maintenant les derniers verdicts connus par modele depuis `automation/reports/*/run.json`
- [x] Normalisation legere des sorties prose pour retirer les code fences et titres `# Chapitre` avant garde-fou
- [x] Heuristique `outline_like` renforcee pour mieux detecter headings, listes, titres et labels structurants
- [x] Prompts `draft_v1`, `rewrite_v1`, `repair_v1` et `gate_v1` resserres contre `outline_like` et `truncated_ending`
- [x] Le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` passe `accepted` sans `repair` et reconfirme la reference Apple locale
- [x] Etat automatise `baselines` clos proprement; `stateful-mistral7b-instruct-int4-coreml` sort du chemin critique
- [x] Suite unitaire etendue a 43 tests verts
## Actif
- [x] Aucun suivi actif ici. Voir `TODO_ACTIVE.md`.
@@ -100,11 +109,10 @@ Regle:
## Prochain ordre
- [x] Mettre a jour ce fichier uniquement quand un nouveau lot est reellement termine.
## Auto-sync
## Auto-sync
<!-- AUTO-SYNC:ANE-TODO-DONE:START -->
- orchestrateur `scripts/run_next_lots.py` disponible
- manifeste `automation/next_lots.toml` charge
- derniers fichiers de suivi synchronisables via marqueurs `AUTO-SYNC`
- dernier cycle automatise observe: 2026-03-09T06:53:02+00:00
- dernier cycle automatise observe: 2026-03-14T14:03:06+00:00
<!-- AUTO-SYNC:ANE-TODO-DONE:END -->
+7 -6
View File
@@ -2,7 +2,11 @@
mascarade_repo = "/Users/electron/mascarade"
core_base_url = "http://127.0.0.1:8100"
apple_runtime_url = "http://127.0.0.1:8201"
ollama_tags_url = "http://127.0.0.1:11435/api/tags"
ollama_tags_url = "http://127.0.0.1:11434/api/tags"
# `native` garde le preflight Ollama `/api/chat`.
# `openai_compatible` saute ce preflight et envoie `ollama:*` vers l'URL ci-dessous.
ollama_runtime = "native"
ollama_openai_base_url = "http://127.0.0.1:8100"
apple_model_ready_timeout_seconds = 30
apple_model_poll_interval_seconds = 2
@@ -28,7 +32,6 @@ ANE_REPAIR_MAX_PASSES = "2"
apple_models = [
"qwen2.5-0.5b-instruct-onnx",
"qwen3.5-4b-onnx-q4f16",
"stateful-mistral7b-instruct-int4-coreml",
]
ollama_models = [
"qwen2.5:7b",
@@ -48,14 +51,12 @@ models = [
]
[lots.preflight_only]
models = [
"apple-coreml:stateful-mistral7b-instruct-int4-coreml",
]
models = []
[tracking.ane]
todo_active = "TODO_ACTIVE.md"
todo_done = "TODO_IMPLEMENTE.md"
plan = "docs/EXECUTION_PLAN_2026-03-08.md"
plan = "docs/EXECUTION_PLAN_2026-03-14.md"
comparison = "docs/MODEL_COMPARISON_2026-03-08.md"
readme = "README.md"
runbook = "docs/runbooks/LOCAL_GENERATION.md"
View File
+5 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
import argparse
from pathlib import Path
import sys
@@ -109,6 +110,9 @@ def cmd_intention_create(root: Path, chapter_value: str | None = None, input_fun
intentions_dir.mkdir(parents=True, exist_ok=True)
raw_chapter = chapter_value or input_func("Numéro du chapitre (ex: 08) : ").strip()
if not re.match(r'^[a-zA-Z0-9_]+$', raw_chapter):
print("Numéro de chapitre invalide (alphanumériques et _ uniquement).")
return 1
chapter = ChapterId.parse(raw_chapter)
path = resolve_chapter_file(intentions_dir, chapter)
@@ -116,7 +120,7 @@ def cmd_intention_create(root: Path, chapter_value: str | None = None, input_fun
print(f"Une intention existe déjà : {path}")
return 1
print("\nDécris lintention (finir par Ctrl+D / Ctrl+Z):\n")
print("\nDécris l'intention (finir par Ctrl+D / Ctrl+Z):\n")
lines = []
try:
while True:
View File
+88 -14
View File
@@ -287,6 +287,55 @@ class GenerationPipeline:
return text
return f"{text[:limit].rstrip()}\n[...]"
def _normalize_generated_prose(self, text: str) -> str:
payload = text.strip()
if not payload:
return ""
if payload.startswith("```"):
lines = payload.splitlines()
if len(lines) >= 3 and lines[-1].strip() == "```":
payload = "\n".join(lines[1:-1]).strip()
cleaned_lines: list[str] = []
for raw_line in payload.splitlines():
stripped = raw_line.strip()
if not stripped:
cleaned_lines.append("")
continue
if stripped in {"```", "```markdown", "```md"}:
continue
if stripped == "---":
continue
if re.match(r"^#\s+(chapitre|chapter)\b", stripped, flags=re.IGNORECASE):
continue
cleaned_lines.append(raw_line)
normalized = "\n".join(cleaned_lines).strip()
return f"{normalized}\n" if normalized else ""
def _repair_focus(self, gate_report: ManuscriptGateReport) -> str:
blockers = set(gate_report.all_blockers())
focus: list[str] = []
if "outline_like" in blockers:
focus.append(
"- priorite absolue: supprimer toute ecriture qui resume ou annonce la scene; montrer l'action, la perception, la decision et la consequence au lieu de les lister"
)
focus.append(
"- chaque paragraphe doit faire avancer concretement la situation; eviter les intitulés, les titres de chapitre et les transitions meta"
)
if "truncated_ending" in blockers:
focus.append(
"- la derniere scene doit se fermer sur une decision nette et sa consequence immediate, dans une phrase pleinement terminee"
)
if "too_short" in blockers:
focus.append(
"- viser au moins 4 paragraphes utiles pour obtenir une scene complete, pas un resume raccourci"
)
if not focus:
focus.append("- conserver une prose continue, concrete et entierement narrative")
return "\n".join(focus)
def _generate_structure(
self,
provider: GenerationProvider,
@@ -332,10 +381,10 @@ class GenerationPipeline:
"Génération du brouillon initial en cours.",
)
response = provider.generate(GenerationRequest(stage="draft", prompt=prompt, temperature=0.4))
draft = response.content.strip()
draft = self._normalize_generated_prose(response.content)
if not draft:
raise ProviderError("Le provider a renvoyé un brouillon vide.")
return f"{draft}\n"
return draft
def _generate_control_report(
self,
@@ -392,10 +441,10 @@ class GenerationPipeline:
"Réécriture guidée par la critique en cours.",
)
response = provider.generate(GenerationRequest(stage="rewrite", prompt=prompt, temperature=0.3))
draft = response.content.strip()
draft = self._normalize_generated_prose(response.content)
if not draft:
raise ProviderError("Le provider a renvoyé une réécriture vide.")
return f"{draft}\n"
return draft
def _repair_until_ready(
self,
@@ -471,6 +520,7 @@ class GenerationPipeline:
gate_json=gate_report.to_dict(),
repair_attempt=attempt,
repair_model=repair_model or "",
repair_focus=self._repair_focus(gate_report),
story_context=context.story_context,
)
model_label = repair_model or self._provider_model_name(provider) or "provider_courant"
@@ -481,10 +531,10 @@ class GenerationPipeline:
f"Réparation prose v{attempt} en cours avec {model_label}.",
)
response = provider.generate(GenerationRequest(stage="repair", prompt=prompt, temperature=0.2))
repaired = response.content.strip()
repaired = self._normalize_generated_prose(response.content)
if not repaired:
raise ProviderError("Le provider a renvoyé une réparation vide.")
return f"{repaired}\n"
return repaired
def _generate_manuscript_gate_report(
self,
@@ -563,6 +613,8 @@ class GenerationPipeline:
override = os.environ.get("ANE_REPAIR_FALLBACK_MODEL", "").strip()
candidate = override or self._default_repair_fallback_model(base_model) or base_model
if not override and self._model_provider_name(candidate) != self._model_provider_name(base_model):
candidate = base_model
if self._is_cross_apple_runtime_switch(base_model, candidate):
raise ProviderError(
"ANE_REPAIR_FALLBACK_MODEL ne peut pas viser un autre modèle apple-coreml pendant un même smoke. "
@@ -587,6 +639,13 @@ class GenerationPipeline:
return False
return base_model.startswith("apple-coreml:") and candidate.startswith("apple-coreml:")
def _model_provider_name(self, model: str | None) -> str | None:
if not model or ":" not in model:
return None
provider, _ = model.split(":", 1)
provider = provider.strip()
return provider or None
def _heuristic_gate_report(self, draft_v2: str) -> ManuscriptGateReport | None:
blockers: list[str] = []
recommendations: list[str] = []
@@ -630,16 +689,31 @@ class GenerationPipeline:
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("## "):
detected_markers.add("heading_level_2")
if stripped.startswith("### "):
detected_markers.add("heading_level_3")
if stripped.startswith("- "):
detected_markers.add("bullet_list")
lowered = stripped.lower()
if "**objectif**" in lowered or "**conflit**" in lowered or "**sortie**" in lowered:
if stripped.startswith("```"):
detected_markers.add("fenced_block")
if re.match(r"^#{1,6}\s", stripped):
detected_markers.add("heading")
if stripped == "---":
detected_markers.add("horizontal_rule")
if stripped.startswith(("- ", "* ")):
detected_markers.add("bullet_list")
if re.match(r"^\d+[.)]\s", stripped):
detected_markers.add("numbered_list")
if (
"**objectif**" in lowered
or "**conflit**" in lowered
or "**sortie**" in lowered
or "objectif:" in lowered
or "conflit:" in lowered
or "sortie:" in lowered
):
detected_markers.add("scene_fields")
if "scène" in lowered or "scene" in lowered:
if re.match(r"^#{0,6}\s*(objectif dramatique|tension|scènes?|scenes?)\b", lowered):
detected_markers.add("structure_label")
if re.match(r"^#{0,6}\s*chapitre\b", lowered):
detected_markers.add("chapter_title")
if "scène" in lowered or "scene" in lowered or "— titre" in lowered:
detected_markers.add("scene_heading")
if len(detected_markers) >= 2:
return True
+28 -2
View File
@@ -4,7 +4,9 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, replace
import json
import os
import random
import socket
import time
from typing import Mapping
from urllib import error, request
@@ -146,19 +148,39 @@ class OpenAICompatibleProvider(GenerationProvider):
method="POST",
)
_RETRYABLE_HTTP_CODES = {429, 500, 502, 503}
_MAX_RETRIES = 3
_BASE_DELAY = 1.0
_MAX_DELAY = 10.0
last_exc: Exception | None = None
for attempt in range(_MAX_RETRIES):
try:
with request.urlopen(http_request, timeout=self.config.timeout) as response:
raw_payload = json.loads(response.read().decode("utf-8"))
break
except error.HTTPError as exc:
if exc.code in _RETRYABLE_HTTP_CODES and attempt < _MAX_RETRIES - 1:
last_exc = exc
delay = min(_BASE_DELAY * (2 ** attempt), _MAX_DELAY)
delay += random.uniform(0, delay * 0.25)
time.sleep(delay)
continue
details = exc.read().decode("utf-8", errors="replace")
raise ProviderError(
f"Le provider a répondu avec HTTP {exc.code} pendant l'étape '{prompt_request.stage}': {details}"
) from exc
except error.URLError as exc:
except (error.URLError, TimeoutError, socket.timeout) as exc:
if attempt < _MAX_RETRIES - 1:
last_exc = exc
delay = min(_BASE_DELAY * (2 ** attempt), _MAX_DELAY)
delay += random.uniform(0, delay * 0.25)
time.sleep(delay)
continue
if isinstance(exc, error.URLError):
raise ProviderError(
f"Impossible de joindre le provider pendant l'étape '{prompt_request.stage}': {exc.reason}"
) from exc
except (TimeoutError, socket.timeout) as exc:
raise ProviderError(
f"Timeout du provider pendant l'étape '{prompt_request.stage}' après {self.config.timeout:.0f}s."
) from exc
@@ -166,6 +188,10 @@ class OpenAICompatibleProvider(GenerationProvider):
raise ProviderError(
f"Réponse non JSON du provider pendant l'étape '{prompt_request.stage}'."
) from exc
else:
raise ProviderError(
f"Le provider a échoué après {_MAX_RETRIES} tentatives pendant l'étape '{prompt_request.stage}'."
) from last_exc
try:
choice = raw_payload["choices"][0]
Binary file not shown.
+253 -12
View File
@@ -3,9 +3,11 @@ from __future__ import annotations
import argparse
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
import inspect
import json
import os
from pathlib import Path
import re
import subprocess
import time
import tomllib
@@ -55,6 +57,8 @@ class Manifest:
core_base_url: str
apple_runtime_url: str
ollama_tags_url: str
ollama_runtime: str
ollama_openai_base_url: str
apple_model_ready_timeout_seconds: float
apple_model_poll_interval_seconds: float
smoke_chapter: str
@@ -77,6 +81,11 @@ class Manifest:
tracking = payload["tracking"]
lots = payload["lots"]
ensure_models = payload["ensure_models"]
ollama_runtime = str(paths.get("ollama_runtime", "native")).strip() or "native"
if ollama_runtime not in {"native", "openai_compatible"}:
raise NextLotsError(
"paths.ollama_runtime doit valoir 'native' ou 'openai_compatible'."
)
mascarade_repo = Path(paths["mascarade_repo"]).expanduser()
return cls(
@@ -98,6 +107,10 @@ class Manifest:
core_base_url=str(paths["core_base_url"]).rstrip("/"),
apple_runtime_url=str(paths["apple_runtime_url"]).rstrip("/"),
ollama_tags_url=str(paths["ollama_tags_url"]).rstrip("/"),
ollama_runtime=ollama_runtime,
ollama_openai_base_url=str(
paths.get("ollama_openai_base_url", paths["core_base_url"])
).rstrip("/"),
apple_model_ready_timeout_seconds=float(paths.get("apple_model_ready_timeout_seconds", 30)),
apple_model_poll_interval_seconds=float(paths.get("apple_model_poll_interval_seconds", 2)),
smoke_chapter=str(smoke["chapter"]),
@@ -122,7 +135,7 @@ class CommandResult:
duration_seconds: float
CommandRunner = Callable[[list[str], Path, dict[str, str] | None], CommandResult]
CommandRunner = Callable[..., CommandResult]
JsonFetcher = Callable[[str, float], Any]
@@ -217,11 +230,17 @@ def _timestamp() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def _default_command_runner(args: list[str], cwd: Path, env: dict[str, str] | None = None) -> CommandResult:
def _default_command_runner(
args: list[str],
cwd: Path,
env: dict[str, str] | None = None,
timeout_seconds: float | None = None,
) -> CommandResult:
merged_env = os.environ.copy()
if env:
merged_env.update(env)
started = time.monotonic()
try:
completed = subprocess.run(
args,
cwd=str(cwd),
@@ -229,6 +248,19 @@ def _default_command_runner(args: list[str], cwd: Path, env: dict[str, str] | No
text=True,
capture_output=True,
check=False,
timeout=timeout_seconds,
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout if isinstance(exc.stdout, str) else (exc.stdout.decode("utf-8", errors="replace") if exc.stdout else "")
stderr = exc.stderr if isinstance(exc.stderr, str) else (exc.stderr.decode("utf-8", errors="replace") if exc.stderr else "")
detail = f"Timed out after {timeout_seconds:.1f}s." if timeout_seconds is not None else "Timed out."
stderr = f"{stderr}\n{detail}".strip()
return CommandResult(
args=args,
returncode=124,
stdout=stdout,
stderr=stderr,
duration_seconds=time.monotonic() - started,
)
return CommandResult(
args=args,
@@ -267,9 +299,69 @@ def replace_auto_section(path: Path, marker_name: str, heading: str, body: str)
else:
suffix = "\n" if text.endswith("\n") else "\n\n"
new_text = f"{text}{suffix}{section}"
repeated_heading_pattern = rf"(?:{re.escape(heading)}\n){{2,}}"
new_text = re.sub(repeated_heading_pattern, f"{heading}\n", new_text)
path.write_text(new_text, encoding="utf-8")
def _safe_timestamp(value: str) -> datetime:
try:
parsed = datetime.fromisoformat(value)
except ValueError:
return datetime.fromtimestamp(0, tz=timezone.utc)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def _load_report_history(reports_root: Path) -> list[RunState]:
history: list[RunState] = []
if not reports_root.exists():
return history
for run_path in sorted(reports_root.glob("*/run.json")):
try:
history.append(RunState.load(run_path))
except (OSError, json.JSONDecodeError, TypeError, ValueError):
continue
history.sort(key=lambda item: (_safe_timestamp(item.updated_at), item.report_dir))
return history
def _result_sort_key(result: ModelRunResult) -> tuple[int, str, str]:
category_order = {
"priority_models": 0,
"baselines": 1,
"preflight_only": 2,
"runtime_preflight": 3,
}
provider = result.model.split(":", 1)[0]
return (category_order.get(result.category, 9), provider, result.model)
def _consolidated_tracking_results(state: RunState, reports_root: Path) -> list[ModelRunResult]:
latest_by_model: dict[str, tuple[tuple[datetime, int], ModelRunResult]] = {}
sequence = 0
for snapshot in [*_load_report_history(reports_root), state]:
stamp = _safe_timestamp(snapshot.updated_at)
for result in snapshot.typed_results():
candidate_key = (stamp, sequence)
current = latest_by_model.get(result.model)
if current is None or candidate_key >= current[0]:
latest_by_model[result.model] = (candidate_key, result)
sequence += 1
return sorted((payload[1] for payload in latest_by_model.values()), key=_result_sort_key)
def _accepted_history_counts(state: RunState, reports_root: Path) -> dict[str, int]:
counts: dict[str, int] = {}
for snapshot in [*_load_report_history(reports_root), state]:
for result in snapshot.typed_results():
if result.classification != "accepted":
continue
counts[result.model] = counts.get(result.model, 0) + 1
return counts
class NextLotsRunner:
def __init__(
self,
@@ -281,6 +373,7 @@ class NextLotsRunner:
self.manifest = manifest
self.command_runner = command_runner
self.json_fetcher = json_fetcher
self._command_runner_supports_timeout = len(inspect.signature(command_runner).parameters) >= 4
def run(
self,
@@ -381,7 +474,7 @@ class NextLotsRunner:
state.notes.append("Dry-run: ensure_models non exécuté.")
return
args = ["bash", "scripts/ensure_apple_models.sh"]
result = self.command_runner(args, self.manifest.tracking.mascarade_repo)
result = self._invoke_command(args, self.manifest.tracking.mascarade_repo, timeout_seconds=900)
log_path = Path(state.report_dir) / "ensure_models.log"
log_path.write_text(_command_log(result), encoding="utf-8")
if result.returncode != 0:
@@ -471,7 +564,7 @@ class NextLotsRunner:
)
def _build_manual_action(self, state: RunState, *, args: list[str], reason: str) -> dict[str, Any]:
result = self.command_runner(args, self.manifest.tracking.mascarade_repo)
result = self._invoke_command(args, self.manifest.tracking.mascarade_repo, timeout_seconds=300)
log_path = Path(state.report_dir) / f"manual_action_{len(state.results):02d}.log"
log_path.write_text(_command_log(result), encoding="utf-8")
return {
@@ -514,20 +607,123 @@ class NextLotsRunner:
return last_seen
return last_seen
def _ollama_base_url(self) -> str:
tags_url = self.manifest.ollama_tags_url.rstrip("/")
suffix = "/api/tags"
if tags_url.endswith(suffix):
return tags_url[: -len(suffix)]
return tags_url
def _openai_base_url_for_model(self, model: str) -> str:
if model.startswith("ollama:") and self.manifest.ollama_runtime == "openai_compatible":
return self.manifest.ollama_openai_base_url
return self.manifest.core_base_url
def _should_run_ollama_native_preflight(self, model: str) -> bool:
return model.startswith("ollama:") and self.manifest.ollama_runtime == "native"
def _run_ollama_native_preflight(self, model: str) -> CommandResult:
timeout_seconds = min(45.0, float(self._timeout_for_model(f"ollama:{model}")))
payload = {
"model": model,
"messages": [{"role": "user", "content": "Respond with exactly: ollama native preflight ok"}],
"stream": False,
"options": {
"temperature": 0,
"num_predict": 16,
},
}
body = json.dumps(payload).encode("utf-8")
started = time.monotonic()
try:
req = request.Request(
f"{self._ollama_base_url()}/api/chat",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with request.urlopen(req, timeout=timeout_seconds) as response:
raw_payload = response.read().decode("utf-8")
except error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
return CommandResult(
args=["ollama-native-preflight", model],
returncode=1,
stdout="",
stderr=f"HTTP {exc.code} {exc.reason}\n{detail}".strip(),
duration_seconds=time.monotonic() - started,
)
except Exception as exc:
return CommandResult(
args=["ollama-native-preflight", model],
returncode=1,
stdout="",
stderr=f"{type(exc).__name__}: {exc}",
duration_seconds=time.monotonic() - started,
)
try:
parsed = json.loads(raw_payload)
except json.JSONDecodeError:
parsed = {"raw": raw_payload}
preview = {
"model": parsed.get("model"),
"content": (parsed.get("message") or {}).get("content", ""),
"done_reason": parsed.get("done_reason"),
}
return CommandResult(
args=["ollama-native-preflight", model],
returncode=0,
stdout=json.dumps(preview, ensure_ascii=False, indent=2),
stderr="",
duration_seconds=time.monotonic() - started,
)
def _invoke_command(
self,
args: list[str],
cwd: Path,
*,
env: dict[str, str] | None = None,
timeout_seconds: float | None = None,
) -> CommandResult:
if self._command_runner_supports_timeout:
return self.command_runner(args, cwd, env, timeout_seconds)
return self.command_runner(args, cwd, env)
def _run_model(self, model: str, *, category: str, preflight_only: bool, report_dir: Path) -> ModelRunResult:
result = ModelRunResult(model=model, category=category, apple_model_active=self._current_apple_model())
model_slug = _slugify(model)
openai_base_url = self._openai_base_url_for_model(model)
if self._should_run_ollama_native_preflight(model):
native_preflight = self._run_ollama_native_preflight(model.split(":", 1)[1])
if native_preflight.returncode != 0:
result.preflight_duration_seconds = native_preflight.duration_seconds
native_log = report_dir / f"{model_slug}_ollama_native_preflight.log"
native_log.write_text(_command_log(native_preflight), encoding="utf-8")
result.preflight_log = str(native_log)
result.preflight_ok = False
result.classification = "provider_failed"
result.status = "ollama_runtime_unhealthy"
result.notes.append("Le preflight Ollama natif a échoué.")
hint = _runtime_error_hint(native_preflight.stderr)
if hint:
result.notes.append(hint)
return result
preflight_args = [
"bash",
"scripts/smoke_openai_compat_ane.sh",
"--url",
self.manifest.core_base_url,
openai_base_url,
"--model",
model,
"--timeout",
str(self._timeout_for_model(model)),
]
preflight = self.command_runner(preflight_args, self.manifest.tracking.mascarade_repo)
preflight = self._invoke_command(
preflight_args,
self.manifest.tracking.mascarade_repo,
timeout_seconds=float(self._timeout_for_model(model) + 30),
)
result.preflight_duration_seconds = preflight.duration_seconds
preflight_log = report_dir / f"{model_slug}_preflight.log"
preflight_log.write_text(_command_log(preflight), encoding="utf-8")
@@ -550,7 +746,7 @@ class NextLotsRunner:
"bash",
"scripts/smoke_local_generation.sh",
"--base-url",
self.manifest.core_base_url,
openai_base_url,
"--model",
model,
"--chapter",
@@ -563,7 +759,12 @@ class NextLotsRunner:
self.manifest.smoke_intention,
"--approve",
]
smoke = self.command_runner(smoke_args, self.manifest.repo_root, env=self.manifest.preset_env)
smoke = self._invoke_command(
smoke_args,
self.manifest.repo_root,
env=self.manifest.preset_env,
timeout_seconds=float(self.manifest.smoke_timeout_seconds + 60),
)
result.smoke_attempted = True
result.smoke_duration_seconds = smoke.duration_seconds
smoke_log = report_dir / f"{model_slug}_smoke.log"
@@ -614,11 +815,22 @@ class NextLotsRunner:
if dry_run:
self._write_report_summary(state)
return
typed_results = state.typed_results()
typed_results = _consolidated_tracking_results(
state,
self.manifest.repo_root / "automation" / "reports",
)
accepted_counts = _accepted_history_counts(
state,
self.manifest.repo_root / "automation" / "reports",
)
project_state = ProjectState(self.manifest.repo_root).summary()
summary = _build_summary(state, typed_results)
comparison = _render_comparison_markdown(state, typed_results)
active_next = _compute_next_lot_recommendation(typed_results, self.manifest.next_code_lot)
active_next = _compute_next_lot_recommendation(
typed_results,
self.manifest.next_code_lot,
accepted_counts=accepted_counts,
)
replace_auto_section(
self.manifest.tracking.ane_todo_active,
@@ -702,6 +914,15 @@ def _optional_string(value: object) -> str | None:
return text or None
def _runtime_error_hint(stderr: str) -> str | None:
for raw_line in stderr.splitlines():
line = raw_line.strip()
if not line:
continue
return line[:240]
return None
def _slugify(value: str) -> str:
return "".join(char if char.isalnum() else "_" for char in value).strip("_").lower()
@@ -732,9 +953,29 @@ def _build_summary(state: RunState, results: list[ModelRunResult]) -> dict[str,
}
def _compute_next_lot_recommendation(results: list[ModelRunResult], fallback: str) -> str:
def _compute_next_lot_recommendation(
results: list[ModelRunResult],
fallback: str,
*,
accepted_counts: dict[str, int] | None = None,
) -> str:
accepted_counts = accepted_counts or {}
provider_failed_models = [item.model for item in results if item.classification == "provider_failed"]
has_quality_blocked = any(item.classification == "quality_blocked" for item in results)
if accepted_counts.get("apple-coreml:qwen3.5-4b-onnx-q4f16", 0) >= 2:
if provider_failed_models:
if has_quality_blocked:
return "Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate."
return "Reference locale reconfirmee; retablir le runtime des modeles provider_failed avant de poursuivre."
if any(item.classification == "quality_blocked" for item in results):
return "Reference locale reconfirmee; resserrer rewrite/repair sur les modeles deja bloques a gate."
return "Reference locale reconfirmee; garder les autres modeles en regression."
if any(item.classification == "accepted" for item in results):
return "Rejouer uniquement les baselines vitesse puis figer la référence locale dans les README/runbooks."
if provider_failed_models:
return "Confirmer la reference accepted puis retablir le runtime des modeles provider_failed."
if any(item.classification == "quality_blocked" for item in results):
return "Confirmer la reference accepted puis resserrer rewrite/repair sur les modeles deja bloques a gate."
return "Figer la reference locale dans les README/runbooks et garder les autres modeles en regression."
if any(item.reached_gate() for item in results):
return "Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats."
return fallback
+89
View File
@@ -0,0 +1,89 @@
# Contexte projet - 14 mars 2026
Document court pour comprendre rapidement ou en est `ai-novel-engine` aujourd'hui.
## Ce que fait le projet
`ai-novel-engine` est un moteur de redaction longue qui garde l'auteur dans la boucle.
Le pipeline reste stable:
`intention -> structure -> draft -> critique -> rewrite -> gate -> validation -> memoire`
Le coeur produit n'est pas un "chat", mais une chaine de production narrative avec:
- intention obligatoire avant toute generation
- garde-fou dur avant promotion manuscrit
- boucle `repair`
- memoire externe par chapitre
## Architecture locale
Sur cette machine:
- `ai-novel-engine` parle un provider OpenAI-compatible
- `mascarade-core` expose `http://127.0.0.1:8100`
- le runtime Apple local expose `http://127.0.0.1:8201`
- Ollama natif expose `http://127.0.0.1:11434`
Le routage actuel repose surtout sur le prefixe de modele:
- `apple-coreml:*`
- `ollama:*`
## Etat produit confirme
- la reference locale reste `apple-coreml:qwen3.5-4b-onnx-q4f16`
- le comparatif versionne montre aujourd'hui:
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `accepted`
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
- `ollama:qwen2.5:7b` -> `provider_failed`
- `ollama:qwen2.5:1.5b` -> `provider_failed`
- `python3 -m unittest discover -s tests -v` passe avec `48` tests
## Faits live du 14 mars 2026
### Apple
- warm-up direct sur `apple-coreml:qwen3.5-4b-onnx-q4f16` via `:8100/v1/chat/completions`: `2.76s`
- requete prose plus representative (`max_tokens=96`) sur le meme modele: `39.99s`
- conclusion: le runtime Apple n'est pas mort; il est lent mais exploitable
### Ollama natif
- `ollama` 0.17.7 sur macOS 26.3.1 / Apple M5 echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b`
- les deux reports cibles du 14 mars tombent en `provider_failed` / `ollama_runtime_unhealthy`
- le symptome utile cote projet est `HTTP 500 Internal Server Error`
### Contournement valide
- `llama-server` local sait charger directement le blob GGUF Ollama de `qwen2.5:1.5b`
- un serveur temporaire sur `127.0.0.1:8082` a repondu en `0.31s`
- implication: le blocage n'est pas le modele lui-meme, mais le runtime Ollama natif
## Ce qui est vraiment bloque
Le projet n'est plus bloque par le pipeline narratif.
Le vrai blocage est maintenant:
- comment servir `qwen2.5:1.5b` et `qwen2.5:7b` via un backend local stable
- sans perdre le routage `provider:model` attendu par `mascarade` et `ai-novel-engine`
## Hypothese de travail la plus rentable
La piste la plus prometteuse n'est plus "reparer les prompts" mais:
1. brancher un chemin `llama.cpp` / `llama-server` reutilisable pour les blobs `qwen2.5`
2. rerun `priority_models` et `baselines`
3. reprendre `rewrite` / `repair` seulement sur les blockers qui survivent apres stabilisation runtime
## Fichiers a ouvrir en premier
- `docs/MEMOIRE_REPRISE_2026-03-14.md`
- `docs/EXECUTION_PLAN_2026-03-14.md`
- `TODO_ACTIVE.md`
- `docs/MODEL_COMPARISON_2026-03-08.md`
- `automation/reports/20260314T085946Z/SUMMARY.md`
- `automation/reports/20260314T100648Z/SUMMARY.md`
+109
View File
@@ -0,0 +1,109 @@
# Plan d'execution - 13 mars 2026
Plan de reprise apres revue des rapports du 9 mars 2026 et remise a plat du suivi.
References:
- memoire de reprise: [`MEMOIRE_REPRISE_2026-03-13.md`](./MEMOIRE_REPRISE_2026-03-13.md)
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
- lots livres: [`../TODO_IMPLEMENTE.md`](../TODO_IMPLEMENTE.md)
- comparatif courant: [`MODEL_COMPARISON_2026-03-08.md`](./MODEL_COMPARISON_2026-03-08.md)
Pilotage:
- lancer un cycle complet: `python3 scripts/run_next_lots.py --lot full`
- resynchroniser seulement les docs: `python3 scripts/run_next_lots.py --lot tracking_sync --report-only`
## Lot 1 - Garder le chemin critique ferme et rerunnable
### Etat constate
- le `state` courant est clos et synchronise
- `stateful-mistral7b-instruct-int4-coreml` est retire du lot critique
- le runtime minimal est revenu: `:8100`, `:8201` et `:11434/api/tags` repondent
- `mascarade-core` et `mascarade-api` ont ete recrees avec le bon `OLLAMA_BASE_URL`
- le blocage live restant est maintenant plus precis: `ollama` natif 0.17.7 plante en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` a cause du backend Metal
### Objectif
- garder `full` focalise sur les modeles utiles
- garder Apple et le core en etat, puis rebrancher un chemin Ollama CPU avant les vrais reruns qualite
### Done quand
- `automation/state/next_lots_state.json` reste sans checkpoint ambigu
- les endpoints Apple et core repondent de nouveau
- un chemin Ollama CPU redevient executable pour les smokes comparatifs
### Risque principal
- un runtime partiellement revenu peut masquer un blocage plus fin du backend Ollama si on s'arrete au simple `api/tags`
## Lot 2 - Corriger les epreuves qualite, pas ajouter de complexite
### Etat constate
- `ollama:qwen2.5:7b` atteint `gate` mais reste bloque sur `outline_like`
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` finissent sur `truncated_ending`
- la reference Apple 4B a deja prouve qu'un cycle `accepted` est possible
- le rerun comparable `automation/reports/apple_rerun_7oY51o` n'a pas reconfirme cette reference: `gate` bloque sur `too_short` + `truncated_ending`, puis `repair` echoue a cause de l'ancien fallback pipeline vers Ollama en `HTTP 502`
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` a depuis fini `accepted` sans `repair`; la reference Apple locale est reconfirmee
### Objectif
- resserrer `rewrite_v1`, `memory_v1` si besoin, et surtout la boucle `repair` sur les deux blocages reels:
- prose trop proche d'un plan
- fin de texte tronquee ou suspendue
### Done quand
- `ollama:qwen2.5:7b` finit au moins une fois `accepted`
- au moins une baseline sort de `truncated_ending` ou est officiellement releguee a simple test de regression
### Risque principal
- augmenter les budgets sans clarifier les consignes risque d'allonger les runs sans faire tomber les blockers
## Lot 3 - Reference locale reconfirmee
### Etat constate
- `apple-coreml:qwen3.5-4b-onnx-q4f16` a maintenant deux runs comparables `accepted`
- le rerun `automation/reports/apple_rerun_preset_20260313T223555Z` est passe de bout en bout avec `repair_attempts=0`
### Objectif
- figer la reference Apple locale et sortir ce sujet du flux d'incertitude; les reruns Apple restent maintenant isoles d'un fallback Ollama par defaut
### Done quand
- deux runs comparables au meme preset passent jusqu'a `accepted`
- la doc peut nommer une reference locale sans ambiguite
### Risque principal
- laisser la doc ou le suivi parler d'une reference "a confirmer" alors que le point est deja tranche
## Lot 4 - Garder une memoire projet exploitable
### Objectif
- faire en sorte que README, TODO, plan et runbook racontent la meme chose
- ne plus perdre les bons resultats lorsqu'un lot partiel plus recent tourne apres un lot complet
### Done quand
- `tracking_sync` consolide les derniers verdicts connus par modele
- les documents de suivi renvoient tous vers ce plan du 13 mars 2026
### Risque principal
- si les docs versionnees ne pointent pas vers le bon plan, la reprise devient plus couteuse que le code
## Auto-sync
<!-- AUTO-SYNC:ANE-PLAN:START -->
- dernier verdict automatise: 2026-03-14T10:06:55+00:00
- accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
- gate atteint: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
- prochain lot calcule: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
<!-- AUTO-SYNC:ANE-PLAN:END -->
+76
View File
@@ -0,0 +1,76 @@
# Plan d'execution - 14 mars 2026
Plan de reprise base sur l'etat reel du 14 mars 2026.
References:
- contexte: [`CONTEXTE_PROJET_2026-03-14.md`](./CONTEXTE_PROJET_2026-03-14.md)
- memoire: [`MEMOIRE_REPRISE_2026-03-14.md`](./MEMOIRE_REPRISE_2026-03-14.md)
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
- comparatif courant: [`MODEL_COMPARISON_2026-03-08.md`](./MODEL_COMPARISON_2026-03-08.md)
## Lot 1 - Industrialiser le contournement `llama.cpp`
### Etat constate
- `ollama:qwen2.5:7b` et `ollama:qwen2.5:1.5b` echouent encore en runtime natif
- `llama-server` sait deja charger un blob Ollama local et repondre vite
- le blob `qwen2.5:1.5b` a deja repondu via `llama-server`
### Objectif
- disposer d'un chemin local stable pour les modeles `qwen2.5` sans dependre du runtime Ollama natif
### Done quand
- un backend `llama.cpp` est branchable de facon reproductible pour `qwen2.5:1.5b`
- le routage `provider:model` reste pilotable depuis `mascarade`
## Lot 2 - Etendre au 7B
### Etat constate
- le 7B reste le meilleur candidat alternatif cote qualite
- son blocage courant est d'abord runtime, pas prompt
### Objectif
- verifier si le meme chemin `llama.cpp` peut servir `qwen2.5:7b`
### Done quand
- un preflight et un smoke simple du 7B passent via le backend alternatif
## Lot 3 - Rejouer les lots utiles
### Objectif
- rerun `priority_models` puis `baselines` sur un runtime local stable
### Done quand
- les deux modeles `qwen2.5` ne sont plus `provider_failed`
- le comparatif reflete enfin des verdicts qualite plutot que des erreurs de runtime
## Lot 4 - Revenir aux vrais blockers qualite
### Objectif
- reprendre `rewrite` / `repair` seulement sur les blockers qui subsistent apres stabilisation runtime
### Cible
- `outline_like` pour le meilleur candidat alternatif
- `truncated_ending` pour la baseline Apple encore bloquee
## Risque a eviter
Ne pas re-rentrer dans une boucle de tuning prompts tant que les reruns `qwen2.5` passent par un runtime non fiable.
## Auto-sync
<!-- AUTO-SYNC:ANE-PLAN:START -->
- dernier verdict automatise: 2026-03-14T14:03:06+00:00
- accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
- gate atteint: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
- prochain lot calcule: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
<!-- AUTO-SYNC:ANE-PLAN:END -->
+67
View File
@@ -0,0 +1,67 @@
# Memoire de reprise - 13 mars 2026
Memoire operationnelle courte pour reprendre `ai-novel-engine` sans relire tout le repo.
## Etat confirme
- la suite locale est saine: `python3 -m unittest discover -s tests -v` passe avec `44` tests
- le pipeline auteur reste le meme: `intention -> structure -> draft -> critique -> rewrite -> gate -> validation -> memoire`
- le lot `priority_models` du `2026-03-09T05:54:57Z` a donne:
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `accepted`
- `ollama:qwen2.5:7b` -> `quality_blocked` avec residu `outline_like`
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` a fini `accepted` le 13 mars 2026 avec `repair_attempts=0` et un `draft_v2` de `323` mots
- le lot `baselines` du `2026-03-09T06:35:12Z` a donne:
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked` avec `truncated_ending`
- `ollama:qwen2.5:1.5b` -> `quality_blocked` avec `truncated_ending`
- l'etat automatise a ete cloture proprement le `2026-03-13T14:15:56+00:00`
- `stateful-mistral7b-instruct-int4-coreml` est sorti du chemin critique; il ne bloque plus `full`
- etat runtime relu le 13 mars 2026 au soir:
- `http://127.0.0.1:8100/health` repond de nouveau
- `http://127.0.0.1:8201/health` et `http://127.0.0.1:8201/models` repondent de nouveau
- `http://127.0.0.1:11434/api/tags` repond de nouveau
- `mascarade-core` et `mascarade-api` ont ete recrees avec `OLLAMA_BASE_URL=http://host.docker.internal:11434`
- le vrai blocage live restant n'est plus un port mort mais `ollama` natif 0.17.7, qui echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal
- le rerun Apple comparable `automation/reports/apple_rerun_7oY51o` n'a pas reconfirme la reference: il a atteint `gate`, a ete bloque par `too_short` + `truncated_ending`, puis a echoue a `repair` a cause de l'ancien fallback pipeline vers l'Ollama natif
## Contradiction resolue
Le repo avait deux verites concurrentes:
- les rapports `priority_models` montraient un vrai modele `accepted`
- les sections `AUTO-SYNC` les plus recentes perdaient cette information des qu'un lot partiel plus recent ecrasait le `state` courant
Le suivi a ete corrige pour consolider les derniers resultats connus par modele depuis `automation/reports/*/run.json`. Le prochain `tracking_sync` ne doit plus oublier la reference `apple-coreml:qwen3.5-4b-onnx-q4f16`.
## Decisions de reprise
- fixer `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference locale reconfirmee sur deux runs comparables (`20260309T055457Z` puis `apple_rerun_preset_20260313T223555Z`)
- traiter `ollama:qwen2.5:7b` comme meilleur candidat alternatif a faire sortir de `outline_like`
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse et regression, pas comme references qualite
- garder `stateful-mistral7b-instruct-int4-coreml` hors chemin critique tant qu'un besoin produit explicite ne justifie pas sa reintroduction
## Fichiers a regarder en premier
- `automation/state/next_lots_state.json`
- `automation/reports/20260309T055457Z/SUMMARY.md`
- `automation/reports/20260309T063512Z/SUMMARY.md`
- `automation/reports/apple_rerun_preset_20260313T223555Z/brouillons/chapitres/chapitre_02/`
- `automation/reports/20260309T055457Z/workspaces/ollama_qwen2_5_7b/brouillons/chapitres/chapitre_02/`
- `automation/reports/20260309T063512Z/workspaces/apple_coreml_qwen2_5_0_5b_instruct_onnx/brouillons/chapitres/chapitre_02/`
- `automation/reports/20260309T063512Z/workspaces/ollama_qwen2_5_1_5b/brouillons/chapitres/chapitre_02/`
## Commandes utiles
```bash
python3 -m unittest discover -s tests -v
python3 scripts/run_next_lots.py --lot tracking_sync --report-only
python3 scripts/run_next_lots.py --lot priority_models
```
## Prochaine hypothese de travail
Le prochain gain utile ne viendra pas d'un nouveau backend mais d'un meilleur controle de `rewrite` et `repair`:
- `ollama:qwen2.5:7b` doit perdre le residu planifie `outline_like`
- les petites baselines doivent finir proprement sans `truncated_ending`
- la reference Apple 4B est reconfirmee; la priorite live se deplace maintenant vers `ollama:qwen2.5:7b`
- la priorite live n'est plus de rallumer `:8100` et `:8201`, mais de retablir un chemin Ollama CPU stable; le rerun Apple ne bascule plus par defaut vers Ollama pendant `repair`
+57
View File
@@ -0,0 +1,57 @@
# Memoire de reprise - 14 mars 2026
Memoire operationnelle pour reprendre `ai-novel-engine` sans refaire toute l'enquete runtime.
## Etat confirme
- la suite locale passe avec `48` tests
- la reference locale reste `apple-coreml:qwen3.5-4b-onnx-q4f16`
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` reste `quality_blocked` sur un blocage de fin / garde-fou
- `ollama:qwen2.5:7b` est maintenant a lire comme un blocage runtime, pas comme un simple blocage qualite
- `ollama:qwen2.5:1.5b` est dans le meme etat runtime que le 7b
## Reports du 14 mars 2026 a retenir
- `automation/reports/20260314T085946Z`:
- `ollama:qwen2.5:7b` -> `provider_failed`
- `status=ollama_runtime_unhealthy`
- note utile: `HTTP 500 Internal Server Error`
- `automation/reports/20260314T100648Z`:
- `ollama:qwen2.5:1.5b` -> `provider_failed`
- meme symptome utile: `HTTP 500 Internal Server Error`
## Mesures live utiles
- Apple warm-up direct sur `:8100`: `2.76s`
- Apple requete prose representative (`96` tokens): `39.99s`
- `llama-server` temporaire sur le blob `qwen2.5:1.5b`: reponse en `0.31s`
## Ce que cela veut dire
- Apple n'est pas le sujet critique du jour
- Ollama natif 0.17.7 reste le composant qui casse
- les blobs GGUF `qwen2.5` presents dans `~/.ollama/models/blobs/` sont sains et reutilisables via `llama.cpp`
## Decision de reprise
- ne plus perdre du temps a tuner `rewrite` / `repair` tant que les reruns `qwen2.5` passent par un runtime instable
- prioriser un contournement `llama-server` / `llama.cpp` avant tout nouveau lot comparatif
- garder `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference stable
## Fichiers et chemins utiles
- blob `qwen2.5:1.5b`: `/Users/electron/.ollama/models/blobs/sha256-183715c435899236895da3869489cc30ac241476b4971a20285b1a462818a5b4`
- blob `qwen2.5:7b`: `/Users/electron/.ollama/models/blobs/sha256-2bada8a7450677000f678be90653b85d364de7db25eb5ea54136ada5f3933730`
- report `7b`: `automation/reports/20260314T085946Z/`
- report `1.5b`: `automation/reports/20260314T100648Z/`
- comparatif courant: `docs/MODEL_COMPARISON_2026-03-08.md`
## Prochaine action utile
Rendre le contournement `llama.cpp` reusable cote runtime, puis rejouer:
```bash
python3 scripts/run_next_lots.py --lot priority_models
python3 scripts/run_next_lots.py --lot baselines
```
+18 -16
View File
@@ -21,13 +21,14 @@ Comparatif realise avec le protocole courant:
Contexte machine:
- `ai-novel-engine` pointe vers `mascarade` sur `http://127.0.0.1:8100`
- `ollama` est route vers un service Docker CPU expose sur `127.0.0.1:11435`
- le host `ollama` natif 0.17.7 reste bloque par un crash Metal sur cette machine
- `ai-novel-engine` resynchronise maintenant les tags Ollama via `127.0.0.1:11434`
- le host `ollama` natif 0.17.7 repond a `api/tags`, mais reste bloque en generation par un crash Metal sur cette machine
- pour rejouer les lots comparatifs Ollama, il faut rebrancher un chemin CPU stable cote Mascarade
- le runtime Apple local n'expose qu'un seul `model_id` a la fois sur `:8201`
- dernier cycle complet termine au 9 mars 2026:
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted`
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` puis finit `quality_blocked`
- le lot `baselines` est relance separement pour les petits modeles
- le lot `baselines` s'est termine separement avec deux `quality_blocked` sur `truncated_ending`
## Resultats
@@ -35,14 +36,14 @@ Contexte machine:
|---|---|---|---|---|---|---:|---|---|---|
| `apple-coreml:qwen3.5-4b-onnx-q4f16` | `apple-coreml` | OK | oui | `accepted` | `memory` | `711s` | meilleure nuance narrative du lot | critique exploitable, gate vert | reference ANE locale actuelle |
| `ollama:qwen2.5:7b` | `ollama` | OK | oui | `quality_blocked` | `gate` | `825s` | correcte, plus sobre que l'Apple 4B | critique exploitable, mais le texte reste trop proche d'un plan | meilleur candidat Ollama, encore bloque |
| `apple-coreml:qwen2.5-0.5b-instruct-onnx` | `apple-coreml` | OK | rerun en cours | n/a | n/a | n/a | baseline vitesse a requalifier | n/a | en attente de verdict courant |
| `ollama:qwen2.5:1.5b` | `ollama` | OK | rerun en cours | n/a | n/a | n/a | baseline vitesse a requalifier | n/a | en attente de verdict courant |
| `apple-coreml:qwen2.5-0.5b-instruct-onnx` | `apple-coreml` | OK | oui | `quality_blocked` | `gate` | `801s` | baseline vitesse Apple encore trop fragile pour la reference | `repair` actif, mais fin tronquee | baseline regression utile, pas reference qualite |
| `ollama:qwen2.5:1.5b` | `ollama` | OK | oui | `quality_blocked` | `gate` | `224s` | baseline vitesse correcte mais trop courte ou suspendue | `repair` actif, mais fin tronquee | temoin regression utile, pas candidat qualite |
Point legacy hors protocole courant:
| Modele | Backend | Preflight | Smoke complet | Statut final |
|---|---|---|---|---|
| `apple-coreml:stateful-mistral7b-instruct-int4-coreml` | `apple-coreml` | OK | bloque > `8 min` a `structure` | `preflight_only` |
| `apple-coreml:stateful-mistral7b-instruct-int4-coreml` | `apple-coreml` | OK | bloque > `8 min` a `structure` | `archive hors chemin critique` |
## Lecture rapide
@@ -59,11 +60,11 @@ Point legacy hors protocole courant:
- c'est le meilleur candidat Ollama actuel, mais il lui manque encore une prose plus continue
### `apple-coreml:qwen2.5-0.5b-instruct-onnx`
- rerun baseline en cours via le lot `baselines`
- reste utile comme candidat vitesse Apple, pas comme reference qualite tant qu'un verdict courant n'est pas resynchronise
- atteint `gate`, exerce `repair`, puis reste bloque sur `truncated_ending`
- reste utile comme candidat vitesse Apple, pas comme reference qualite
### `ollama:qwen2.5:1.5b`
- rerun baseline en cours via le lot `baselines`
- atteint `gate`, exerce `repair`, puis reste bloque sur `truncated_ending`
- reste un temoin de regression plus qu'un candidat qualite
## Verdicts
@@ -80,23 +81,24 @@ Le cycle `priority_models` atteint enfin un objectif produit minimal:
- la boucle `repair` est implementée, testee et visible dans `status` / `meta.json`
- `repair` a maintenant une validation live sur `ollama:qwen2.5:7b`
- un premier modele est `accepted` sous protocole courant: `apple-coreml:qwen3.5-4b-onnx-q4f16`
- la reference Apple locale est reconfirmee sur deux runs comparables: `apple-coreml:qwen3.5-4b-onnx-q4f16`
- le prochain enjeu n'est plus de trouver un premier succes, mais de finir les baselines et de sortir `ollama:qwen2.5:7b` de `outline_like`
Le prochain lot logique n'est plus "ajouter un garde-fou", mais:
1. finir le lot `baselines`
2. confirmer `apple-coreml:qwen3.5-4b-onnx-q4f16` sur rerun
1. garder `automation/reports/apple_rerun_preset_20260313T223555Z` comme rerun Apple de reference pour les comparaisons futures
2. retablir un chemin Ollama CPU puis rejouer `priority_models` pour `ollama:qwen2.5:7b`
3. regler `rewrite` et `repair` pour faire tomber `outline_like` sur `ollama:qwen2.5:7b`
4. ne garder `qwen2.5-0.5b` et `qwen2.5:1.5b` que comme baselines vitesse
4. ne garder `qwen2.5-0.5b` et `qwen2.5:1.5b` que comme baselines vitesse ou regression
## Auto-sync
## Auto-sync
<!-- AUTO-SYNC:ANE-COMPARISON:START -->
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
| Modele | Categorie | Preflight | Smoke | Classification | Failed stage | Gate | Repairs | Notes |
|---|---|---|---|---|---|---|---:|---|
| apple-coreml:qwen3.5-4b-onnx-q4f16 | priority_models | OK | oui | accepted | | oui | 0 | Smoke manuel comparable rejoue avec les budgets preset du manifeste. |
| ollama:qwen2.5:7b | priority_models | KO | non | provider_failed | | non | 0 | Le preflight Ollama natif a échoué.; HTTP 500 Internal Server Error |
| apple-coreml:qwen2.5-0.5b-instruct-onnx | baselines | OK | oui | quality_blocked | gate | oui | 2 | |
| ollama:qwen2.5:1.5b | baselines | OK | oui | quality_blocked | gate | oui | 2 | |
| ollama:qwen2.5:1.5b | baselines | KO | non | provider_failed | | non | 0 | Le preflight Ollama natif a échoué.; HTTP 500 Internal Server Error |
<!-- AUTO-SYNC:ANE-COMPARISON:END -->
+18 -15
View File
@@ -1,28 +1,31 @@
# Roadmap v2
Roadmap courte et concrete, alignee sur l'etat reel du repo.
Roadmap courte et concrete, alignee sur l'etat reel du repo au 13 mars 2026.
## Priorite 1 - Passer au moins un modele jusqu'a `gate`
## Priorite 1 - Stabiliser la reprise
- compacter `rewrite` pour qu'au moins un modele atteigne `gate`
- conserver la boucle `repair` et le garde-fou comme blocages durs
- viser d'abord `apple-coreml:qwen3.5-4b-onnx-q4f16` et `ollama:qwen2.5:7b`
- garder `automation/state/next_lots_state.json` sans checkpoint ambigu
- garder `:8100` et `:8201` operationnels, puis retablir un vrai chemin Ollama CPU avant de rejouer `priority_models`
- garder `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference provisoire tant qu'un rerun comparable ne la contredit pas
- traiter le rerun `automation/reports/apple_rerun_7oY51o` comme une alerte de stabilite: la reference Apple n'est pas encore reconfirmee
- ne plus laisser les docs perdre un resultat `accepted` lorsqu'un lot partiel plus recent tourne ensuite
## Priorite 2 - Requalifier les modeles plus lourds
## Priorite 2 - Faire tomber les blockers reels
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse
- rejouer `qwen3.5:9b` seulement si `qwen2.5:7b` termine un smoke complet
- maintenir les modeles toujours explicites dans les smokes et la doc
- tenir compte du fait que le runtime Apple local ne sert qu'un `model_id` a la fois
- sortir `ollama:qwen2.5:7b` de `outline_like`
- faire disparaitre `truncated_ending` sur au moins une baseline
- limiter les changements a `rewrite`, `repair` et leurs budgets tant qu'aucun autre blocker n'apparait
## Priorite 3 - Exploitation locale et docs
## Priorite 3 - Resserer la matrice locale
- runbook local ANE centre sur `rewrite`, `gate_v1.json`, `repair_vN.md` et `quality_blocked`
- runbook Apple local cote `mascarade` aligne sur les statuts reels
- README et suivi croises pointent vers `EXECUTION_PLAN_2026-03-08.md`
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse ou regression
- garder `stateful-mistral7b-instruct-int4-coreml` hors chemin critique tant qu'un besoin produit n'impose pas son retour
- maintenir les modeles et le runtime Apple explicites dans chaque smoke et chaque doc
## Source de verite
- contexte courant: [`CONTEXTE_PROJET_2026-03-14.md`](./CONTEXTE_PROJET_2026-03-14.md)
- memoire de reprise: [`MEMOIRE_REPRISE_2026-03-14.md`](./MEMOIRE_REPRISE_2026-03-14.md)
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
- etat livre: [`../TODO_IMPLEMENTE.md`](../TODO_IMPLEMENTE.md)
- ordre d'execution: [`EXECUTION_PLAN_2026-03-08.md`](./EXECUTION_PLAN_2026-03-08.md)
- ordre d'execution: [`EXECUTION_PLAN_2026-03-14.md`](./EXECUTION_PLAN_2026-03-14.md)
+10 -10
View File
@@ -42,8 +42,10 @@ Le driver:
- dernier cycle complet termine au 9 mars 2026:
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted` de bout en bout sous garde-fou
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live, puis finit `quality_blocked` sur `outline_like`
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste `preflight_only`
- le lot `baselines` pour `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` est rejoue separement
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` est retire du chemin critique et garde comme reference experimentale
- rerun comparable du 13 mars 2026:
- `automation/reports/apple_rerun_preset_20260313T223555Z` est `accepted` sans `repair` et reconfirme la reference Apple locale
- le lot `baselines` pour `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` a ete rejoue separement; les deux modeles restent `quality_blocked` sur `truncated_ending`
- le runtime Apple local ne sert qu'un seul `model_id` a la fois
- le fallback `repair` n'essaie plus de changer de modele `apple-coreml` en plein smoke; tout switch Apple reste une action runtime explicite
@@ -73,8 +75,8 @@ Notes:
- `ANE_MAX_TOKENS_GATE` permet de regler le budget du garde-fou LLM
- `ANE_MAX_TOKENS_REPAIR` et `ANE_REPAIR_MAX_PASSES` reglent la boucle `repair`
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` reste le candidat vitesse Apple a requalifier en baseline
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est la reference Apple locale actuelle
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste preflight-only sur cette machine: il repond, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est la reference Apple locale actuelle, reconfirmee par `apple_rerun_preset_20260313T223555Z`
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste archive hors lot utile sur cette machine: il repond, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes
## Smoke Ollama
@@ -87,7 +89,7 @@ bash /Users/electron/mascarade/scripts/smoke_openai_compat_ane.sh \
```
Le provider `ollama` doit apparaitre dans `providers` et le modele cible doit etre deja installe.
Sur cette machine, le meilleur candidat Ollama courant est `ollama:qwen2.5:7b`; `qwen2.5:1.5b` reste une baseline a rerun.
Sur cette machine, le meilleur candidat Ollama courant est `ollama:qwen2.5:7b`; `qwen2.5:1.5b` reste une baseline de regression, pas un candidat qualite.
Smoke chapitre complet:
@@ -131,12 +133,10 @@ Champs utiles:
- `provider.base_url`
- `provider.model`
## Etat auto-synchronise
## Etat auto-synchronise
<!-- AUTO-SYNC:ANE-RUNBOOK:START -->
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
- chapitre courant detecte: chapitre_01
- reference locale actuelle: aucun accepted, meilleur diagnostic: apple-coreml:qwen2.5-0.5b-instruct-onnx
- prochain lot utile: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
- reprise attendue apres action manuelle: /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json
- reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16
- prochain lot utile: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
<!-- AUTO-SYNC:ANE-RUNBOOK:END -->
+2 -1
View File
@@ -16,11 +16,12 @@ Consignes:
- répondre uniquement avec le chapitre en Markdown
- produire uniquement de la prose narrative continue, sous forme de paragraphes
- ne jamais recopier la structure sous forme de plan
- interdit: titres Markdown (`#`, `##`, `###`), listes a puces, numerotations, labels `objectif`, `conflit`, `sortie`, section `Tension`, section `Scènes`, code fences
- interdit: titres Markdown (`#`, `##`, `###`), listes a puces, numerotations, labels `objectif`, `conflit`, `sortie`, section `Tension`, section `Scènes`, code fences, titre `Chapitre XX`
- garder une voix cohérente
- matérialiser la tension annoncée
- ouvrir directement dans l'action ou dans la scene, sans preambule meta
- transformer chaque beat de structure en action, perception, decision, consequence et, si utile, dialogue
- ne pas commenter la structure ni annoncer les scenes; les jouer directement
- finir sur une vraie phrase complete avec une ponctuation finale
- viser un chapitre bref mais complet, d'au moins 3 paragraphes substantiels
- ne pas ajouter d'explication hors texte narratif
+2
View File
@@ -4,6 +4,8 @@ Ne mets aucun texte avant ou après le JSON.
Ne mets aucun bloc Markdown.
Le but est de decider si ce texte peut etre promu dans le manuscrit.
Bloque si le texte ressemble encore a un plan, s'il semble coupe avant sa fin, ou s'il manque une vraie continuite narrative.
Ne bloque pas `outline_like` si le texte reste en prose continue, sans titres, sans puces et sans labels visibles, meme s'il est simple ou sobre.
Utilise `outline_like` seulement s'il reste des traces visibles de plan, de checklist, de resume de scene, d'intitules structurants ou d'ecriture meta.
Limite-toi a 1 phrase de resume, 4 blockers max et 4 recommandations max.
Chapitre cible: $chapter_slug
+5 -1
View File
@@ -23,10 +23,14 @@ $draft_markdown
Consignes impératives:
- répondre uniquement avec la nouvelle version du chapitre en Markdown
- produire uniquement de la prose narrative continue en paragraphes
- ne garder aucun titre, aucune puce, aucune numérotation, aucun label de plan visible
- ne garder aucun titre, aucune puce, aucune numérotation, aucun code fence, aucun titre `Chapitre XX`, aucun label de plan visible
- supprimer completement les mots `objectif`, `conflit`, `sortie`, `Scène`, `scene`, `Tension` s'ils apparaissent comme labels ou sous-titres
- transformer toute structure, note ou checklist en scene(s) jouee(s) avec actions, perceptions et consequences
- conserver l'intention et les informations utiles deja presentes
- allonger si besoin pour obtenir une scene complete et continue
- montrer la scene au lieu de la resumer depuis l'exterieur
- chaque paragraphe doit produire une action, une reaction ou une consequence concrete
- finir le dernier paragraphe sur une decision nette suivie d'une consequence immediate
$repair_focus
- finir obligatoirement sur une vraie phrase complete avec une ponctuation finale
- ne rien ajouter avant ou apres le chapitre
+3 -2
View File
@@ -18,10 +18,11 @@ $critique_json
Consignes de réécriture:
- répondre uniquement avec la version réécrite du chapitre en Markdown
- produire uniquement de la prose narrative continue, sous forme de paragraphes
- supprimer completement tout titre, toute puce, toute numérotation et tout label de plan
- supprimer completement tout titre, toute puce, toute numérotation, tout code fence et tout label de plan
- si le brouillon ressemble a une structure ou a des notes, le convertir integralement en scene(s) racontee(s)
- ne jamais garder les termes `objectif`, `conflit`, `sortie`, `Tension`, `Scène` comme titres ou labels visibles
- ne jamais garder les termes `objectif`, `conflit`, `sortie`, `Tension`, `Scène`, `Chapitre` comme titres ou labels visibles
- conserver l'intention, mais augmenter la continuite dramatique d'une scene a l'autre
- materialiser les decisions et leurs consequences
- ne pas resumer la scene depuis l'exterieur; montrer les gestes, les perceptions, les hesitations et les consequences dans le fil du texte
- finir sur une vraie phrase complete avec une ponctuation finale
- ne rien ajouter avant ou apres le chapitre
+39 -1
View File
@@ -313,6 +313,44 @@ class GenerationPipelineTests(unittest.TestCase):
self.assertEqual(meta["artifacts"]["repair_latest"], str(self.root / "brouillons" / "chapitres" / "chapitre_01" / "repair_v1.md"))
self.assertEqual(meta["draft_final"], str(self.root / "brouillons" / "chapitres" / "chapitre_01" / "repair_v1.md"))
def test_rewrite_strips_code_fences_and_chapter_title_before_promotion(self):
rewritten = f"```markdown\n# Chapitre 01\n\n{self._narrative_text().strip()}\n```"
provider = MockGenerationProvider(
{
"structure": "# Structure — chapitre_01\n\n## Objectif dramatique\nPoser une menace.\n",
"draft": "# Chapitre 01\n\nUn premier jet tendu.\n",
"critique": {
"summary": "Le brouillon doit etre reraconte proprement.",
"rewrite_required": True,
"deviations": ["Le texte garde un habillage markdown inutile."],
"recommendations": ["Supprimer les marqueurs de presentation."],
},
"rewrite": rewritten,
"gate": {
"ready_for_manuscript": True,
"summary": "Le chapitre peut etre promu.",
"blockers": [],
"recommendations": [],
"heuristic_blockers": [],
},
"memory": {
"summary": "Le chapitre garde sa tension sans habillage markdown.",
"characters": [{"name": "Ariane", "description": "Traverse la scene sans filtre meta."}],
"locations": [{"name": "Port-Vieux", "description": "Reste nocturne et menaçant."}],
"timeline_events": [{"event": "Ariane avance sans retour en arriere.", "order_hint": "nuit"}],
},
}
)
pipeline = GenerationPipeline(self.root, provider=provider)
outcome = pipeline.generate_chapter("01", approval_callback=lambda _report, _path: True)
self.assertTrue(outcome.accepted)
manuscript_text = outcome.manuscript_path.read_text(encoding="utf-8")
self.assertNotIn("```", manuscript_text)
self.assertNotIn("# Chapitre 01", manuscript_text)
self.assertTrue(manuscript_text.startswith("Ariane longe le quai"))
def test_truncated_ending_triggers_repair_before_promotion(self):
provider = MockGenerationProvider(
{
@@ -919,7 +957,7 @@ class ProviderConfigTests(unittest.TestCase):
)
self.assertEqual(
pipeline._repair_model_for_attempt(apple_provider, 2),
"ollama:qwen2.5:7b",
"apple-coreml:qwen2.5-0.5b-instruct-onnx",
)
def test_repair_fallback_override_env_wins(self):
+349 -1
View File
@@ -13,6 +13,7 @@ from core.next_lots import (
ModelRunResult,
NextLotsRunner,
RunState,
_default_command_runner,
replace_auto_section,
)
@@ -49,7 +50,7 @@ class NextLotsTests(unittest.TestCase):
f"mascarade_repo = \"{self.mascarade}\"\n"
"core_base_url = \"http://127.0.0.1:8100\"\n"
"apple_runtime_url = \"http://127.0.0.1:8201\"\n"
"ollama_tags_url = \"http://127.0.0.1:11435/api/tags\"\n\n"
"ollama_tags_url = \"http://127.0.0.1:11434/api/tags\"\n\n"
"apple_model_ready_timeout_seconds = 0\n"
"apple_model_poll_interval_seconds = 0.01\n\n"
"[smoke]\n"
@@ -97,6 +98,8 @@ class NextLotsTests(unittest.TestCase):
self.assertEqual(manifest.tracking.mascarade_repo, self.mascarade)
self.assertEqual(manifest.tracking.ane_todo_active, self.root / "TODO_ACTIVE.md")
self.assertEqual(manifest.apple_model_ready_timeout_seconds, 0)
self.assertEqual(manifest.ollama_runtime, "native")
self.assertEqual(manifest.ollama_openai_base_url, "http://127.0.0.1:8100")
def test_replace_auto_section_only_replaces_managed_block(self) -> None:
path = self.root / "TODO_ACTIVE.md"
@@ -119,6 +122,33 @@ class NextLotsTests(unittest.TestCase):
self.assertIn("- nouveau", rendered)
self.assertNotIn("ancien", rendered)
def test_replace_auto_section_deduplicates_repeated_heading(self) -> None:
path = self.root / "README.md"
path.write_text(
"## Etat auto-synchronise\n"
"## Etat auto-synchronise\n"
"<!-- AUTO-SYNC:ANE-README:START -->\n"
"ancien\n"
"<!-- AUTO-SYNC:ANE-README:END -->\n",
encoding="utf-8",
)
replace_auto_section(path, "ANE-README", "## Etat auto-synchronise", "- propre")
rendered = path.read_text(encoding="utf-8")
self.assertEqual(rendered.count("## Etat auto-synchronise\n"), 1)
self.assertIn("- propre", rendered)
def test_default_command_runner_returns_timeout_result(self) -> None:
result = _default_command_runner(
["python3", "-c", "import time; time.sleep(1)"],
self.root,
timeout_seconds=0.1,
)
self.assertEqual(result.returncode, 124)
self.assertIn("Timed out after 0.1s.", result.stderr)
def test_runner_creates_checkpoint_when_apple_model_differs(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
prepare_calls: list[list[str]] = []
@@ -228,6 +258,13 @@ class NextLotsTests(unittest.TestCase):
command_runner=command_runner,
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
)
runner._run_ollama_native_preflight = lambda model: CommandResult( # type: ignore[method-assign]
args=["ollama-native-preflight", model],
returncode=0,
stdout='{"content": "ollama native preflight ok"}',
stderr="",
duration_seconds=0.1,
)
report_dir = self.root / "automation" / "reports" / "test"
report_dir.mkdir(parents=True, exist_ok=True)
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
@@ -236,6 +273,99 @@ class NextLotsTests(unittest.TestCase):
self.assertEqual(result.repair_attempts, 1)
self.assertIn("gate", result.completed_stages)
def test_run_model_short_circuits_when_ollama_native_preflight_fails(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
command_calls: list[list[str]] = []
def command_runner(args: list[str], cwd: Path, env=None) -> CommandResult:
command_calls.append(args)
raise AssertionError("OpenAI-compatible preflight should not run when native Ollama preflight fails")
runner = NextLotsRunner(
manifest,
command_runner=command_runner,
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
)
runner._run_ollama_native_preflight = lambda model: CommandResult( # type: ignore[method-assign]
args=["ollama-native-preflight", model],
returncode=1,
stdout="",
stderr="HTTP 500 Internal Server Error",
duration_seconds=0.3,
)
report_dir = self.root / "automation" / "reports" / "native_fail"
report_dir.mkdir(parents=True, exist_ok=True)
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
self.assertEqual(command_calls, [])
self.assertEqual(result.classification, "provider_failed")
self.assertEqual(result.status, "ollama_runtime_unhealthy")
self.assertFalse(result.preflight_ok)
self.assertIn("Le preflight Ollama natif a échoué.", result.notes)
self.assertIn("HTTP 500 Internal Server Error", result.notes)
self.assertIsNotNone(result.preflight_log)
self.assertTrue(Path(result.preflight_log).exists())
def test_run_model_can_use_openai_compatible_ollama_runtime(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
manifest = Manifest(
**{
**manifest.__dict__,
"ollama_runtime": "openai_compatible",
"ollama_openai_base_url": "http://127.0.0.1:9100",
}
)
chapter = ChapterId.parse("02")
command_calls: list[list[str]] = []
def command_runner(args: list[str], cwd: Path, env=None) -> CommandResult:
command_calls.append(args)
if "smoke_openai_compat_ane.sh" in " ".join(args):
self.assertEqual(args[args.index("--url") + 1], "http://127.0.0.1:9100")
return CommandResult(args=args, returncode=0, stdout="ok", stderr="", duration_seconds=0.2)
if "smoke_local_generation.sh" in " ".join(args):
self.assertEqual(args[args.index("--base-url") + 1], "http://127.0.0.1:9100")
workspace = Path(args[args.index("--workspace") + 1])
meta_path = workspace / "brouillons" / "chapitres" / chapter.slug / "meta.json"
meta_path.parent.mkdir(parents=True, exist_ok=True)
meta_path.write_text(
json.dumps(
{
"status": "accepted",
"accepted": True,
"completed_stages": ["structure", "draft", "critique", "rewrite", "gate", "memory"],
"artifacts": {
"draft_v2": str(meta_path.parent / "draft_v2.md"),
"gate_v1": str(meta_path.parent / "gate_v1.json"),
"manuscript": str(workspace / "manuscrit" / chapter.filename),
},
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
return CommandResult(args=args, returncode=0, stdout="smoke ok", stderr="", duration_seconds=1.1)
raise AssertionError(args)
runner = NextLotsRunner(
manifest,
command_runner=command_runner,
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
)
runner._run_ollama_native_preflight = lambda model: (_ for _ in ()).throw( # type: ignore[method-assign]
AssertionError("Le preflight Ollama natif ne doit pas être appelé en mode openai_compatible")
)
report_dir = self.root / "automation" / "reports" / "openai_runtime"
report_dir.mkdir(parents=True, exist_ok=True)
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
self.assertEqual(result.classification, "accepted")
self.assertEqual(len(command_calls), 2)
def test_tracking_sync_updates_docs_with_auto_sync_sections(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
runner = NextLotsRunner(
@@ -270,6 +400,224 @@ class NextLotsTests(unittest.TestCase):
self.assertIn("Compacter rewrite", (self.root / "docs" / "EXECUTION_PLAN_2026-03-08.md").read_text(encoding="utf-8"))
self.assertIn("ollama:qwen2.5:7b", (self.root / "docs" / "MODEL_COMPARISON_2026-03-08.md").read_text(encoding="utf-8"))
def test_tracking_sync_consolidates_latest_results_across_reports(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
runner = NextLotsRunner(
manifest,
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
json_fetcher=lambda url, timeout: {"status": "ok"},
)
previous_state = RunState.new(
manifest,
lot="priority_models",
report_dir=self.root / "automation" / "reports" / "20260309T055457Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "models", "name": "priority_models", "models": manifest.priority_models, "preflight_only": False}],
)
previous_state.updated_at = "2026-03-09T06:20:33+00:00"
previous_state.results = [
asdict(
ModelRunResult(
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
category="priority_models",
classification="accepted",
preflight_ok=True,
smoke_attempted=True,
status="accepted",
accepted=True,
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
)
)
]
previous_report_dir = Path(previous_state.report_dir)
previous_report_dir.mkdir(parents=True, exist_ok=True)
(previous_report_dir / "run.json").write_text(json.dumps(previous_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
current_state = RunState.new(
manifest,
lot="tracking_sync",
report_dir=self.root / "automation" / "reports" / "20260309T063512Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "tracking_sync"}],
)
current_state.updated_at = "2026-03-09T06:53:02+00:00"
current_state.results = [
asdict(
ModelRunResult(
model="apple-coreml:qwen2.5-0.5b-instruct-onnx",
category="baselines",
classification="quality_blocked",
preflight_ok=True,
smoke_attempted=True,
status="quality_blocked",
failed_stage="gate",
quality_blockers=["truncated_ending"],
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "repair"],
)
)
]
runner._sync_tracking(current_state, dry_run=False)
readme = (self.root / "README.md").read_text(encoding="utf-8")
comparison = (self.root / "docs" / "MODEL_COMPARISON_2026-03-08.md").read_text(encoding="utf-8")
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
self.assertIn("reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16", readme)
self.assertIn("apple-coreml:qwen3.5-4b-onnx-q4f16", comparison)
self.assertIn("apple-coreml:qwen2.5-0.5b-instruct-onnx", comparison)
self.assertIn("Confirmer la reference accepted puis resserrer rewrite/repair", todo_active)
def test_tracking_sync_marks_reference_reconfirmed_after_two_accepted_runs(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
runner = NextLotsRunner(
manifest,
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
json_fetcher=lambda url, timeout: {"status": "ok"},
)
first_state = RunState.new(
manifest,
lot="priority_models",
report_dir=self.root / "automation" / "reports" / "20260309T055457Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "models", "name": "priority_models", "models": manifest.priority_models, "preflight_only": False}],
)
first_state.updated_at = "2026-03-09T06:20:33+00:00"
first_state.results = [
asdict(
ModelRunResult(
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
category="priority_models",
classification="accepted",
preflight_ok=True,
smoke_attempted=True,
status="accepted",
accepted=True,
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
)
)
]
first_report_dir = Path(first_state.report_dir)
first_report_dir.mkdir(parents=True, exist_ok=True)
(first_report_dir / "run.json").write_text(json.dumps(first_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
second_state = RunState.new(
manifest,
lot="priority_models",
report_dir=self.root / "automation" / "reports" / "20260313T225017Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "models", "name": "priority_models", "models": ["apple-coreml:qwen3.5-4b-onnx-q4f16"], "preflight_only": False}],
)
second_state.updated_at = "2026-03-13T22:50:17+00:00"
second_state.results = [
asdict(
ModelRunResult(
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
category="priority_models",
classification="accepted",
preflight_ok=True,
smoke_attempted=True,
status="accepted",
accepted=True,
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
)
)
]
second_report_dir = Path(second_state.report_dir)
second_report_dir.mkdir(parents=True, exist_ok=True)
(second_report_dir / "run.json").write_text(json.dumps(second_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
current_state = RunState.new(
manifest,
lot="tracking_sync",
report_dir=self.root / "automation" / "reports" / "20260313T230000Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "tracking_sync"}],
)
current_state.updated_at = "2026-03-13T23:00:00+00:00"
current_state.results = [
asdict(
ModelRunResult(
model="ollama:qwen2.5:7b",
category="priority_models",
classification="quality_blocked",
preflight_ok=True,
smoke_attempted=True,
status="quality_blocked",
failed_stage="gate",
quality_blockers=["outline_like"],
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "repair"],
)
)
]
runner._sync_tracking(current_state, dry_run=False)
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
self.assertIn("Reference locale reconfirmee; resserrer rewrite/repair", todo_active)
def test_tracking_sync_prioritizes_runtime_fix_when_reference_reconfirmed_but_provider_failed(self) -> None:
manifest = Manifest.load(self.root, self.manifest_path)
runner = NextLotsRunner(
manifest,
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
json_fetcher=lambda url, timeout: {"status": "ok"},
)
for stamp in ("20260309T055457Z", "20260313T225017Z"):
accepted_state = RunState.new(
manifest,
lot="priority_models",
report_dir=self.root / "automation" / "reports" / stamp,
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "models", "name": "priority_models", "models": ["apple-coreml:qwen3.5-4b-onnx-q4f16"], "preflight_only": False}],
)
accepted_state.updated_at = "2026-03-13T22:50:17+00:00"
accepted_state.results = [
asdict(
ModelRunResult(
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
category="priority_models",
classification="accepted",
preflight_ok=True,
smoke_attempted=True,
status="accepted",
accepted=True,
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
)
)
]
report_dir = Path(accepted_state.report_dir)
report_dir.mkdir(parents=True, exist_ok=True)
(report_dir / "run.json").write_text(json.dumps(accepted_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
current_state = RunState.new(
manifest,
lot="tracking_sync",
report_dir=self.root / "automation" / "reports" / "20260314T000000Z",
state_path=self.root / "automation" / "state" / "next_lots_state.json",
steps=[{"type": "tracking_sync"}],
)
current_state.updated_at = "2026-03-14T00:00:00+00:00"
current_state.results = [
asdict(
ModelRunResult(
model="ollama:qwen2.5:7b",
category="priority_models",
classification="provider_failed",
preflight_ok=False,
status="ollama_runtime_unhealthy",
notes=["Le preflight Ollama natif a échoué."],
)
)
]
runner._sync_tracking(current_state, dry_run=False)
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
self.assertIn("retablir le runtime des modeles provider_failed", todo_active)
def asdict(result: ModelRunResult) -> dict[str, object]:
return {