Architecture (pour contributeurs)
Vue d'ensemble concise du codebase lore. Pour les directives de contribution, voir CONTRIBUTING.md à la racine du projet.
Structure du projet
cmd/ Commandes Cobra — un fichier par commande CLI (le "quoi")
internal/
domain/ Interfaces et types partagés — contrat entre packages (aucune dep interne)
config/ Cascade de configuration — pourquoi : système de surcharge à 5 niveaux pour la flexibilité
git/ Adaptateur Git — pourquoi : abstraire Git pour ne jamais shell-outer de façon non sécurisée
storage/ Stockage documents — pourquoi : Markdown est la source de vérité, tout en dérive
plain_reader.go — PlainCorpusStore pour le mode standalone (tout répertoire Markdown, sans front matter obligatoire)
workflow/ Réactif (hook) + proactif (lore new) — pourquoi : deux points d'entrée, même pipeline
generator/ Génération de documents — pourquoi : découpler le rendu de templates du stockage
angela/ Logique IA — pourquoi : maintenir l'IA séparée du core (opt-in, non obligatoire)
langdetect.go — détection de 24 langues (dont la syntaxe de tapes VHS)
vhs_signals.go — vérification croisée tape↔doc↔GIF↔commandes CLI
multipass.go — découpe les grands docs en sections pour le polissage séquentiel
preflight.go — estimation tokens/coût/timeout avant les appels API
postprocess.go — balisage auto des blocs de code, normalisation indentation Mermaid
ai/ Fournisseurs IA — pourquoi : interface-based, permuter Anthropic/OpenAI/Ollama librement
i18n/ Catalogues bilingues — pourquoi : EN/FR dès le départ, pas rajouté après
ui/ Interface terminal — pourquoi : pattern IOStreams (stderr=humain, stdout=machine)
engagement/ Milestones, prompt star — pourquoi : hooks comportementaux pour ancrer l'habitude de documentation
fileutil/ Écritures atomiques — pourquoi : .tmp + rename évite la corruption sur Ctrl+C
notify/ Notification IDE — pourquoi : les commits non-TTY ont besoin de visibilité
status/ Collecteur de santé — pourquoi : un seul endroit pour rassembler toutes les métriques
template/ Templates Go — pourquoi : stdlib, aucune dépendance à un moteur externe
.lore/
docs/ Le corpus — LA source de vérité. Supprimez tout le reste, reconstruisez depuis ici.
pending/ Commits différés — pourquoi : ne jamais perdre un commit, même sur Ctrl+C
store.db Index LKS — reconstructible. Si corrompu : lore doctor --rebuild-store
Flux de données
graph LR
A[git commit] --> B[hook post-commit]
B --> C{Decision Engine}
C -->|score ≥ 60| D[Questions complètes]
C -->|score 35-59| E[Questions réduites]
C -->|score < 15| F[Auto-skip]
D --> G[Moteur templates]
E --> G
G --> H[Pipeline générateur]
H --> I[Écriture atomique .lore/docs/]
I --> J[Mise à jour index]
En résumé :
commit → hook → Decision Engine → questions (si nécessaire)
→ template → générateur → écriture atomique → mise à jour index
Qu'est-ce que le LKS ?
Le LKS (Lore Knowledge Store) est la base de données SQLite dans .lore/store.db. C'est un index dérivé — une couche de recherche et de requêtes construite sur le corpus Markdown de .lore/docs/.
| Propriété | Valeur |
|---|---|
| Format | SQLite (.lore/store.db) |
| Reconstructible | Oui — lore doctor --rebuild-store reconstruit depuis .lore/docs/ |
| Ce qu'il stocke | Métadonnées, tags, associations commits, scope/branche |
| Pourquoi il existe | Requêtes rapides sans parser chaque fichier Markdown à chaque fois |
Le LKS n'est jamais la source de vérité. En cas de désaccord entre la base de données et les fichiers Markdown, les fichiers Markdown l'emportent. Traitez store.db comme un artefact de build.
Patterns clés
- Markdown = source de vérité — index, cache, LKS sont tous reconstructibles
- Écritures atomiques —
.tmp+os.Rename()évite la corruption - IOStreams —
stderrpour les humains,stdoutpour les machines - Zéro réseau implicite — l'IA est opt-in, tout fonctionne hors ligne
- Front-matter en tête — chaque document porte ses métadonnées YAML
Scores du Decision Engine
Le Decision Engine applique trois seuils pour déterminer combien de questions poser :
| Plage de score | Comportement | Seuil par défaut |
|---|---|---|
| ≥ 60 | Questions complètes (Quoi + Pourquoi + Alternatives + Impact) | threshold_full: 60 |
| 35 – 59 | Questions réduites (Quoi + Pourquoi uniquement) | threshold_reduced: 35 |
| 15 – 34 | Suggestion seulement | threshold_suggest: 15 |
| < 15 | Auto-skip — aucune question | — |
Les seuils sont configurables dans .lorerc. Voir Détection contextuelle pour les 7 signaux de scoring.
Comment étendre
Les patterns ci-dessous sont les chemins les plus courts dans le code quand on ajoute une fonctionnalité qui touche Angela, la couche IA ou les TUI. Ils sont pérennes — les noms de fichiers et signatures peuvent évoluer, mais la forme du point d'extension reste stable.
Ajouter une langue pour les blocs de code
Ajoutez une entrée au slice langRules dans internal/angela/langdetect.go :
{Language: "rust", Prefixes: []string{"fn ", "let ", "use ", "impl ", "pub ", "mod "}},
Prefixes: correspondance avecstrings.HasPrefix.Contains: correspondance avecstrings.Contains(priorité plus basse).- Positionnez
CaseInsensitive: truepour les langages type SQL. - En cas d'égalité, l'index de la première ligne-votante départage (déterministe entre OS).
Ajouter un fixer autofix
Dans internal/angela/autofix.go, implémentez l'interface fixer et ajoutez-le au slice safeFixer ou aggressiveFixer (le tier est l'intégralité du contrat) :
var myFixer fixer = func(content string, meta *domain.DocumentMeta) (string, []FixedItem, error) {
// retourner le contenu modifié, la liste des fixes appliqués, toute erreur
}
Toutes les écritures passent par fileutil.AtomicWrite ; un backup est écrit dans .lore/backups/ avant modification.
Ajouter un critère de score qualité
Dans internal/angela/score.go, étendez ScoreDocument() :
if yourCondition(content, meta) {
score.Total += N
score.Details = append(score.Details, ScoreDetail{
Category: "your-category", Points: N, MaxPoints: N, Present: true,
})
} else {
score.Missing = append(score.Missing, "Ajouter X pour améliorer Y")
}
Mettez à jour la constante maxScore et ajustez les seuils de notation si nécessaire. Le score a deux profils (scoreStrict pour decision/feature/bugfix/refactor, scoreFreeForm pour tout le reste) — choisissez celui qui correspond au type visé par votre critère.
Ajouter un persona
Dans internal/angela/persona.go, ajoutez au personaRegistry :
{
Name: "your-key",
DisplayName: "Nom affiché",
Icon: "🔧",
Expertise: "focus sur une ligne",
DraftDirective: "Instructions pour polish/draft...",
ReviewDirective: "Instructions pour review (cohérence corpus)...",
...
}
ReviewDirective est spécifique à review — il cadre la lentille du persona sur la cohérence inter-documents, pas sur le polish d'un seul document. Si le persona doit être boosté pour une audience, ajoutez à audiencePersonaBoosts :
"your-audience-keyword": {"your-key", "other-persona-key"},
Tous les champs de persona passent par sanitizeShortField / sanitizePromptContent avant d'atteindre le prompt LLM — tout nouveau champ doit être sanitisé de la même façon.
Ajouter une clé i18n pour l'UI Angela
- Ajoutez le champ dans
internal/i18n/messages_angela.go. - Ajoutez la chaîne EN dans
internal/i18n/catalog_en.go. - Ajoutez la chaîne FR dans
internal/i18n/catalog_fr.go. - Utilisez-la via
i18n.T().Angela.YourKey.
Les arguments de format doivent correspondre entre les catalogues EN et FR — le test de complétude par réflexion dans i18n_test.go attrape les désalignements.
Appeler Preflight depuis une nouvelle commande
pf := angela.Preflight(userContent, systemPrompt, cfg.AI.Model, maxTokens, timeout)
if pf.ShouldAbort {
return fmt.Errorf("interrompu : %s", pf.AbortReason)
}
for _, w := range pf.Warnings {
fmt.Fprintf(streams.Err, "⚠ %s\n", w)
}
if pf.EstimatedCost >= 0 {
fmt.Fprintf(streams.Err, "Coût : ~$%.4f\n", pf.EstimatedCost)
}
Les lookups coût/fenêtre-de-contexte/vitesse sont exposés via angela.ModelContextLimit, angela.ModelOutputSpeed, angela.ExpectedOutputTokens pour les appelants qui ont besoin des mêmes chiffres sans dupliquer la formule (voir cmd/angela_review_preview.go).
Étendre un TUI (Bubbletea)
Les trois TUIs (review_interactive.go, draft_interactive.go, polish_interactive.go) partagent des patterns dans internal/angela/tui_common.go :
IsTTYAvailable()— à appeler en entrée de tout TUI ; fallback mode texte quand false.isSafePath(filename)— obligatoire avant de lancer$EDITOR; rejette les chemins vides, absolus ou contenant...- Styles Lipgloss exportés (
TUIStyleTitle,TUIStyleDim,TUIStyleHelpKey,TUIStyleCursor,TUIStyleSpinner,TUIStyleError,TUIStyleWarning,TUIStyleInfo) — ne pas redéfinir par fichier.
Ajouter une action clavier est un seul case dans le handler Update() :
case "x":
m.applyMyAction(m.findings[m.cursor])
return m, nil
Suivi d'usage côté provider
Les nouveaux providers IA doivent implémenter à la fois domain.AIProvider et domain.UsageTracker :
type UsageTracker interface {
LastUsage() *AIUsage
}
LastUsage est protégé par un sync.Mutex dans les trois providers existants — les nouveaux providers doivent suivre le même pattern pour que la lecture des stats et l'appel provider ne puissent pas courser.
Choix architecturaux à connaître
| Choix | Pourquoi |
|---|---|
atomic.Bool pour le flag warned du spinner |
Lock-free, pattern set-once |
sync.Mutex pour lastUsage des providers |
Read/write simple, pas de contention |
Variadique configMaxTokens ...int |
Rétro-compatible, aucune rupture callsite |
unicode.IsLetter() dans sanitizeAudience |
Accents français dans les noms de fichiers |
| Post-processing après l'IA, pas dans le prompt | Déterministe, gratuit, l'IA oublie ~30% des règles en prompt |
| Cap de 200 caractères sur l'audience | Éviter le gonflement de prompt depuis l'input CLI |
os.Lstat pour la détection .lore/ |
Rejette les répertoires corpus symlinkés |
Registry persona via sync.Once |
Deep-copy à la lecture : une mutation appelant ne pollue pas le registre |
Comment contribuer
- Fork depuis
main - Écrire des tests (
go test ./...) - Exécuter
go vet ./... - Ouvrir une PR — voir le modèle dans
.github/PULL_REQUEST_TEMPLATE.md