Regles d'architecture, conventions de code et patterns obligatoires. Ce document complete
CLAUDE.md(specification stricte) avec les details pratiques d'implementation.
Le projet est organise en 3 apps avec des responsabilites distinctes :
| App | Responsabilite | Type de reponse |
|---|---|---|
core |
API JSON pour l'extension navigateur + modeles de donnees | JSON uniquement |
front |
Interface de lecture 3 colonnes (HTMX partials) | HTML uniquement |
hypostasis_extractor |
Pipeline LangExtract, analyseurs, tests LLM | HTML + JSON |
core/views.pyne sert que l'extension navigateur :list()etcreate()surPageViewSet, plustest_sidebar_view. Aucun template de page complete.front/views.pygere toute l'interface utilisateur web : arbre de dossiers, lecture, extractions manuelles/IA, configuration IA.- Les deux apps partagent les modeles de
coremais jamais les vues.
/ → front:bibliotheque (page racine 3 colonnes)
/arbre/ → front:ArbreViewSet (arbre de dossiers HTMX)
/lire/{id}/ → front:LectureViewSet (zone de lecture)
/lire/{id}/analyser/ → front:LectureViewSet.analyser (extraction IA)
/dossiers/ → front:DossierViewSet (CRUD dossiers)
/pages/{id}/classer/ → front:PageViewSet.classer (classer une page)
/extractions/manuelle/ → front:ExtractionViewSet.manuelle
/extractions/creer_manuelle/ → front:ExtractionViewSet.creer_manuelle
/extractions/editer/ → front:ExtractionViewSet.editer
/extractions/modifier/ → front:ExtractionViewSet.modifier
/config-ia/status/ → front:ConfigurationIAViewSet.status
/config-ia/toggle/ → front:ConfigurationIAViewSet.toggle
/import/fichier/ → front:ImportViewSet.fichier (import document ou audio)
/import/status/ → front:ImportViewSet.status (polling transcription audio)
/api/pages/ → core:PageViewSet (extension navigateur)
/api/test-sidebar/ → core:test_sidebar_view (extension sidebar)
/api/analyseurs/ → hypostasis_extractor (analyseurs)
Tout le code Django de ce projet suit le skill stack-ccc (voir skills/stack-ccc/SKILL.md). Ce skill impose des conventions strictes de lisibilite.
# OUI — ViewSet explicite avec requetes ecrites a la main
# YES — Explicit ViewSet with hand-written queries
class MonViewSet(viewsets.ViewSet):
def list(self, request):
tous_les_objets = MonModele.objects.all()
return render(request, "mon_template.html", {"objets": tous_les_objets})
# NON — ModelViewSet avec magie cachee
# NO — ModelViewSet with hidden magic
class MonViewSet(viewsets.ModelViewSet): # INTERDIT / FORBIDDEN
queryset = MonModele.objects.all()Regle : viewsets.ViewSet toujours, ModelViewSet jamais. Chaque requete ORM est ecrite explicitement dans la methode.
# OUI — Serializer DRF pour la validation
# YES — DRF Serializer for validation
serializer = MonSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
donnees = serializer.validated_data
# NON — Django Forms
# NO — Django Forms
form = MonForm(request.POST) # INTERDIT / FORBIDDENRegle : Jamais de forms.Form ou forms.ModelForm. Toute validation passe par serializers.Serializer.
# OUI — on comprend ce que c'est en lisant le nom
# YES — you understand what it is just by reading the name
toutes_les_entites_du_job = job_extraction.entities.all()
html_panneau_analyse = render_to_string("front/includes/panneau_analyse.html", contexte)
dernier_job_termine = ExtractionJob.objects.filter(page=page, status="completed").first()
# NON — abbreviations cryptiques
# NO — cryptic abbreviations
ents = job.entities.all()
html = render_to_string("t.html", ctx)
j = ExtractionJob.objects.filter(page=p, status="completed").first()Chaque bloc de logique a un commentaire en francais suivi de sa traduction anglaise :
# Recupere le dernier job d'extraction termine pour cette page
# / Retrieve the last completed extraction job for this page
dernier_job_termine = ExtractionJob.objects.filter(
page=page, status="completed",
).order_by("-created_at").first()- Les ViewSets du front renvoient des partials HTML, jamais du JSON pour l'UI.
- Les actions custom (
@action) renvoient du HTML viarender()ouHttpResponse(). - Les mises a jour multi-zones utilisent le pattern OOB swap (
hx-swap-oob). - Le CSRF token est transmis via
hx-headerssur le<body>.
<!-- Pattern OOB : mise a jour de 2 zones en une seule reponse HTMX -->
<!-- OOB pattern: update 2 zones in a single HTMX response -->
<div id="zone-principale">contenu principal</div>
<div id="zone-secondaire" hx-swap-oob="innerHTML:#zone-secondaire">
contenu secondaire mis a jour en meme temps
</div>| Interdit | Faire a la place |
|---|---|
ModelViewSet + get_queryset() |
ViewSet + requetes explicites |
| Django Forms | DRF Serializers |
| Reponses JSON pour l'UI | Partials HTML + HTMX |
| Comprehensions complexes | Boucles for avec noms verbeux |
| Decorateurs cachant la logique metier | Appels de methodes explicites |
@action qui renvoie du JSON pour l'UI |
@action qui renvoie du HTML |
Toutes les URLs sont generees par DefaultRouter. Jamais de path() manuel pour des vues DRF.
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r"lire", LectureViewSet, basename="lire")
router.register(r"dossiers", DossierViewSet, basename="dossier")
urlpatterns = [
path("", include(router.urls)),
]Les actions custom (@action) generent automatiquement leurs URLs :
@action(detail=True)→/lire/{pk}/analyser/@action(detail=False)→/extractions/manuelle/
front/templates/front/
├── bibliotheque.html # Page complete 3 colonnes (shell)
├── base.html # Page complete pour acces direct (F5)
└── includes/ # Partials HTMX
├── arbre_dossiers.html # Arbre de navigation
├── lecture_principale.html # Zone de lecture
├── panneau_analyse.html # Panneau droit (extractions)
├── extraction_results.html # Cartes d'extraction
├── extraction_manuelle_form.html # Formulaire extraction manuelle
├── config_ia_toggle.html # Toggle IA on/off
└── transcription_en_cours.html # Polling HTMX transcription audio
Les templates de core/ ne servent que l'extension navigateur (sidebar).
Toutes les commandes Django se lancent via uv run :
uv run python manage.py runserver 0.0.0.0:8123
uv run python manage.py migrate
uv run python manage.py check
# Worker Celery (requis pour la transcription audio)
uv run celery -A hypostasia worker --loglevel=info
# Charger les fixtures de demo (idempotent)
uv run python manage.py charger_fixtures_demoLe front utilise Tailwind CSS (via CDN, sera localise en PHASE-02). Les polices cibles sont Lora, B612, B612 Mono et Srisakdi (PHASE-02 + PHASE-11).
Les taches longues (transcription audio) sont traitees en asynchrone via Celery.
- Broker : Redis (
redis://localhost:6379/0) - Backend :
django-db(viadjango-celery-results) - Config :
hypostasia/celery.py+ namespaceCELERY_danssettings.py - Taches :
front/tasks.py
# OUI — @shared_task dans front/tasks.py
# YES — @shared_task in front/tasks.py
@shared_task(bind=True)
def ma_tache_longue(self, job_id, chemin_fichier):
# Imports Django dans le corps de la tache (pas en haut du fichier)
from core.models import MonModele
# ...ImportViewSet.fichier()detecte un fichier audio viaest_fichier_audio()- Sauvegarde le fichier dans
AUDIO_TEMP_DIR(nom UUID) - Cree une
Pageen statusprocessing+ unTranscriptionJob - Lance
transcrire_audio_task.delay(job_id, chemin) - Retourne
transcription_en_cours.html(polling HTMX toutes les 3s) ImportViewSet.status()est appele en polling, retourne le resultat quand termine
En production, supervisord.conf gere gunicorn + celery worker dans un seul conteneur.
Le start.sh fait les migrations puis lance supervisord.
Le pipeline d'extraction repose sur une sous-classe custom de Annotator de langextract
(AnnotateurAvecProgression dans front/tasks.py). Cette classe surcharge
_annotate_documents_single_pass() — une methode interne non documentee, couplee a
la v1.1.1 de la lib.
Workarounds actifs :
- Auto-wrap des tableaux JSON nus : certains LLM renvoient
[...]au lieu de{"extractions": [...]}. Le code detecte et wrappe automatiquement avant de passer auResolver.resolve(). Sans ce fix, les extractions sont silencieusement perdues (resolve retourne 0 extractions avecsuppress_parse_errors=True).
Risque : toute mise a jour de langextract peut casser AnnotateurAvecProgression.
Voir PLAN/LANGEXTRACT_OVERRIDES.md pour la procedure de verification a chaque montee
de version.
Le suivi de progression de l'analyse IA utilise Django Channels (WebSocket) avec des OOB swaps HTMX. Voici les patterns valides et les pieges a eviter.
Celery task (front/tasks.py)
↓ envoyer_progression_websocket()
NotificationConsumer (front/consumers.py)
↓ send(text_data=html_oob)
HTMX WS extension (htmx-ext-ws)
↓ applique les OOB swaps dans le DOM
Pour forcer HTMX a recharger une zone apres un signal WS, injecter un div
OOB contenant un hx-get avec hx-trigger="load" :
# Consumer envoie un fragment qui declenche un GET auto
# / Consumer sends a fragment that auto-triggers a GET
html_signal = (
f'<div id="signal-div" hx-swap-oob="innerHTML:#signal-div">'
f'<div hx-get="/lire/{page_id}/analyse_status/" '
f'hx-target="#zone-cible" hx-swap="innerHTML" '
f'hx-trigger="load"></div>'
f'</div>'
)
await self.send(text_data=html_signal)NE PAS utiliser un MutationObserver JS pour detecter un OOB swap HTMX-WS.
L'extension HTMX-WS applique les swaps sans declencher les MutationObserver.
Utiliser hx-trigger="load" dans le fragment OOB a la place.
Si le signal A met a jour #zone-complete (tout le drawer) et le signal B
met a jour #barre-progression (sous-zone du drawer), le signal A ecrase
le contenu de B. Solution : cibler des sous-zones distinctes.
Signal rafraichir_drawer → cible #drawer-cartes-liste (sous-zone cartes)
Signal analyse_progression → cible #barre-progression-analyse (sous-zone barre)
Signal analyse_terminee → cible #drawer-contenu (refresh complet final)
Utiliser un seul template pour l'etat "en cours" et l'etat "termine".
La variable analyse_en_cours conditionne le bandeau (progression vs succes).
| Message | Quand | Cible | Source |
|---|---|---|---|
analyse_progression |
Chaque chunk | #barre-progression-analyse (OOB template) |
front/tasks.py callback |
rafraichir_drawer |
Chaque chunk | #drawer-cartes-liste via ?cartes_only=1 |
front/tasks.py callback |
analyse_terminee |
Fin du job | #drawer-contenu (refresh complet) |
front/tasks.py fin |
Tout tourne dans Docker. Un seul docker-compose.yml gere dev et prod.
La variable DEBUG dans .env determine le comportement :
DEBUG=true (dev) |
DEBUG=false (prod) |
|
|---|---|---|
| Demarrage | sleep infinity |
start.sh (supervisord) |
| Serveur | runserver lance a la main |
Gunicorn (3 workers) |
| Celery | Lance a la main | Supervisord (2 workers) |
| Nginx | NGINX_CONF=dev.conf (proxy host) |
default.conf (proxy interne) |
install.sh: idempotent — sync, mkdir, migrate, collectstatic, charger_fixtures_demostart.sh: attend PostgreSQL → appelleinstall.sh→ lance supervisord
install.sh peut etre relance a chaque redemarrage sans risque : les fixtures
utilisent get_or_create, les migrations sautent celles deja appliquees.
Priorite : champ DB > variable d'environnement .env.
Variables supportees : GOOGLE_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, MISTRAL_API_KEY.
- Ne JAMAIS commit ni push. L'utilisateur gere lui-meme ses commits et ses push. Ne pas proposer de commit, ne pas lancer
git add/commit/push.
viewsets.ViewSetexplicite, jamaisModelViewSetDefaultRouterDRF, jamaispath()manuel pour DRFserializers.SerializerDRF, jamais Django Forms- HTMX pour toute interactivite, jamais de SPA
- Noms de variables verbeux et explicites
- Commentaires bilingues FR/EN
core= API JSON extension,front= interface HTML HTMXuv runpour toutes les commandes- Celery pour les taches longues (transcription audio), broker Redis
- Supervisord en Docker pour gerer gunicorn + celery worker
front/views.pyfait 3269 lignes — si une phase ajoute un ViewSet, envisager de le mettre dans un fichier separe (front/views/nouveau_viewset.py) et l'importer dansfront/views.py