Skip to content

Latest commit

 

History

History
378 lines (279 loc) · 14 KB

File metadata and controls

378 lines (279 loc) · 14 KB

Guidelines — Hypostasia V3

Regles d'architecture, conventions de code et patterns obligatoires. Ce document complete CLAUDE.md (specification stricte) avec les details pratiques d'implementation.


1. Architecture des apps Django

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

Separation des responsabilites

  • core/views.py ne sert que l'extension navigateur : list() et create() sur PageViewSet, plus test_sidebar_view. Aucun template de page complete.
  • front/views.py gere toute l'interface utilisateur web : arbre de dossiers, lecture, extractions manuelles/IA, configuration IA.
  • Les deux apps partagent les modeles de core mais jamais les vues.

Routing

/                          → 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)

2. Skill obligatoire : stack-ccc

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.

2.1 ViewSets explicites

# 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.

2.2 Validation par DRF Serializers

# 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 / FORBIDDEN

Regle : Jamais de forms.Form ou forms.ModelForm. Toute validation passe par serializers.Serializer.

2.3 Noms de variables verbeux

# 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()

2.4 Commentaires bilingues FR/EN

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()

2.5 HTMX pour toute interactivite

  • Les ViewSets du front renvoient des partials HTML, jamais du JSON pour l'UI.
  • Les actions custom (@action) renvoient du HTML via render() ou HttpResponse().
  • Les mises a jour multi-zones utilisent le pattern OOB swap (hx-swap-oob).
  • Le CSRF token est transmis via hx-headers sur 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>

2.6 Anti-patterns a eviter

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

3. Routing DRF avec DefaultRouter

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/

4. Templates front

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).


5. Commandes

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_demo

6. CSS

Le 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).


7. Celery et traitement asynchrone

Les taches longues (transcription audio) sont traitees en asynchrone via Celery.

Infrastructure

  • Broker : Redis (redis://localhost:6379/0)
  • Backend : django-db (via django-celery-results)
  • Config : hypostasia/celery.py + namespace CELERY_ dans settings.py
  • Taches : front/tasks.py

Pattern pour les taches Celery

# 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
    # ...

Import audio : flux complet

  1. ImportViewSet.fichier() detecte un fichier audio via est_fichier_audio()
  2. Sauvegarde le fichier dans AUDIO_TEMP_DIR (nom UUID)
  3. Cree une Page en status processing + un TranscriptionJob
  4. Lance transcrire_audio_task.delay(job_id, chemin)
  5. Retourne transcription_en_cours.html (polling HTMX toutes les 3s)
  6. ImportViewSet.status() est appele en polling, retourne le resultat quand termine

Supervisord (Docker)

En production, supervisord.conf gere gunicorn + celery worker dans un seul conteneur. Le start.sh fait les migrations puis lance supervisord.

Surcharges LangExtract (dette technique)

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 au Resolver.resolve(). Sans ce fix, les extractions sont silencieusement perdues (resolve retourne 0 extractions avec suppress_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.


7b. WebSocket + OOB : patterns et pieges

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.

Architecture du flux temps reel

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

Pattern valide : hx-trigger="load" dans un OOB swap

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)

Piege 1 : MutationObserver ne detecte pas les OOB swaps

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.

Piege 2 : deux zones OOB qui se battent

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)

Piege 3 : template different pendant et apres

Utiliser un seul template pour l'etat "en cours" et l'etat "termine". La variable analyse_en_cours conditionne le bandeau (progression vs succes).

Consumer : 3 types de messages

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

8. Docker : environnement unique dev/prod

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)

Scripts

  • install.sh : idempotent — sync, mkdir, migrate, collectstatic, charger_fixtures_demo
  • start.sh : attend PostgreSQL → appelle install.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.

Cles API

Priorite : champ DB > variable d'environnement .env. Variables supportees : GOOGLE_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, MISTRAL_API_KEY.


9. Preferences utilisateur

  • 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.

10. Resume des regles

  1. viewsets.ViewSet explicite, jamais ModelViewSet
  2. DefaultRouter DRF, jamais path() manuel pour DRF
  3. serializers.Serializer DRF, jamais Django Forms
  4. HTMX pour toute interactivite, jamais de SPA
  5. Noms de variables verbeux et explicites
  6. Commentaires bilingues FR/EN
  7. core = API JSON extension, front = interface HTML HTMX
  8. uv run pour toutes les commandes
  9. Celery pour les taches longues (transcription audio), broker Redis
  10. Supervisord en Docker pour gerer gunicorn + celery worker
  11. front/views.py fait 3269 lignes — si une phase ajoute un ViewSet, envisager de le mettre dans un fichier separe (front/views/nouveau_viewset.py) et l'importer dans front/views.py