- Add CARD_SYSTEM_GUIDE.md: Complete documentation for card creation, management, and workflow - Add emotional_calibration.md card (COMM-001): New critical card for managing emotions during conflicts - Add Python automation scripts: - get_daily_cards.py: Intelligent card selection based on priority scoring - log_session.py: Automated session logging and stats updates - Update CLAUDE.md: New protocol using automated scripts instead of manual selection - Update card_database.md: Restructured with cleaner table format + new COMM category - Update all card review history: Automated stats from recent sessions - Update daily_sessions.md: Add automated session logs (3 new sessions from 2025-11-19) System improvements: - Centralized card database with individual card files - Automated priority calculation (Critical, Times Failed, Never reviewed, Overdue) - Full stats automation (no more manual updates) - Better spaced repetition logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
441 lines
14 KiB
Python
441 lines
14 KiB
Python
#!/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()
|