@nodefony/documentation 10.0.0-alpha.1
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.
- package/LICENSE +544 -0
- package/README.md +168 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +75 -0
- package/dist/nodefony/config/config.js +71 -0
- package/dist/nodefony/config/defineModuleConfig.js +49 -0
- package/dist/nodefony/controller/DocumentationController.js +102 -0
- package/dist/nodefony/interfaces/IDocumentation.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DocumentationService.js +421 -0
- package/dist/nodefony/src/docScanner.js +58 -0
- package/dist/nodefony/src/errors/DocumentationError.js +45 -0
- package/dist/nodefony/src/frontmatter.js +63 -0
- package/dist/nodefony/src/linkResolver.js +68 -0
- package/dist/nodefony/src/search.js +120 -0
- package/dist/nodefony/src/slug.js +62 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/nodefony/config/config.d.ts +37 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +17 -0
- package/dist/types/nodefony/controller/DocumentationController.d.ts +33 -0
- package/dist/types/nodefony/interfaces/IDocumentation.d.ts +144 -0
- package/dist/types/nodefony/interfaces/index.d.ts +5 -0
- package/dist/types/nodefony/service/DocumentationService.d.ts +78 -0
- package/dist/types/nodefony/src/docScanner.d.ts +47 -0
- package/dist/types/nodefony/src/errors/DocumentationError.d.ts +32 -0
- package/dist/types/nodefony/src/frontmatter.d.ts +37 -0
- package/dist/types/nodefony/src/linkResolver.d.ts +55 -0
- package/dist/types/nodefony/src/search.d.ts +64 -0
- package/dist/types/nodefony/src/slug.d.ts +48 -0
- package/docs/architecture.md +647 -0
- package/docs/index.md +369 -0
- package/package.json +81 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Architecture — du fichier .md au portail navigable"
|
|
3
|
+
navTitle: Architecture
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/documentation"
|
|
6
|
+
topic: documentation
|
|
7
|
+
coverageModule: documentation
|
|
8
|
+
section: "Documentation"
|
|
9
|
+
audience: [developer]
|
|
10
|
+
tags:
|
|
11
|
+
[
|
|
12
|
+
documentation,
|
|
13
|
+
architecture,
|
|
14
|
+
scan,
|
|
15
|
+
frontmatter,
|
|
16
|
+
slug,
|
|
17
|
+
liens,
|
|
18
|
+
allowlist,
|
|
19
|
+
data-plane,
|
|
20
|
+
portail,
|
|
21
|
+
]
|
|
22
|
+
version: "doc"
|
|
23
|
+
status: stable
|
|
24
|
+
updated: 2026-07-19
|
|
25
|
+
source: "src/packages/@nodefony/documentation/docs/architecture.md"
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Architecture — du fichier `.md` au portail navigable
|
|
29
|
+
|
|
30
|
+
> Tu écris un fichier Markdown à côté de ton code. Quelques secondes plus tard, il est
|
|
31
|
+
> dans le portail, rangé dans la bonne section, avec ses liens qui marchent et un bouton
|
|
32
|
+
> « voir la source ». Cette page décrit la machinerie entre les deux : ce que le module va
|
|
33
|
+
> chercher sur le disque, ce qu'il lit dans ton frontmatter, comment il fabrique une **cote**
|
|
34
|
+
> à partir d'un chemin, et pourquoi c'est **le serveur** — pas ton navigateur — qui traduit
|
|
35
|
+
> tes liens relatifs. Tout est ancré sur
|
|
36
|
+
> `src/packages/@nodefony/documentation/nodefony/`.
|
|
37
|
+
|
|
38
|
+
📍 [Documentation](../../../../../docs/index.md) › [Documentation (module)](index.md) › **Architecture**
|
|
39
|
+
|
|
40
|
+
## 🧠 Le modèle mental — un bibliothécaire, un catalogue, une cote
|
|
41
|
+
|
|
42
|
+
Les livres sont dispersés : certains dans la salle commune (`docs/` à la racine du projet),
|
|
43
|
+
la plupart rangés à côté de l'atelier qui les a écrits (`<module>/docs/`). Trois objets
|
|
44
|
+
suffisent à comprendre l'ensemble :
|
|
45
|
+
|
|
46
|
+
- Le **bibliothécaire**, c'est le service. Il fait le tour des étagères une fois, retient
|
|
47
|
+
où chaque livre se trouve **réellement**, et garde ce tour de piste en mémoire.
|
|
48
|
+
- Le **catalogue**, c'est l'index. Il ne contient pas les livres, seulement de quoi les
|
|
49
|
+
choisir : titre, section, persona, statut.
|
|
50
|
+
- La **cote**, c'est le _slug_. Tu la demandes, on va chercher le livre. Tu ne peux pas
|
|
51
|
+
fabriquer une cote pour un livre qui n'est pas au catalogue — c'est ce qui rend le
|
|
52
|
+
rayonnage inviolable.
|
|
53
|
+
|
|
54
|
+
```mermaid
|
|
55
|
+
flowchart TD
|
|
56
|
+
subgraph DISQUE["Le disque — la doc vit à côté du code"]
|
|
57
|
+
R["docs/ (racine)<br/>guides · ADR · architecture"]
|
|
58
|
+
M["<module>/docs/*.md<br/>ADR-0001"]
|
|
59
|
+
N["node_modules/@nodefony/*/docs<br/>paquets installés, même non chargés"]
|
|
60
|
+
end
|
|
61
|
+
R --> SCAN["scanDocsDir()<br/>best-effort, récursif"]
|
|
62
|
+
M --> SCAN
|
|
63
|
+
N --> SCAN
|
|
64
|
+
SCAN --> FM["parseFrontmatter()<br/>YAML plat, 0 dépendance"]
|
|
65
|
+
FM --> IDX["Index en cache<br/>slug → ScannedDoc · chemin repo → slug"]
|
|
66
|
+
IDX --> TREE["GET /api/tree<br/>sections ordonnées, hub en tête"]
|
|
67
|
+
IDX --> PAGE["GET /api/page/{slug}"]
|
|
68
|
+
PAGE --> RES["variables {{ }} résolues<br/>+ liens relatifs traduits en slugs"]
|
|
69
|
+
RES --> UI["Portail Studio · site statique · RAG"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Trois règles tiennent tout l'édifice :
|
|
73
|
+
|
|
74
|
+
1. **Le slug est une clé, jamais un chemin.** On ne reconstruit jamais un chemin de fichier
|
|
75
|
+
à partir de ce que le client envoie.
|
|
76
|
+
2. **Le cache porte sur l'index, pas sur le contenu.** Une page est **toujours** relue sur
|
|
77
|
+
le disque — on ne sert jamais un Markdown périmé.
|
|
78
|
+
3. **La traduction des liens appartient au serveur.** Lui seul connaît la table
|
|
79
|
+
chemin → slug ; le client n'a aucun moyen de deviner à quoi correspond `../../..`.
|
|
80
|
+
|
|
81
|
+
## 📖 Lexique
|
|
82
|
+
|
|
83
|
+
| Terme | Sens |
|
|
84
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| Data plane | Le plan de **données** : des routes qui rendent du JSON structuré, sans rien afficher. Par opposition au plan de présentation (l'écran). |
|
|
86
|
+
| Headless | « Sans tête » : le module produit de la donnée, jamais du HTML. Le rendu appartient au consommateur. |
|
|
87
|
+
| Frontmatter | Le bloc encadré de `---` en tête d'un `.md`, qui porte les métadonnées de la page (titre, persona, statut, date). |
|
|
88
|
+
| Slug | La **cote** d'une page : un identifiant URL-safe tenant sur un seul segment de route (`mod~security~cors`). |
|
|
89
|
+
| Allowlist | Liste blanche : seules les entrées connues du scan sont servables. Tout le reste est refusé, sans discussion. |
|
|
90
|
+
| Traversée de répertoire | _Path traversal_ : faire lire au serveur un fichier hors du périmètre prévu, en glissant des `..` dans un identifiant. |
|
|
91
|
+
| Hub | La page d'entrée d'une section (`index.md`) : elle oriente au lieu d'expliquer. |
|
|
92
|
+
| ADR | _Architecture Decision Record_ : une décision d'architecture écrite, datée et motivée. |
|
|
93
|
+
| ADR-0001 | La décision d'**emplacement hybride** : la doc d'un module vit DANS le module, le transverse reste à la racine. |
|
|
94
|
+
| TTL | _Time To Live_ : durée de fraîcheur. Ici, celle de l'index — passé le délai, le disque est re-parcouru. |
|
|
95
|
+
| Real-path | Le chemin réel d'un fichier, liens symboliques résolus. En dépôt workspace, `node_modules/@nodefony/x` mène à la source. |
|
|
96
|
+
| POSIX (chemin) | Forme de chemin à séparateur `/`. Le module normalise tout dessus, y compris ce qui arrive de Windows. |
|
|
97
|
+
| YAML plat | Le sous-ensemble de YAML accepté ici : `clé: valeur`, listes inline `[a, b]`, listes en bloc. Ni objet imbriqué, ni multi-lignes. |
|
|
98
|
+
| Fence typée | Un bloc de code dont le langage déclare un composant (` ```nodefony-cards `) plutôt qu'un langage de programmation. |
|
|
99
|
+
| RBAC | _Role-Based Access Control_ : qui a le droit, décidé par les rôles portés par l'identité. |
|
|
100
|
+
| RAG | _Retrieval-Augmented Generation_ : donner à un modèle les documents pertinents avant qu'il réponde. Le Markdown en est la matière. |
|
|
101
|
+
| Cliquet (test) | Un garde-fou qui n'autorise qu'un sens : une liste de dette connue qui ne peut que **rétrécir**, jamais s'allonger. |
|
|
102
|
+
|
|
103
|
+
## Qu'est-ce qu'un data plane de documentation ?
|
|
104
|
+
|
|
105
|
+
Le réflexe habituel, pour publier de la doc, c'est un **générateur de site statique** : on
|
|
106
|
+
compile le Markdown en HTML, on déploie le résultat. Ça marche — tant que la doc et le code
|
|
107
|
+
ne bougent pas ensemble.
|
|
108
|
+
|
|
109
|
+
Nodefony prend l'autre bout du problème : la doc est **servie par l'application elle-même**,
|
|
110
|
+
en direct, depuis les fichiers du dépôt. Le module ne compile rien, ne rend rien, ne cache
|
|
111
|
+
aucun contenu. Il répond à deux questions, et à deux seulement :
|
|
112
|
+
|
|
113
|
+
1. **Qu'est-ce qu'il y a à lire ?** → l'index, avec ses sections et ses pages.
|
|
114
|
+
2. **Donne-moi cette page-là.** → le Markdown, métadonnées à part, prêt à afficher.
|
|
115
|
+
|
|
116
|
+
C'est ce que veut dire **headless** (`Documentation` — `index.ts:31`) : la sortie est du JSON
|
|
117
|
+
(`IDocPage`, `IDocumentation.ts:67`), et trois consommateurs très différents s'en servent —
|
|
118
|
+
le portail Studio (React), un futur générateur de site, et l'indexation RAG qui réingère le
|
|
119
|
+
Markdown brut.
|
|
120
|
+
|
|
121
|
+
> [!TIP]
|
|
122
|
+
> C'est aussi ce qui rend ta doc **vraie**. Un site statique se régénère quand quelqu'un y
|
|
123
|
+
> pense ; ici, la page servie est le fichier tel qu'il est sur le disque, à l'instant de la
|
|
124
|
+
> requête.
|
|
125
|
+
|
|
126
|
+
## La vision Nodefony — la doc vit à côté du code, l'index la rassemble
|
|
127
|
+
|
|
128
|
+
Quatre partis pris expliquent la forme du module.
|
|
129
|
+
|
|
130
|
+
**La doc appartient au module** (ADR-0001). Tu écris `mon-module/docs/ma-page.md`, tu ne
|
|
131
|
+
déclares rien, tu n'inscris rien nulle part : le scan la trouve au prochain passage. La
|
|
132
|
+
contrepartie, c'est qu'il faut un **index transverse** pour recoller des dizaines de dossiers
|
|
133
|
+
séparés — c'est précisément le travail de ce module.
|
|
134
|
+
|
|
135
|
+
**Les briques de base sont pures.** Le découpage du frontmatter (`parseFrontmatter()`,
|
|
136
|
+
`frontmatter.ts:51`), la fabrication du slug (`pathToSlug()`, `slug.ts:60`), le parcours du
|
|
137
|
+
disque (`scanDocsDir()`, `docScanner.ts:55`) et la traduction des liens
|
|
138
|
+
(`rewriteInternalLinks()`, `linkResolver.ts:90`) sont des fonctions sans état et sans Kernel.
|
|
139
|
+
Elles sont exportées telles quelles, donc réutilisables par un générateur statique ou un
|
|
140
|
+
indexeur RAG — et testables sans démarrer un serveur.
|
|
141
|
+
|
|
142
|
+
**Le slug est une clé d'allowlist, jamais un chemin.** Le module lit des fichiers du disque
|
|
143
|
+
sur ordre d'un client : c'est la définition d'une surface de traversée de répertoire. La
|
|
144
|
+
parade n'est pas un filtre de caractères, c'est un **changement de nature** — le détail est
|
|
145
|
+
plus bas.
|
|
146
|
+
|
|
147
|
+
**Ce que tu écris reste lisible sur GitHub.** Tes liens sont des chemins relatifs — un lien
|
|
148
|
+
markdown dont la cible est `cors.md` — et tes ancres suivent la convention GitHub. Le portail
|
|
149
|
+
s'adapte à ton Markdown, pas l'inverse.
|
|
150
|
+
|
|
151
|
+
**Le compromis, dit franchement** : l'index est un instantané. Un `.md` ajouté n'apparaît
|
|
152
|
+
qu'au prochain scan — 30 secondes par défaut, immédiatement si tu mets le cache à zéro.
|
|
153
|
+
|
|
154
|
+
## 🚀 Démarrage rapide
|
|
155
|
+
|
|
156
|
+
Le but : publier la doc d'une application créée par `nodefony create app`, et y injecter une
|
|
157
|
+
valeur calculée par le serveur.
|
|
158
|
+
|
|
159
|
+
### 1. Charger le module
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// nodefony.config.ts — l'orchestrateur de l'application
|
|
163
|
+
import { defineConfig, use } from "nodefony";
|
|
164
|
+
|
|
165
|
+
export default defineConfig(() => ({
|
|
166
|
+
modules: [
|
|
167
|
+
"@nodefony/framework",
|
|
168
|
+
use("@nodefony/documentation", {
|
|
169
|
+
// La doc transverse de l'app. Les `<module>/docs/` s'ajoutent tout seuls.
|
|
170
|
+
scan: { rootDir: "docs" },
|
|
171
|
+
// Le lien « voir la source » de chaque page pointe vers TON dépôt.
|
|
172
|
+
repo: { url: "https://github.com/acme/monapp", editPathPrefix: "blob" },
|
|
173
|
+
// 0 = pas de cache : un nouveau `.md` apparaît à la requête suivante.
|
|
174
|
+
cache: { ttlMs: 0 },
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
}));
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2. Écrire une page
|
|
181
|
+
|
|
182
|
+
Le fichier `docs/prise-en-main.md` de ton application, avec son frontmatter :
|
|
183
|
+
|
|
184
|
+
```markdown
|
|
185
|
+
---
|
|
186
|
+
title: "Prise en main"
|
|
187
|
+
audience: [developer]
|
|
188
|
+
status: stable
|
|
189
|
+
updated: 2026-07-19
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
# Prise en main
|
|
193
|
+
|
|
194
|
+
Besoin d'aide ? Écris à {{ support }}.
|
|
195
|
+
|
|
196
|
+
Suite : [le sommaire](index.md).
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Deux détails qui font tout le reste : `{{ support }}` sera remplacé **côté serveur**, et le
|
|
200
|
+
lien relatif sera traduit en slug navigable — sans cesser de fonctionner sur GitHub.
|
|
201
|
+
|
|
202
|
+
### 3. Fournir la variable `{{ support }}`
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// nodefony/modules/glossaire/index.ts — un module de ton application
|
|
206
|
+
import { Kernel, Module } from "nodefony";
|
|
207
|
+
import type { IDocumentationService } from "@nodefony/documentation";
|
|
208
|
+
|
|
209
|
+
class Glossaire extends Module {
|
|
210
|
+
constructor(kernel: Kernel) {
|
|
211
|
+
super("glossaire", kernel, import.meta.url, {});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// `onKernelReady` : tous les modules sont bootés, le service existe.
|
|
215
|
+
override async onKernelReady(): Promise<this> {
|
|
216
|
+
const docs = this.get<IDocumentationService>("documentation");
|
|
217
|
+
// Valeur SÛRE et synchrone : jamais un secret, jamais un chemin absolu.
|
|
218
|
+
docs?.registerVar("support", () => "support@acme.example");
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default Glossaire;
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Ce qu'on observe
|
|
227
|
+
|
|
228
|
+
L'index annonce la page, rangée dans la section de son dossier :
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
curl -s http://127.0.0.1:5151/nodefony/documentation/api/tree | head -20
|
|
232
|
+
# {
|
|
233
|
+
# "generatedAt": "2026-07-19T10:12:03.114Z",
|
|
234
|
+
# "audiences": [{ "key": "developer", "label": "Développeur", "desc": "…" }, …],
|
|
235
|
+
# "sections": [
|
|
236
|
+
# { "id": "root-racine", "label": "docs/ (racine)",
|
|
237
|
+
# "pages": [{ "slug": "root~prise-en-main", "title": "Prise en main",
|
|
238
|
+
# "audience": ["developer"], "isHub": false, "status": "stable" }] }
|
|
239
|
+
# ]
|
|
240
|
+
# }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Puis la page elle-même, variable résolue et lien traduit :
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
curl -s http://127.0.0.1:5151/nodefony/documentation/api/page/root~prise-en-main
|
|
247
|
+
# {
|
|
248
|
+
# "slug": "root~prise-en-main", "title": "Prise en main",
|
|
249
|
+
# "version": "doc", "status": "stable", "updated": "2026-07-19",
|
|
250
|
+
# "source": "docs/prise-en-main.md",
|
|
251
|
+
# "sourceUrl": "https://github.com/acme/monapp/blob/main/docs/prise-en-main.md",
|
|
252
|
+
# "markdown": "\n# Prise en main\n\nBesoin d'aide ? Écris à support@acme.example.\n…"
|
|
253
|
+
# }
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Le `markdown` rendu ne porte plus ni frontmatter, ni `{{ }}`, ni chemin relatif : la cible
|
|
257
|
+
`index.md` du lien y est devenue `root~index.md`. Trois transformations, une seule lecture de
|
|
258
|
+
fichier.
|
|
259
|
+
|
|
260
|
+
## 🏗️ Architecture interne — trois couches
|
|
261
|
+
|
|
262
|
+
Chaque couche ne connaît que sa voisine du dessous, et la plus volatile est la plus mince.
|
|
263
|
+
|
|
264
|
+
| Couche | Qui | Sa seule responsabilité | Ce qu'elle ignore |
|
|
265
|
+
| ------------- | ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------ |
|
|
266
|
+
| Contrôleur | `DocumentationController` — sans état | traduire un résultat (ou une erreur) en réponse HTTP | comment l'index est construit |
|
|
267
|
+
| Service | `DocumentationService` — le seul stateful | scanner, cacher, indexer, résoudre | qui l'appelle, et par quel transport |
|
|
268
|
+
| Briques pures | `frontmatter` · `slug` · `docScanner` · `linkResolver` | une transformation, sans état ni Kernel | qu'un serveur existe |
|
|
269
|
+
|
|
270
|
+
Le contrôleur est **réinstancié à chaque requête** : il ne peut donc rien retenir, et c'est
|
|
271
|
+
voulu. Le service est un singleton par process ; il porte l'index caché (`#cache`,
|
|
272
|
+
`DocumentationService.ts:138`) et le registre des variables (`#vars`,
|
|
273
|
+
`DocumentationService.ts:140`), tous deux à `null` tant que personne n'a rien demandé.
|
|
274
|
+
|
|
275
|
+
### Le scan — trois sources, et une qui surprend
|
|
276
|
+
|
|
277
|
+
`#scanAll()` (`DocumentationService.ts:324`) interroge le disque dans cet ordre :
|
|
278
|
+
|
|
279
|
+
| Source | Où | Pourquoi |
|
|
280
|
+
| ------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
281
|
+
| La doc transverse | `docs/` à la racine du projet | guides, ADR, architecture — ce qui n'appartient à aucun module |
|
|
282
|
+
| Les modules **chargés** | `<module>/docs/` de chaque module du manifeste | ADR-0001 : la doc vit à côté du code qu'elle décrit |
|
|
283
|
+
| Les paquets **installés** | `node_modules/@nodefony/*/docs` + `nodefony` | lire la doc d'un module **avant** de l'activer — c'est justement à ce moment-là |
|
|
284
|
+
|
|
285
|
+
La troisième mérite l'explication. Un module qu'on n'a pas encore activé est précisément
|
|
286
|
+
celui dont on lit la doc : pour décider de l'activer. `#installedDocDirs()`
|
|
287
|
+
(`DocumentationService.ts:386`) parcourt donc le scope npm et **dédoublonne** avec les
|
|
288
|
+
modules déjà chargés. Ses chemins sont résolus en real-path : en dépôt workspace,
|
|
289
|
+
`node_modules/@nodefony/x` est un lien vers la source, et c'est la source qui doit indexer —
|
|
290
|
+
sinon un même fichier aurait deux chemins, et les liens entre pages ne se résoudraient plus.
|
|
291
|
+
|
|
292
|
+
Le parcours lui-même, `scanDocsDir()` (`docScanner.ts:55`), est **best-effort** par
|
|
293
|
+
construction : un dossier absent rend une liste vide au lieu de lever une erreur. C'est ce
|
|
294
|
+
qui permet de balayer les `docs/` de modules qui n'en ont pas, sans que rien ne plante. Un
|
|
295
|
+
fichier illisible garde un titre dérivé de son nom (`humanizeFilename()`, `docScanner.ts:29`)
|
|
296
|
+
et un frontmatter vide.
|
|
297
|
+
|
|
298
|
+
L'exclusion (`isExcluded()`, `docScanner.ts:38`) compare **par segment de chemin**, pas par
|
|
299
|
+
préfixe : `node_modules` exclut le dossier, jamais un fichier nommé `node_modules-guide.md`.
|
|
300
|
+
|
|
301
|
+
### Le frontmatter — ce que le module lit vraiment
|
|
302
|
+
|
|
303
|
+
`parseFrontmatter()` (`frontmatter.ts:51`) est un parseur maison de **YAML plat**. Pas de
|
|
304
|
+
`gray-matter` : la doc n'utilise qu'un sous-ensemble minuscule, et on ne paie pas des
|
|
305
|
+
dépendances transitives pour ça.
|
|
306
|
+
|
|
307
|
+
| Tu écris | Tu obtiens |
|
|
308
|
+
| ---------------------- | ---------------------- |
|
|
309
|
+
| `title: Mon titre` | une chaîne |
|
|
310
|
+
| `title: "Mon titre"` | idem (guillemets ôtés) |
|
|
311
|
+
| `audience: [a, b]` | une liste |
|
|
312
|
+
| `audience:` puis `- a` | une liste |
|
|
313
|
+
| `audience:` (seul) | une liste **vide** |
|
|
314
|
+
| `# commentaire` | ignoré |
|
|
315
|
+
|
|
316
|
+
**Non supporté, volontairement** : objets imbriqués, multi-lignes `|` / `>`, ancres YAML.
|
|
317
|
+
Une ligne mal formée est simplement sautée — elle ne fait jamais échouer la page.
|
|
318
|
+
|
|
319
|
+
Le service ne consomme ensuite qu'une poignée de clés (`getPage()`,
|
|
320
|
+
`DocumentationService.ts:151`) : `title`, `version` (défaut `"doc"`), `status`, `updated`,
|
|
321
|
+
`source`, plus `audience` pour l'index. **Toutes les autres clés sont conservées dans le
|
|
322
|
+
fichier et ignorées** — elles servent au RAG et aux outils, pas au portail.
|
|
323
|
+
|
|
324
|
+
Deux valeurs sont **contraintes**, et le hors-piste est silencieusement écarté :
|
|
325
|
+
|
|
326
|
+
| Clé | Valeurs retenues | Sinon |
|
|
327
|
+
| ---------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
328
|
+
| `audience` | `developer` · `devops` · `supervisor` · `admin` (`DocAudience`, `IDocumentation.ts:10`) | la valeur est filtrée (`#toPageRef()`, `DocumentationService.ts:511`) |
|
|
329
|
+
| `status` | `stable` · `draft` · `temporary` · `experimental` · `deprecated` (`DocStatus`, `IDocumentation.ts:13`) | le champ devient absent (`#coerceStatus()`, `DocumentationService.ts:532`) |
|
|
330
|
+
|
|
331
|
+
> [!WARNING]
|
|
332
|
+
> Une `audience: [human, ai]` ne provoque **aucune erreur** : les deux valeurs sont
|
|
333
|
+
> écartées, la page se retrouve avec une liste vide — c'est-à-dire « visible par toutes les
|
|
334
|
+
> personas ». L'inverse de ce que l'auteur croyait écrire. Le vocabulaire exact est celui de
|
|
335
|
+
> `AUDIENCES` (`DocumentationService.ts:37`), qui porte aussi les libellés affichés par le
|
|
336
|
+
> sélecteur de vue.
|
|
337
|
+
|
|
338
|
+
Et un point à ne pas confondre : **l'audience n'est pas un contrôle d'accès**. Elle n'existe
|
|
339
|
+
que dans l'index, comme filtre de vue ; `getPage()` sert n'importe quelle page indexée quel
|
|
340
|
+
que soit le persona du lecteur. Le vrai garde est le RBAC posé sur les routes.
|
|
341
|
+
|
|
342
|
+
### Le slug — une cote, jamais un chemin
|
|
343
|
+
|
|
344
|
+
`pathToSlug()` (`slug.ts:60`) transforme un chemin en identifiant d'un seul segment :
|
|
345
|
+
|
|
346
|
+
| Fichier | Slug |
|
|
347
|
+
| --------------------------------- | ---------------------------- |
|
|
348
|
+
| `docs/index.md` | `root~index` |
|
|
349
|
+
| `docs/architecture/pipeline.md` | `root~architecture~pipeline` |
|
|
350
|
+
| `@nodefony/security/docs/cors.md` | `mod~security~cors` |
|
|
351
|
+
|
|
352
|
+
La recette : normaliser les `\` en `/`, retirer `.md`, remplacer chaque `/` par `~`, préfixer
|
|
353
|
+
par l'origine. Le nom du module perd son scope npm et tout caractère exotique
|
|
354
|
+
(`sanitizeSegment()`, `slug.ts:71`) — un module nommé n'importe comment produit quand même un
|
|
355
|
+
slug sûr.
|
|
356
|
+
|
|
357
|
+
Pourquoi `~` : le slug doit tenir dans **un** segment de route (`/api/page/{slug}`), donc il
|
|
358
|
+
ne peut pas contenir de `/`. Et la transformation est **à sens unique** : rien, nulle part,
|
|
359
|
+
ne reconstruit un chemin depuis un slug.
|
|
360
|
+
|
|
361
|
+
> [!IMPORTANT]
|
|
362
|
+
> Ne confonds pas deux slugs qui n'ont rien à voir. Celui-ci nomme une **page**
|
|
363
|
+
> (`mod~security~cors`). Les ancres de titre — celles de la forme `#pièges` — suivent une
|
|
364
|
+
> tout autre règle, celle de **GitHub** : accents **conservés**, ponctuation et emoji retirés, espaces en
|
|
365
|
+
> tirets — `slugifyHeading()` (`DocToc.tsx:54`). Retirer les accents côté portail rendait
|
|
366
|
+
> morts des liens qui marchaient sur GitHub. Toute divergence entre le portail et le gate
|
|
367
|
+
> `anchor-inpage` casse les sommaires **en silence**.
|
|
368
|
+
|
|
369
|
+
### La résolution des liens — pourquoi c'est le serveur qui traduit
|
|
370
|
+
|
|
371
|
+
Tes pages se lient par **chemin relatif**, parce que c'est ce qui les rend lisibles partout :
|
|
372
|
+
sur GitHub, dans ton éditeur, dans une revue de diff. Mais le portail ne navigue pas par
|
|
373
|
+
chemin — il navigue par slug.
|
|
374
|
+
|
|
375
|
+
Le pont, c'est une table `chemin repo → slug` construite au scan (`#ensureCache()`,
|
|
376
|
+
`DocumentationService.ts:298`) et appliquée à la lecture par `#resolveLinks()`
|
|
377
|
+
(`DocumentationService.ts:282`). **Seul le serveur peut le faire** : le client reçoit
|
|
378
|
+
`../../../../../docs/index.md` sans le moindre moyen de savoir à quel fichier ça correspond —
|
|
379
|
+
il ne connaît ni l'arborescence du dépôt, ni le point de départ de la page.
|
|
380
|
+
|
|
381
|
+
`rewriteInternalLinks()` (`linkResolver.ts:90`) applique quatre règles :
|
|
382
|
+
|
|
383
|
+
1. **Seules les cibles `.md` internes** sont touchées (`MD_LINK`, `linkResolver.ts:31`). Les
|
|
384
|
+
URL absolues, les `mailto:`, les ancres pures `#section`, les images et les `.ts` restent
|
|
385
|
+
intacts.
|
|
386
|
+
2. **Le chemin est résolu contre le dossier de la page** (`resolveRelative()`,
|
|
387
|
+
`linkResolver.ts:43`), en saturant à la racine : une remontée excessive ne peut pas sortir
|
|
388
|
+
du dépôt.
|
|
389
|
+
3. **Une cible non indexée reste telle quelle.** Mieux vaut un lien inerte qu'un slug inventé
|
|
390
|
+
qui produirait un 404.
|
|
391
|
+
4. **Les fences typées aussi.** Un catalogue de hub porte ses cibles dans du JSON
|
|
392
|
+
(`"href": "cors.md"`) : sans traduction, `JSON_HREF` (`linkResolver.ts:40`), les cards
|
|
393
|
+
d'un hub renverraient dans le vide.
|
|
394
|
+
|
|
395
|
+
L'ancre de section est **préservée** : `pipeline.md#etapes` devient `root~…~pipeline.md#etapes`.
|
|
396
|
+
Et le `.md` est conservé après réécriture — c'est à cette extension que le rendu reconnaît un
|
|
397
|
+
lien interne.
|
|
398
|
+
|
|
399
|
+
### L'ordre des pages — le hub ouvre sa section
|
|
400
|
+
|
|
401
|
+
Un tri purement alphabétique enterre `index.md` au milieu de ses propres pages : pour la
|
|
402
|
+
sécurité, entre `headers` et `lexique`. Le point d'entrée devient invisible.
|
|
403
|
+
|
|
404
|
+
`#orderPages()` (`DocumentationService.ts:486`) trie donc en deux temps : le hub d'abord, le
|
|
405
|
+
reste par titre. Un hub est reconnu à son nom de fichier — `index.md`, à n'importe quelle
|
|
406
|
+
profondeur — et le drapeau `isHub` (`IDocPageRef`, `IDocumentation.ts:24`) remonte jusqu'à
|
|
407
|
+
l'interface, où le portail s'en sert pour choisir la page d'atterrissage d'une section.
|
|
408
|
+
|
|
409
|
+
Les sections elles-mêmes (`#buildSections()`, `DocumentationService.ts:410`) viennent du
|
|
410
|
+
**dossier parent** du fichier, jamais d'une clé `section` du frontmatter. Seuls les groupes
|
|
411
|
+
DÉCLARÉS descendent dans le menu, dans l'ordre où ils sont écrits (`ROOT_GROUPS`,
|
|
412
|
+
`DocumentationService.ts:89`) : un dossier de `docs/` absent de cette liste — décisions
|
|
413
|
+
d'architecture, plan de publication, documents de pilotage — n'apparaît pas. C'est un choix, pas
|
|
414
|
+
un oubli : cette référence de mainteneur noyait le chemin de lecture. Les pages posées à la
|
|
415
|
+
racine de `docs/` ont leur propre liste (`ROOT_PAGES`, `DocumentationService.ts:103`) sous le
|
|
416
|
+
libellé « Pour commencer ». Les sections de module sont préfixées `mod-`, celles de la racine
|
|
417
|
+
`root-`.
|
|
418
|
+
|
|
419
|
+
### Le cache — l'index, pas le contenu
|
|
420
|
+
|
|
421
|
+
`#ensureCache()` (`DocumentationService.ts:298`) sert son instantané tant qu'il est dans le
|
|
422
|
+
TTL, et rescanne sinon. Ce qui est caché tient dans `CacheEntry`
|
|
423
|
+
(`DocumentationService.ts:113`) : l'arbre, l'index `slug → doc`, et la table `chemin → slug`.
|
|
424
|
+
|
|
425
|
+
Le **contenu d'une page ne l'est jamais**. Chaque `getPage()` relit le fichier. La raison est
|
|
426
|
+
simple : le coût est celui d'une lecture froide sur un chemin d'administration, et la
|
|
427
|
+
contrepartie serait de servir un Markdown périmé à quelqu'un qui vient justement de le
|
|
428
|
+
corriger.
|
|
429
|
+
|
|
430
|
+
`invalidate()` (`DocumentationService.ts:177`) remet le cache à `null` — c'est la porte de
|
|
431
|
+
sortie quand un outil sait, lui, que le disque a bougé.
|
|
432
|
+
|
|
433
|
+
## ⚙️ Configuration
|
|
434
|
+
|
|
435
|
+
Table dérivée du schéma Zod (`documentationConfigSchema`, `config.ts:134`), qui est la source
|
|
436
|
+
unique des défauts.
|
|
437
|
+
|
|
438
|
+
<!-- prettier-ignore -->
|
|
439
|
+
| Clé | Type | Défaut | Effet |
|
|
440
|
+
| --- | --- | --- | --- |
|
|
441
|
+
| `enabled` | booléen | `true` | drapeau d'activation déclaré au schéma (`config.ts:136`) |
|
|
442
|
+
| `scan.rootDir` | chaîne | `"docs"` | dossier transverse, relatif à la racine du projet |
|
|
443
|
+
| `scan.includeModules` | booléen | `true` | ajoute les `<module>/docs/` des modules chargés |
|
|
444
|
+
| `scan.includeInstalled` | booléen | `true` | ajoute les paquets installés non chargés (`config.ts:64`) |
|
|
445
|
+
| `scan.exclude` | liste de chaînes | `["session-retros", "node_modules", "dist"]` | segments de chemin ignorés (`config.ts:75`) |
|
|
446
|
+
| `repo.url` | chaîne | dépôt nodefony-core | base du lien « voir la source » |
|
|
447
|
+
| `repo.branch` | chaîne (option.) | — → branche git réelle, sinon `main` | branche du lien source |
|
|
448
|
+
| `repo.editPathPrefix` | `edit` \| `blob` \| `tree` | `"edit"` | segment GitHub : éditeur web, lecture, ou dossier |
|
|
449
|
+
| `cache.ttlMs` | entier ≥ 0 | `30000` | fraîcheur de l'index ; `0` = rescan à chaque requête (`config.ts:120`) |
|
|
450
|
+
|
|
451
|
+
Deux variables d'environnement écrasent la config, appliquées **après** le parse pour que le
|
|
452
|
+
schéma reste pur et sérialisable (`defineDocumentationConfig()`, `defineModuleConfig.ts:32`) :
|
|
453
|
+
|
|
454
|
+
| Variable | Écrase | Quand c'est utile |
|
|
455
|
+
| ------------------ | ------------- | -------------------------------------------------------- |
|
|
456
|
+
| `DOCS_REPO_URL` | `repo.url` | image de conteneur partagée entre plusieurs dépôts |
|
|
457
|
+
| `DOCS_REPO_BRANCH` | `repo.branch` | CI ou production détachée de git (pas de `.git` lisible) |
|
|
458
|
+
|
|
459
|
+
La validation a lieu au `onKernelRegister` (`index.ts:50`), **avant** l'instanciation du
|
|
460
|
+
service : une config invalide arrête le démarrage avec un message qui nomme le champ fautif,
|
|
461
|
+
plutôt qu'un `undefined.x` trois phases plus loin. Le JSON Schema publié par
|
|
462
|
+
`configSchema()` (`index.ts:40`) alimente le panneau de configuration Studio.
|
|
463
|
+
|
|
464
|
+
Enfin, le module est déclaré **non critique** (`index.ts:33`) : son échec ne tue jamais le
|
|
465
|
+
process — une application ne tombe pas parce que sa documentation est indisponible.
|
|
466
|
+
|
|
467
|
+
## 🔌 Data plane — deux routes, deux formes
|
|
468
|
+
|
|
469
|
+
`DocumentationController` (`DocumentationController.ts:31`) est monté sur `/nodefony` et
|
|
470
|
+
respecte la convention d'administration : jamais de route mono-segment, toujours
|
|
471
|
+
`/nodefony/<module>/api/*`.
|
|
472
|
+
|
|
473
|
+
| Route | Rend | Contrat |
|
|
474
|
+
| --------------------------------------------- | ----------------------------------- | ----------------------------------- |
|
|
475
|
+
| `GET /nodefony/documentation/api/tree` | l'index complet, sections ordonnées | `IDocTree` (`IDocumentation.ts:57`) |
|
|
476
|
+
| `GET /nodefony/documentation/api/page/{slug}` | une page résolue | `IDocPage` (`IDocumentation.ts:67`) |
|
|
477
|
+
|
|
478
|
+
Les deux exigent un rôle (`@IsGranted`, `DocumentationController.ts:48`) : `ROLE_DEV` ou
|
|
479
|
+
`ROLE_SUPERVISOR`. C'est de la doc technique de framework — architecture, internals — pas du
|
|
480
|
+
contenu destiné à l'utilisateur final d'une application.
|
|
481
|
+
|
|
482
|
+
Les réponses d'erreur sont **génériques par principe** :
|
|
483
|
+
|
|
484
|
+
| Situation | Statut | Corps | Journal serveur |
|
|
485
|
+
| ------------------------ | ------ | --------------------------------------------------- | ----------------- |
|
|
486
|
+
| slug inconnu | 404 | `{ slug, error: "Document inconnu." }` | `DOC_NOT_FOUND` |
|
|
487
|
+
| slug rejeté par la garde | 404 | `{ slug, error: "Document inconnu." }` | `DOC_UNSAFE_SLUG` |
|
|
488
|
+
| lecture impossible | 500 | `{ slug, error: "Lecture de la page impossible." }` | l'erreur complète |
|
|
489
|
+
| index indisponible | 500 | `{ error: "Index de documentation indisponible." }` | l'erreur complète |
|
|
490
|
+
|
|
491
|
+
Les deux premiers cas rendent **la même chose au client**, volontairement : lui dire qu'un
|
|
492
|
+
slug a été « rejeté » plutôt qu'« introuvable », c'est lui confirmer que sa tentative a été
|
|
493
|
+
détectée — et lui apprendre où chercher. Le détail vit côté serveur, porté par un code machine
|
|
494
|
+
stable (`docCode`, `DocumentationError.ts:19`).
|
|
495
|
+
|
|
496
|
+
## 🔐 Sécurité — la traversée de répertoire, bloquée deux fois
|
|
497
|
+
|
|
498
|
+
Le module lit des fichiers sur ordre d'un client. C'est la définition d'une surface de
|
|
499
|
+
traversée de répertoire — et un filtre de caractères ne suffit jamais à la fermer (encodages,
|
|
500
|
+
double-encodage, normalisation Unicode…).
|
|
501
|
+
|
|
502
|
+
La parade est un **changement de nature**, doublé d'un garde :
|
|
503
|
+
|
|
504
|
+
1. **Allowlist par construction.** `getPage()` (`DocumentationService.ts:244`) cherche une
|
|
505
|
+
entrée par **égalité de slug** dans l'index, puis lit l'`absPath` mémorisé au scan
|
|
506
|
+
(`ScannedDoc`, `docScanner.ts:11`). Le slug n'est jamais concaténé à un chemin. Un slug
|
|
507
|
+
inconnu ne mène nulle part, quelle que soit sa forme.
|
|
508
|
+
2. **Défense en profondeur, avant même la recherche.** `isSafeSlug()` (`slug.ts:39`) rejette
|
|
509
|
+
la chaîne vide, la longueur au-delà de 512 (`MAX_SLUG_LENGTH`, `slug.ts:28`), l'octet nul,
|
|
510
|
+
tout caractère hors du charset autorisé (`SAFE_SLUG`, `slug.ts:25`) et tout segment `..`,
|
|
511
|
+
même déguisé en séparateur `~`.
|
|
512
|
+
|
|
513
|
+
Le charset exclut `%`, donc `%2e%2e` est refusé comme n'importe quel autre caractère
|
|
514
|
+
inattendu — la question du double-décodage ne se pose pas.
|
|
515
|
+
|
|
516
|
+
Une troisième règle protège une surface différente : les variables `{{ }}` sont résolues par
|
|
517
|
+
des fournisseurs enregistrés côté serveur (`DocVarProvider`, `IDocumentation.ts:89`), et ne
|
|
518
|
+
doivent rendre que des valeurs **sûres** — version, identité git, information publique. Jamais
|
|
519
|
+
un secret, jamais un chemin absolu. Une variable inconnue est **laissée telle quelle**
|
|
520
|
+
(`#resolveVars()`, `DocumentationService.ts:541`), ce qui signale à l'auteur qu'il manque un
|
|
521
|
+
fournisseur au lieu de masquer le trou. Un fournisseur qui lève une exception ne casse pas le
|
|
522
|
+
rendu.
|
|
523
|
+
|
|
524
|
+
Enfin, le lien « voir la source » est assemblé depuis un chemin **relatif au dépôt**
|
|
525
|
+
(`#buildSourceUrl()`, `DocumentationService.ts:560`) : aucun chemin du système de fichiers ne
|
|
526
|
+
sort jamais du serveur.
|
|
527
|
+
|
|
528
|
+
## ⚡ Performance & mémoire
|
|
529
|
+
|
|
530
|
+
Le module vit sur un chemin **froid** — un humain qui lit de la doc, pas dix mille requêtes
|
|
531
|
+
par seconde. La discipline reste la même.
|
|
532
|
+
|
|
533
|
+
- **Tout est alloué paresseusement.** L'index (`#cache`, `DocumentationService.ts:138`) et le
|
|
534
|
+
registre de variables (`#vars`, `DocumentationService.ts:140`) valent `null` jusqu'au
|
|
535
|
+
premier usage. Une application qui charge le module sans jamais ouvrir la doc ne paie ni un
|
|
536
|
+
objet, ni une lecture disque.
|
|
537
|
+
- **Le scan est mutualisé.** Les modules sont parcourus en parallèle, et le résultat sert
|
|
538
|
+
toutes les requêtes de la fenêtre de TTL. Le coût du disque suit le nombre de rescans, pas
|
|
539
|
+
le nombre de lecteurs.
|
|
540
|
+
- **Aucun écouteur, aucun minuteur.** L'expiration est calculée à la lecture (une
|
|
541
|
+
comparaison de dates), pas par un `setInterval` qui tournerait au repos.
|
|
542
|
+
- **Une seule lecture par page servie.** Frontmatter, variables et liens sont traités sur la
|
|
543
|
+
même chaîne, en un passage chacun.
|
|
544
|
+
- **Rien n'est retenu entre deux requêtes HTTP.** Le contrôleur est réinstancié et sans état ;
|
|
545
|
+
la mémoire du module est bornée par la taille de l'index, pas par le trafic.
|
|
546
|
+
|
|
547
|
+
Le seul vrai facteur de coût est le **nombre de fichiers scannés**, multiplié par la fréquence
|
|
548
|
+
des rescans. En développement, `cache.ttlMs: 0` échange ce coût contre l'immédiateté ; en
|
|
549
|
+
production, les 30 secondes par défaut le rendent négligeable.
|
|
550
|
+
|
|
551
|
+
## 📡 Observabilité — Studio
|
|
552
|
+
|
|
553
|
+
- **Le portail** (`/nodefony/documentation`) consomme les deux routes : arbre à gauche,
|
|
554
|
+
sommaire à droite, page au centre. C'est le premier endroit où vérifier qu'une nouvelle page
|
|
555
|
+
est bien indexée, bien rangée, et que ses liens cliquent.
|
|
556
|
+
- **La carte du module** (`/nodefony/modules/documentation`) montre sa doc, ses symboles, ses
|
|
557
|
+
tests et sa configuration validée — le formulaire y est dérivé du JSON Schema publié par
|
|
558
|
+
`configSchema()` (`index.ts:40`), jamais écrit à la main.
|
|
559
|
+
- **Le journal** nomme chaque refus avec son code stable (`DOC_NOT_FOUND`, `DOC_UNSAFE_SLUG`).
|
|
560
|
+
Une page qui « n'apparaît pas » se diagnostique là, en une ligne.
|
|
561
|
+
|
|
562
|
+
Le module n'expose **rien de plus** : pas de compteur, pas de sonde. Ce qu'il fait est déjà
|
|
563
|
+
entièrement lisible dans ses deux réponses.
|
|
564
|
+
|
|
565
|
+
## 🧩 Extension — trois points d'accroche
|
|
566
|
+
|
|
567
|
+
**1. Une variable `{{ }}`** — le point d'extension du contenu. `registerVar()`
|
|
568
|
+
(`DocumentationService.ts:138`) accepte un fournisseur **synchrone** qui rend une chaîne
|
|
569
|
+
(l'exemple du Démarrage rapide). Le module en enregistre trois lui-même au `onKernelReady`
|
|
570
|
+
(`index.ts:70`) : `version`, `branch`, `commit`.
|
|
571
|
+
|
|
572
|
+
**2. Les briques pures** — le point d'extension de l'outillage. `parseFrontmatter()`,
|
|
573
|
+
`scanDocsDir()`, `pathToSlug()`, `isSafeSlug()` sont exportées par le paquet et n'ont besoin
|
|
574
|
+
ni de Kernel ni de conteneur. Un générateur de site statique, un indexeur RAG ou un script de
|
|
575
|
+
vérification les réutilisent directement, avec exactement la sémantique du portail.
|
|
576
|
+
|
|
577
|
+
**3. Le data plane lui-même** — le point d'extension de l'affichage. Le module étant headless,
|
|
578
|
+
tout consommateur capable de lire du JSON peut se substituer au portail Studio sans qu'une
|
|
579
|
+
ligne change côté serveur.
|
|
580
|
+
|
|
581
|
+
## ⚠️ Pièges
|
|
582
|
+
|
|
583
|
+
| Symptôme | Cause | Correction |
|
|
584
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
585
|
+
| Une nouvelle page n'apparaît pas | l'index est encore dans son TTL (30 s par défaut) | attendre, ou poser `cache.ttlMs: 0` en développement |
|
|
586
|
+
| Une page reste introuvable même après rescan | son dossier porte un segment exclu (`node_modules`, `dist`, `session-retros`) | déplacer la page, ou ajuster `scan.exclude` (`config.ts:75`) |
|
|
587
|
+
| `audience` sans effet, page visible par tous | valeur hors `DocAudience` — silencieusement filtrée (`#toPageRef()`) | n'utiliser que `developer` · `devops` · `supervisor` · `admin` |
|
|
588
|
+
| `status` absent de l'arbre alors qu'il est écrit | valeur hors `DocStatus` — ramenée à « absent » (`#coerceStatus()`) | s'en tenir aux cinq statuts du contrat |
|
|
589
|
+
| La date de la page ne s'affiche pas | clé `last-updated` au lieu de `updated` — le service ne lit que `updated` | renommer la clé en `updated` |
|
|
590
|
+
| Un lien relatif reste inerte dans le portail | la cible n'est pas indexée (`CLAUDE.md`, `MEMORY.md`, fichier supprimé) → laissée telle quelle | lier une page de doc, ou accepter le lien inerte |
|
|
591
|
+
| Un lien de card ne mène nulle part | `href` d'une fence typée mal compté (le JSON est traduit comme le markdown, mais pas deviné) | vérifier le chemin relatif ; le banc de corpus l'attrape |
|
|
592
|
+
| Une ancre `#section` marche sur GitHub, morte dans le portail | divergence entre `slugifyHeading()` (`DocToc.tsx:54`) et le gate `anchor-inpage` | garder les deux implémentations identiques — accents conservés |
|
|
593
|
+
| Le bouton « voir la source » pointe vers un mauvais fichier | le frontmatter `source:` **écrase** le chemin réel dans `#buildSourceUrl()` | tenir `source:` à jour, ou l'omettre pour laisser le chemin réel gagner |
|
|
594
|
+
| Le lien source pointe vers une branche absente en production | pas de `.git` lisible dans le conteneur → repli sur `main` | poser `DOCS_REPO_BRANCH` (ou `repo.branch`) |
|
|
595
|
+
| Une clé de frontmatter n'a aucun effet | seules `title` · `audience` · `version` · `status` · `updated` · `source` sont consommées | comportement voulu : les autres clés servent au RAG |
|
|
596
|
+
| Un frontmatter multi-lignes (` | `) casse le titre | non supporté par le parseur plat (`frontmatter.ts:51`) | rester en YAML plat : scalaire ou liste |
|
|
597
|
+
|
|
598
|
+
## 🧪 Tests & couverture
|
|
599
|
+
|
|
600
|
+
Cinq fichiers, tous **unitaires** : les briques pures se testent sans serveur, sans Kernel et
|
|
601
|
+
sans conteneur — c'est précisément la raison de les avoir isolées. Les compteurs exacts vivent
|
|
602
|
+
dans la carte de l'aperçu, régénérés depuis les résultats réels.
|
|
603
|
+
|
|
604
|
+
| Banc | Ce qui est réellement exercé |
|
|
605
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
606
|
+
| `frontmatter.test.ts` | scalaires, guillemets, listes inline et en bloc, clé vide, commentaires, BOM, CRLF, ligne mal formée |
|
|
607
|
+
| `slug.test.ts` | forme des slugs racine et module, et surtout les **refus** : vide, > 512, octet nul, `/`, `\`, `..`, `%` |
|
|
608
|
+
| `docScanner.test.ts` | dossier absent → `[]`, filtre `.md`, segments exclus, tri, groupe, titre humanisé, tag de source |
|
|
609
|
+
| `linkResolver.test.ts` | lien plat, remontée profonde, module voisin, ancre préservée, cible non indexée, fences typées |
|
|
610
|
+
| `corpusLinks.test.ts` | le **corpus réel** du dépôt : liens morts, unicité des slugs, hubs atteignables |
|
|
611
|
+
|
|
612
|
+
Le dernier mérite qu'on s'y arrête. Les autres travaillent sur un index fabriqué ; celui-là
|
|
613
|
+
parcourt les vraies pages et attrape ce qu'aucun double ne peut voir : un `../` mal compté,
|
|
614
|
+
une page renommée, un lien vers un fichier supprimé (`analyze()`, `corpusLinks.test.ts:120`).
|
|
615
|
+
|
|
616
|
+
Il porte un **cliquet** : `LEGACY_BROKEN_LINKS` (`corpusLinks.test.ts:147`) liste les pages
|
|
617
|
+
pas encore reprises au standard, qui traînent des liens faux hérités. Deux assertions
|
|
618
|
+
l'encadrent — les pages hors liste ne doivent avoir **aucun** lien mort, et une page de la
|
|
619
|
+
liste qui a été réparée doit en **sortir**. Sans cette seconde garde, la liste se relâcherait
|
|
620
|
+
en silence et une régression future passerait inaperçue. La règle est simple : cette liste ne
|
|
621
|
+
peut que rétrécir.
|
|
622
|
+
|
|
623
|
+
**Ce qui n'est pas couvert, et qu'il faut savoir :**
|
|
624
|
+
|
|
625
|
+
- **Ni le service ni le contrôleur n'ont de test unitaire** : ils dépendent du Kernel et du
|
|
626
|
+
conteneur. Le cache, le dédoublonnage des paquets installés, la résolution des variables et
|
|
627
|
+
les réponses HTTP sont vérifiés en **intégration sur serveur réel** (`curl` sur les deux
|
|
628
|
+
routes), pas par cette suite.
|
|
629
|
+
- **Pas de banc de charge ni de test mémoire dédiés** — le module vit sur un chemin froid.
|
|
630
|
+
Pour dimensionner, le skill `nodefony-load-test` ; pour la mémoire du pipeline,
|
|
631
|
+
`nodefony-check-memory-health`.
|
|
632
|
+
- **Pas de test d'attaque** (`*.attack.test.ts`) : les refus de slug sont couverts par les
|
|
633
|
+
tests unitaires de `isSafeSlug()`, pas par une campagne offensive.
|
|
634
|
+
|
|
635
|
+
Couverture : `npm run coverage` dans `@nodefony/documentation`.
|
|
636
|
+
|
|
637
|
+
## 🔗 Pour aller plus loin
|
|
638
|
+
|
|
639
|
+
- ⬆️ **Retour au hub** : [Documentation — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
640
|
+
- 📐 **La décision fondatrice** : [ADR-0001 — emplacement hybride de la doc](../../../../../docs/adr/0001-docs-modules-emplacement-hybride.md)
|
|
641
|
+
- 🖥️ **Le consommateur** : [Studio — l'application d'administration](../../studio/docs/index.md)
|
|
642
|
+
- 🧰 **Écrire le contrôleur qui consomme le data plane** : [Controller](../../framework/docs/controller.md)
|
|
643
|
+
- 🔐 **Le rôle exigé par les deux routes** : [Autorisation](../../security/docs/authorization.md)
|
|
644
|
+
- ⚙️ **Où la config du module est validée** : [Configuration](../../../../../docs/architecture/configuration.md) ·
|
|
645
|
+
[cycle de démarrage du kernel](../../../../../docs/architecture/cycle-boot-kernel.md)
|
|
646
|
+
- Les signatures exactes ne sont jamais recopiées ici : elles vivent dans le graphe symbolique
|
|
647
|
+
`.ai/symbols.json`, régénéré depuis les TSDoc du code.
|