@kiubi/mcp-designer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +219 -0
  3. package/package.json +57 -0
  4. package/src/app.ts +39 -0
  5. package/src/index.ts +21 -0
  6. package/src/lib/stemmer.ts +38 -0
  7. package/src/lib/theme-checker/block-validator.ts +353 -0
  8. package/src/lib/theme-checker/checker.ts +897 -0
  9. package/src/lib/theme-checker/filter-validator.ts +163 -0
  10. package/src/lib/theme-checker/index.ts +6 -0
  11. package/src/lib/theme-checker/inspector.ts +193 -0
  12. package/src/lib/theme-checker/lang.ts +42 -0
  13. package/src/lib/theme-checker/parsers/component-xml.ts +120 -0
  14. package/src/lib/theme-checker/parsers/model-xml.ts +82 -0
  15. package/src/lib/theme-checker/parsers/post-type-xml.ts +92 -0
  16. package/src/lib/theme-checker/parsers/symbol-xml.ts +162 -0
  17. package/src/lib/theme-checker/parsers/theme-xml.ts +102 -0
  18. package/src/lib/theme-checker/types.ts +97 -0
  19. package/src/logger.ts +28 -0
  20. package/src/tool.ts +13 -0
  21. package/src/tools/create_type.ts +535 -0
  22. package/src/tools/detail_type.ts +208 -0
  23. package/src/tools/get_doc_section.ts +201 -0
  24. package/src/tools/list_types.ts +87 -0
  25. package/src/tools/list_widgets.ts +181 -0
  26. package/src/tools/ruleset.ts +25 -0
  27. package/src/tools/search_doc.ts +160 -0
  28. package/src/tools/validate_theme.ts +63 -0
  29. package/storage/definitions/blog.json +1444 -0
  30. package/storage/definitions/catalogue.json +2817 -0
  31. package/storage/definitions/cms.json +1002 -0
  32. package/storage/definitions/commandes.json +2409 -0
  33. package/storage/definitions/comptes.json +780 -0
  34. package/storage/definitions/modules.json +418 -0
  35. package/storage/definitions/recherche.json +804 -0
  36. package/storage/documents/CLAUDE.exemple.md +281 -0
  37. package/storage/documents/generated/exemples.md +8054 -0
  38. package/storage/documents/generated/integration/01_theme_perso.md +182 -0
  39. package/storage/documents/generated/integration/02_modeles.md +220 -0
  40. package/storage/documents/generated/integration/03_widgets.md +139 -0
  41. package/storage/documents/generated/integration/04_blocs.md +45 -0
  42. package/storage/documents/generated/integration/05_balises.md +43 -0
  43. package/storage/documents/generated/integration/06_balises_globales.md +201 -0
  44. package/storage/documents/generated/integration/07_balises_widget.md +257 -0
  45. package/storage/documents/generated/integration/08_types_billets.md +154 -0
  46. package/storage/documents/generated/integration/09_types_produits.md +77 -0
  47. package/storage/documents/generated/integration/10_types_composants.md +142 -0
  48. package/storage/documents/generated/integration/11_symboles.md +156 -0
  49. package/storage/documents/generated/integration/12_mediatheque.md +66 -0
  50. package/storage/documents/generated/integration/13_includes.md +25 -0
  51. package/storage/documents/generated/integration/14_404.md +32 -0
  52. package/storage/documents/generated/integration/15_arianne.md +62 -0
  53. package/storage/documents/generated/integration/16_fermeture.md +36 -0
  54. package/storage/documents/generated/integration/17_recherche.md +36 -0
  55. package/storage/documents/generated/integration/18_cms.md +582 -0
  56. package/storage/documents/generated/integration/19_modules.md +479 -0
  57. package/storage/documents/generated/integration/20_blog.md +946 -0
  58. package/storage/documents/generated/integration/21_catalogue.md +1834 -0
  59. package/storage/documents/generated/integration/22_commandes.md +2772 -0
  60. package/storage/documents/generated/integration/23_comptes.md +1195 -0
  61. package/storage/documents/generated/integration/24_recherche.md +542 -0
  62. package/storage/documents/hors_sujet_regles.md +43 -0
  63. package/storage/documents/kiubi_api.md +6690 -0
  64. package/storage/documents/kiubi_ruleset.md +419 -0
  65. package/storage/documents/kiubi_workflows.md +121 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Kiubi
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # MCP Designer
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40kiubi%2Fmcp-designer.svg)](https://www.npmjs.com/package/@kiubi/mcp-designer)
4
+ [![license](https://img.shields.io/npm/l/%40kiubi%2Fmcp-designer.svg)](https://github.com/Kiubi/mcp-designer/blob/main/LICENSE)
5
+ [![node](https://img.shields.io/node/v/%40kiubi%2Fmcp-designer.svg)](https://github.com/Kiubi/mcp-designer/blob/main/package.json)
6
+
7
+ MCP Designer est un serveur MCP local dédié à l'intégration de thèmes graphiques **[Kiubi](https://www.kiubi.com)**. Il
8
+ expose aux agents IA (Claude, Codex, Gemini…) un ensemble d'outils dédiés : documentation opérationnelle, introspection
9
+ du thème courant, création de fichiers de configurations et validation statique des fichiers.
10
+
11
+ Intégrer un thème Kiubi, c'est jongler avec une syntaxe de templating maison (balises, filtres, blocs), une doc
12
+ technique qu'on rouvre dans un onglet à chaque doute, et un thème existant dont il faut respecter les conventions sans
13
+ les avoir toutes en tête. Le serveur MCP met ce contexte directement à disposition de l'agent IA pendant que vous codez,
14
+ au lieu de vous laisser chercher, deviner, ou tester en prod pour voir.
15
+
16
+ Concrètement :
17
+
18
+ - **Plus besoin de se souvenir de la syntaxe exacte d'une balise ou d'un filtre** — les règles du moteur de
19
+ template et la documentation d'intégration sont consultées à la demande par l'agent, pas retrouvées trois
20
+ onglets plus loin sur un site d'aide.
21
+ - **Modifier un template existant sans casser la cohérence du projet** — avant de toucher un fichier, l'agent peut
22
+ lister les types, widgets et champs déjà en place et s'aligner dessus, plutôt que de réinventer une convention
23
+ de nommage ou dupliquer un champ qui existe déjà sous un autre nom.
24
+ - **Créer un type de contenu sans réécrire le XML à la main** — champs, zones, collection : la structure de
25
+ fichiers correcte est générée directement, au lieu de copier un type existant en espérant ne rien avoir oublié.
26
+ - **Repérer les erreurs avant le déploiement, pas après** — un `theme.xml` mal formé ou une incohérence entre
27
+ fichiers, ça se découvre d'habitude en preview, ou pire une fois en ligne. La validation tourne en local, avant
28
+ le `npm run deploy`.
29
+
30
+ L'agent applique des procédures optimisées pour Kiubi mais reste piloté par vous : sur les tâches de création ou de
31
+ conversion de template, il expose son plan d'action (mode de conversion, champs prévus, zones envisagées) et attend une
32
+ validation explicite avant de produire quoi que ce soit.
33
+
34
+ ---
35
+
36
+ ## Table des matières
37
+
38
+ - [Prérequis](#prérequis)
39
+ - [Installation](#installation)
40
+ - [Configuration pour Claude Code](#configuration-pour-claude-code)
41
+ - [Configuration pour Codex](#configuration-pour-codex)
42
+ - [Configuration pour Gemini](#configuration-pour-gemini)
43
+ - [Mise à jour](#mise-à-jour)
44
+ - [Usage](#usage)
45
+ - [Intégration dans un projet Kiubi](#intégration-dans-un-projet-kiubi)
46
+ - [Structure du projet](#structure-du-projet)
47
+ - [Déploiement de thème avec kiubi-sync](#déploiement-de-thème-avec-kiubi-sync)
48
+ - [Déploiement du CLAUDE.md](#déploiement-du-claudemd)
49
+ - [Tools disponibles](#tools-disponibles)
50
+
51
+ ---
52
+
53
+ ## Prérequis
54
+
55
+ | Outil | Version minimale | Notes |
56
+ |:---------|:-----------------|:------------------------------------------|
57
+ | Node.js | 22.18 | Requis pour l'exécution native TypeScript |
58
+ | npm | 10+ | Inclus avec Node.js |
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ Le serveur est distribué via le paquet npm **`@kiubi/mcp-designer`**. Aucune installation manuelle n'est nécessaire, la
65
+ commande : `npx -y @kiubi/mcp-designer@latest` suffit à télécharger et exécuter automatiquement la dernière version du
66
+ serveur MCP. Il ne reste plus qu'à ajouter ce MCP à votre agent IA.
67
+
68
+
69
+ ### Configuration pour Claude Code
70
+
71
+ Pour ajouter le serveur MCP, ouvrir un terminal, **se rendre dans le dossier du projet**, puis :
72
+
73
+ ```bash
74
+ claude mcp add kiubi-mcp --scope project -- npx -y @kiubi/mcp-designer@latest
75
+ ```
76
+
77
+ Télécharger le fichier https://github.com/Kiubi/mcp-designer/blob/main/storage/documents/CLAUDE.exemple.md et
78
+ l'enregister à la racine de votre projet sous le nom `CLAUDE.md`.
79
+
80
+
81
+ ### Configuration pour Codex
82
+
83
+ Pour ajouter le serveur MCP, ouvrir un terminal, **se rendre dans le dossier du projet**, puis :
84
+
85
+ ```bash
86
+ mkdir .codex
87
+
88
+ cat >> .codex/config.toml <<'EOF'
89
+ [mcp_servers.kiubi-mcp-designer]
90
+ command = "npx"
91
+ args = ["-y", "@kiubi/mcp-designer@latest"]
92
+ enabled = true
93
+ required = true
94
+ default_tools_approval_mode = "auto"
95
+ EOF
96
+ ```
97
+
98
+ Télécharger le fichier https://github.com/Kiubi/mcp-designer/blob/main/storage/documents/CLAUDE.exemple.md et
99
+ l'enregister à la racine de votre projet sous le nom `AGENTS.md`.
100
+
101
+
102
+ ### Configuration pour Gemini
103
+
104
+ Pour ajouter le serveur MCP, ouvrir un terminal, **se rendre dans le dossier du projet**, puis :
105
+
106
+ ```bash
107
+ gemini mcp add --scope project kiubi-mcp-designer npx -y @kiubi/mcp-designer@latest
108
+ ```
109
+
110
+ Télécharger le fichier https://github.com/Kiubi/mcp-designer/blob/main/storage/documents/CLAUDE.exemple.md et
111
+ l'enregister à la racine de votre projet sous le nom `GEMINI.md`.
112
+
113
+
114
+ ### Mise à jour
115
+
116
+ La commande `npx -y @kiubi/mcp-designer@latest` résout le tag `latest` à chaque lancement du serveur : il n'y a donc rien à
117
+ mettre à jour manuellement. En pratique, cette résolution n'a lieu qu'au **démarrage** du serveur MCP, c'est-à-dire à
118
+ l'ouverture d'une session ou au redémarrage de votre agent — une session déjà en cours continue de tourner avec la
119
+ version chargée au lancement. Pour forcer la prise en compte d'une nouvelle version, redémarrer l'agent (ou la
120
+ connexion MCP) suffit.
121
+
122
+ Si `npx` sert malgré tout une version obsolète (paquet mis en cache localement, environnement hors ligne au moment de
123
+ la résolution), vider le cache npm force un nouveau téléchargement :
124
+
125
+ ```bash
126
+ npm cache clean --force
127
+ ```
128
+
129
+ > **Important** — le fichier `CLAUDE.exemple.md` (ou `AGENTS.md`/`GEMINI.md`) téléchargé lors de l'installation n'est
130
+ > pas mis à jour automatiquement : c'est une copie locale. Si son contenu évolue entre deux versions du serveur,
131
+ > le retélécharger et le comparer à votre version versionnée du projet.
132
+
133
+ ---
134
+
135
+ ## Usage
136
+
137
+ Une fois le serveur MCP connecté, l'agent décide seul quand invoquer les tools en fonction de votre demande — il n'y
138
+ a pas de commande spécifique à taper. Il suffit de discuter normalement avec l'agent en ayant ouvert votre éditeur à
139
+ la racine du projet intégrateur (celle qui contient le dossier `theme/`). Quelques exemples de demandes :
140
+
141
+ - *« Valide le thème avant que je déploie »* → l'agent appelle `validate_theme` et rapporte les erreurs trouvées.
142
+ - *« Quels champs a le type `produit` ? »* → l'agent appelle `detail_type` et répond à partir du résultat.
143
+ - *« Crée un type `temoignage` avec les champs titre, texte et note »* → l'agent expose son plan (champs, zones
144
+ prévues) et attend votre validation avant d'appeler `create_type`.
145
+ - *« Comment fonctionne le filtre `date` du moteur de template ? »* → l'agent consulte `ruleset` et/ou `search_doc`
146
+ plutôt que de deviner la syntaxe.
147
+
148
+ ---
149
+
150
+ ## Intégration dans un projet Kiubi
151
+
152
+ ### Structure du projet
153
+
154
+ Un projet d'intégration de thème Kiubi se structure ainsi :
155
+
156
+ ```
157
+ mon-projet-kiubi/
158
+ ├── theme/ ← thème graphique Kiubi (synchronisé avec kiubi-sync)
159
+ │ ├── theme.xml
160
+ │ └── fr/
161
+ │ ├── templates/
162
+ │ ├── widgets/
163
+ │ ├── billets/
164
+ │ └── ...
165
+ ├── gulpfile.js
166
+ ├── config.json ← credentials FTP (à exclure du dépôt git)
167
+ ├── package.json
168
+ └── CLAUDE.md ← voir ci-dessous
169
+ ```
170
+
171
+ ### Déploiement de thème avec kiubi-sync
172
+
173
+ [kiubi-sync](https://aide.kiubi.com/environnement-de-travail.html) est le module npm officiel Kiubi pour synchroniser
174
+ les fichiers du thème entre l'environnement local et le serveur. Il remplace le client FTP et s'intègre via Gulp.
175
+
176
+ Il expose trois commandes principales :
177
+
178
+ | Commande | Rôle |
179
+ |:------------------|:----------------------------------------------------------|
180
+ | `npm run pull` | Télécharge tous les fichiers du thème depuis le serveur |
181
+ | `npm run deploy` | Envoie les fichiers locaux vers le serveur |
182
+ | `npm run watch` | Surveille les modifications et synchronise en temps réel |
183
+
184
+ **Procédure recommandée** : exécuter d'abord un `pull` pour récupérer le thème existant, puis démarrer `watch` pendant
185
+ le développement. Le serveur MCP travaillera sur ces fichiers locaux.
186
+
187
+ Consulter la [documentation kiubi-sync](https://aide.kiubi.com/environnement-de-travail.html) pour l'installation
188
+ complète et la configuration du `gulpfile.js`.
189
+
190
+ ### Déploiement du CLAUDE.md
191
+
192
+ Le fichier [CLAUDE.exemple.md](https://github.com/Kiubi/mcp-designer/blob/main/storage/documents/CLAUDE.exemple.md)
193
+ contient les instructions nécessaires pour que l'agent IA comprenne le contexte Kiubi et utilise correctement les tools
194
+ du serveur MCP. Il doit être copié à la racine du projet intégrateur sous un nom propre à chaque agent (`CLAUDE.md`,
195
+ `AGENTS.md`, `GEMINI.md`, ...).
196
+
197
+ Ce fichier est lu automatiquement par Claude à chaque session. Il définit notamment :
198
+
199
+ - Le comportement de l'agent face au thème (validation obligatoire avant toute modification)
200
+ - Les workflows d'intégration (micro-correction, intervention standard)
201
+ - Les règles CSS et les conventions git à respecter
202
+ - Le pipeline complet d'une tâche d'intégration
203
+
204
+ > **Important** — `CLAUDE.md` est spécifique à chaque projet. Il peut être versionné dans le dépôt git du projet intégrateur.
205
+
206
+ ---
207
+
208
+ ## Tools disponibles
209
+
210
+ | Tool | Description |
211
+ |:------------------|:----------------------------------------------------------------------------|
212
+ | `validate_theme` | Valide la syntaxe et la cohérence de tous les fichiers du thème |
213
+ | `ruleset` | Retourne les règles du moteur de template Kiubi (balises, filtres, syntaxe) |
214
+ | `get_doc_section` | Accède à la documentation : guide d'intégration, exemples, API front-office |
215
+ | `list_types` | Liste les éléments du thème par type (billets, composants, produits…) |
216
+ | `detail_type` | Retourne le détail d'un élément (champs, zones, collection) |
217
+ | `list_widgets` | Liste tous les widgets et leurs templates alternatifs |
218
+ | `create_type` | Crée ou met à jour un type d'élément (génère les fichiers XML et HTML) |
219
+ | `search_doc` | Recherche transversale dans la documentation |
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@kiubi/mcp-designer",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "tsc --noEmit",
8
+ "dev": "node src/index.ts"
9
+ },
10
+ "bin": {
11
+ "kiubi-mcp-designer": "src/index.ts"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "storage/documents",
16
+ "storage/definitions",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "imports": {
24
+ "#tools/*": "./src/tools/*.ts",
25
+ "#lib/*": "./src/lib/*.ts"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.18.0"
29
+ },
30
+ "author": "Kiubi",
31
+ "license": "ISC",
32
+ "description": "Serveur MCP local pour l'intégration de thèmes graphiques Kiubi.",
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "kiubi",
37
+ "theme",
38
+ "cms"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Kiubi/mcp-designer.git"
43
+ },
44
+ "homepage": "https://github.com/Kiubi/mcp-designer#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/Kiubi/mcp-designer/issues"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.28.0",
50
+ "fast-xml-parser": "5.7.0",
51
+ "zod": "^3.23.8"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^25.5.0",
55
+ "typescript": "^5.8.0"
56
+ }
57
+ }
package/src/app.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ToolDefinition } from "./tool.ts";
3
+ import { validateThemeTool } from "#tools/validate_theme";
4
+ import { listTypesTool } from "#tools/list_types";
5
+ import { detailTypeTool } from "#tools/detail_type";
6
+ import { listWidgetsTool } from "#tools/list_widgets";
7
+ import { integrationRulesTool } from "#tools/ruleset";
8
+ import { getDocSectionTool } from "#tools/get_doc_section";
9
+ import { searchDocTool } from "#tools/search_doc";
10
+ import { createTypeTool } from "#tools/create_type";
11
+ import { logger } from "./logger.ts";
12
+
13
+ const tools = [validateThemeTool, listTypesTool, detailTypeTool, listWidgetsTool, integrationRulesTool, getDocSectionTool, searchDocTool, createTypeTool];
14
+
15
+ export function createServer(): McpServer {
16
+ const server = new McpServer({
17
+ name: "local-stdio-ts-server",
18
+ version: "1.0.0",
19
+ });
20
+
21
+ for (const tool of tools) {
22
+ // L'union des types empêche l'inférence générique ; le cast est sûr car chaque tool
23
+ // est enregistré avec son propre inputSchema et handler cohérents.
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const t = tool as ToolDefinition<any>;
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ server.registerTool(t.name, t, async (args: any, extra: any) => {
28
+ logger.tool(t.name, args);
29
+ try {
30
+ return await t.handler(args, extra);
31
+ } catch (err) {
32
+ logger.error(`tool:${t.name}`, err instanceof Error ? err.message : err);
33
+ throw err;
34
+ }
35
+ });
36
+ }
37
+
38
+ return server;
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./app.ts";
4
+ import { logger } from "./logger.ts";
5
+
6
+ async function main(): Promise<void> {
7
+ const server = createServer();
8
+ const transport = new StdioServerTransport();
9
+
10
+ await server.connect(transport);
11
+
12
+ logger.info("Serveur MCP démarré");
13
+ console.error("MCP server démarré sur stdio");
14
+ }
15
+
16
+ main().catch((error) => {
17
+ logger.error("Erreur fatale au démarrage du serveur MCP", error instanceof Error ? error.message : error);
18
+ console.error("Erreur fatale au démarrage du serveur MCP");
19
+ console.error(error);
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,38 @@
1
+
2
+ // Savoy (1999) light French stemmer
3
+ // Règles ordonnées du suffixe le plus long au plus court — première correspondance appliquée.
4
+ // Condition : word.length >= minLen avant application.
5
+ type SuffixRule = [suffix: string, replacement: string, minLen: number];
6
+
7
+ const SUFFIX_RULES: SuffixRule[] = [
8
+ ["eaux", "eau", 5], // "beaux" → "beau"
9
+ ["aux", "al", 5], // "locaux" → "local"
10
+ ["issements", "", 11], // "établissements" → "établi"
11
+ ["issement", "", 10], // "établissement" → "établi"
12
+ ["ements", "", 9], // "traitements" → "traite"
13
+ ["ement", "", 8], // "traitement" → "traite"
14
+ ["ations", "", 9], // "configurations" → "configur"
15
+ ["ation", "", 8], // "configuration" → "configur"
16
+ ["ités", "", 7], // "fiabilités" → "fiabl"
17
+ ["ité", "", 6], // "fiabilité" → "fiabl"
18
+ ["eurs", "", 7], // "valeurs" → "val"
19
+ ["eur", "", 6], // "valeur" → "val"
20
+ ["s", "", 4], // "blocs" → "bloc", "templates" → "template"
21
+ ["x", "", 4], // "voix" → "voi"
22
+ ];
23
+
24
+ export function normalize(text: string): string {
25
+ return text
26
+ .normalize("NFD")
27
+ .replace(/\p{Mn}/gu, "")
28
+ .toLowerCase();
29
+ }
30
+
31
+ export function stem(word: string): string {
32
+ for (const [suffix, replacement, minLen] of SUFFIX_RULES) {
33
+ if (word.length >= minLen && word.endsWith(suffix)) {
34
+ return word.slice(0, word.length - suffix.length) + replacement;
35
+ }
36
+ }
37
+ return word;
38
+ }