@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.
Files changed (34) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +168 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +75 -0
  7. package/dist/nodefony/config/config.js +71 -0
  8. package/dist/nodefony/config/defineModuleConfig.js +49 -0
  9. package/dist/nodefony/controller/DocumentationController.js +102 -0
  10. package/dist/nodefony/interfaces/IDocumentation.js +1 -0
  11. package/dist/nodefony/interfaces/index.js +1 -0
  12. package/dist/nodefony/service/DocumentationService.js +421 -0
  13. package/dist/nodefony/src/docScanner.js +58 -0
  14. package/dist/nodefony/src/errors/DocumentationError.js +45 -0
  15. package/dist/nodefony/src/frontmatter.js +63 -0
  16. package/dist/nodefony/src/linkResolver.js +68 -0
  17. package/dist/nodefony/src/search.js +120 -0
  18. package/dist/nodefony/src/slug.js +62 -0
  19. package/dist/types/index.d.ts +58 -0
  20. package/dist/types/nodefony/config/config.d.ts +37 -0
  21. package/dist/types/nodefony/config/defineModuleConfig.d.ts +17 -0
  22. package/dist/types/nodefony/controller/DocumentationController.d.ts +33 -0
  23. package/dist/types/nodefony/interfaces/IDocumentation.d.ts +144 -0
  24. package/dist/types/nodefony/interfaces/index.d.ts +5 -0
  25. package/dist/types/nodefony/service/DocumentationService.d.ts +78 -0
  26. package/dist/types/nodefony/src/docScanner.d.ts +47 -0
  27. package/dist/types/nodefony/src/errors/DocumentationError.d.ts +32 -0
  28. package/dist/types/nodefony/src/frontmatter.d.ts +37 -0
  29. package/dist/types/nodefony/src/linkResolver.d.ts +55 -0
  30. package/dist/types/nodefony/src/search.d.ts +64 -0
  31. package/dist/types/nodefony/src/slug.d.ts +48 -0
  32. package/docs/architecture.md +647 -0
  33. package/docs/index.md +369 -0
  34. package/package.json +81 -0
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @nodefony/documentation
2
+
3
+ **Data plane de documentation transverse de Nodefony** — un module _headless_ (back pur) qui indexe
4
+ toute la documentation du projet, résout des variables dynamiques côté serveur, et l'expose en JSON
5
+ sous `/nodefony/documentation/api/*`.
6
+
7
+ Il ne rend **aucune page HTML**. Le rendu est laissé au consommateur : le front Studio (page React),
8
+ un générateur de site statique, ou le pipeline RAG. Le module se contente de fournir un **index** et
9
+ le **contenu résolu** des pages.
10
+
11
+ ## Ce qu'il indexe
12
+
13
+ Conformément à [ADR-0001](https://github.com/nodefony/nodefony-core/blob/claude-ts/docs/adr/0001-docs-modules-emplacement-hybride.md) (emplacement
14
+ hybride), la doc vit à deux endroits, et les deux sont scannés :
15
+
16
+ 1. **`docs/` racine** — la doc transverse, qui n'appartient à aucun module (guides, ADR, audits, releases).
17
+ 2. **`<module>/docs/*.md`** — la doc co-localisée à chaque module (`src/packages/@nodefony/<m>/docs/`,
18
+ `src/nodefony/docs/`). Activable/désactivable via `scan.includeModules`.
19
+
20
+ ## Installation / activation
21
+
22
+ Le module est déclaré dans les `@modules()` de l'application, **après** `@nodefony/framework`
23
+ (dépendance des décorateurs `@controller`) et **avant** `@nodefony/studio` (dont le front consomme ce
24
+ data plane) :
25
+
26
+ ```ts
27
+ // index.ts (racine app)
28
+ @modules([
29
+ // …
30
+ "@nodefony/documentation",
31
+ "@nodefony/studio",
32
+ ])
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Surcharge depuis la config applicative sous la clé `module-documentation` (fusion récursive) :
38
+
39
+ ```ts
40
+ // src/modules/app/nodefony/config/config.ts
41
+ export default {
42
+ "module-documentation": {
43
+ scan: { includeModules: false }, // racine seule
44
+ repo: { url: "https://github.com/acme/app", editPathPrefix: "blob" },
45
+ cache: { ttlMs: 0 }, // rescan à chaque requête (dev)
46
+ },
47
+ };
48
+ ```
49
+
50
+ <!-- prettier-ignore -->
51
+ | Option | Défaut | Rôle |
52
+ | --- | --- | --- |
53
+ | `enabled` | `true` | Active le data plane au boot |
54
+ | `scan.rootDir` | `"docs"` | Dossier de doc transverse, relatif à `kernel.path` |
55
+ | `scan.includeModules` | `true` | Scanne aussi les `<module>/docs/*.md` |
56
+ | `scan.exclude` | `["session-retros","node_modules","dist"]` | Segments de chemin ignorés |
57
+ | `repo.url` | dépôt nodefony-core | Base du lien « Modifier sur GitHub » (URL publique) |
58
+ | `repo.branch` | _(auto)_ | Branche du lien ; si omise → branche git réelle (`GitService`) |
59
+ | `repo.editPathPrefix` | `"edit"` | Segment GitHub : `edit` / `blob` / `tree` |
60
+ | `cache.ttlMs` | `30000` | TTL (ms) du cache de l'**index** ; `0` = pas de cache |
61
+
62
+ **Variables d'environnement** (précédence maximale, utiles en CI/conteneur sans `.git`) :
63
+ `DOCS_REPO_URL`, `DOCS_REPO_BRANCH`.
64
+
65
+ ## API HTTP
66
+
67
+ ### `GET /nodefony/documentation/api/tree`
68
+
69
+ Index transverse — sections (par dossier racine, et par module) → pages, taguées par audience.
70
+
71
+ ```jsonc
72
+ {
73
+ "generatedAt": "2026-05-31T…",
74
+ "audiences": [{ "key": "developer", "label": "Développeur", "desc": "…" }, …],
75
+ "sections": [
76
+ { "id": "root-guides", "label": "Guides", "pages": [{ "slug": "root~guides~intro", "title": "Intro", "audience": [] }] },
77
+ { "id": "mod-http", "label": "Module @nodefony/http", "module": "@nodefony/http", "pages": [ … ] }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### `GET /nodefony/documentation/api/page/{slug}`
83
+
84
+ Contenu d'une page : markdown sans frontmatter, variables `{{ }}` résolues, lien source assemblé.
85
+
86
+ ```jsonc
87
+ {
88
+ "slug": "mod~http~index",
89
+ "title": "…",
90
+ "version": "10.0.0",
91
+ "status": "stable",
92
+ "updated": "2026-05-31",
93
+ "source": "src/packages/@nodefony/http/docs/index.md",
94
+ "sourceUrl": "https://github.com/nodefony/nodefony-core/edit/claude-ts/…",
95
+ "markdown": "# …",
96
+ }
97
+ ```
98
+
99
+ Slug inconnu ou non sûr → **404** avec un corps générique (`{ "slug": "…", "error": "Document inconnu." }`).
100
+
101
+ ## Schéma de slug
102
+
103
+ Le slug est un identifiant **URL-safe sur un seul segment** (`{slug}` dans la route). C'est une **clé
104
+ d'allowlist**, jamais un chemin de fichier.
105
+
106
+ | Source | Exemple de fichier | Slug |
107
+ | ------ | -------------------------------- | ----------------------------- |
108
+ | Racine | `docs/guides/session-storage.md` | `root~guides~session-storage` |
109
+ | Module | `@nodefony/http/docs/index.md` | `mod~http~index` |
110
+
111
+ Le `/` devient `~` ; le scope npm (`@nodefony/`) est retiré.
112
+
113
+ ## Frontmatter supporté
114
+
115
+ Bloc optionnel en tête de fichier, encadré de `---`. YAML **plat** uniquement (clé → scalaire ou liste) :
116
+
117
+ ```markdown
118
+ ---
119
+ title: La Socket Nodefony
120
+ audience: [developer, devops]
121
+ version: "10.0.0"
122
+ status: stable
123
+ updated: 2026-05-31
124
+ ---
125
+
126
+ # Contenu…
127
+ ```
128
+
129
+ Champs lus : `title`, `audience` (parmi `developer`/`devops`/`supervisor`/`admin`), `version`,
130
+ `status` (`stable`/`draft`/`temporary`/`experimental`/`deprecated`), `updated`, `source`.
131
+ Non supporté (volontaire) : objets imbriqués, blocs multilignes `|`/`>`, ancres YAML.
132
+
133
+ ## Variables dynamiques `{{ }}`
134
+
135
+ Le serveur remplace `{{ name }}` dans le markdown par la valeur d'un fournisseur enregistré. Built-in :
136
+ `{{ version }}`, `{{ branch }}`, `{{ commit }}`. Un nom inconnu est laissé tel quel (signale à l'auteur
137
+ qu'il manque un provider). Enregistrer une variable :
138
+
139
+ ```ts
140
+ documentationService.registerVar("rps", () => String(computeRps()));
141
+ ```
142
+
143
+ > ⚠️ Un provider doit retourner une valeur **sûre** (publique, dérivée) — jamais un secret ni un
144
+ > chemin FS absolu.
145
+
146
+ ## Sécurité
147
+
148
+ - Le slug est validé (`isSafeSlug`) **avant** toute recherche ou lecture : rejet de `..`, `/`, `\`,
149
+ octet nul, charset non autorisé, longueur > 512.
150
+ - La lecture se fait toujours sur le **chemin réel** mémorisé au scan, jamais reconstruit depuis le slug.
151
+ - Les erreurs renvoient un message **générique** au client ; le détail est journalisé côté serveur.
152
+
153
+ ## Briques pures réutilisables
154
+
155
+ Exportées pour un futur générateur de site statique ou le RAG (testées unitairement) :
156
+ `parseFrontmatter`, `metaString`, `metaList`, `scanDocsDir`, `isSafeSlug`, `pathToSlug`.
157
+
158
+ ## Développement
159
+
160
+ ```bash
161
+ cd src/packages/@nodefony/documentation
162
+ npm test # vitest — briques pures (frontmatter / slug / docScanner)
163
+ npm run build # rolldown + tsgo → dist/ + dist/types
164
+ ```
165
+
166
+ ## Licence
167
+
168
+ CeCILL-B — Christophe CAMENSULI.
@@ -0,0 +1,9 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+ //#endregion
9
+ export { __decorate as default };
@@ -0,0 +1,6 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
2
+ function __decorateMetadata(k, v) {
3
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
+ }
5
+ //#endregion
6
+ export { __decorateMetadata as default };
@@ -0,0 +1,8 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateParam.js
2
+ function __decorateParam(paramIndex, decorator) {
3
+ return function(target, key) {
4
+ decorator(target, key, paramIndex);
5
+ };
6
+ }
7
+ //#endregion
8
+ export { __decorateParam as default };
package/dist/index.js ADDED
@@ -0,0 +1,75 @@
1
+ import config, { documentationConfigSchema } from "./nodefony/config/config.js";
2
+ import { defineDocumentationConfig, documentationConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
3
+ import { metaList, metaString, parseFrontmatter } from "./nodefony/src/frontmatter.js";
4
+ import { isSafeSlug, pathToSlug } from "./nodefony/src/slug.js";
5
+ import { scanDocsDir } from "./nodefony/src/docScanner.js";
6
+ import { rewriteInternalLinks } from "./nodefony/src/linkResolver.js";
7
+ import { extractSearchText, foldText, searchDocs, splitSearchTerms } from "./nodefony/src/search.js";
8
+ import { DocNotFoundError, DocUnsafeSlugError, DocumentationError } from "./nodefony/src/errors/DocumentationError.js";
9
+ import DocumentationService, { ROOT_GROUPS, ROOT_PAGES } from "./nodefony/service/DocumentationService.js";
10
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
11
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
12
+ import DocumentationController_default from "./nodefony/controller/DocumentationController.js";
13
+ import { GitService, Kernel, Module, services } from "nodefony";
14
+ import { controllers } from "@nodefony/framework";
15
+ //#region index.ts
16
+ /**
17
+ * @nodefony/documentation — Data plane de documentation transverse de Nodefony.
18
+ *
19
+ * Module **headless** (back pur) : il indexe la documentation co-localisée
20
+ * (`docs/` racine transverse + `<module>/docs/*.md`, ADR-0001), résout les
21
+ * variables dynamiques `{{ }}` côté serveur, et expose le tout sous
22
+ * `/nodefony/documentation/api/*`. Il ne rend AUCUN HTML — le front Studio (et,
23
+ * demain, un générateur de site statique ou le RAG P12) consomme ce data plane.
24
+ *
25
+ * Pourquoi un module dédié (et pas un simple controller Studio) : la doc porte
26
+ * de l'**état** (index scanné, cache TTL, registre de providers `{{ }}`) → un
27
+ * cycle de vie propre, hors hot path request (tout lazy). Cf
28
+ * [[project_doc_portal_faisabilite]].
29
+ *
30
+ * Voir aussi : CLAUDE.md (décisions figées), MEMORY.md (internals IA),
31
+ * README.md (usage humain), docs/ (doc vulgarisée surfacée dans Studio).
32
+ */
33
+ let Documentation = class Documentation extends Module {
34
+ /** Module optionnel : un échec de son boot ne tue jamais le process (résilience Ph.3). */
35
+ static critical = false;
36
+ constructor(kernel) {
37
+ super("documentation", kernel, import.meta.url, config);
38
+ }
39
+ /** JSON Schema de la config documentation → data plane admin (config riche Studio). */
40
+ configSchema() {
41
+ return documentationConfigJsonSchema();
42
+ }
43
+ /**
44
+ * Phase `onRegister` : valide la config (défauts + override `module-documentation`
45
+ * + env) via `defineDocumentationConfig`, puis la ré-assigne à `this.options`
46
+ * AVANT l'instanciation du `@services` (phase `onBoot`). Plante propre avec
47
+ * messages clairs si la config est invalide (convention Zod figée 2026-05-28).
48
+ */
49
+ async onKernelRegister() {
50
+ this.options = defineDocumentationConfig(this.options);
51
+ return this;
52
+ }
53
+ /**
54
+ * Phase `onReady` : enregistre les fournisseurs de variables `{{ }}` built-in
55
+ * sur le service (tous les modules sont alors bootés). Sources SÛRES seulement
56
+ * (version, identité git) — jamais de secret ni de chemin FS absolu.
57
+ */
58
+ async onKernelReady() {
59
+ const svc = this.get("documentation");
60
+ if (!svc) return this;
61
+ const root = this.kernel?.path;
62
+ svc.registerVar("version", () => this.kernel?.version ?? "");
63
+ svc.registerVar("branch", () => GitService.branch(root));
64
+ svc.registerVar("commit", () => GitService.read(root).commit);
65
+ return this;
66
+ }
67
+ };
68
+ Documentation = __decorate([
69
+ services([DocumentationService]),
70
+ controllers([DocumentationController_default]),
71
+ __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])
72
+ ], Documentation);
73
+ var documentation_default = Documentation;
74
+ //#endregion
75
+ export { DocNotFoundError, DocUnsafeSlugError, DocumentationController_default as DocumentationController, DocumentationError, DocumentationService, ROOT_GROUPS, ROOT_PAGES, documentation_default as default, defineDocumentationConfig, documentationConfigJsonSchema, documentationConfigSchema, extractSearchText, foldText, isSafeSlug, metaList, metaString, parseFrontmatter, pathToSlug, rewriteInternalLinks, scanDocsDir, searchDocs, splitSearchTerms };
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ //#region nodefony/config/config.ts
3
+ /**
4
+ * @nodefony/documentation — CONFIGURATION DU MODULE (schéma Zod = source unique).
5
+ *
6
+ * ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` = la valeur d'usine ;
7
+ * changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
8
+ * surcharge via `use("@nodefony/...", { … })` dans SON `nodefony.config.ts`.
9
+ *
10
+ * RÈGLE D'OR (ADR-0006) : ce fichier porte le **schéma Zod commenté** (type +
11
+ * validation + défaut + doc) ET matérialise les défauts via `parse({})`. Aucune
12
+ * valeur n'est re-tapée ailleurs. Le builder (`defineModuleConfig.ts` →
13
+ * `defineDocumentationConfig`) importe le schéma D'ICI (nœud bas : ce fichier
14
+ * n'importe que `zod` → pas de cycle).
15
+ *
16
+ * La config est validée au boot du Module class (hook `onKernelRegister`, via
17
+ * le builder {@link defineDocumentationConfig}) → plante propre avec messages
18
+ * clairs si la config est invalide, plutôt qu'un `undefined.x` silencieux.
19
+ *
20
+ * ⚠️ ENV : ce schéma reste PUR (pas de lecture `process.env` ici, sinon il
21
+ * deviendrait non déterministe et non sérialisable en JSON Schema pour Studio).
22
+ * La surcharge par variables d'environnement (`DOCS_REPO_URL`,
23
+ * `DOCS_REPO_BRANCH`) est appliquée dans {@link defineDocumentationConfig},
24
+ * APRÈS le parse.
25
+ *
26
+ * SURCHARGE PAR L'APPLICATION (fusion récursive) :
27
+ *
28
+ * // nodefony.config.ts
29
+ * use("@nodefony/documentation", {
30
+ * scan: { includeModules: false },
31
+ * repo: { url: "https://github.com/acme/app", editPathPrefix: "blob" },
32
+ * cache: { ttlMs: 0 },
33
+ * })
34
+ *
35
+ * ⚠️ NE PAS éditer les défauts matérialisés en bas de fichier : modifier les
36
+ * `.default(...)` du schéma. La validation + le merge env finaux sont faits dans
37
+ * `index.ts` au hook `onKernelRegister` via `defineDocumentationConfig`.
38
+ */
39
+ const scanSchema = z.strictObject({
40
+ rootDir: z.string().min(1).default("docs").describe("Dossier de documentation transverse, relatif à la racine du projet (`kernel.path`). Défaut `docs`. Scanné récursivement pour les `.md`. C'est la doc qui n'appartient à aucun module (guides, ADR, audits)."),
41
+ includeModules: z.boolean().default(true).describe("Scanne aussi les `<module>/docs/*.md` co-localisés à chaque module chargé (ADR-0001 : la doc d'un module vit DANS le module). true = index transverse complet (racine + modules). false = racine seule (parité POC). La découverte des modules passe par `kernel.modules`."),
42
+ includeInstalled: z.boolean().default(true).describe("Scanne aussi la doc des paquets Nodefony INSTALLÉS mais pas encore chargés (`node_modules/@nodefony/*/docs` + le cœur `nodefony`). Sans cela, la doc d'un module non activé est introuvable — alors que c'est précisément le moment où on la lit : pour décider de l'activer. Les chemins sont résolus en real-path, donc un lien de workspace pointe vers la source, pas vers le lien symbolique."),
43
+ exclude: z.array(z.string().min(1)).default([
44
+ "session-retros",
45
+ "node_modules",
46
+ "dist"
47
+ ]).describe("Noms de segments de chemin EXCLUS du scan (comparaison par segment, pas par préfixe). Défaut : retex de session, deps et build. Évite de surfacer du bruit ou des fichiers générés dans le portail.")
48
+ }).describe("Sources scannées pour construire l'index transverse de la doc.");
49
+ const repoSchema = z.strictObject({
50
+ url: z.string().min(1).default("https://github.com/nodefony/nodefony-core").describe("URL de base du dépôt (sans slash final), pour construire le lien « Modifier sur GitHub » d'une page. Surchargeable par l'env `DOCS_REPO_URL`. Aucun secret — URL publique uniquement."),
51
+ branch: z.string().min(1).optional().describe("Branche utilisée dans le lien d'édition. Si OMISE (défaut), la branche RÉELLE est résolue au runtime via `GitService.branch()` du core (lecture `.git/HEAD`, 0 spawn) → le lien suit toujours la branche courante. Surchargeable par l'env `DOCS_REPO_BRANCH` (utile en CI/prod détaché de git, ex. conteneur sans `.git`)."),
52
+ editPathPrefix: z.enum([
53
+ "edit",
54
+ "blob",
55
+ "tree"
56
+ ]).default("edit").describe("Segment GitHub du lien source : `edit` (éditeur web), `blob` (lecture du fichier), `tree` (dossier). Défaut `edit`.")
57
+ }).describe("Identité du dépôt pour les liens d'édition des pages.");
58
+ const cacheSchema = z.strictObject({ ttlMs: z.number().int().nonnegative().default(3e4).describe("Durée de vie (ms) du cache de l'index (l'arbre des pages). Le scan FS n'est refait qu'à l'expiration. Défaut 30 s. 0 = pas de cache (chaque requête rescanne — pratique en dev pour voir un nouveau `.md` immédiatement). Le contenu d'une page n'est PAS caché (toujours relu).") }).describe("Politique de cache de l'index (chemin froid admin, lazy).");
59
+ const documentationConfigSchema = z.strictObject({
60
+ enabled: z.boolean().default(true).describe("Active le data plane de documentation au boot. false = module chargé mais inerte (endpoints inactifs) — utile pour couper la doc en prod si non désirée."),
61
+ scan: scanSchema.default(() => scanSchema.parse({})),
62
+ repo: repoSchema.default(() => repoSchema.parse({})),
63
+ cache: cacheSchema.default(() => cacheSchema.parse({}))
64
+ }).describe("Configuration de @nodefony/documentation.");
65
+ /**
66
+ * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
67
+ * valides par construction ; passés au `super(..., config)` du Module class.
68
+ */
69
+ const config = documentationConfigSchema.parse({});
70
+ //#endregion
71
+ export { config as default, documentationConfigSchema };
@@ -0,0 +1,49 @@
1
+ import { documentationConfigSchema } from "./config.js";
2
+ import { parseModuleConfig } from "nodefony";
3
+ import { z } from "zod";
4
+ //#region nodefony/config/defineModuleConfig.ts
5
+ /**
6
+ * @nodefony/documentation — Builder de configuration validée (Zod) + ENV.
7
+ *
8
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
9
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
10
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
11
+ * env + freeze) et publie le JSON Schema Studio.
12
+ *
13
+ * Sépare la VALIDATION (schéma pur de `config.ts`) de l'APPLICATION des variables
14
+ * d'environnement : le schéma reste déterministe (sérialisable en JSON Schema
15
+ * pour Studio), et l'env est appliqué APRÈS le parse, ici.
16
+ *
17
+ * @see ./config.ts — source de vérité (types dérivés via z.infer)
18
+ */
19
+ /** Lit une variable d'env non vide, ou `undefined` si absente/vide. */
20
+ function env(name) {
21
+ const v = process.env[name];
22
+ return v && v.trim() !== "" ? v.trim() : void 0;
23
+ }
24
+ /**
25
+ * Valide une config partielle (défauts du schéma) PUIS applique la surcharge par
26
+ * variables d'environnement (précédence max).
27
+ *
28
+ * @param input - config partielle (depuis `module.options.documentation`)
29
+ * @returns config validée, défauts appliqués, env mergé
30
+ * @throws ZodError si l'input viole le schéma
31
+ */
32
+ function defineDocumentationConfig(input = {}) {
33
+ const parsed = parseModuleConfig(documentationConfigSchema, input ?? {}, "@nodefony/documentation");
34
+ const repoUrlEnv = env("DOCS_REPO_URL");
35
+ if (repoUrlEnv) parsed.repo.url = repoUrlEnv;
36
+ const branchEnv = env("DOCS_REPO_BRANCH");
37
+ if (branchEnv) parsed.repo.branch = branchEnv;
38
+ return parsed;
39
+ }
40
+ /**
41
+ * JSON Schema introspectable de la config documentation — destiné au panneau de
42
+ * config Studio (`/nodefony/config`). N'inclut PAS la surcharge ENV (appliquée
43
+ * hors schéma, dans le builder).
44
+ */
45
+ function documentationConfigJsonSchema() {
46
+ return z.toJSONSchema(documentationConfigSchema);
47
+ }
48
+ //#endregion
49
+ export { defineDocumentationConfig, documentationConfigJsonSchema, documentationConfigSchema };
@@ -0,0 +1,102 @@
1
+ import { DocNotFoundError, DocUnsafeSlugError } from "../src/errors/DocumentationError.js";
2
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
3
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
4
+ import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
5
+ import { Controller, Get, IsGranted, Param, Query, controller } from "@nodefony/framework";
6
+ import { Context } from "@nodefony/http";
7
+ //#region nodefony/controller/DocumentationController.ts
8
+ /**
9
+ * Data plane HTTP de la documentation Nodefony.
10
+ *
11
+ * Expose l'index transverse et le contenu des pages sous
12
+ * `/nodefony/documentation/api/*` (convention figée : un module admin sert
13
+ * toujours `/nodefony/<module>/api/*`, jamais une route mono-segment).
14
+ *
15
+ * **Mince par design** : toute la logique (scan, cache, allowlist, résolution
16
+ * `{{ }}`) vit dans `DocumentationService` (singleton stateful). Le controller
17
+ * est réinstancié par requête → il ne porte aucun état, il délègue.
18
+ *
19
+ * Sécurité (Zero Trust) : un slug invalide/inconnu renvoie un message
20
+ * **générique** au client ; le détail (slug, raison) est loggé côté serveur.
21
+ * La garde anti-traversée est dans le service ({@link isSafeSlug}).
22
+ */
23
+ let DocumentationController = class DocumentationController extends Controller {
24
+ constructor(context) {
25
+ super("DocumentationController", context);
26
+ }
27
+ /** Résout le service de documentation depuis le container partagé. */
28
+ #service() {
29
+ const svc = this.get("documentation");
30
+ if (!svc) throw new Error("DocumentationService non enregistré");
31
+ return svc;
32
+ }
33
+ /** Index transverse : sections → pages, taguées par audience. */
34
+ async tree() {
35
+ try {
36
+ return this.renderJson(await this.#service().getTree());
37
+ } catch (e) {
38
+ this.log(e, "ERROR");
39
+ return this.renderJson({ error: "Index de documentation indisponible." }, 500);
40
+ }
41
+ }
42
+ /**
43
+ * Cherche dans le corpus — titres ET corps — et rend des extraits situés.
44
+ *
45
+ * Filtrer l'arbre ne répondait qu'à « quelle page s'appelle ainsi ? » ; la
46
+ * question posée est « où est-ce expliqué ? ».
47
+ */
48
+ async search(q) {
49
+ try {
50
+ return this.renderJson(await this.#service().search(q ?? ""));
51
+ } catch (e) {
52
+ this.log(e, "ERROR");
53
+ return this.renderJson({ error: "Recherche indisponible." }, 500);
54
+ }
55
+ }
56
+ /** Contenu d'une page + variables `{{ }}` résolues côté serveur. */
57
+ async page(slug) {
58
+ try {
59
+ return this.renderJson(await this.#service().getPage(slug));
60
+ } catch (e) {
61
+ if (e instanceof DocNotFoundError || e instanceof DocUnsafeSlugError) {
62
+ this.log(`${e.docCode}: ${e.message}`, "WARNING");
63
+ return this.renderJson({
64
+ slug,
65
+ error: "Document inconnu."
66
+ }, 404);
67
+ }
68
+ this.log(e, "ERROR");
69
+ return this.renderJson({
70
+ slug,
71
+ error: "Lecture de la page impossible."
72
+ }, 500);
73
+ }
74
+ }
75
+ };
76
+ __decorate([
77
+ IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
78
+ Get("/documentation/api/tree"),
79
+ __decorateMetadata("design:type", Function),
80
+ __decorateMetadata("design:paramtypes", []),
81
+ __decorateMetadata("design:returntype", Promise)
82
+ ], DocumentationController.prototype, "tree", null);
83
+ __decorate([
84
+ IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
85
+ Get("/documentation/api/search"),
86
+ __decorateParam(0, Query("q")),
87
+ __decorateMetadata("design:type", Function),
88
+ __decorateMetadata("design:paramtypes", [String]),
89
+ __decorateMetadata("design:returntype", Promise)
90
+ ], DocumentationController.prototype, "search", null);
91
+ __decorate([
92
+ IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
93
+ Get("/documentation/api/page/{slug}"),
94
+ __decorateParam(0, Param("slug")),
95
+ __decorateMetadata("design:type", Function),
96
+ __decorateMetadata("design:paramtypes", [String]),
97
+ __decorateMetadata("design:returntype", Promise)
98
+ ], DocumentationController.prototype, "page", null);
99
+ DocumentationController = __decorate([controller("/nodefony"), __decorateMetadata("design:paramtypes", [typeof Context === "undefined" ? Object : Context])], DocumentationController);
100
+ var DocumentationController_default = DocumentationController;
101
+ //#endregion
102
+ export { DocumentationController_default as default };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};