diff --git a/.gitignore b/.gitignore index 1b28d31..8ceeecd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ coverage/ .impeccable/ .codex/apex/ .claude/ +MEMORY/ # bun.lock IS committed on purpose (bun docs: "Should it be committed to git? Yes."), # so `--frozen-lockfile` installs stay reproducible. Do not add it here. diff --git a/MEMORY/LESSON.md b/MEMORY/LESSON.md deleted file mode 100644 index 518bf07..0000000 --- a/MEMORY/LESSON.md +++ /dev/null @@ -1,20 +0,0 @@ -# Lessons — cursor-plugins - -- [2026-09-01 20:34] Le proprio a demandé un **portage** de l'installeur Claude Code ; j'ai livré une adaptation maison en `.mjs` (modules `harness-*`, `env-*`, `mcp-*` écrits from scratch) au lieu de cloner `claude-plugins/scripts/` (TS + bun, 46 fichiers) et d'en transposer les chemins. Deux heures perdues, trois agents lancés pour rien. → **Porter = cloner la source puis substituer, jamais réécrire dans une autre techno.** Le `.mjs` préexistant du repo n'était pas une convention à honorer, c'était justement ce qu'il fallait remplacer. Quand la demande dit « comme X », la première commande est `rsync` depuis X, pas un brief d'agent. -- [2026-09-01 20:34] Après le clone, `mcp-installer.ts:24` shellait encore `` $`claude mcp add-json --scope=user` `` : le service portait le bon nom, compilait, et n'aurait **rien installé** sous Cursor. Le typecheck était vert. → Après tout portage, **grep les invocations du binaire de l'outil SOURCE** (`claude`, `claude mcp`, `$`…``) et les placeholders (`CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_*`) : un renommage de chemins ne les voit pas, et `tsc` non plus. Même piège pour `settings.json` — Cursor ne le lit pas, donc tout `configureHooks` qui y écrit travaille dans le vide. -- [2026-09-01 20:34] J'ai prouvé que les hooks se déclenchaient via `~/.cursor/hooks.json` (canal **user-level**) puis conclu que les 62 hooks des plugins fonctionnaient — alors que le canal **plugin** (`/hooks/hooks.json`) n'a jamais été mesuré : sonde posée 3 fois, log resté vide, y compris sur un témoin en chemin absolu. → Un plugin **chargé** (`loadUserLocalPlugins ... 24 plugins loaded`) ne prouve pas que ses hooks **s'exécutent**. Toute sonde de hook exige les DEUX canaux armés en parallèle sur le même script, un argument différent par canal ; sans ce contrôle négatif, un log vide ne distingue pas « canal mort » de « aucun événement déclenché ». -- [2026-09-01 20:45] J'ai remplacé les 20 `.cursor-plugin/scripts/*.mjs` par le clone TS/bun sans toucher `install.sh`, resté sur `exec node .../install.mjs` — **fichier supprimé**. L'installeur était donc totalement inutilisable, et `bunx tsc --noEmit` + `bun test` étaient **verts** (182 pass) parce qu'aucun test ne franchit le point d'entrée shell. Détecté uniquement par un `test -f` manuel sur la cible du `exec`. → Après tout remplacement de moteur, **suivre la chaîne depuis le point d'entrée utilisateur** (`install.sh`, `install.ps1`, `verify*.sh`, `tests/*.sh`, `docs/`) et vérifier l'EXISTENCE de chaque cible : `grep -rn "\.mjs" install.sh tests/ docs/`. Vert au typecheck ne prouve rien sur ce qui n'est pas typé — le shell d'entrée est hors du graphe TypeScript. -- [2026-09-01 20:53] En clonant l'installeur amont par-dessus, j'ai effacé ce que la CIBLE avait **en propre et que la source n'a jamais eu** : déploiement transactionnel avec rollback (`commitGlobalSnapshot`), `receipt.json` à hash par plugin, `preflightOwned`, et les options `--dry-run` / `--uninstall` / `--project`. Claude Code n'ayant aucun de ces mécanismes, le clone les a supprimés en silence — et rien ne l'a signalé, ni `tsc` ni les 182 tests. → Avant de cloner par-dessus, **inventorier les capacités de la cible qui n'existent pas dans la source** (`git show HEAD:` + liste des options CLI acceptées) ; ce qui est unique à la cible se RESTAURE (`git checkout -- `), il ne se remplace pas. Bon découpage final : deux moteurs côte à côte — le `.mjs` transactionnel garde le déploiement, le TS cloné prend la configuration. -- [2026-09-01 20:59] `setup-runner` copiait `hooks-loader.ts` **seul** vers `~/.cursor/scripts/`, alors qu'il importe 5 modules relatifs (`./src/services/…`). Les 62 hooks câblés dessus mouraient sur `Cannot find module './src/services/deps-targets'` — invisible au `tsc` (le fichier compile très bien à sa place d'origine) et aux 182 tests. Trouvé seulement en **exécutant la copie isolée**. Même famille : `setHarnessRefs(join(marketplace,"plugins"))` visait un niveau trop haut → `FUSE_HARNESS_REFS` vide, 0 dossier au lieu de 14. → Un artefact **déplacé** doit être **exécuté depuis sa destination**, jamais validé depuis sa source : copier un fichier n'emporte pas son graphe d'imports, et un chemin dérivé par `join()` se vérifie par `existsSync` + comptage, pas par relecture. Règle : après toute copie ou tout chemin construit, `bun ` ou `ls ` — un compte de 0 est un défaut, pas un résultat. -- [2026-09-01 21:08] Portage `.mjs` → TS mené par 4 agents en parallèle : chacun a créé ses propres types dans `src/interfaces/marketplace.ts` et écrasé ceux du voisin, et deux shims de compatibilité (`legacy-mjs.d.ts` en racine ET dans `interfaces/`) ont maintenu un `tsc` VERT alors que 4 fichiers TS importaient encore des `.mjs` censés disparaître. Le vert masquait un portage inachevé. → Sur un portage multi-agents, **le fichier de types partagé appartient à UN SEUL owner** nommé dans les briefs, et **interdire les shims de compatibilité** : ils font passer le typecheck sans que le portage soit fini. Contrôle de fin obligatoire, dans cet ordre : `grep -rn 'from "[^"]*\.mjs"' src/` = 0, puis `ls *.d.ts` pour débusquer les shims, puis seulement `ls *.mjs` = 0. Et une référence dans un `.sh`/`.ps1` ne se voit ni au `tsc` ni aux tests — la vérifier séparément par `test -f` sur chaque cible d'`exec`. -- [2026-09-01 21:15] Portage `project-verify.mjs` → `.ts` déclaré « fidèle 1:1 », diff ligne à ligne parfait : `spawnSync(process.execPath, [".cursor/fusengine/load-plugins.mjs"])` était **identique au caractère près**. Mais le shebang passait de `#!/usr/bin/env node` à `#!/usr/bin/env bun`, donc `process.execPath` valait désormais le binaire bun — alors que le hook déployé lance `node .cursor/fusengine/load-plugins.mjs` (`project-hooks.ts::baseCommand`). La vérification testait un interpréteur que la production n'utilise jamais ; ni `tsc` ni les 182 tests ne pouvaient le voir. → Lors d'un changement de runtime, **les API dépendantes de l'environnement changent de valeur sans changer de texte** : auditer nommément `process.execPath`, `process.argv[0]`, `import.meta.*`, `__dirname`, `process.platform`. Quand le code doit REPRODUIRE une commande externe, écrire l'interpréteur en dur (`"node"`) plutôt que d'hériter du sien. Corollaire : un diff propre ne prouve pas l'équivalence sémantique — comparer les VALEURS mesurées des deux côtés, pas les lignes. -- [2026-09-01 21:25] J'ai fait écrire à l'installeur `~/.cursor/AGENTS.md` comme canal d'instructions global, en transposant `~/.claude/CLAUDE.md`. Le challenger l'avait marqué « NON VÉRIFIÉ » et je l'ai quand même livré et documenté. Le propriétaire a douté ; mesure dans le binaire Cursor 3.18.25 : `getWorkspaceFolder(uri); if(!c) return !1; l === "/AGENTS.md"` — **AGENTS.md n'est reconnu qu'à la racine d'un workspace folder**, donc un fichier sous `~/.cursor/` n'est jamais lu. Le vrai canal global est `~/.cursor/rules/` (`getRuleTargetDirectory` → `joinPath(userHome, ".cursor", "rules")`), ce que l'ancien installeur faisait déjà avec `fuse-global.mdc`. → Un « NON VÉRIFIÉ » du challenger est une dette bloquante, pas une note de bas de page : le lever AVANT de livrer, et jamais l'écrire dans la doc entre-temps. Pour un chemin lu par un binaire fermé, la doc éditeur ne suffit pas — `grep` le bundle (`Cursor.app/Contents/Resources/app/out/**/*.js`) et lire la condition réelle tranche en une minute. -- [2026-09-01 21:31] Pendant toute la refonte j'ai validé l'installeur au `tsc` + `bun test` (182 pass) sans jamais l'exécuter — or aucun de ces 182 tests ne franchit `install.sh`. Le seul test concluant a été un run réel avec `HOME=/tmp/fake ./install.sh --skip-env` : il a prouvé en 30 s les 24 plugins déployés, le receipt à 24 hashes, `fuse-global.mdc` posée, le loader qui démarre depuis sa destination et les 21 wrappers exécutables. → Un installeur se vérifie **en l'installant dans un HOME jetable**, jamais par lecture : `HOME=$(mktemp -d) ./install.sh [--dry-run]` est sans risque pour l'environnement réel et c'est le seul contrôle qui couvre le chemin d'entrée shell, les chemins construits et les artefacts copiés. À faire AVANT d'annoncer quoi que ce soit, pas après. Corollaire : dans ce genre de sandbox, `~/Library/Caches/bun/**` apparaît et n'est PAS une écriture de l'installeur — compter sous `~/.cursor/` uniquement avant de crier au bug. -- [2026-09-01 22:02] Deux agents ont diagnostiqué le crash de `install.sh --project` sur `$HOME` vierge ; l'un a livré ses suites VERTES en pré-créant `~/.cursor/plugins/local` dans son propre test, avec un commentaire honnête « bug de production, hors de mon lot ». Vert au tableau de bord, défaut intact en production, et le contournement aurait survécu au commit. → Un agent cantonné à un lot de fichiers ne peut PAS corriger un défaut situé hors de son lot : quand son rapport dit « contourné dans le test », c'est une dette à ENCAISSER immédiatement au niveau du lead — corriger la source, puis RETIRER le contournement et re-mesurer. Un test qui pré-crée l'état que la production est censée créer ne teste plus rien. -- [2026-09-01 22:02] Assertion « l'install projet n'écrit rien dans `$HOME` » en échec permanent : le coupable n'était pas l'installeur mais le cache de transpilation de **Bun** (`$HOME/Library/Caches/bun/@t@/*.pile` sur macOS, `~/.cache/bun` sur Linux), semé au premier `bun` lancé avec un HOME neuf. → Après un passage à Bun, tout test qui compare un instantané de `$HOME` doit neutraliser le runtime lui-même (`BUN_RUNTIME_TRANSPILER_CACHE_PATH=0`, vérifié par contrôle négatif) plutôt que d'élargir l'assertion : scoper à `$HOME/.cursor` affaiblit la garantie, neutraliser la conserve. Vaut pour toute migration de runtime — le nouvel interpréteur écrit chez l'utilisateur, l'ancien non. -- [2026-09-01 22:52] `--uninstall` laissait 64 fichiers (`~/.cursor/.fusengine-global/scripts/**`) chez l'utilisateur, définitivement. Pire que le défaut : l'assertion de test avait été **relâchée pour l'entériner** — `test ! -e .fusengine-global` remplacé par le seul `receipt.json`, avec un commentaire citant le `/* preserved if non-empty */` du code comme s'il s'agissait du contrat. Un commentaire de code décrit ce que le code FAIT, il ne prouve pas que ce soit voulu. → Quand une assertion doit être affaiblie pour passer, la question n'est jamais « comment la formuler » mais « quel défaut suis-je en train de graver dans le contrat » ; le relâchement se remonte au propriétaire, il ne se commente pas. Corollaire appliqué ici : avant de réécrire une assertion de suppression, **lui donner des dents** en vérifiant d'abord que la chose à supprimer EXISTE (`test -d .../scripts` avant l'uninstall) — sinon `test ! -e` passe sur un répertoire jamais peuplé et ne teste rien. -- [2026-09-01 22:52] Cause réelle, contre-intuitive : le `rmdirSync` non récursif n'était pas le bug. `receipt.json` est le registre de propriété (hash par plugin, `ruleHash`) et `scripts/` n'y figurait pas — écrit par `setup-runner.ts`, un module que le moteur de déploiement n'importe jamais. Aucun chemin de code ne pouvait donc le réclamer. → Sur un installeur transactionnel, un artefact déployé par un module hors du registre est **orphelin par construction** : lister les écritures RÉELLES sous le répertoire de contrôle (`find` après un install en HOME jetable) plutôt que de se fier au registre, qui ne connaît que ce que son propre module a écrit. -- [2026-09-01 22:35] Premier run du CI rouge sur UN test (`zsh loader`, absent d'`ubuntu-latest`). Le vrai défaut était ailleurs : le cas `fish` du même fichier portait déjà `test.skipIf(!Bun.which("fish"))` et se **skippait silencieusement** en CI depuis toujours — un CI vert me l'aurait caché indéfiniment. → Une garde `skipIf(outil absent)` protège la machine du contributeur mais **supprime la couverture là où elle compte le plus**. Sur le runner, installer l'outil (`sudo apt-get install -y zsh fish` ; ni l'un ni l'autre n'est préinstallé sur Ubuntu 24.04) ET faire échouer le job sur tout `skip` — sinon la garde transforme un test mort en test vert. Corollaire d'API : `bun:test` n'a pas de skip par ligne dans `test.each` (oven-sh/bun#4469 ouverte) — passer par une boucle `for`, et vérifier la garde par contrôle négatif (PATH privé du binaire → `1 skip / 0 fail`, contre `1 fail` avant). -- [2026-09-01 22:35] J'ai annoncé au proprio « `runSetup` n'écrit que sous `~/.cursor` » pour justifier un correctif — le challenger a mesuré des `append` dans `~/.zshrc` / `~/.bashrc` et un `conf.d/*.fish` (`shell-installers.ts:25,60,68-74`). Le correctif restait bon, mon argument était faux, et il minimisait ce qu'il évitait. → Un périmètre d'écriture s'énonce après un `grep` des primitives d'écriture du module (`writeFileSync`, `appendFileSync`, `spawn`), jamais d'après le nom des chemins qu'on a en tête. Se tromper « en faveur » de sa propre conclusion reste une erreur — et ici la vérité renforçait le correctif. -- [2026-09-01 22:02] `tests/marketplace-sparse-install.test.sh` compare le worktree à `git archive HEAD` : un `git rm` non commité le fait échouer en ENOENT sur un fichier que HEAD contient encore — échec purement mécanique, sans rapport avec le défaut testé. → Face à une suite qui se réfère à HEAD, tout changement d'INDEX non commité est un faux échec : trancher dans un `git clone . /tmp/x` où l'on peut commiter localement, jamais en commitant sur la branche pour « voir si ça passe ». -- [2026-09-04 07:55] Cycle de release mené moitié-moitié : le bump `1.2.40 → 1.2.41` est bien passé par la PR #4 (CI verte, merge `--merge`), puis j'ai poussé `chore(release): 0.1.2` — CHANGELOG + tag — **directement sur `main`**. Le skill `fuse-commit-pro:git-flow` l'interdit noir sur blanc (« `main` → never commit directly », « ❌ Commits on main "to save time" — bypasses review, breaks CI gates »). Le workflow écoutant aussi `push: [main]`, la CI est passée au vert APRÈS coup et le défaut de processus est resté invisible : `v0.1.2` tague un commit que personne n'a revu. → Le CHANGELOG et le bump appartiennent à la **branche de feature, avant le merge** — c'est précisément la raison d'être du `--merge` (jamais `--squash`) : le tag doit pointer sur le commit de bump qui a transité par la PR. Corollaire de méthode, plus coûteux que l'erreur elle-même : j'ai demandé au propriétaire « veux-tu que les releases passent par une PR ? » alors que la réponse était écrite dans un skill du dépôt. Avant toute question de convention (git, commit, release, versionnage), **grep les skills d'abord** — `fuse-commit-pro:git-flow`, `commit-detection`, `post-commit` ; demander ce qui est déjà écrit fait perdre un tour et signale qu'on n'a pas lu.