Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .claude/hooks/verify-gate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* Hook Stop — CraftCode : porte de vérification.
*
* À chaque fin de tour, vérifie qu'aucune édition de code (frontend/ ou backend/)
* ne reste sans `npm run check` postérieur. Si c'est le cas, BLOQUE la clôture et
* renvoie à Claude la consigne de lancer le check du bon dossier. But : faire
* respecter par l'outil — et non par la vigilance de l'assistant — le principe
* « ne jamais affirmer 'c'est fait' sans avoir vérifié ». Cf. DECISIONS.md.
*
* Pendant Stop du hook PostToolUse summarize-change.mjs : l'un montre chaque
* changement, l'autre garantit qu'il a été vérifié.
*
* Signal objectif (pas d'analyse du langage naturel) : on repère dans le
* transcript la dernière exécution de `npm run check`, puis les dossiers de code
* édités APRÈS elle. S'il en reste, on bloque.
*
* Garde anti-boucle : si `stop_hook_active` est vrai (Claude poursuit déjà à
* cause d'un blocage de ce hook), on laisse passer.
*
* Ne lève jamais d'exception ni de code d'erreur : un hook qui plante ne doit
* jamais interrompre le travail (toute erreur ⇒ exit 0, la clôture passe).
*/
import { readFileSync } from 'node:fs';

const ROOT = '/Users/julien/Dev/CraftCode';

/** Dossiers de code soumis à la porte, avec la commande de vérification associée. */
const GATED = [
{ dir: 'frontend', command: 'cd frontend && npm run check' },
{ dir: 'backend', command: 'cd backend && npm run check' },
];

const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);

function readStdin() {
try {
return readFileSync(0, 'utf8');
} catch {
return '';
}
}

/** Dossier gardé (frontend|backend) d'un chemin de fichier, ou null. */
function gatedDirOf(filePath) {
if (typeof filePath !== 'string') return null;
// Normalise vers un chemin relatif à la racine du projet.
const rel = filePath.startsWith(ROOT + '/') ? filePath.slice(ROOT.length + 1) : filePath;
const match = GATED.find((g) => rel === g.dir || rel.startsWith(g.dir + '/'));
return match ? match.dir : null;
}

/** Une commande Bash est-elle un `npm run check` ? */
function isCheckCommand(command) {
return typeof command === 'string' && /npm run check/.test(command);
}

/**
* Parcourt le transcript (JSONL) dans l'ordre et renvoie l'ensemble des dossiers
* de code édités APRÈS le dernier `npm run check` (ou depuis le début si aucun).
*/
function dirsEditedSinceLastCheck(transcriptPath) {
const raw = readFileSync(transcriptPath, 'utf8');
const pending = new Set();

for (const line of raw.split('\n')) {
if (!line.trim()) continue;
let entry;
try {
entry = JSON.parse(line);
} catch {
continue; // ligne illisible : on ignore, on ne bloque pas pour autant
}

const content = entry?.message?.content;
if (!Array.isArray(content)) continue;

for (const block of content) {
if (block?.type !== 'tool_use') continue;

if (EDIT_TOOLS.has(block.name)) {
const dir = gatedDirOf(block?.input?.file_path);
if (dir) pending.add(dir);
} else if (block.name === 'Bash' && isCheckCommand(block?.input?.command)) {
// Un check est propre à un dossier (`cd frontend && npm run check`) : il ne
// dédouane que le(s) dossier(s) qu'il cible. S'il n'en cible aucun
// explicitement, on considère qu'il couvre tout (best-effort, évite de
// bloquer à tort).
const command = block.input.command;
const targeted = GATED.filter((g) => command.includes(g.dir));
if (targeted.length === 0) pending.clear();
else targeted.forEach((g) => pending.delete(g.dir));
}
}
}

return pending;
}

function main() {
let payload;
try {
payload = JSON.parse(readStdin());
} catch {
process.exit(0);
}

// Anti-boucle : ne pas re-bloquer une clôture déjà relancée par ce hook.
if (payload?.stop_hook_active) process.exit(0);

const transcriptPath = payload?.transcript_path;
if (!transcriptPath) process.exit(0);

let pending;
try {
pending = dirsEditedSinceLastCheck(transcriptPath);
} catch {
process.exit(0); // transcript illisible : on laisse passer
}

if (pending.size === 0) process.exit(0);

const commands = GATED.filter((g) => pending.has(g.dir))
.map((g) => ` ${g.command}`)
.join('\n');
const zones = [...pending].join(' et ');

const reason =
`Porte de vérification : des fichiers de ${zones} ont été modifiés sans ` +
`\`npm run check\` depuis. Avant de clore, lance la vérification du ou des ` +
`dossiers touchés et colle sa sortie :\n${commands}\n` +
`Si un test échoue, dis-le plutôt que d'affirmer que c'est fait.`;

process.stdout.write(JSON.stringify({ decision: 'block', reason }));
process.exit(0);
}

main();
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/verify-gate.mjs\""
}
]
}
]
}
}
39 changes: 39 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,45 @@ Entrées **antéchronologiques** (la plus récente en haut). La date au format `

---

## 2026-06-17 — Porte de vérification (hook Stop) avant clôture

- **Contexte** : le principe « ne jamais affirmer 'c'est fait' sans avoir lancé
`npm run check` » reposait uniquement sur la vigilance de l'assistant. Le hook
`summarize-change.mjs` rend les changements visibles, mais rien ne garantit
qu'ils ont été vérifiés.
- **Décision** : un hook `Stop` `verify-gate.mjs` qui, en fin de tour, repère
dans le transcript les éditions de code (`frontend/`/`backend/`) postérieures au
dernier `npm run check` et **bloque la clôture** (`decision: "block"`) tant
qu'il en reste, en renvoyant la commande exacte à lancer. Garde anti-boucle via
`stop_hook_active` ; toute erreur du hook ⇒ `exit 0` (ne bloque jamais le
travail). Version générique ajoutée au `claude-code-starter-kit/`.
- **Options écartées** : un simple rappel non bloquant (rejeté — repose encore sur
la vigilance, ce que l'idée vise à supprimer) ; détecter l'affirmation « c'est
fait » en langage naturel (rejeté — non fiable, on gate sur un signal objectif :
édition de code sans check postérieur).
- **Pourquoi** : faire respecter par l'outil la porte qualité (= la CI), pendant
`Stop` du résumé `PostToolUse` — l'un montre, l'autre vérifie.
- **Trace** : branche `main` (session du 2026-06-17).

## 2026-06-17 — Outil « Mettre en place Claude Code » + kit de démarrage

- **Contexte** : besoin de documenter, dans l'app elle-même, comment installer
Claude Code dans un projet (fichiers markdown, hooks, slash-commands & skills,
settings.json & MCP), et de fournir un point de départ réutilisable hors de
CraftCode.
- **Décision** : (1) un nouvel outil pédagogique `claude-code-setup` (hub +
détail, 4 sujets) suivant ADD-A-TOOL.md à la lettre, contenu statique dans
`core/data/` ; (2) un kit versionné `claude-code-starter-kit/` à la racine,
généralisé depuis le setup réel de CraftCode (hooks, commandes `/brief` et
`/add-module`, CLAUDE.md squelette, settings.json avec placeholder MCP par ENV).
- **Options écartées** : servir le contenu pédagogique via l'API/seed (rejeté —
le contenu enseigné reste statique côté front, seul le registre Tool passe par
la base, cf. STATE-AND-DATA.md) ; lier le kit aux chemins de CraftCode (rejeté
au profit d'un kit autoportant et générique).
- **Pourquoi** : rendre la mise en place de Claude Code apprenable dans l'app et
rejouable sur tout projet, sans secret ni dépendance au dépôt CraftCode.
- **Trace** : branche `main` (session du 2026-06-17).

## 2026-06-17 — Journal de décisions + résumé automatique des changements

- **Contexte** : les décisions prises en session avec Claude Code partaient trop vite,
Expand Down
61 changes: 44 additions & 17 deletions backend/seed/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ const tools = [
available: true,
order: 4,
},
{
slug: 'claude-code-setup',
name: 'Mettre en place Claude Code',
description:
'Les quatre briques pour installer Claude Code dans un projet : fichiers markdown, hooks, slash-commands & skills, settings.json & serveurs MCP.',
icon: 'terminal',
route: '/claude-code-setup',
available: true,
order: 5,
},
];

const checklist = [
Expand All @@ -58,111 +68,128 @@ const checklist = [
{
category: 'Lisibilité & Nommage',
label: 'Pas de code commenté laissé en place',
description: 'Le code mort ou commenté a été supprimé ; l’historique git suffit à le retrouver.',
description:
'Le code mort ou commenté a été supprimé ; l’historique git suffit à le retrouver.',
order: 2,
},
{
category: 'Lisibilité & Nommage',
label: 'Les commentaires expliquent le « pourquoi »',
description: 'Les commentaires justifient les choix non évidents plutôt que de paraphraser le code.',
description:
'Les commentaires justifient les choix non évidents plutôt que de paraphraser le code.',
order: 3,
},
{
category: 'Lisibilité & Nommage',
label: 'Formatage cohérent',
description: 'Indentation, style et conventions sont homogènes avec le reste du projet (linter/formatter).',
description:
'Indentation, style et conventions sont homogènes avec le reste du projet (linter/formatter).',
order: 4,
},

// --- Tests ---
{
category: 'Tests',
label: 'Les nouveaux comportements sont testés',
description: 'Chaque nouvelle fonctionnalité ou correction est couverte par au moins un test.',
description:
'Chaque nouvelle fonctionnalité ou correction est couverte par au moins un test.',
order: 1,
},
{
category: 'Tests',
label: 'Les cas limites sont couverts',
description: 'Valeurs nulles, vides, négatives ou extrêmes sont testées, pas seulement le cas nominal.',
description:
'Valeurs nulles, vides, négatives ou extrêmes sont testées, pas seulement le cas nominal.',
order: 2,
},
{
category: 'Tests',
label: 'Les tests sont lisibles et isolés',
description: 'Chaque test vérifie une chose, sans dépendre de l’ordre d’exécution ni d’un état partagé.',
description:
'Chaque test vérifie une chose, sans dépendre de l’ordre d’exécution ni d’un état partagé.',
order: 3,
},

// --- Sécurité ---
{
category: 'Sécurité',
label: 'Aucun secret codé en dur',
description: 'Mots de passe, clés API et tokens passent par des variables d’environnement, jamais le code.',
description:
'Mots de passe, clés API et tokens passent par des variables d’environnement, jamais le code.',
order: 1,
},
{
category: 'Sécurité',
label: 'Les entrées utilisateur sont validées',
description: 'Toute donnée externe est validée et assainie avant traitement (injection, XSS).',
description:
'Toute donnée externe est validée et assainie avant traitement (injection, XSS).',
order: 2,
},
{
category: 'Sécurité',
label: 'Les erreurs ne fuitent pas d’infos sensibles',
description: 'Les messages d’erreur exposés ne révèlent ni stack trace ni détails internes au client.',
description:
'Les messages d’erreur exposés ne révèlent ni stack trace ni détails internes au client.',
order: 3,
},
{
category: 'Sécurité',
label: 'Les dépendances sont à jour et sûres',
description: 'Pas de dépendance vulnérable connue ; les versions sont épinglées de façon raisonnable.',
description:
'Pas de dépendance vulnérable connue ; les versions sont épinglées de façon raisonnable.',
order: 4,
},

// --- Performance ---
{
category: 'Performance',
label: 'Pas de requête dans une boucle (N+1)',
description: 'Les accès base de données ou réseau sont regroupés plutôt que répétés dans une boucle.',
description:
'Les accès base de données ou réseau sont regroupés plutôt que répétés dans une boucle.',
order: 1,
},
{
category: 'Performance',
label: 'Pas de calcul inutile répété',
description: 'Les résultats coûteux et constants sont mémorisés ou sortis de la boucle.',
description:
'Les résultats coûteux et constants sont mémorisés ou sortis de la boucle.',
order: 2,
},
{
category: 'Performance',
label: 'Les ressources sont libérées',
description: 'Connexions, fichiers et abonnements sont fermés/désabonnés pour éviter les fuites.',
description:
'Connexions, fichiers et abonnements sont fermés/désabonnés pour éviter les fuites.',
order: 3,
},

// --- Architecture & SOLID ---
{
category: 'Architecture & SOLID',
label: 'Les fonctions font une seule chose',
description: 'Chaque fonction a une responsabilité unique et un niveau d’abstraction cohérent.',
description:
'Chaque fonction a une responsabilité unique et un niveau d’abstraction cohérent.',
order: 1,
},
{
category: 'Architecture & SOLID',
label: 'Pas de duplication (DRY)',
description: 'La logique répétée est factorisée dans une fonction ou un module réutilisable.',
description:
'La logique répétée est factorisée dans une fonction ou un module réutilisable.',
order: 2,
},
{
category: 'Architecture & SOLID',
label: 'Faible couplage entre modules',
description: 'Les modules dépendent d’abstractions, pas d’implémentations concrètes (inversion de dépendance).',
description:
'Les modules dépendent d’abstractions, pas d’implémentations concrètes (inversion de dépendance).',
order: 3,
},
{
category: 'Architecture & SOLID',
label: 'Le code respecte les conventions du projet',
description: 'La structure des dossiers et les patterns suivent ceux déjà établis dans la base de code.',
description:
'La structure des dossiers et les patterns suivent ceux déjà établis dans la base de code.',
order: 4,
},
];
Expand Down
31 changes: 31 additions & 0 deletions claude-code-starter-kit/.claude/commands/add-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
description: Scaffolder un nouveau module en suivant la procédure du projet
argument-hint: <slug> "<Nom affiché>"
---

Tu vas ajouter un nouveau module au projet en suivant **exactement** la procédure
documentée (ex. un fichier ADD-A-MODULE.md) et les conventions du dépôt
(@CONVENTIONS.md). La source de vérité reste le code existant : imite un module
déjà en place plutôt que d'inventer une structure.

Arguments fournis : `$ARGUMENTS`
(format attendu : `slug "Nom affiché"` ; si le slug ou le nom manque, demande-le
avant de commencer.)

Déroule les étapes dans l'ordre, sans en sauter. Adapte cette trame à ta stack ;
exemple type pour une feature front + back :

1. **Modèle / types** — l'interface ou le schéma du module.
2. **Données / contenu** — la source de vérité (constante, fixture, migration).
3. **Vue principale** — le composant ou l'endpoint d'entrée du module.
4. **Vue de détail** (si applicable) — résolution réactive d'un paramètre, repli
propre si l'identifiant est inconnu.
5. **Routage** — branche le module dans la table de routes / le routeur.
6. **Navigation** — ajoute le point d'entrée (menu, index).
7. **Persistance** — l'entrée de seed / migration si le module y figure.

Puis **vérifie** avec la commande de contrôle du projet (lint + tests/build) et
colle sa sortie.

Termine par un résumé des fichiers créés/modifiés. Ne committe rien sans demande
explicite.
Loading
Loading