@ogygie/cli 0.1.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.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ogygie/cli",
3
+ "version": "0.1.0",
4
+ "description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ogygie": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "typecheck": "tsc -p tsconfig.json",
21
+ "build": "tsx scripts/build.ts",
22
+ "record-checksums": "tsx scripts/record-governed-checksums.ts"
23
+ },
24
+ "devDependencies": {
25
+ "@ogygie/core": "workspace:*",
26
+ "@types/node": "^24.0.0",
27
+ "esbuild": "^0.28.0",
28
+ "tsx": "^4.20.0",
29
+ "typescript": "^5.8.0",
30
+ "zod": "^4.0.0"
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ # templates/feature — réservé (non câblé)
2
+
3
+ Ce dossier est POSÉ par la spec `ogygie-standalone-repo` mais volontairement
4
+ vide : le scaffolding de feature (`ogygie generate feature`) fera l'objet d'une
5
+ spec ultérieure. En attendant, la forme canonique d'une feature est décrite
6
+ dans `AGENTS.md` §Structure d'une feature, et l'exemple vivant est la feature
7
+ `notes` du repo framework.
8
+
9
+ Rien ici n'est lu par `ogygie init`, `upgrade` ni `check`.
@@ -0,0 +1,483 @@
1
+ # AGENTS.md — Ogygie v1 (conventions GÉNÉRIQUES du framework)
2
+
3
+ > **Tu es un agent qui travaille sur ce repo. Lis ce fichier EN ENTIER, puis
4
+ > `AGENTS.product.md` (les liaisons produit : noms réels, alias, listes fermées),
5
+ > puis `ogygie.manifest.json`. Ne lis rien d'autre avant d'avoir une tâche précise.**
6
+ >
7
+ > Ce fichier est GÉNÉRIQUE — aucune règle ici ne nomme le produit ni son
8
+ > domaine ; ses exemples utilisent un domaine neutre (`orders`). Tout ce qui
9
+ > lie ces règles au produit réel vit dans `AGENTS.product.md`. Les décisions
10
+ > structurantes du repo se consignent dans `DECISIONS.md` (framework) et
11
+ > `DECISIONS.product.md` (produit) — numérotation `D<n>` globale unique aux
12
+ > deux fichiers, vérifiée par verify-specs. Ils naissent vierges : ce produit
13
+ > écrira ses propres décisions, en commençant au premier numéro.
14
+
15
+ ## Ordre de lecture obligatoire pour toute tâche
16
+
17
+ 1. `AGENTS.md` (ce fichier) — les règles.
18
+ 2. `AGENTS.product.md` — l'extension produit (alias, colonnes tenant, listes fermées).
19
+ 3. `ogygie.manifest.json` — la carte du repo (features, routes, dépendances).
20
+ 4. `specs/<feature>.spec.md` — la spec de la feature concernée (si elle existe).
21
+ 5. Le dossier `features/<feature>/` concerné — et LUI SEUL.
22
+
23
+ Interdiction d'explorer le repo au-delà. Si le manifeste ne suffit pas à localiser ce dont tu as besoin, c'est un bug du manifeste : le signaler dans `FAILURES.md`.
24
+
25
+ ## Les 8 principes (version opérationnelle)
26
+
27
+ 1. **Localité radicale** — une feature = un dossier `features/<nom>/` autonome (route + schémas + logique + tests). Aucun fichier magique, aucun comportement par convention de nommage. Tout module chargé l'est par un import explicite.
28
+ 2. **Contrats aux frontières** — toute entrée/sortie (HTTP, DB, job, événement) passe par un schéma Zod. Le schéma est LA source de vérité : validation runtime + type TS inféré (`z.infer`) + doc.
29
+ 3. **Manifeste machine-lisible** — `ogygie.manifest.json` indexe tout. Toute PR qui ajoute/modifie une feature met à jour le manifeste dans le même commit. La CI casse si le manifeste ment.
30
+ 4. **Une seule façon idiomatique** — pour chaque besoin, copie le template canonique ci-dessous. N'invente pas de variante. Si le template est inadapté, ne contourne pas : propose un amendement de ce fichier.
31
+ 5. **Effets déclarés** — toute fonction qui touche DB/réseau/fs/horloge reçoit ses dépendances en paramètre (injection). Jamais d'import global de client au fond d'un helper.
32
+ 6. **Erreurs pour agents** — toute erreur suit le format `OgygieError` (voir `packages/ogygie-core/src/error.schema.ts`) : code stable, `file`, `cause`, `suggestedFix`.
33
+ 7. **Testabilité en secondes** — `pnpm test -- features/<nom>` doit passer en < 10 s, sans réseau ni DB réelle (voir template test).
34
+ 8. **Verbeux plutôt qu'ambigu** — noms longs et descriptifs, bloc `@intent` en tête de chaque fichier, imports complets. On écrit pour un lecteur sans contexte. **Le code est en ANGLAIS MAXIMAL** : identifiants, tables/colonnes BDD, routes HTTP, codes d'erreur, valeurs d'enum, noms de schémas et de patterns, et TOUS les commentaires (dont `@intent`) — sans exception. Le français reste réservé aux documents (specs, décisions, PR) et aux textes UI via clés ICU.
35
+
36
+ ## Structure d'une feature
37
+
38
+ ```
39
+ features/orders/
40
+ ├── index.ts # SEUL point d'export public de la feature
41
+ ├── routes.ts # Enregistrement Fastify des endpoints
42
+ ├── schemas.ts # Contrats Zod (input/output/erreurs métier)
43
+ ├── service.ts # Logique métier PURE (dépendances injectées)
44
+ ├── repository.ts # Accès DB (seul fichier autorisé à parler à Drizzle)
45
+ ├── service.test.ts # Tests de la logique (rapides, sans I/O)
46
+ └── routes.test.ts # Tests des endpoints (fastify.inject, DB mémoire)
47
+ ```
48
+
49
+ Règle de dépendance : `routes → service → repository`. Jamais l'inverse, jamais de saut (une route n'appelle pas le repository directement). Une feature importe une autre feature UNIQUEMENT via son `index.ts`, et cette dépendance est déclarée dans le manifeste.
50
+
51
+ **Exception unique et bornée — les CONTRATS (amendement 2026-08).**
52
+ `schemas.ts` est le seul fichier d'une feature qui n'importe QUE des contrats :
53
+ zod, un contrat produit partagé (la liste fermée est dans `AGENTS.product.md`),
54
+ et — quand une dépendance déclarée au manifeste l'exige — le `schemas.ts` d'une
55
+ autre feature. JAMAIS son `index.ts`. Motif : un `index.ts` porte la surface
56
+ SERVEUR (routes, service, chargeurs de données) ; or `schemas.ts` est importé
57
+ par le FRONT (`@<produit>/api/features/<nom>/schemas` — c'est le zéro-drift de
58
+ la stack front). Un contrat qui traîne du code serveur casse le build du front (constaté :
59
+ un `schemas.ts` → l'`index.ts` d'une feature voisine → son loader → `node:fs`
60
+ dans le bundle navigateur, FAILURES.md) ou y embarque Drizzle. Un `schemas.ts`
61
+ reste donc importable par un navigateur, et ce n'est pas une promesse : le gate
62
+ `pnpm ogygie:verify-contracts` parcourt tout le graphe d'imports depuis
63
+ chaque `schemas.ts` — la TRANSITIVITÉ est le cœur du contrôle, c'est par un
64
+ intermédiaire que le serveur revient — et casse la CI sur tout paquet hors
65
+ liste (`zod` seul), tout `index.ts` atteint, et tout module illisible.
66
+
67
+ ## Templates canoniques
68
+
69
+ ### 1. Endpoint HTTP
70
+
71
+ ```ts
72
+ // features/<nom>/routes.ts
73
+ /**
74
+ * @intent HTTP endpoints of the <nom> feature.
75
+ * No business logic here: schema validation + delegation to the service.
76
+ */
77
+ import type { FastifyInstance } from "fastify";
78
+ import {
79
+ CreateOrderInputSchema,
80
+ CreateOrderOutputSchema,
81
+ } from "./schemas.js";
82
+ import { createOrder } from "./service.js";
83
+
84
+ export async function registerOrderRoutes(app: FastifyInstance): Promise<void> {
85
+ app.post("/api/orders", {
86
+ schema: {
87
+ body: CreateOrderInputSchema,
88
+ response: { 201: CreateOrderOutputSchema },
89
+ },
90
+ handler: async (request, reply) => {
91
+ const result = await createOrder(
92
+ { db: app.db, clock: app.clock },
93
+ request.body,
94
+ );
95
+ return reply.code(201).send(result);
96
+ },
97
+ });
98
+ }
99
+ ```
100
+
101
+ ### 2. Contrat Zod
102
+
103
+ ```ts
104
+ // features/<nom>/schemas.ts
105
+ /**
106
+ * @intent Data contracts of the <nom> feature.
107
+ * Single source of truth: runtime validation + TS types + documentation.
108
+ */
109
+ import { z } from "zod";
110
+
111
+ export const CreateOrderInputSchema = z.object({
112
+ customerId: z.uuid(),
113
+ reference: z.string().min(1).max(120),
114
+ quantity: z.number().int().positive().max(1_000_000),
115
+ });
116
+ export type CreateOrderInput = z.infer<typeof CreateOrderInputSchema>;
117
+ ```
118
+
119
+ Règles : suffixe `Schema` obligatoire, type inféré exporté juste en dessous, jamais de type TS écrit à la main pour une donnée qui traverse une frontière. Union discriminée persistée = colonnes nullables par branche + contrainte CHECK miroir de la forme de l'union dans la migration.
120
+
121
+ ### 3. Service (logique pure, effets déclarés)
122
+
123
+ ```ts
124
+ // features/<nom>/service.ts
125
+ /**
126
+ * @intent Business logic of the <nom> feature. Pure functions:
127
+ * every effectful dependency (db, clock, network) is injected.
128
+ */
129
+ import type { Database } from "@ogygie/core/db";
130
+ import type { Clock } from "@ogygie/core/clock";
131
+ import type { CreateOrderInput, CreateOrderOutput } from "./schemas.js";
132
+ import { insertOrder } from "./repository.js";
133
+
134
+ export interface CreateOrderDeps {
135
+ db: Database;
136
+ clock: Clock;
137
+ }
138
+
139
+ export async function createOrder(
140
+ deps: CreateOrderDeps,
141
+ input: CreateOrderInput,
142
+ ): Promise<CreateOrderOutput> {
143
+ // ... business logic ...
144
+ return insertOrder(deps.db, /* ... */);
145
+ }
146
+ ```
147
+
148
+ ### 4. Accès DB
149
+
150
+ Voir le template repository du §Multi-tenant (il REMPLACE tout accès direct) :
151
+ tout repository reçoit le contexte tenant en premier paramètre et passe par
152
+ `tenantDb`.
153
+
154
+ ### 5. Test
155
+
156
+ ```ts
157
+ // features/<nom>/service.test.ts
158
+ /**
159
+ * @intent Proves the behaviors described in specs/<nom>.spec.md.
160
+ * Each Given/When/Then block of the spec = one identically named test.
161
+ */
162
+ import { describe, expect, it } from "vitest";
163
+ import { createOrder } from "./service.js";
164
+ import { createInMemoryDb, fixedClock } from "@ogygie/core/test-kit";
165
+
166
+ describe("orders", () => {
167
+ it("ÉTANT DONNÉ un client connu QUAND on crée une commande ALORS elle est numérotée séquentiellement", async () => {
168
+ const deps = { db: await createInMemoryDb(migrations), clock: fixedClock("2026-07-01") };
169
+ const result = await createOrder(deps, /* fixture */); // fixture from the spec
170
+ expect(result.sequenceNumber).toBe(1);
171
+ });
172
+ });
173
+ ```
174
+
175
+ ### 6. Erreur métier
176
+
177
+ ```ts
178
+ import { ogygieError } from "@ogygie/core/errors";
179
+
180
+ throw ogygieError({
181
+ code: "ORDER_CUSTOMER_NOT_FOUND",
182
+ message: "The targeted customer does not exist for this tenant.",
183
+ file: "src/features/orders/service.ts",
184
+ cause: `customerId=${input.customerId} absent from the customers table`,
185
+ suggestedFix: "Create the customer via POST /api/customers before creating an order.",
186
+ });
187
+ ```
188
+
189
+ ## Workflow de livraison d'une feature
190
+
191
+ 1. Lire la spec (`specs/<nom>.spec.md`, status `validated`). Pas de spec validée = pas de code.
192
+ Amender une spec en cours de session = commit séparé dédié, signalé dans la PR et validé en revue.
193
+ 2. Écrire `schemas.ts` d'abord (les contrats), puis les tests (depuis les Given/When/Then), puis l'implémentation.
194
+ 3. Mettre à jour `ogygie.manifest.json` (feature, routes, dépendances).
195
+ 4. `pnpm test -- features/<nom>` vert en < 10 s.
196
+ 5. Si un pattern se répète pour la 3e fois → le signaler comme candidat à l'extraction `@ogygie/core`.
197
+ Exception : l'outillage et l'infrastructure GÉNÉRIQUES peuvent naître ou
198
+ monter dans le core sans attendre 3 répétitions ; le code métier, jamais. Critère : le
199
+ module contient-il une seule décision propre au produit ? Si oui, il reste dans `apps/`.
200
+ 6. Si tu as halluciné une API, cassé une feature ou tourné en rond → consigner dans `FAILURES.md` (date, symptôme, cause racine, convention qui l'aurait empêché).
201
+ 7. Tout gate CI (verify-*, lint) naît et se durcit avec son test négatif : un test de mutation prouve qu'une violation connue est détectée.
202
+ 8. Git : force-push, réécriture d'historique et isolation des sessions
203
+ (un worktree par session) → voir §Évolution d'une feature livrée,
204
+ « Règles git ». Une seule formulation dans ce fichier, pas deux.
205
+
206
+ ## Interdits absolus
207
+
208
+ - `any`, `as unknown as`, `@ts-ignore` (utiliser `@ts-expect-error` avec justification si inévitable).
209
+ - Import d'un fichier interne d'une autre feature (passer par son `index.ts`).
210
+ - Client DB/HTTP importé globalement dans un helper.
211
+ - Type écrit à la main pour une donnée qui a un schéma Zod.
212
+ - Commit qui modifie une feature sans mettre à jour manifeste + tests.
213
+ - **Garde de nullité écrite `=== null` / `!== null` sur un champ OPTIONNEL ou
214
+ ÉLARGI.** Elle s'écrit `== null` / `!= null` — qui attrape `null` ET
215
+ `undefined` — ou la donnée passe par son schéma Zod (dont le `.default(null)`
216
+ normalise). Motif, constaté (FAILURES.md 2026-07-31) : une donnée AU REPOS
217
+ écrite avant l'ajout du champ ne porte pas la clé, donc vaut `undefined` et
218
+ non `null` ; la garde stricte y lit « présent » et le code croit à une valeur
219
+ saisie qui n'existe pas. Le contrat porte bien le défaut, mais rien n'oblige
220
+ un appelant à parser : fixtures, tests et scripts lisent le JSON brut.
221
+
222
+ ## Internationalisation (règles worldwide — non négociables)
223
+
224
+ 1. **Domaine en unités SI canoniques.** La base et les services stockent et calculent
225
+ TOUJOURS dans les unités SI du domaine (mètres, m², kg, litres…). Jamais une unité
226
+ locale (impériale…) dans un calcul. La conversion est un problème de PRÉSENTATION,
227
+ résolu une fois dans le front via `preferences.unitSystem` du tenant.
228
+ 2. **Argent = jamais un float.** Tout montant est `{ amountCents: int, currency: ISO4217 }`.
229
+ Les calculs monétaires passent par les helpers de `@ogygie/core/money` (à créer).
230
+ Un tenant a UNE devise de travail ; pas de conversion de change en v1.
231
+ 3. **Aucune chaîne UI en dur.** Tout texte visible passe par une clé de traduction ICU
232
+ (`t("orders.create.title")` — clés de traduction en anglais). Les libellés saisis
233
+ par l'utilisateur sont des DONNÉES, jamais traduits.
234
+ 4. **Les messages d'OgygieError ont deux canaux** : `message` (anglais technique, pour
235
+ les logs et les agents) + `userMessageKey` (clé ICU pour l'affichage).
236
+ `suggestedFix` reste en anglais technique.
237
+ 5. **Temps** : dates stockées en UTC ISO 8601 ; chaque tenant a un fuseau IANA.
238
+ Les semaines sont des semaines ISO 8601 (lundi = premier jour) partout — l'affichage
239
+ local peut différer, pas le calcul.
240
+ 6. **Identifiants code en anglais, contenu en langue de l'utilisateur.** Les enums du
241
+ domaine sont en anglais ; leur affichage est traduit.
242
+
243
+ ## Front (apps/web) — règles et templates canoniques
244
+
245
+ ### Stack imposée (toute autre lib = amendement DECISIONS.md d'abord)
246
+ Vite + React 19 + TanStack Router (routing EN CODE) + TanStack Query (état serveur) ;
247
+ PAS de lib d'état client (useState suffit) ; Tailwind v4 thémé depuis `design/tokens.json`
248
+ (valeurs arbitraires `p-[13px]` interdites par lint) ; primitives UI copiées dans
249
+ `apps/web/src/patterns/` (jamais consommées directement depuis node_modules par une
250
+ feature) ; react-hook-form UNIQUEMENT via le pattern `ContractForm` ; TanStack
251
+ Table UNIQUEMENT via `DataTable` ; Recharts UNIQUEMENT via `VarianceChart` ;
252
+ date-fns via les helpers `@ogygie/core` (jamais de `new Date()` nu) ; i18n Lingui
253
+ (ICU, extraction vérifiée en CI). Noms de patterns en anglais.
254
+
255
+ ### Structure d'une feature front (miroir de l'API)
256
+ ```
257
+ apps/web/features/orders/
258
+ ├── index.ts # seul export public
259
+ ├── routes.tsx # définition TanStack Router de la feature
260
+ ├── queries.ts # hooks TanStack Query (fetch + invalidations)
261
+ ├── OrdersList.tsx # écrans = composition de patterns, rien d'autre
262
+ ├── OrderForm.tsx
263
+ └── OrdersList.test.tsx # un test par état UI de la spec
264
+ ```
265
+
266
+ ### Template canonique — hook de requête
267
+ ```ts
268
+ // apps/web/features/orders/queries.ts
269
+ /** @intent Data access of the orders feature. Re-validates contracts in dev. */
270
+ import { useQuery } from "@tanstack/react-query";
271
+ import { ListOrdersOutputSchema } from "@product/api/features/orders/schemas";
272
+ import { fetchContract } from "../../lib/fetchContract"; // Zod parse in dev, OgygieError on broken contract
273
+
274
+ export function useOrders() {
275
+ return useQuery({
276
+ queryKey: ["orders"],
277
+ queryFn: () => fetchContract("/api/orders", ListOrdersOutputSchema),
278
+ });
279
+ }
280
+ ```
281
+
282
+ ### Template canonique — écran
283
+ ```tsx
284
+ // apps/web/features/orders/OrdersList.tsx
285
+ /** @intent Orders list screen: composition of patterns, zero business logic. */
286
+ import { Trans } from "@lingui/react/macro";
287
+ import { DataTable } from "../../patterns/DataTable";
288
+ import { EmptyState } from "../../patterns/EmptyState";
289
+ import { ErrorBanner } from "../../patterns/ErrorBanner";
290
+ import { useOrders } from "./queries";
291
+
292
+ export function OrdersList() {
293
+ const { data, isPending, error } = useOrders();
294
+ if (error) return <ErrorBanner error={error} />;
295
+ if (isPending) return <DataTable.Skeleton />;
296
+ if (data.orders.length === 0)
297
+ return <EmptyState title={<Trans>Ajoutez votre première commande</Trans>} />;
298
+ return <DataTable rows={data.orders} columns={orderColumns} />;
299
+ }
300
+ ```
301
+
302
+ ### Règles front absolues
303
+ - Un écran est une COMPOSITION de patterns : jamais de `<table>`, `<form>` ou fetch nus
304
+ dans une feature. Si un pattern manque, on le crée dans `patterns/` (et on met à jour
305
+ `design/patterns.md`), on ne bricole pas dans la feature.
306
+ - Chaque état UI listé dans la spec (vide/chargement/nominal/erreur) a son test.
307
+ - Toute chaîne visible passe par Lingui (`<Trans>`/`t`) — la CI casse sinon.
308
+ - Chiffres financiers : `tabular-nums`, alignés à droite, unité explicite.
309
+
310
+ ### La boucle design (specs/ogygie-design-brief.spec.md)
311
+ Avant de maquetter un écran, générer le brief DÉRIVÉ de la feature :
312
+ ```
313
+ pnpm ogygie:design-brief <feature> → design/briefs/<feature>.md
314
+ ```
315
+ Le brief tire tout des sources de vérité : tokens (`design/tokens.json`),
316
+ patterns (`design/patterns.md` + les modules des apps front), champs des
317
+ contrats Zod des routes, actions par méthode, états UI de la spec, codes
318
+ d'erreur réels, volumétries des fixtures. L'outil de design (humain ou IA)
319
+ produit une mise en page — jamais du code ; la décision humaine revient dans
320
+ la spec de la feature (section « # Design »), puis l'implémentation suit la
321
+ spec. Règle non négociable : le design ne crée jamais de token ni de
322
+ composant one-off — une couleur ou un espacement absent est un amendement de
323
+ `design/tokens.json`, un pattern manquant naît dans le catalogue.
324
+
325
+ ## Multi-tenant (SaaS) — sécurité structurelle, non optionnelle
326
+
327
+ Le produit est un SaaS multi-tenant. La fuite inter-tenants est le risque n°1
328
+ d'un code écrit par agents : elle doit être IMPOSSIBLE par construction, pas évitée
329
+ par vigilance.
330
+
331
+ 1. **Toute fonction de repository reçoit un contexte tenant en premier paramètre**,
332
+ fourni par le middleware d'auth — jamais par le body ou la query string du client.
333
+ Le core expose le type GÉNÉRIQUE `TenantContext` (`{ tenantId: uuid, userId: uuid }`) ;
334
+ le produit l'aliase sous son propre nom (déclaré dans `AGENTS.product.md`) — le core
335
+ ne connaît AUCUN mot du domaine du produit (règle de séparation, le journal des décisions).
336
+ 2. **Tout accès Drizzle passe par `tenantDb(ctx)`** (`@ogygie/core/tenant`) qui
337
+ applique systématiquement le filtre de la colonne tenant (nom déclaré dans
338
+ `AGENTS.product.md`). Accès Drizzle direct hors de ce helper = interdit
339
+ (règle lint `ogygie/no-raw-db`).
340
+ 3. **Row-Level Security PostgreSQL activée sur toute table portant la colonne
341
+ tenant** (second rempart : même une requête buggée ne peut pas lire un
342
+ autre tenant). Toute migration créant une table tenant inclut sa policy RLS —
343
+ le template de migration l'impose.
344
+ 4. Les tables de référentiel PARTAGÉ — connaissance universelle qu'aucun tenant
345
+ ne possède, chargée par un script d'extraction depuis une source externe
346
+ versionnée — sont déclarées explicitement au manifeste sous la forme objet
347
+ `{ "name": "<table>", "shared": true }` (clé anglaise : le manifeste est
348
+ machine-lisible). Elles échappent alors à la règle 3 (pas de colonne
349
+ tenant, pas de policy RLS tenant) ET à la règle 2 : n'ayant aucune colonne
350
+ sur laquelle filtrer, elles se lisent hors de `tenantDb`. Ce droit se paie
351
+ comptant, et les gates le vérifient au lieu de faire confiance :
352
+ - une feature ne gagne l'accès brut à la base que si TOUTES ses tables sont
353
+ partagées (lint-interdits, dérivé du manifeste) — une seule table tenant
354
+ dans la même feature le lui retire ;
355
+ - une table déclarée partagée qui porte encore la colonne tenant casse
356
+ verify-manifest (`SHARED_TABLE_WITH_TENANT_COLUMN`) ;
357
+ - côté API ces tables sont en LECTURE SEULE : seul le script d'extraction
358
+ les alimente.
359
+ La liste des tables partagées du produit est dans `AGENTS.product.md`.
360
+
361
+ ### Template repository (le SEUL accès DB autorisé)
362
+ ```ts
363
+ // features/<nom>/repository.ts
364
+ /** @intent Only file of the feature allowed to touch the DB, always through tenantDb. */
365
+ import type { ProductContext } from "../../context.js"; // the product's alias of TenantContext (real name: AGENTS.product.md)
366
+ import { tenantDb } from "@ogygie/core/tenant";
367
+ import { orders } from "./db.schema.js";
368
+
369
+ export async function insertOrder(ctx: ProductContext, values: NewOrder) {
370
+ const [row] = await tenantDb(ctx).insert(orders).values(values).returning();
371
+ return row; // tenant column injected and filtered by tenantDb, never by hand
372
+ }
373
+ ```
374
+ Les services propagent `ctx` tel quel (routes → service → repository) sans jamais le
375
+ reconstruire ni le modifier.
376
+
377
+ ### Piège connu de la façade tenantDb (TS2353)
378
+ L'excess-property-check TypeScript bute sur les littéraux inline passés au
379
+ `set()` générique de `TenantDb` (TS2353 alors que la propriété existe).
380
+ Convention : passer des VARIABLES TYPÉES sur les types concrets
381
+ (`Partial<Omit<NewX, ...>>`), jamais des littéraux inline. Plus lisible, et
382
+ le compilateur est d'accord.
383
+
384
+ ## Sécurité — interdits vérifiables
385
+
386
+ La sécurité est structurelle : un agent suit des rails, il ne « fait pas attention ».
387
+ S'ajoutent au multi-tenant (§ précédent) et aux contrats Zod :
388
+
389
+ 1. **Secrets** : uniquement via variables d'environnement, validées au démarrage par
390
+ le schéma de `apps/api/src/env.ts` (boot refusé si un secret manque). Aucun secret
391
+ en dur nulle part — gitleaks casse la CI sinon.
392
+ 2. **Tout identifiant venant du client est hostile.** L'appartenance fait foi via
393
+ le contexte tenant ; jamais d'identifiant tenant lu depuis body/query/params
394
+ (seule exception : la route de bascule du tenant actif, qui vérifie l'appartenance).
395
+ 3. **Front** : `dangerouslySetInnerHTML` interdit (lint). On ne débranche jamais
396
+ l'échappement React.
397
+ 4. **Bootstrap Fastify** : `@fastify/helmet` + `@fastify/rate-limit` posés une fois
398
+ dans `apps/api/src/server.ts` — jamais feature par feature.
399
+ 5. **Sessions** : cookies `httpOnly + secure + sameSite=lax` (vérifier la config
400
+ Better Auth, ne pas présumer).
401
+ 6. **Dépendances** : ajouter un paquet npm est un événement RARE. Justification
402
+ obligatoire dans la PR, nom du paquet vérifié sur le registre (anti-slopsquatting),
403
+ `--frozen-lockfile` en CI. En cas de doute sur un nom de paquet : STOP et demander.
404
+ 7. **Erreurs en production** : seuls les champs d'OgygieError sortent vers le client ;
405
+ jamais de stack trace ni de détail interne.
406
+ 8. **URL d'API en dur côté front : interdit.** Le front appelle `/api/...`
407
+ en relatif — il ne connaît ni l'hôte ni le port de l'API. Toute
408
+ URL absolue vers l'API dans apps/web est une violation (lint).
409
+ 9. **La passerelle n'autorise jamais.** Aucune règle d'accès, aucun filtre
410
+ métier dans apps/gateway : l'autorisation vit dans l'API (session →
411
+ contexte tenant → tenantDb). Un relais qui autorise devient un confused
412
+ deputy.
413
+ 10. **Toute route publique passe par le manifeste.** Une route API absente
414
+ du manifeste est inatteignable depuis l'extérieur (la passerelle ne la
415
+ monte pas) — le manifeste est aussi une surface de sécurité.
416
+
417
+ ## Évolution d'une feature livrée
418
+
419
+ Modifier une feature existante suit un protocole aussi canonique que sa création.
420
+ Le contexte complet d'une feature vit dans TROIS artefacts : son entrée au
421
+ manifeste, sa spec, son dossier — jamais dans d'anciennes PR ni de mémoire de
422
+ session. Si ces trois-là ne suffisent pas à comprendre la feature : STOP,
423
+ FAILURES.md.
424
+
425
+ ### Cycle de vie de la spec
426
+ - Une évolution ROUVRE la spec : commit « spec amendment » qui modifie les
427
+ comportements ET repasse `status: implemented → validated`.
428
+ - La spec décrit toujours L'ÉTAT COURANT VOULU (jamais un empilement de deltas) ;
429
+ l'historique vit dans une section `# Changelog` en fin de spec :
430
+ `## v2 — AAAA-MM — <ajouts / retraits / motif>` + le git log.
431
+ - Retour à `implemented` : chaque nouveau comportement a son test, les anciens
432
+ tests restent verts, SAUF ceux d'un comportement explicitement retiré au
433
+ Changelog (le retrait d'un test sans ligne de Changelog = violation).
434
+ - Gate verify-specs : feature `delivered` + spec `validated` = « évolution en
435
+ cours », état légitime et visible ; jamais un état permanent (> 1 PR = signal).
436
+
437
+ ### Première commande obligatoire : le rayon d'impact
438
+ Avant toute modification d'une feature livrée, lancer :
439
+ ```
440
+ pnpm ogygie:impact <feature>
441
+ ```
442
+ Tout dépendant listé doit rester vert à la fin de la session. Un dépendant
443
+ cassé sciemment (contrat qui change) est migré DANS LA MÊME PR.
444
+
445
+ ### Contrats exportés : élargir libre, restreindre = événement
446
+ - ÉLARGIR un schéma exporté (champ optionnel en plus, nouvelle valeur d'enum
447
+ en sortie) : libre.
448
+ - RESTREINDRE ou RENOMMER (champ retiré/renommé, contrainte durcie, valeur
449
+ d'enum supprimée, route déplacée) : exige `ogygie:impact`, migration de tous
450
+ les consommateurs dans la même PR, et mention explicite dans la description
451
+ de PR. C'est notre semver à l'échelle du monorepo : tout bouge ensemble ou
452
+ rien ne bouge.
453
+
454
+ ### Migrations en évolution : additives d'abord (expand → migrate → contract)
455
+ À la création d'une feature, une migration est libre. En évolution, il y a des
456
+ données réelles :
457
+ 1. EXPAND : ajouter (colonne nullable, nouvelle table, nouvel index) — jamais
458
+ casser l'existant dans ce pas.
459
+ 2. MIGRATE : remplir/backfiller, basculer le code sur le nouveau chemin.
460
+ 3. CONTRACT : supprimer l'ancien (colonne, contrainte) — dans une release
461
+ ULTÉRIEURE, jamais le même lot que le code qui l'exploite.
462
+ - Interdits en un seul lot : RENAME de colonne/table exploitée, DROP d'une
463
+ colonne encore lue, changement de type en place. Toute migration destructive
464
+ = section dédiée dans la description de PR.
465
+ - Rappel : toute nouvelle table tenant garde sa policy RLS.
466
+
467
+ ### Règles git
468
+ - **Force-push : sur une branche de feature avant revue, OUI ; sur `main`,
469
+ JAMAIS** (la protection de branche doit l'interdire côté GitHub).
470
+ - Toute réécriture d'historique (rebase, cherry-pick, reconstruction depuis
471
+ le reflog) se conclut par DEUX preuves avant push : `git diff` vide contre
472
+ l'état validé, et boucle complète re-passée verte. Sans ces deux preuves,
473
+ ne pas pousser.
474
+ - **`git checkout/restore -- <chemin>` ne vise JAMAIS un répertoire** tant que
475
+ le travail n'est pas committé : il écrase l'arbre sans confirmation et sans
476
+ rien laisser au reflog (constaté, FAILURES.md 2026-07-31 — dix fichiers de
477
+ session effacés par une commande de commodité). Pour comparer avec HEAD,
478
+ extraire une COPIE (`git show HEAD:<fichier>`), ne pas remettre l'arbre à
479
+ HEAD.
480
+ - **Une session = un worktree** : ne bascule JAMAIS la branche d'un
481
+ répertoire que tu ne sais pas exclusivement tien. Si `git status` montre
482
+ des modifications qui ne sont pas les tiennes : STOP, signale-le, ne
483
+ stash pas.
@@ -0,0 +1,48 @@
1
+ # AGENTS.product.md — Extension produit : __PRODUCT_NAME__
2
+
3
+ > Lu APRÈS `AGENTS.md` (les règles génériques du framework Ogygie), qu'il ne
4
+ > répète jamais : ce fichier ne contient que les LIAISONS — les noms réels que
5
+ > les règles génériques laissent en placeholder, et les règles qui n'existent
6
+ > que pour ce produit. C'est LE premier fichier à remplir après `ogygie init` :
7
+ > tant qu'une section dit « à définir », la règle générique correspondante n'a
8
+ > pas de prise sur ce produit.
9
+
10
+ ## Identité
11
+
12
+ - Produit : **__PRODUCT_NAME__** — à décrire : le job-to-be-done du produit en
13
+ une phrase (reporter aussi la description dans `ogygie.manifest.json`).
14
+ - Les ids de features/specs sont en FRANÇAIS : ils nomment des documents, pas
15
+ du code.
16
+
17
+ ## Liaisons multi-tenant
18
+
19
+ - Tenant du produit : **à définir** (le mot du domaine qui possède les
20
+ données). L'alias produit de `TenantContext` est **`<Tenant>Context`**
21
+ (`apps/api/src/context.ts`) : clé TS de colonne `<tenant>Id`, colonne SQL
22
+ **`<tenant>_id`** — à déclarer aussi dans `scripts/verify-manifest.ts`
23
+ (`TENANT_COLUMN_NAMES`).
24
+ - Liste fermée du socle autorisé à toucher la base brute : **à définir** (la
25
+ feature qui CONSTRUIT les contextes tenant) — déclarée dans
26
+ `scripts/lint-interdits.ts` (`RAW_DB_SOCLE_ALLOWLIST`). Toute autre feature
27
+ passe par `tenantDb`, sauf les features à tables 100 % partagées (dérivé du
28
+ manifeste).
29
+ - Seule route autorisée à recevoir un identifiant de tenant côté client : la
30
+ route de bascule du tenant actif (appartenance vérifiée, 403 opaque sinon) —
31
+ l'exception prévue par AGENTS.md §Sécurité 2. À nommer ici quand elle existe.
32
+
33
+ ## Contrats produit partagés (exception §Structure)
34
+
35
+ - Aucun pour l'instant : la liste fermée est VIDE. Le jour où deux features
36
+ partagent un contrat transverse, il naît sous `apps/api/src/` et s'ajoute
37
+ ici.
38
+
39
+ ## Tables partagées du produit (§Multi-tenant règle 4)
40
+
41
+ - Aucune pour l'instant. Une table de référentiel partagé se déclare au
42
+ manifeste (`{ "name": "...", "shared": true }`) et s'ajoute ici avec sa
43
+ source et son script d'extraction.
44
+
45
+ ## Règles produit
46
+
47
+ - (aucune pour l'instant — toute règle qui ne vaut que pour ce produit se
48
+ consigne ici, jamais dans AGENTS.md, qui est gouverné par Ogygie)
@@ -0,0 +1,29 @@
1
+ # Instructions pour Claude Code
2
+
3
+ Avant TOUTE tâche, lis dans cet ordre :
4
+ 1. `AGENTS.md` — les règles et templates canoniques GÉNÉRIQUES (obligatoire, en entier)
5
+ 2. `AGENTS.product.md` — l'extension produit : alias, colonnes tenant, listes fermées (obligatoire, en entier)
6
+ 3. `ogygie.manifest.json` — la carte du repo
7
+ 4. `DECISIONS.md` (framework) et `DECISIONS.product.md` (produit) — les décisions actées (ne jamais les rouvrir ; numérotation D<n> globale sur les deux fichiers)
8
+ 5. La spec de la feature concernée dans `specs/` (une spec `status: validated` = droit d'implémenter)
9
+
10
+ N'explore pas le repo au-delà du dossier de la feature concernée.
11
+ Si les fichiers ci-dessus ne suffisent pas pour accomplir la tâche : STOP,
12
+ consigne le manque dans `FAILURES.md` et demande, plutôt que de deviner.
13
+
14
+ Communication en français. Frontière linguistique : tout ce qui est
15
+ MACHINE-LISIBLE est en ANGLAIS — identifiants, fonctions, tables et colonnes
16
+ BDD, routes HTTP, codes d'erreur, valeurs d'enum, noms de schémas/contrats et
17
+ de patterns, clés du manifeste, champs OgygieError, et TOUS les commentaires
18
+ (y compris `@intent`) — aucune exception. Tout ce qui est destiné aux HUMAINS
19
+ du projet reste en FRANÇAIS : specs (prose et titres de sections),
20
+ DECISIONS.md, DECISIONS.product.md, FAILURES.md, PR, textes UI via clés ICU,
21
+ ids de features/specs.
22
+
23
+ La boucle verte du repo est `pnpm verify` (gates → typecheck → lint → tests).
24
+ Elle doit être verte avant tout commit.
25
+
26
+ N'utilise AUCUN skill ou workflow de processus externe (superpowers, brainstorming,
27
+ plans multi-étapes génériques) : le processus de ce repo est défini par AGENTS.md
28
+ et les specs, rien d'autre. Si ce processus te semble insuffisant pour une tâche,
29
+ consigne le manque dans FAILURES.md au lieu de compenser par un skill.
@@ -0,0 +1,18 @@
1
+ # DECISIONS.md — Décisions structurantes du FRAMEWORK
2
+
3
+ > Chaque décision est datée, motivée, et cite ce qu'elle exclut. Un agent ne
4
+ > rouvre pas une décision actée : il la lit ici et s'y conforme (ou propose un
5
+ > amendement). Ce fichier porte les décisions FRAMEWORK (conventions,
6
+ > outillage, gates) ; les décisions propres au produit vivent dans
7
+ > `DECISIONS.product.md`. La numérotation `D<n>` est GLOBALE aux deux fichiers
8
+ > — un même numéro ne peut exister qu'à un seul endroit (gate
9
+ > DECISION_DECLARED_TWICE).
10
+ >
11
+ > Format d'une décision :
12
+ >
13
+ > ## D<n> — AAAA-MM-JJ — <titre>
14
+ > - **Décision** : …
15
+ > - **Motivation** : …
16
+ > - **Exclut** : …
17
+
18
+ (aucune décision pour l'instant — la première sera D1)
@@ -0,0 +1,11 @@
1
+ # DECISIONS.product.md — Décisions structurantes du PRODUIT
2
+
3
+ > Chaque décision est datée, motivée, et cite ce qu'elle exclut. Un agent ne
4
+ > rouvre pas une décision actée : il la lit ici et s'y conforme (ou propose un
5
+ > amendement). Ce fichier porte les décisions propres au PRODUIT (périmètre,
6
+ > domaine, arbitrages métier) ; les décisions du framework vivent dans
7
+ > `DECISIONS.md`. La numérotation `D<n>` est GLOBALE aux deux fichiers — un
8
+ > même numéro ne peut exister qu'à un seul endroit (gate
9
+ > DECISION_DECLARED_TWICE).
10
+
11
+ (aucune décision pour l'instant)
@@ -0,0 +1,16 @@
1
+ # FAILURES.md — Journal des échecs
2
+
3
+ > Spec vivante du framework : chaque fois qu'un agent hallucine une API, casse
4
+ > une feature ou tourne en rond, l'échec se consigne ICI — date, symptôme,
5
+ > cause racine, et la convention qui l'aurait empêché. Un échec non consigné
6
+ > se reproduira.
7
+ >
8
+ > Format d'une entrée :
9
+ >
10
+ > ## AAAA-MM-JJ — <symptôme en une ligne>
11
+ > - **Symptôme** : …
12
+ > - **Cause racine** : …
13
+ > - **Convention qui l'aurait empêché** : … (et si elle n'existe pas : la
14
+ > proposer en amendement d'AGENTS.md ou en décision)
15
+
16
+ (aucun échec consigné pour l'instant)