diff --git a/00_README.md b/00_README.md index fb76d12..1478cbe 100644 --- a/00_README.md +++ b/00_README.md @@ -111,3 +111,21 @@ Cette variable constitue la **référence portable** vers le repo. > **Si ça a déjà coûté du temps une fois, > ça mérite d’être documenté pour ne jamais le reperdre.** + +--- + +## Sync IA (Claude + Codex) + +Pour synchroniser les instructions globales **et** les skills locaux (`skills/*/SKILL.md`) vers les emplacements attendus par Claude et Codex : + +```bash +bash "$LEADTECH/scripts/sync-ai-instructions.sh" +``` + +Effets attendus : + +- met à jour `~/.claude/CLAUDE.md` +- aligne `~/.codex/AGENTS.md` dessus (symlink) +- publie les skills locaux dans : + - `~/.claude/skills/` + - `~/.codex/skills/` diff --git a/95_a_capitaliser.md b/95_a_capitaliser.md index 49c095e..babf17b 100644 --- a/95_a_capitaliser.md +++ b/95_a_capitaliser.md @@ -193,6 +193,67 @@ Sans ce check, les erreurs 404/500 passent silencieusement si le service appelan --- +2026-03-28 — app-alexandrie + +FILE_UPDATE_PROPOSAL +Fichier cible : knowledge/frontend/risques/state.md + +Pourquoi : +Anti-pattern découvert en review 5.3 : `followIsLoading: boolean` global dans un store Zustand bloquait tous les boutons "Suivre" de l'annuaire simultanément. Un seul flag booléen ne peut pas représenter des états par entité. + +Proposition : + +## État de chargement par entité dans les stores Zustand — préférer un Set + +Quand plusieurs instances d'un même composant peuvent déclencher une action async en parallèle (ex: bouton "Suivre" sur chaque carte d'une liste), un flag booléen global `isLoading: boolean` est insuffisant — il désactive toutes les instances dès qu'une action est en cours. + +Pattern correct : + +```typescript +// État +followingInProgress: Set; // userId actuellement en cours de follow/unfollow + +// Selector +isFollowInProgress: (userId: string) => boolean; +// dans le store : (userId) => get().followingInProgress.has(userId) + +// Mutation +set((state) => { + const next = new Set(state.followingInProgress); + next.add(targetUserId); + return { followingInProgress: next }; +}); +// Après succès/erreur : next.delete(targetUserId) +``` + +Appliquer ce pattern pour toute action async dans une liste (like, bookmark, follow, reaction, etc.). + +--- + +2026-03-28 — app-alexandrie + +FILE_UPDATE_PROPOSAL +Fichier cible : knowledge/frontend/risques/state.md + +Pourquoi : +Review 5.3 : `followError` partagé entre les actions follow/unfollow et les erreurs de chargement des listes followers/followings. Un ancien `followError` de type "ALREADY_FOLLOWING" pouvait s'afficher comme "Erreur de chargement" dans l'écran followers. + +Proposition : + +## Séparer les erreurs d'action et les erreurs de liste dans les stores Zustand + +Quand un store gère à la fois des actions (mutations) et des listes (fetches paginés), ne pas partager la même clé d'erreur. Nommer explicitement : + +```typescript +followError: string | null; // erreur de followUser/unfollowUser +followersError: string | null; // erreur de fetchFollowers/loadMoreFollowers +followingsError: string | null; // erreur de fetchFollowings/loadMoreFollowings +``` + +Les écrans de liste doivent afficher leur propre erreur (`followersError`) et non l'erreur d'action globale (`followError`). + +--- + # Format attendu Chaque proposition doit suivre ce format : diff --git a/_projects.conf b/_projects.conf index 8e707ae..2bea1ff 100644 --- a/_projects.conf +++ b/_projects.conf @@ -50,4 +50,4 @@ # - automatiser certains audits inter-projets # # Les lignes commençant par # sont ignorées. -app-alexandrie|NestJS + Expo (React Native) + Prisma + pnpm monorepo|mindleaf|Epic 2 en préparation +app-alexandrie|NestJS + Expo (React Native) + Prisma + pnpm monorepo|mindleaf|Epic 6 en cours diff --git a/scripts/sync-ai-instructions.sh b/scripts/sync-ai-instructions.sh index c46365b..4cd00ca 100755 --- a/scripts/sync-ai-instructions.sh +++ b/scripts/sync-ai-instructions.sh @@ -12,6 +12,7 @@ SOURCE="$REPO_ROOT/_AI_INSTRUCTIONS.md" # --- Détection machine --- OS="$(uname -s)" CHANGED=0 +SKILLS_SOURCE_DIR="$REPO_ROOT/skills" generate_repo_claude() { local header="$1" @@ -57,6 +58,40 @@ ensure_symlink() { CHANGED=1 } +sync_skills_for_target() { + local target_root="$1" + local source_dir="$2" + local skill_dir + local skill_name + local target_link + + mkdir -p "$target_root" + [ -d "$source_dir" ] || return 0 + + for skill_dir in "$source_dir"/*; do + [ -d "$skill_dir" ] || continue + [ -f "$skill_dir/SKILL.md" ] || continue + + skill_name="$(basename "$skill_dir")" + target_link="$target_root/$skill_name" + + if [ -L "$target_link" ]; then + local current_target + current_target="$(readlink "$target_link")" + if [ "$current_target" = "$skill_dir" ] && [ -e "$target_link" ]; then + continue + fi + rm -f "$target_link" + elif [ -e "$target_link" ]; then + echo "WARN: skill déjà présent et non symlink, conservé: $target_link" >&2 + continue + fi + + ln -s "$skill_dir" "$target_link" + CHANGED=1 + done +} + CLAUDE_HEADER="# Instructions globales — Lead Tech Copilote Ce fichier est chargé automatiquement par Claude Code ou Codex à chaque session. @@ -65,6 +100,8 @@ Il constitue la porte d'entrée principale de la base de connaissance Lead_tech generate_repo_claude "$CLAUDE_HEADER" "$HOME/.claude/CLAUDE.md" ensure_symlink "$HOME/.claude/CLAUDE.md" "$HOME/.codex/AGENTS.md" +sync_skills_for_target "$HOME/.claude/skills" "$SKILLS_SOURCE_DIR" +sync_skills_for_target "$HOME/.codex/skills" "$SKILLS_SOURCE_DIR" if [ "$CHANGED" -eq 1 ]; then echo "Sync AI instructions (OS: $OS)" diff --git a/scripts/sync-projects-conf.sh b/scripts/sync-projects-conf.sh index 7f0b0c1..4c49c68 100755 --- a/scripts/sync-projects-conf.sh +++ b/scripts/sync-projects-conf.sh @@ -137,12 +137,124 @@ infer_scope() { esac } +infer_state() { + local root="$1" + local sprint_status_file="$root/_bmad-output/implementation-artifacts/sprint-status.yaml" + local stories_dir="$root/_bmad-output/implementation-artifacts/stories" + local first_story + local latest_story + local epic_num + + if [ -f "$sprint_status_file" ]; then + local in_dev="false" + local line trimmed key val story_epic + local active_epic="" active_ts="-1" + local prep_epic="" + local declared_in_progress_epic="" + local story_file story_ts + local -a matches=() + + while IFS= read -r line || [ -n "$line" ]; do + if [ "$line" = "development_status:" ]; then + in_dev="true" + continue + fi + + if [ "$in_dev" = "true" ] && [ -n "$(trim "$line")" ] && [[ ! "$line" =~ ^[[:space:]] ]]; then + in_dev="false" + fi + [ "$in_dev" = "true" ] || continue + + trimmed="$(trim "$line")" + [[ "$trimmed" == *:* ]] || continue + key="$(trim "${trimmed%%:*}")" + val="$(trim "${trimmed#*:}")" + + if [[ "$key" =~ ^epic-([0-9]+)$ ]]; then + story_epic="${BASH_REMATCH[1]}" + if [ "$val" = "in-progress" ]; then + if [ -z "$declared_in_progress_epic" ] || [ "$story_epic" -lt "$declared_in_progress_epic" ]; then + declared_in_progress_epic="$story_epic" + fi + fi + continue + fi + + if [[ "$key" =~ ^([0-9]+)-[0-9]+[a-z]?-.*$ ]]; then + story_epic="${BASH_REMATCH[1]}" + if [ "$val" = "in-progress" ] || [ "$val" = "review" ]; then + matches=() + shopt -s nullglob + matches=( "$stories_dir/epic-$story_epic/${key}"*.md ) + shopt -u nullglob + story_ts="0" + if [ ${#matches[@]} -gt 0 ]; then + story_file="${matches[0]}" + story_ts="$(stat -c %Y "$story_file" 2>/dev/null || printf '0')" + fi + if [ "$story_ts" -gt "$active_ts" ] || { [ "$story_ts" -eq "$active_ts" ] && { [ -z "$active_epic" ] || [ "$story_epic" -gt "$active_epic" ]; }; }; then + active_ts="$story_ts" + active_epic="$story_epic" + fi + elif [ "$val" = "ready-for-dev" ] || [ "$val" = "backlog" ]; then + if [ -z "$prep_epic" ] || [ "$story_epic" -lt "$prep_epic" ]; then + prep_epic="$story_epic" + fi + fi + fi + done < "$sprint_status_file" + + if [ -n "$active_epic" ]; then + printf 'Epic %s en cours' "$active_epic" + return + fi + if [ -n "$prep_epic" ]; then + printf 'Epic %s en préparation' "$prep_epic" + return + fi + if [ -n "$declared_in_progress_epic" ]; then + printf 'Epic %s en préparation' "$declared_in_progress_epic" + return + fi + fi + + if [ -d "$stories_dir" ]; then + first_story="$(find "$stories_dir" -maxdepth 2 -type f -name '*.md' \ + ! -name 'epic-*-retro*.md' -print -quit 2>/dev/null)" + if [ -n "$first_story" ]; then + latest_story="$(find "$stories_dir" -maxdepth 2 -type f -name '*.md' \ + ! -name 'epic-*-retro*.md' -print 2>/dev/null \ + | xargs ls -1t 2>/dev/null \ + | head -n 1)" + if [ -n "$latest_story" ]; then + epic_num="$(echo "$latest_story" | sed -n 's#.*\/epic-\([0-9][0-9]*\)\/.*#\1#p')" + if [ -n "$epic_num" ]; then + printf 'Epic %s en préparation' "$epic_num" + return + fi + fi + fi + fi + + printf '%s' "dev" +} + +is_progress_state() { + local state="$1" + [ -z "$state" ] && return 0 + case "$state" in + dev|active) return 0 ;; + esac + echo "$state" | grep -Eq '^Epic [0-9]+ en (préparation|cours)$' +} + PROJECT_NAME="${PROJECT_NAME:-$(basename "$PROJECT_ROOT")}" STACK_INFERRED="$(infer_stack "$PROJECT_ROOT")" SCOPE_INFERRED="$(infer_scope "$PROJECT_ROOT")" +STATE_INFERRED="$(infer_state "$PROJECT_ROOT")" STACK="${STACK_OVERRIDE:-$STACK_INFERRED}" SCOPE="${SCOPE_OVERRIDE:-$SCOPE_INFERRED}" -STATE="${STATE_OVERRIDE:-dev}" +STATE="${STATE_OVERRIDE:-$STATE_INFERRED}" if ! [[ "$SCOPE" =~ ^(perso|mindleaf|lab|archive)$ ]]; then echo "Erreur: scope invalide '$SCOPE' (attendu: perso|mindleaf|lab|archive)" >&2 @@ -183,6 +295,8 @@ EOF if [ -n "$STATE_OVERRIDE" ]; then TARGET_STATE="$STATE_OVERRIDE" + elif is_progress_state "$existing_state"; then + TARGET_STATE="$STATE_INFERRED" else TARGET_STATE="$existing_state" fi diff --git a/skills/capitalisation-triage/SKILL.md b/skills/capitalisation-triage/SKILL.md index 5fa4766..313349c 100644 --- a/skills/capitalisation-triage/SKILL.md +++ b/skills/capitalisation-triage/SKILL.md @@ -1,6 +1,13 @@ --- name: capitalisation-triage -description: Analyse et trie les propositions de `95_a_capitaliser.md` pour décider si elles doivent être intégrées dans la base globale `Lead_tech` ou déplacées vers le `CLAUDE.md` du projet source. Utiliser ce skill quand il faut: (1) détecter les doublons avec `knowledge/`, (2) filtrer ce qui est réellement global et réutilisable, (3) préparer des propositions d’intégration propres par fichier cible, et (4) router les apprentissages trop spécifiques vers la mémoire projet. +description: >- + Analyse et trie les propositions de `95_a_capitaliser.md` pour décider si + elles doivent être intégrées dans la base globale `Lead_tech` ou déplacées + vers le `CLAUDE.md` du projet source. Utiliser ce skill quand il faut : + (1) détecter les doublons avec `knowledge/`, (2) filtrer ce qui est + réellement global et réutilisable, (3) préparer des propositions + d’intégration propres par fichier cible, et (4) router les apprentissages + trop spécifiques vers la mémoire projet. --- # Objectif