couple-repo/anki_tingting/scripts/log_session.py
StillHammer bdbe17a3a0 Enhance daily check system with automation and new cards
- 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>
2025-11-19 11:16:26 +08:00

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