@ogygie/cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @ogygie/cli
2
2
 
3
- CLI du framework Ogygie (spec `ogygie-standalone-repo`). Trois commandes,
4
- toutes déterministes :
3
+ CLI du framework Ogygie (specs `ogygie-standalone-repo` et
4
+ `product-vocabulary-gate`). Quatre commandes, toutes déterministes :
5
5
 
6
6
  - `ogygie init <nom-produit>` — scaffolde un projet Ogygie dans le répertoire
7
7
  courant (vide) depuis `templates/project/`, injecte le nom du produit et
@@ -12,6 +12,10 @@ toutes déterministes :
12
12
  ou si le CLI est plus ancien que le projet.
13
13
  - `ogygie check` — gate CI : versions en lockstep (manifeste = @ogygie/cli =
14
14
  @ogygie/core, D2) et checksums des fichiers gouvernés.
15
+ - `ogygie verify-vocabulary` — gate CI du vocabulaire déclaré au manifeste
16
+ (bloc `vocabulary`) : aucun alias interdit dans les identifiants du code
17
+ produit, aucun mot du produit dans un fichier gouverné (D6). Silencieux tant
18
+ qu'aucun vocabulaire n'est déclaré.
15
19
 
16
20
  `--json` sur toute commande sort les erreurs au format OgygieError brut.
17
21
 
package/dist/cli.js CHANGED
@@ -14644,6 +14644,16 @@ var FrameworkManifestSchema = external_exports.object({
14644
14644
  }),
14645
14645
  governedFiles: external_exports.string().describe("Module declaring the exhaustive list of files `upgrade` may overwrite (D3)")
14646
14646
  });
14647
+ var VocabularyEntrySchema = external_exports.object({
14648
+ canonical: external_exports.string().regex(/^[a-z][a-zA-Z0-9]*$/, "camelCase English term required").describe("The one machine term for this concept, e.g. gadget"),
14649
+ forbiddenAliases: external_exports.array(
14650
+ external_exports.string().min(2).regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "lowercase root (kebab-case allowed) required")
14651
+ ).min(1).describe("Roots the code may never carry for this concept, e.g. widget"),
14652
+ reference: external_exports.string().optional().describe("Anchor into AGENTS.product.md where the concept is explained")
14653
+ });
14654
+ var ProductVocabularySchema = external_exports.object({
14655
+ entries: external_exports.array(VocabularyEntrySchema)
14656
+ });
14647
14657
  var OgygieManifestSchema = external_exports.object({
14648
14658
  version: external_exports.literal(1),
14649
14659
  product: external_exports.object({
@@ -14661,12 +14671,54 @@ var OgygieManifestSchema = external_exports.object({
14661
14671
  })
14662
14672
  ),
14663
14673
  features: external_exports.array(FeatureManifestSchema),
14674
+ vocabulary: ProductVocabularySchema.optional().describe(
14675
+ "The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
14676
+ ),
14664
14677
  conventions: external_exports.object({
14665
14678
  agentsMd: external_exports.literal("AGENTS.md"),
14666
14679
  failuresMd: external_exports.literal("FAILURES.md"),
14667
14680
  specsFolder: external_exports.literal("specs/")
14668
14681
  })
14669
14682
  }).superRefine((manifest, context) => {
14683
+ const canonicalOwners = /* @__PURE__ */ new Map();
14684
+ const aliasOwners = /* @__PURE__ */ new Map();
14685
+ (manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
14686
+ const canonicalRoot = entry.canonical.toLowerCase();
14687
+ const firstOwner = canonicalOwners.get(canonicalRoot);
14688
+ if (firstOwner !== void 0) {
14689
+ context.addIssue({
14690
+ code: "custom",
14691
+ path: ["vocabulary", "entries", index, "canonical"],
14692
+ message: `canonical "${entry.canonical}" is already declared by entry ${firstOwner}`
14693
+ });
14694
+ } else {
14695
+ canonicalOwners.set(canonicalRoot, index);
14696
+ }
14697
+ });
14698
+ (manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
14699
+ entry.forbiddenAliases.forEach((alias, aliasIndex) => {
14700
+ const path = ["vocabulary", "entries", index, "forbiddenAliases", aliasIndex];
14701
+ const canonicalOwner = canonicalOwners.get(alias);
14702
+ if (canonicalOwner !== void 0) {
14703
+ context.addIssue({
14704
+ code: "custom",
14705
+ path,
14706
+ message: `alias "${alias}" is the canonical term of entry ${canonicalOwner} \u2014 a term cannot be both`
14707
+ });
14708
+ return;
14709
+ }
14710
+ const firstOwner = aliasOwners.get(alias);
14711
+ if (firstOwner !== void 0) {
14712
+ context.addIssue({
14713
+ code: "custom",
14714
+ path,
14715
+ message: `alias "${alias}" is already forbidden by entry ${firstOwner} \u2014 one alias belongs to one concept`
14716
+ });
14717
+ return;
14718
+ }
14719
+ aliasOwners.set(alias, index);
14720
+ });
14721
+ });
14670
14722
  const appsByName = new Map(manifest.apps.map((app) => [app.name, app]));
14671
14723
  const knownInvalidationTargets = new Set(
14672
14724
  manifest.features.map((feature) => feature.name)
@@ -15044,6 +15096,189 @@ ${diff}`);
15044
15096
  };
15045
15097
  }
15046
15098
 
15099
+ // src/commands/verify-vocabulary.ts
15100
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
15101
+ import { join as join5, relative, sep } from "node:path";
15102
+
15103
+ // ../ogygie-core/src/vocabulary.ts
15104
+ var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
15105
+ function segmentsOfIdentifier(token) {
15106
+ return token.split(/[-_$]+/).flatMap(
15107
+ (part) => part.replace(new RegExp("(?<=\\p{Ll}|\\p{N})(?=\\p{Lu})", "gu"), " ").replace(new RegExp("(?<=\\p{Lu})(?=\\p{Lu}\\p{Ll})", "gu"), " ").split(" ")
15108
+ ).filter((segment) => segment.length > 0).map((segment) => segment.toLowerCase());
15109
+ }
15110
+ function tokenCarriesRoot(tokenSegments, rootSegments) {
15111
+ const lastIndex = rootSegments.length - 1;
15112
+ for (let start = 0; start + rootSegments.length <= tokenSegments.length; start += 1) {
15113
+ let leadingSegmentsMatch = true;
15114
+ for (let offset = 0; offset < lastIndex; offset += 1) {
15115
+ if (tokenSegments[start + offset] !== rootSegments[offset]) {
15116
+ leadingSegmentsMatch = false;
15117
+ break;
15118
+ }
15119
+ }
15120
+ const lastTokenSegment = tokenSegments[start + lastIndex];
15121
+ if (leadingSegmentsMatch && lastTokenSegment !== void 0 && lastTokenSegment.startsWith(rootSegments[lastIndex] ?? "")) {
15122
+ return true;
15123
+ }
15124
+ }
15125
+ return false;
15126
+ }
15127
+ function findProductVocabulary(sourceText, vocabularyRoots) {
15128
+ const roots = vocabularyRoots.map((root) => ({
15129
+ root,
15130
+ segments: segmentsOfIdentifier(root)
15131
+ }));
15132
+ const matches = [];
15133
+ const alreadyReported = /* @__PURE__ */ new Set();
15134
+ sourceText.split("\n").forEach((lineText, lineIndex) => {
15135
+ for (const token of lineText.match(TOKEN_PATTERN) ?? []) {
15136
+ const tokenSegments = segmentsOfIdentifier(token);
15137
+ for (const { root, segments } of roots) {
15138
+ if (segments.length === 0) continue;
15139
+ if (!tokenCarriesRoot(tokenSegments, segments)) continue;
15140
+ const reportKey = `${root} ${token}`;
15141
+ if (alreadyReported.has(reportKey)) continue;
15142
+ alreadyReported.add(reportKey);
15143
+ matches.push({ root, identifier: token, line: lineIndex + 1 });
15144
+ }
15145
+ }
15146
+ });
15147
+ return matches;
15148
+ }
15149
+ function stripComments(sourceText) {
15150
+ const blankKeepingNewlines = (text) => text.replace(/[^\n]/g, " ");
15151
+ return sourceText.replace(/\/\*[\s\S]*?\*\//g, blankKeepingNewlines).replace(/\/\/[^\n]*/g, blankKeepingNewlines);
15152
+ }
15153
+
15154
+ // ../ogygie-core/src/vocabulary-audit.ts
15155
+ function vocabularyRootsOf(vocabulary) {
15156
+ return vocabulary.entries.flatMap((entry) => [entry.canonical, ...entry.forbiddenAliases]);
15157
+ }
15158
+ function canonicalByAlias(vocabulary) {
15159
+ const byAlias = /* @__PURE__ */ new Map();
15160
+ for (const entry of vocabulary.entries) {
15161
+ for (const alias of entry.forbiddenAliases) byAlias.set(alias, entry.canonical);
15162
+ }
15163
+ return byAlias;
15164
+ }
15165
+ function auditProductVocabulary(input) {
15166
+ const { vocabulary } = input;
15167
+ if (vocabulary === void 0 || vocabulary.entries.length === 0) return [];
15168
+ const canonicalOfAlias = canonicalByAlias(vocabulary);
15169
+ const forbiddenAliases = [...canonicalOfAlias.keys()];
15170
+ const everyRoot = vocabularyRootsOf(vocabulary);
15171
+ const violations = [];
15172
+ for (const file2 of input.sourceFiles) {
15173
+ for (const match of findProductVocabulary(stripComments(file2.content), forbiddenAliases)) {
15174
+ violations.push({
15175
+ kind: "FORBIDDEN_ALIAS",
15176
+ filePath: file2.path,
15177
+ match,
15178
+ canonical: canonicalOfAlias.get(match.root) ?? null
15179
+ });
15180
+ }
15181
+ }
15182
+ for (const file2 of input.governedFiles) {
15183
+ for (const match of findProductVocabulary(file2.content, everyRoot)) {
15184
+ violations.push({
15185
+ kind: "GOVERNED_FILE",
15186
+ filePath: file2.path,
15187
+ match,
15188
+ canonical: null
15189
+ });
15190
+ }
15191
+ }
15192
+ return violations;
15193
+ }
15194
+
15195
+ // src/commands/verify-vocabulary.ts
15196
+ var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
15197
+ var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
15198
+ function readManifest(projectRoot) {
15199
+ const manifestPath = join5(projectRoot, MANIFEST_FILE_NAME);
15200
+ if (!existsSync5(manifestPath)) {
15201
+ throwOgygieError({
15202
+ code: "OGYGIE_CLI_USAGE",
15203
+ message: `No ${MANIFEST_FILE_NAME} here \u2014 this is not an Ogygie project root.`,
15204
+ file: MANIFEST_FILE_NAME,
15205
+ cause: `${manifestPath} does not exist`,
15206
+ suggestedFix: "Run the command from the project root."
15207
+ });
15208
+ }
15209
+ const parsed = OgygieManifestSchema.safeParse(
15210
+ JSON.parse(readFileSync5(manifestPath, "utf8"))
15211
+ );
15212
+ if (!parsed.success) {
15213
+ throwOgygieError({
15214
+ code: "MANIFEST_INVALID",
15215
+ message: `${MANIFEST_FILE_NAME} does not satisfy OgygieManifestSchema \u2014 the vocabulary cannot be read from it.`,
15216
+ file: MANIFEST_FILE_NAME,
15217
+ cause: parsed.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join(" ; "),
15218
+ suggestedFix: "Fix the listed fields, then run the gate again."
15219
+ });
15220
+ }
15221
+ return parsed.data;
15222
+ }
15223
+ function collectCodeFiles(projectRoot, folder) {
15224
+ const absoluteFolder = join5(projectRoot, folder);
15225
+ if (!existsSync5(absoluteFolder)) return [];
15226
+ const files = [];
15227
+ const walk = (directory) => {
15228
+ for (const entry of readdirSync2(directory)) {
15229
+ if (IGNORED_DIRS.has(entry)) continue;
15230
+ const entryPath = join5(directory, entry);
15231
+ if (statSync2(entryPath).isDirectory()) {
15232
+ walk(entryPath);
15233
+ continue;
15234
+ }
15235
+ if (!CODE_EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
15236
+ files.push({
15237
+ path: relative(projectRoot, entryPath).split(sep).join("/"),
15238
+ content: readFileSync5(entryPath, "utf8")
15239
+ });
15240
+ }
15241
+ };
15242
+ walk(absoluteFolder);
15243
+ return files;
15244
+ }
15245
+ function productCodeFoldersOf(manifest) {
15246
+ return [
15247
+ ...manifest.apps.map((app) => app.path),
15248
+ ...(manifest.framework?.packages ?? []).map((packageEntry) => packageEntry.path)
15249
+ ];
15250
+ }
15251
+ function verifyVocabulary(deps) {
15252
+ const manifest = readManifest(deps.projectRoot);
15253
+ if (manifest.vocabulary === void 0) return [];
15254
+ const sourceFiles = productCodeFoldersOf(manifest).flatMap(
15255
+ (folder) => collectCodeFiles(deps.projectRoot, folder)
15256
+ );
15257
+ const governed = governedFiles.filter((file2) => existsSync5(join5(deps.projectRoot, file2.targetPath))).map((file2) => ({
15258
+ path: file2.targetPath,
15259
+ content: readFileSync5(join5(deps.projectRoot, file2.targetPath), "utf8")
15260
+ }));
15261
+ return auditProductVocabulary({
15262
+ vocabulary: manifest.vocabulary,
15263
+ sourceFiles,
15264
+ governedFiles: governed
15265
+ }).map(
15266
+ (violation) => violation.kind === "FORBIDDEN_ALIAS" ? ogygieError({
15267
+ code: "OGYGIE_VOCABULARY_FORBIDDEN_ALIAS",
15268
+ message: `${violation.filePath}:${violation.match.line} uses "${violation.match.identifier}", built on the forbidden alias "${violation.match.root}".`,
15269
+ file: violation.filePath,
15270
+ cause: `identifier ${violation.match.identifier} carries the root "${violation.match.root}" declared forbidden in ${MANIFEST_FILE_NAME}`,
15271
+ suggestedFix: `Rename it around the canonical term: use "${violation.canonical ?? ""}" instead of "${violation.match.root}".`
15272
+ }) : ogygieError({
15273
+ code: "OGYGIE_VOCABULARY_IN_GOVERNED_FILE",
15274
+ message: `Governed file ${violation.filePath}:${violation.match.line} names the product term "${violation.match.identifier}".`,
15275
+ file: violation.filePath,
15276
+ cause: `a governed file is identical in every Ogygie project, so it may name no product domain \u2014 found the declared root "${violation.match.root}"`,
15277
+ suggestedFix: "Move the product-specific part to AGENTS.product.md (or to the product's own files), and restore the governed file to its pristine content."
15278
+ })
15279
+ );
15280
+ }
15281
+
15047
15282
  // src/governed-checksums.ts
15048
15283
  var governedChecksumsByVersion = {
15049
15284
  "0.1.0": {
@@ -15051,24 +15286,33 @@ var governedChecksumsByVersion = {
15051
15286
  "AGENTS.md": "de700f99aaf949b1d85c6ae6d199897736084f4c19adf132cdf5a3568af59f69",
15052
15287
  "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15053
15288
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15289
+ },
15290
+ "0.2.0": {
15291
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15292
+ "AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
15293
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15294
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15054
15295
  }
15055
15296
  };
15056
15297
 
15057
15298
  // src/paths.ts
15058
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
15059
- import { dirname as dirname3, join as join5 } from "node:path";
15299
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
15300
+ import { dirname as dirname3, join as join6 } from "node:path";
15060
15301
  import { fileURLToPath } from "node:url";
15061
- var PACKAGE_ROOT = join5(dirname3(fileURLToPath(import.meta.url)), "..");
15302
+ var PACKAGE_ROOT = join6(dirname3(fileURLToPath(import.meta.url)), "..");
15062
15303
  var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
15063
15304
  function cliVersion() {
15064
15305
  return PackageVersionSchema2.parse(
15065
- JSON.parse(readFileSync5(join5(PACKAGE_ROOT, "package.json"), "utf8"))
15306
+ JSON.parse(readFileSync6(join6(PACKAGE_ROOT, "package.json"), "utf8"))
15066
15307
  ).version;
15067
15308
  }
15068
15309
  function templatesDir() {
15069
- const embedded = join5(PACKAGE_ROOT, "templates");
15070
- if (existsSync5(embedded)) return embedded;
15071
- return join5(PACKAGE_ROOT, "..", "..", "templates");
15310
+ const embedded = join6(PACKAGE_ROOT, "templates");
15311
+ if (existsSync6(embedded)) return embedded;
15312
+ return repoTemplatesDir();
15313
+ }
15314
+ function repoTemplatesDir() {
15315
+ return join6(PACKAGE_ROOT, "..", "..", "templates");
15072
15316
  }
15073
15317
 
15074
15318
  // src/cli.ts
@@ -15133,14 +15377,23 @@ ogygie check \u2014 ${problems.length} probl\xE8me(s).`);
15133
15377
  console.log(
15134
15378
  `ogygie check \u2014 OK, lockstep ${cliVersion()} et fichiers gouvern\xE9s intacts.`
15135
15379
  );
15380
+ } else if (command === "verify-vocabulary") {
15381
+ const problems = verifyVocabulary({ projectRoot: process.cwd() });
15382
+ if (problems.length > 0) {
15383
+ for (const problem of problems) printError(problem);
15384
+ console.error(`
15385
+ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
15386
+ process.exit(1);
15387
+ }
15388
+ console.log("ogygie verify-vocabulary \u2014 OK, le vocabulaire d\xE9clar\xE9 est respect\xE9.");
15136
15389
  } else {
15137
15390
  printError(
15138
15391
  ogygieError({
15139
15392
  code: "OGYGIE_CLI_USAGE",
15140
15393
  message: `Unknown command ${JSON.stringify(command ?? "")}.`,
15141
15394
  file: null,
15142
- cause: `expected one of: init <product-name>, upgrade, check (flags: --json, --yes)`,
15143
- suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade` or `ogygie check`."
15395
+ cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary (flags: --json, --yes)`,
15396
+ suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check` or `ogygie verify-vocabulary`."
15144
15397
  })
15145
15398
  );
15146
15399
  process.exit(1);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ogygie/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "ogygie": "./dist/cli.js"
8
+ "ogygie": "dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
@@ -16,17 +16,17 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
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
19
  "devDependencies": {
25
- "@ogygie/core": "workspace:*",
26
20
  "@types/node": "^24.0.0",
27
21
  "esbuild": "^0.28.0",
28
22
  "tsx": "^4.20.0",
29
23
  "typescript": "^5.8.0",
30
- "zod": "^4.0.0"
24
+ "zod": "^4.0.0",
25
+ "@ogygie/core": "0.2.0"
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc -p tsconfig.json",
29
+ "build": "tsx scripts/build.ts",
30
+ "record-checksums": "tsx scripts/record-governed-checksums.ts"
31
31
  }
32
- }
32
+ }
@@ -240,6 +240,38 @@ throw ogygieError({
240
240
  6. **Identifiants code en anglais, contenu en langue de l'utilisateur.** Les enums du
241
241
  domaine sont en anglais ; leur affichage est traduit.
242
242
 
243
+ ## Vocabulaire du produit — un concept, un terme
244
+
245
+ Le produit déclare son vocabulaire fermé dans le bloc `vocabulary` d'`ogygie.manifest.json` :
246
+ pour chaque concept, LE terme canonique (identifiant machine, anglais) et les
247
+ alias que le code n'a pas le droit de porter. La prose qui explique le métier
248
+ reste dans `AGENTS.product.md` et cite les termes canoniques : le manifeste
249
+ déclare, la prose explique, la partie machine n'existe qu'à un seul endroit.
250
+
251
+ Le gate `pnpm exec ogygie verify-vocabulary` (posé dans la CI gouvernée)
252
+ applique deux règles :
253
+
254
+ - **Le code du produit ne porte aucun alias interdit** — l'appariement se fait
255
+ sur les identifiants machine (découpage camelCase / snake_case / kebab-case,
256
+ par racine), commentaires exclus. La prose du métier garde ses mots ; le code
257
+ n'a qu'un terme par concept. Erreur `OGYGIE_VOCABULARY_FORBIDDEN_ALIAS`, dont
258
+ la correction suggérée est le terme canonique.
259
+ - **Aucun fichier gouverné ne nomme un terme du produit**, canonique compris :
260
+ un fichier gouverné est identique dans tous les projets Ogygie, il ne peut
261
+ donc connaître le domaine d'aucun. Erreur
262
+ `OGYGIE_VOCABULARY_IN_GOVERNED_FILE` — la spécificité locale se déplace vers
263
+ `AGENTS.product.md`, elle ne se négocie pas dans un fichier gouverné.
264
+
265
+ Tant qu'aucun bloc `vocabulary` n'est déclaré, le gate passe sans rien dire :
266
+ il devient actif le jour où le produit déclare, jamais avant. Renommer un
267
+ concept est donc une opération à un seul endroit — on change le canonique, le
268
+ gate liste tous les identifiants à corriger.
269
+
270
+ Choisir les racines est un acte de conception, pas une formalité : l'appariement
271
+ se fait PAR RACINE, donc une racine trop courte attrape des mots génériques
272
+ (déclarer `memo` interdit fait tomber `memory`, `createInMemoryDb`…). Une racine
273
+ se déclare assez longue pour ne nommer que le concept.
274
+
243
275
  ## Front (apps/web) — règles et templates canoniques
244
276
 
245
277
  ### Stack imposée (toute autre lib = amendement DECISIONS.md d'abord)
@@ -42,6 +42,14 @@
42
42
  manifeste (`{ "name": "...", "shared": true }`) et s'ajoute ici avec sa
43
43
  source et son script d'extraction.
44
44
 
45
+ ## Vocabulaire du produit (§Vocabulaire)
46
+
47
+ - À définir, concept par concept : chaque terme canonique et ses alias
48
+ interdits se déclarent dans le bloc `vocabulary` d'`ogygie.manifest.json`
49
+ (la partie machine), et sa définition métier se rédige ICI, sous une ancre
50
+ que l'entrée du manifeste référence. Tant que le bloc est absent, le gate
51
+ `ogygie verify-vocabulary` passe sans rien dire.
52
+
45
53
  ## Règles produit
46
54
 
47
55
  - (aucune pour l'instant — toute règle qui ne vaut que pour ce produit se
@@ -29,6 +29,7 @@ jobs:
29
29
  - run: pnpm ogygie:verify-manifest # manifest ↔ actual code (routes, folders, tables)
30
30
  - run: pnpm ogygie:verify-specs # frontmatter + sections + spec lifecycle + decision refs
31
31
  - run: pnpm ogygie:verify-contracts # transitive purity of every schemas.ts
32
+ - run: pnpm exec ogygie verify-vocabulary # declared vocabulary: forbidden aliases + governed-file purity
32
33
  - run: pnpm typecheck
33
34
  - run: pnpm lint # any, ts-ignore, raw-db, dangerouslySetInnerHTML
34
35
  - run: pnpm test
@@ -13,9 +13,10 @@
13
13
  "ogygie:verify-manifest": "tsx scripts/verify-manifest.ts",
14
14
  "ogygie:verify-specs": "tsx scripts/verify-specs.ts",
15
15
  "ogygie:verify-contracts": "tsx scripts/verify-contracts.ts",
16
+ "ogygie:verify-vocabulary": "ogygie verify-vocabulary",
16
17
  "ogygie:impact": "tsx scripts/ogygie-impact.ts",
17
18
  "ogygie:design-brief": "tsx scripts/ogygie-design-brief.ts",
18
- "verify": "pnpm ogygie:verify-manifest && pnpm ogygie:verify-specs && pnpm ogygie:verify-contracts && pnpm typecheck && pnpm lint && pnpm test"
19
+ "verify": "pnpm ogygie:verify-manifest && pnpm ogygie:verify-specs && pnpm ogygie:verify-contracts && pnpm ogygie:verify-vocabulary && pnpm typecheck && pnpm lint && pnpm test"
19
20
  },
20
21
  "dependencies": {
21
22
  "@ogygie/core": "__OGYGIE_VERSION__"
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Deliberate scope choices:
14
14
  * - Depth 1 only (direct dependents) — spec §Non-objectifs.
15
- * - The owned-tables scan ignores *.test.ts files: seeding ANOTHER feature's
15
+ * - The owned-tables scan ignores *.test.ts files: populating ANOTHER feature's
16
16
  * table as a fixture is the canonical bench pattern ("the socle feature owns
17
17
  * that model" — see any service.test.ts); flagging it would make every run
18
18
  * red and the tool useless. Production code referencing a foreign table
@@ -289,7 +289,7 @@ if (featureArgument === undefined) {
289
289
  message: "No feature name given. Usage: pnpm ogygie:impact <feature> [--json].",
290
290
  file: null,
291
291
  cause: "process.argv carries no non-flag argument",
292
- suggestedFix: "Pass a feature name from ogygie.manifest.json, e.g. `pnpm ogygie:impact cultures`.",
292
+ suggestedFix: "Pass a feature name from ogygie.manifest.json, e.g. `pnpm ogygie:impact orders`.",
293
293
  });
294
294
  console.error(JSON.stringify(failure, null, 2));
295
295
  process.exit(1);
@@ -3,8 +3,8 @@
3
3
  * feature's `schemas.ts`, walk the WHOLE import graph and prove that nothing
4
4
  * with a runtime effect is reachable — the transitivity is the point, since
5
5
  * that is how the server came back into the browser bundle
6
- * (`cultures/schemas.ts` → `especes-eppo/index.ts` → `seed.ts` → `node:fs`,
7
- * FAILURES.md).
6
+ * (`orders/schemas.ts` → a sibling feature's `index.ts` → its data loader →
7
+ * `node:fs`, FAILURES.md).
8
8
  *
9
9
  * The decision logic lives in @ogygie/core/contract-purity, born with its
10
10
  * mutation tests (D22) — this script only reads the manifest, the disk, and
@@ -15,7 +15,7 @@ existe, pas comment elle est construite.
15
15
 
16
16
  # Comportements
17
17
  (prose en français ; codes d'erreur, noms de schémas et valeurs d'enum
18
- directement en anglais — D11/D14, ex. « ALORS OgygieError PLOT_CODE_ALREADY_USED »)
18
+ directement en anglais — D11/D14, ex. « ALORS OgygieError ORDER_REFERENCE_ALREADY_USED »)
19
19
  - ÉTANT DONNÉ <contexte>
20
20
  QUAND <action>
21
21
  ALORS <résultat observable et vérifiable>