#!/usr/bin/env python3 """ Script pour logger une session de daily check et mettre à jour les stats. Usage: python log_session.py --cards "CORE-001,ACTION-006,FUTURE-001" --scores "✅,⚠️,❌" OU en mode interactif: python log_session.py --interactive Options: --cards : Liste des IDs de cartes (séparées par virgules) --scores : Liste des scores ✅/⚠️/❌ (séparées par virgules) --interactive: Mode interactif qui demande les infos --dry-run : Test sans modifier les fichiers """ import os import re import argparse from datetime import datetime from pathlib import Path # Chemins relatifs au script SCRIPT_DIR = Path(__file__).parent BASE_DIR = SCRIPT_DIR.parent CARD_DB_PATH = BASE_DIR / "card_database.md" CARDS_DIR = BASE_DIR / "cards" SESSIONS_LOG = BASE_DIR / "daily_sessions.md" def parse_card_database(): """Parse le fichier card_database.md et retourne un dict {card_id: card_info}.""" with open(CARD_DB_PATH, 'r', encoding='utf-8') as f: content = f.read() # Trouver la table markdown table_pattern = r'\| ID \| Card \| Difficulty.*?\n\|---.*?\n((?:\|.*?\n)+)' match = re.search(table_pattern, content, re.DOTALL) if not match: raise ValueError("Impossible de trouver la table dans card_database.md") table_rows = match.group(1).strip().split('\n') cards = {} for row in table_rows: cols = [col.strip() for col in row.split('|')[1:-1]] if len(cols) < 8: continue card_id, card_file, difficulty, frequency, last_review, success_rate, times_failed, critical = cols cards[card_id] = { 'id': card_id, 'file': card_file, 'difficulty': difficulty, 'frequency': frequency, 'last_review': last_review, 'success_rate': success_rate, 'times_failed': int(times_failed) if times_failed.isdigit() else 0, 'critical': critical == '⚠️' } return cards def update_card_file(card_file, score, today_str): """Met à jour les stats dans le fichier de carte individuel.""" card_path = CARDS_DIR / card_file with open(card_path, 'r', encoding='utf-8') as f: content = f.read() # Update Last Review content = re.sub( r'\*\*Last Review\*\*: .*', f'**Last Review**: {today_str}', content ) # Update Times Failed if score == '❌': # Incrémenter Times Failed def increment_failed(match): current = int(match.group(1)) if match.group(1).isdigit() else 0 return f'**Times Failed**: {current + 1}' content = re.sub( r'\*\*Times Failed\*\*: (\d+)', increment_failed, content ) # Update Success Rate # Extraire le success rate actuel success_match = re.search(r'\*\*Success Rate\*\*: (.*)', content) if success_match: current_rate_str = success_match.group(1).strip() # Parse le success rate actuel if current_rate_str in ['N/A', 'N/A (nouveau)']: # Première review if score == '✅': new_rate = '100% (1/1)' elif score == '⚠️': new_rate = '50% (0.5/1)' else: # ❌ new_rate = '0% (0/1)' else: # Parse "X% (n/total)" ou juste "X%" rate_match = re.match(r'(\d+)%\s*\(?([\d.]+)/(\d+)\)?', current_rate_str) if rate_match: current_percent, successes, total = rate_match.groups() successes = float(successes) total = int(total) else: # Fallback: juste un pourcentage rate_match = re.match(r'(\d+)%', current_rate_str) if rate_match: current_percent = int(rate_match.group(1)) # Estimer successes/total basé sur le pourcentage successes = 1 if current_percent >= 50 else 0 total = 1 else: successes = 0 total = 0 # Ajouter la nouvelle review total += 1 if score == '✅': successes += 1 elif score == '⚠️': successes += 0.5 new_percent = int((successes / total) * 100) if total > 0 else 0 new_rate = f'{new_percent}% ({successes}/{total})' content = re.sub( r'\*\*Success Rate\*\*: .*', f'**Success Rate**: {new_rate}', content ) return content def update_card_database(cards_dict, today_str): """Met à jour la table dans card_database.md avec les nouvelles stats.""" with open(CARD_DB_PATH, 'r', encoding='utf-8') as f: content = f.read() # Pour chaque carte, update sa ligne dans la table for card_id, card_info in cards_dict.items(): # Trouver la ligne correspondante row_pattern = rf'\| {re.escape(card_id)} \| ([^\n]+)' match = re.search(row_pattern, content) if match: old_row = match.group(0) # Reconstruire la nouvelle ligne critical_symbol = '⚠️' if card_info['critical'] else '' new_row = ( f"| {card_id} | {card_info['file']} | {card_info['difficulty']} | " f"{card_info['frequency']} | {card_info['last_review']} | " f"{card_info['success_rate']} | {card_info['times_failed']} | {critical_symbol} |" ) content = content.replace(old_row, new_row) # Update "Dernière mise à jour" content = re.sub( r'\*\*Dernière mise à jour\*\* : \d{2}/\d{2}/\d{4}', f'**Dernière mise à jour** : {today_str}', content ) return content def calculate_streak(sessions_content): """Calcule le streak actuel basé sur le fichier daily_sessions.md.""" # Trouver toutes les sessions session_pattern = r'### (\d{4}-\d{2}-\d{2})' dates = re.findall(session_pattern, sessions_content) if not dates: return 1 # Première session # Convertir en datetime session_dates = [] for date_str in dates: try: session_dates.append(datetime.strptime(date_str, '%Y-%m-%d')) except ValueError: continue if not session_dates: return 1 # Trier par date décroissante session_dates.sort(reverse=True) today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) # Calculer le streak streak = 1 # Aujourd'hui compte last_date = today for session_date in session_dates: session_date = session_date.replace(hour=0, minute=0, second=0, microsecond=0) # Check si c'est le jour précédent if (last_date - session_date).days == 1: streak += 1 last_date = session_date elif session_date == last_date: # Même jour, ignorer continue else: # Gap détecté, arrêter break return streak def log_session(card_ids, scores, responses=None, feedback_notes=None, dry_run=False): """ Log une session complète et met à jour toutes les stats. Args: card_ids: Liste des IDs de cartes (ex: ['CORE-001', 'ACTION-006']) scores: Liste des scores (ex: ['✅', '⚠️', '❌']) responses: (Optionnel) Liste des réponses données feedback_notes: (Optionnel) Liste des feedbacks dry_run: Si True, affiche ce qui serait fait sans modifier les fichiers """ if len(card_ids) != len(scores): raise ValueError(f"Nombre de cartes ({len(card_ids)}) != nombre de scores ({len(scores)})") # Valider les scores valid_scores = ['✅', '⚠️', '❌'] for score in scores: if score not in valid_scores: raise ValueError(f"Score invalide: {score}. Utilise ✅, ⚠️ ou ❌") today = datetime.now() today_str = today.strftime('%d/%m/%Y') today_iso = today.strftime('%Y-%m-%d') # Parse card database cards_dict = parse_card_database() # Vérifier que toutes les cartes existent for card_id in card_ids: if card_id not in cards_dict: raise ValueError(f"Carte inconnue: {card_id}") print(f"📝 LOGGING SESSION - {today_str}") print("=" * 60) # Update chaque carte updated_cards = {} for i, (card_id, score) in enumerate(zip(card_ids, scores)): card = cards_dict[card_id] print(f"\n🎯 Carte {i+1}: {card_id} → {score}") # Update le fichier de carte individuel if not dry_run: updated_content = update_card_file(card['file'], score, today_str) card_path = CARDS_DIR / card['file'] with open(card_path, 'w', encoding='utf-8') as f: f.write(updated_content) print(f" ✅ Fichier {card['file']} mis à jour") else: print(f" [DRY RUN] Mettrait à jour {card['file']}") # Re-parse le fichier pour obtenir les nouvelles stats if not dry_run: with open(CARDS_DIR / card['file'], 'r', encoding='utf-8') as f: new_content = f.read() # Extraire les nouvelles stats success_match = re.search(r'\*\*Success Rate\*\*: (.*)', new_content) failed_match = re.search(r'\*\*Times Failed\*\*: (\d+)', new_content) card['success_rate'] = success_match.group(1).strip() if success_match else card['success_rate'] card['times_failed'] = int(failed_match.group(1)) if failed_match else card['times_failed'] card['last_review'] = today_str updated_cards[card_id] = card else: print(f" [DRY RUN] Stats seraient mises à jour") # Update card_database.md if not dry_run: db_content = update_card_database(updated_cards, today_str) with open(CARD_DB_PATH, 'w', encoding='utf-8') as f: f.write(db_content) print(f"\n✅ card_database.md mis à jour") else: print(f"\n[DRY RUN] card_database.md serait mis à jour") # Calculer le score total score_values = {'✅': 1, '⚠️': 0.5, '❌': 0} total_score = sum(score_values[s] for s in scores) max_score = len(scores) # Calculer le streak if not dry_run: with open(SESSIONS_LOG, 'r', encoding='utf-8') as f: sessions_content = f.read() streak = calculate_streak(sessions_content) else: streak = 1 # Créer l'entrée de log pour daily_sessions.md log_entry = f""" ### {today_iso} [Time Unknown] **Triggered by** : User (manual "daily check") **Duration** : ~5 minutes **Questions Asked** : """ for i, (card_id, score) in enumerate(zip(card_ids, scores), 1): card = cards_dict[card_id] # Charger la question depuis le fichier with open(CARDS_DIR / card['file'], 'r', encoding='utf-8') as f: content = f.read() question_match = re.search(r'## Question\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL) question = question_match.group(1).strip() if question_match else "Question non trouvée" response = responses[i-1] if responses and i-1 < len(responses) else "[Non enregistrée]" feedback = feedback_notes[i-1] if feedback_notes and i-1 < len(feedback_notes) else "[Généré automatiquement]" log_entry += f"""{i}. [{card_id}] {question} → Score: {score} - Ta réponse : "{response}" - Feedback : {feedback} """ log_entry += f"""**Total Score** : {total_score}/{max_score} **Streak** : {streak} jour{'s' if streak > 1 else ''} **Notes** : Session loggée automatiquement via log_session.py **Action Items** : - [À compléter manuellement si nécessaire] --- """ # Append au fichier de sessions if not dry_run: with open(SESSIONS_LOG, 'a', encoding='utf-8') as f: f.write(log_entry) print(f"✅ Session loggée dans daily_sessions.md") else: print(f"[DRY RUN] Session serait loggée dans daily_sessions.md") print("=" * 60) print(f"✅ SESSION COMPLÈTE") print(f"Score: {total_score}/{max_score}") print(f"Streak: {streak} jour{'s' if streak > 1 else ''}") return { 'score': f"{total_score}/{max_score}", 'streak': streak, 'cards_updated': len(card_ids) } def interactive_mode(): """Mode interactif pour logger une session.""" print("=" * 60) print("📝 MODE INTERACTIF - LOG SESSION") print("=" * 60) # Demander les IDs de cartes cards_input = input("\n🎯 IDs des cartes (séparées par virgules, ex: CORE-001,ACTION-006): ") card_ids = [c.strip() for c in cards_input.split(',')] # Demander les scores scores_input = input(f"📊 Scores pour {len(card_ids)} cartes (✅/⚠️/❌, séparés par virgules): ") scores = [s.strip() for s in scores_input.split(',')] if len(card_ids) != len(scores): print(f"❌ ERREUR: {len(card_ids)} cartes mais {len(scores)} scores") return # Confirm print(f"\n📋 RÉCAPITULATIF:") for card_id, score in zip(card_ids, scores): print(f" {card_id} → {score}") confirm = input("\n✅ Confirmer et logger ? (y/n): ") if confirm.lower() != 'y': print("❌ Annulé") return # Log log_session(card_ids, scores) def main(): parser = argparse.ArgumentParser(description='Log une session de daily check') parser.add_argument('--cards', type=str, help='IDs de cartes séparées par virgules') parser.add_argument('--scores', type=str, help='Scores (✅/⚠️/❌) séparés par virgules') parser.add_argument('--interactive', action='store_true', help='Mode interactif') parser.add_argument('--dry-run', action='store_true', help='Test sans modifier les fichiers') args = parser.parse_args() try: if args.interactive: interactive_mode() elif args.cards and args.scores: card_ids = [c.strip() for c in args.cards.split(',')] scores = [s.strip() for s in args.scores.split(',')] log_session(card_ids, scores, dry_run=args.dry_run) else: parser.print_help() print("\n💡 Astuce: Utilise --interactive pour un usage facile") except Exception as e: print(f"❌ ERREUR: {e}") import traceback traceback.print_exc() import sys sys.exit(1) if __name__ == '__main__': import sys main()