@ogygie/cli 0.1.0 → 0.3.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)
@@ -14939,6 +14991,19 @@ function diffLines(oldText, newText) {
14939
14991
  return output.join("\n");
14940
14992
  }
14941
14993
 
14994
+ // src/framework-gates.ts
14995
+ var frameworkGates = [
14996
+ {
14997
+ scriptName: "ogygie:verify-vocabulary",
14998
+ command: "ogygie verify-vocabulary",
14999
+ intent: "Le vocabulaire d\xE9clar\xE9 au manifeste : alias interdits dans les identifiants, puret\xE9 des fichiers gouvern\xE9s."
15000
+ }
15001
+ ];
15002
+ function missingFrameworkGates(scripts) {
15003
+ const commands = Object.values(scripts ?? {}).join(" ; ");
15004
+ return frameworkGates.filter((gate) => !commands.includes(gate.command));
15005
+ }
15006
+
14942
15007
  // src/version.ts
14943
15008
  function compareVersions(a, b) {
14944
15009
  const partsA = a.split(".").map(Number);
@@ -14953,6 +15018,30 @@ function compareVersions(a, b) {
14953
15018
  }
14954
15019
 
14955
15020
  // src/commands/upgrade.ts
15021
+ function reportMissingGates(deps) {
15022
+ const packageJsonPath = join4(deps.projectRoot, "package.json");
15023
+ if (!existsSync4(packageJsonPath)) return;
15024
+ let scripts;
15025
+ try {
15026
+ const parsed = JSON.parse(readFileSync4(packageJsonPath, "utf8"));
15027
+ const scriptsValue = typeof parsed === "object" && parsed !== null ? parsed.scripts : void 0;
15028
+ if (typeof scriptsValue === "object" && scriptsValue !== null) {
15029
+ scripts = scriptsValue;
15030
+ }
15031
+ } catch {
15032
+ return;
15033
+ }
15034
+ const missing = missingFrameworkGates(scripts);
15035
+ if (missing.length === 0) return;
15036
+ deps.log("");
15037
+ deps.log(
15038
+ `\u26A0 ${missing.length} gate(s) du framework ne tournent pas dans ta boucle locale (package.json n'est pas gouvern\xE9 \u2014 \xE0 toi de l'ajouter, la CI gouvern\xE9e les appelle d\xE9j\xE0) :`
15039
+ );
15040
+ for (const gate of missing) {
15041
+ deps.log(` "${gate.scriptName}": "${gate.command}" # ${gate.intent}`);
15042
+ }
15043
+ deps.log(" puis appelle ces scripts depuis ton script `verify`.");
15044
+ }
14956
15045
  async function upgradeProject(deps) {
14957
15046
  const installedVersion = readPinnedOgygieVersion(deps.projectRoot);
14958
15047
  if (compareVersions(deps.cliVersion, installedVersion) < 0) {
@@ -14961,7 +15050,7 @@ async function upgradeProject(deps) {
14961
15050
  message: `The running CLI (${deps.cliVersion}) is older than the project's ogygieVersion (${installedVersion}) \u2014 a downgrade does not exist.`,
14962
15051
  file: "ogygie.manifest.json",
14963
15052
  cause: `cli ${deps.cliVersion} < ogygieVersion ${installedVersion}`,
14964
- suggestedFix: "Run `pnpm update @ogygie/cli` first, then retry the upgrade."
15053
+ suggestedFix: `Install a CLI at least as recent as the project, WITH an explicit version (init pins exact ranges, so a bare \`pnpm update\` moves nothing): pnpm update @ogygie/core@${installedVersion} @ogygie/cli@${installedVersion} \u2014 then retry the upgrade.`
14965
15054
  });
14966
15055
  }
14967
15056
  if (compareVersions(deps.cliVersion, installedVersion) === 0) {
@@ -15037,6 +15126,7 @@ ${diff}`);
15037
15126
  deps.log(
15038
15127
  `Rappel lockstep (D2) : mettre @ogygie/core ET @ogygie/cli \xE0 ${deps.cliVersion} (\`pnpm update @ogygie/core@${deps.cliVersion} @ogygie/cli@${deps.cliVersion}\`), puis committer l'upgrade SEUL \u2014 jamais m\xEAl\xE9 \xE0 une feature.`
15039
15128
  );
15129
+ reportMissingGates(deps);
15040
15130
  return {
15041
15131
  fromVersion: installedVersion,
15042
15132
  toVersion: deps.cliVersion,
@@ -15044,6 +15134,186 @@ ${diff}`);
15044
15134
  };
15045
15135
  }
15046
15136
 
15137
+ // src/commands/verify-vocabulary.ts
15138
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
15139
+ import { join as join5, relative, sep } from "node:path";
15140
+
15141
+ // ../ogygie-core/src/vocabulary.ts
15142
+ var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
15143
+ function segmentsOfIdentifier(token) {
15144
+ return token.split(/[-_$]+/).flatMap(
15145
+ (part) => part.replace(new RegExp("(?<=\\p{Ll}|\\p{N})(?=\\p{Lu})", "gu"), " ").replace(new RegExp("(?<=\\p{Lu})(?=\\p{Lu}\\p{Ll})", "gu"), " ").split(" ")
15146
+ ).filter((segment) => segment.length > 0).map((segment) => segment.toLowerCase());
15147
+ }
15148
+ function tokenCarriesRoot(tokenSegments, rootSegments) {
15149
+ const lastIndex = rootSegments.length - 1;
15150
+ for (let start = 0; start + rootSegments.length <= tokenSegments.length; start += 1) {
15151
+ let leadingSegmentsMatch = true;
15152
+ for (let offset = 0; offset < lastIndex; offset += 1) {
15153
+ if (tokenSegments[start + offset] !== rootSegments[offset]) {
15154
+ leadingSegmentsMatch = false;
15155
+ break;
15156
+ }
15157
+ }
15158
+ const lastTokenSegment = tokenSegments[start + lastIndex];
15159
+ if (leadingSegmentsMatch && lastTokenSegment !== void 0 && lastTokenSegment.startsWith(rootSegments[lastIndex] ?? "")) {
15160
+ return true;
15161
+ }
15162
+ }
15163
+ return false;
15164
+ }
15165
+ function findProductVocabulary(sourceText, vocabularyRoots) {
15166
+ const roots = vocabularyRoots.map((root) => ({
15167
+ root,
15168
+ segments: segmentsOfIdentifier(root)
15169
+ }));
15170
+ const matches = [];
15171
+ const alreadyReported = /* @__PURE__ */ new Set();
15172
+ sourceText.split("\n").forEach((lineText, lineIndex) => {
15173
+ for (const token of lineText.match(TOKEN_PATTERN) ?? []) {
15174
+ const tokenSegments = segmentsOfIdentifier(token);
15175
+ for (const { root, segments } of roots) {
15176
+ if (segments.length === 0) continue;
15177
+ if (!tokenCarriesRoot(tokenSegments, segments)) continue;
15178
+ const reportKey = `${root} ${token}`;
15179
+ if (alreadyReported.has(reportKey)) continue;
15180
+ alreadyReported.add(reportKey);
15181
+ matches.push({ root, identifier: token, line: lineIndex + 1 });
15182
+ }
15183
+ }
15184
+ });
15185
+ return matches;
15186
+ }
15187
+ function stripComments(sourceText) {
15188
+ const blankKeepingNewlines = (text) => text.replace(/[^\n]/g, " ");
15189
+ return sourceText.replace(/\/\*[\s\S]*?\*\//g, blankKeepingNewlines).replace(/\/\/[^\n]*/g, blankKeepingNewlines);
15190
+ }
15191
+
15192
+ // ../ogygie-core/src/vocabulary-audit.ts
15193
+ function canonicalByAlias(vocabulary) {
15194
+ const byAlias = /* @__PURE__ */ new Map();
15195
+ for (const entry of vocabulary.entries) {
15196
+ for (const alias of entry.forbiddenAliases) byAlias.set(alias, entry.canonical);
15197
+ }
15198
+ return byAlias;
15199
+ }
15200
+ function auditProductVocabulary(input) {
15201
+ const { vocabulary } = input;
15202
+ if (vocabulary === void 0 || vocabulary.entries.length === 0) return [];
15203
+ const canonicalOfAlias = canonicalByAlias(vocabulary);
15204
+ const forbiddenAliases = [...canonicalOfAlias.keys()];
15205
+ const canonicalTerms = vocabulary.entries.map((entry) => entry.canonical);
15206
+ const violations = [];
15207
+ for (const file2 of input.sourceFiles) {
15208
+ for (const match of findProductVocabulary(stripComments(file2.content), forbiddenAliases)) {
15209
+ violations.push({
15210
+ kind: "FORBIDDEN_ALIAS",
15211
+ filePath: file2.path,
15212
+ match,
15213
+ canonical: canonicalOfAlias.get(match.root) ?? null
15214
+ });
15215
+ }
15216
+ }
15217
+ for (const file2 of input.governedFiles) {
15218
+ for (const match of findProductVocabulary(file2.content, canonicalTerms)) {
15219
+ violations.push({
15220
+ kind: "GOVERNED_FILE",
15221
+ filePath: file2.path,
15222
+ match,
15223
+ canonical: null
15224
+ });
15225
+ }
15226
+ }
15227
+ return violations;
15228
+ }
15229
+
15230
+ // src/commands/verify-vocabulary.ts
15231
+ var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
15232
+ var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
15233
+ function readManifest(projectRoot) {
15234
+ const manifestPath = join5(projectRoot, MANIFEST_FILE_NAME);
15235
+ if (!existsSync5(manifestPath)) {
15236
+ throwOgygieError({
15237
+ code: "OGYGIE_CLI_USAGE",
15238
+ message: `No ${MANIFEST_FILE_NAME} here \u2014 this is not an Ogygie project root.`,
15239
+ file: MANIFEST_FILE_NAME,
15240
+ cause: `${manifestPath} does not exist`,
15241
+ suggestedFix: "Run the command from the project root."
15242
+ });
15243
+ }
15244
+ const parsed = OgygieManifestSchema.safeParse(
15245
+ JSON.parse(readFileSync5(manifestPath, "utf8"))
15246
+ );
15247
+ if (!parsed.success) {
15248
+ throwOgygieError({
15249
+ code: "MANIFEST_INVALID",
15250
+ message: `${MANIFEST_FILE_NAME} does not satisfy OgygieManifestSchema \u2014 the vocabulary cannot be read from it.`,
15251
+ file: MANIFEST_FILE_NAME,
15252
+ cause: parsed.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join(" ; "),
15253
+ suggestedFix: "Fix the listed fields, then run the gate again."
15254
+ });
15255
+ }
15256
+ return parsed.data;
15257
+ }
15258
+ function collectCodeFiles(projectRoot, folder) {
15259
+ const absoluteFolder = join5(projectRoot, folder);
15260
+ if (!existsSync5(absoluteFolder)) return [];
15261
+ const files = [];
15262
+ const walk = (directory) => {
15263
+ for (const entry of readdirSync2(directory)) {
15264
+ if (IGNORED_DIRS.has(entry)) continue;
15265
+ const entryPath = join5(directory, entry);
15266
+ if (statSync2(entryPath).isDirectory()) {
15267
+ walk(entryPath);
15268
+ continue;
15269
+ }
15270
+ if (!CODE_EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
15271
+ files.push({
15272
+ path: relative(projectRoot, entryPath).split(sep).join("/"),
15273
+ content: readFileSync5(entryPath, "utf8")
15274
+ });
15275
+ }
15276
+ };
15277
+ walk(absoluteFolder);
15278
+ return files;
15279
+ }
15280
+ function productCodeFoldersOf(manifest) {
15281
+ return [
15282
+ ...manifest.apps.map((app) => app.path),
15283
+ ...(manifest.framework?.packages ?? []).map((packageEntry) => packageEntry.path)
15284
+ ];
15285
+ }
15286
+ function verifyVocabulary(deps) {
15287
+ const manifest = readManifest(deps.projectRoot);
15288
+ if (manifest.vocabulary === void 0) return [];
15289
+ const sourceFiles = productCodeFoldersOf(manifest).flatMap(
15290
+ (folder) => collectCodeFiles(deps.projectRoot, folder)
15291
+ );
15292
+ const governed = governedFiles.filter((file2) => existsSync5(join5(deps.projectRoot, file2.targetPath))).map((file2) => ({
15293
+ path: file2.targetPath,
15294
+ content: readFileSync5(join5(deps.projectRoot, file2.targetPath), "utf8")
15295
+ }));
15296
+ return auditProductVocabulary({
15297
+ vocabulary: manifest.vocabulary,
15298
+ sourceFiles,
15299
+ governedFiles: governed
15300
+ }).map(
15301
+ (violation) => violation.kind === "FORBIDDEN_ALIAS" ? ogygieError({
15302
+ code: "OGYGIE_VOCABULARY_FORBIDDEN_ALIAS",
15303
+ message: `${violation.filePath}:${violation.match.line} uses "${violation.match.identifier}", built on the forbidden alias "${violation.match.root}".`,
15304
+ file: violation.filePath,
15305
+ cause: `identifier ${violation.match.identifier} carries the root "${violation.match.root}" declared forbidden in ${MANIFEST_FILE_NAME}`,
15306
+ suggestedFix: `Rename it around the canonical term: use "${violation.canonical ?? ""}" instead of "${violation.match.root}".`
15307
+ }) : ogygieError({
15308
+ code: "OGYGIE_VOCABULARY_IN_GOVERNED_FILE",
15309
+ message: `Governed file ${violation.filePath}:${violation.match.line} names the product term "${violation.match.identifier}".`,
15310
+ file: violation.filePath,
15311
+ 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}"`,
15312
+ 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."
15313
+ })
15314
+ );
15315
+ }
15316
+
15047
15317
  // src/governed-checksums.ts
15048
15318
  var governedChecksumsByVersion = {
15049
15319
  "0.1.0": {
@@ -15051,24 +15321,39 @@ var governedChecksumsByVersion = {
15051
15321
  "AGENTS.md": "de700f99aaf949b1d85c6ae6d199897736084f4c19adf132cdf5a3568af59f69",
15052
15322
  "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15053
15323
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15324
+ },
15325
+ "0.2.0": {
15326
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15327
+ "AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
15328
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15329
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15330
+ },
15331
+ "0.3.0": {
15332
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15333
+ "AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
15334
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15335
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15054
15336
  }
15055
15337
  };
15056
15338
 
15057
15339
  // 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";
15340
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
15341
+ import { dirname as dirname3, join as join6 } from "node:path";
15060
15342
  import { fileURLToPath } from "node:url";
15061
- var PACKAGE_ROOT = join5(dirname3(fileURLToPath(import.meta.url)), "..");
15343
+ var PACKAGE_ROOT = join6(dirname3(fileURLToPath(import.meta.url)), "..");
15062
15344
  var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
15063
15345
  function cliVersion() {
15064
15346
  return PackageVersionSchema2.parse(
15065
- JSON.parse(readFileSync5(join5(PACKAGE_ROOT, "package.json"), "utf8"))
15347
+ JSON.parse(readFileSync6(join6(PACKAGE_ROOT, "package.json"), "utf8"))
15066
15348
  ).version;
15067
15349
  }
15068
15350
  function templatesDir() {
15069
- const embedded = join5(PACKAGE_ROOT, "templates");
15070
- if (existsSync5(embedded)) return embedded;
15071
- return join5(PACKAGE_ROOT, "..", "..", "templates");
15351
+ const embedded = join6(PACKAGE_ROOT, "templates");
15352
+ if (existsSync6(embedded)) return embedded;
15353
+ return repoTemplatesDir();
15354
+ }
15355
+ function repoTemplatesDir() {
15356
+ return join6(PACKAGE_ROOT, "..", "..", "templates");
15072
15357
  }
15073
15358
 
15074
15359
  // src/cli.ts
@@ -15133,14 +15418,23 @@ ogygie check \u2014 ${problems.length} probl\xE8me(s).`);
15133
15418
  console.log(
15134
15419
  `ogygie check \u2014 OK, lockstep ${cliVersion()} et fichiers gouvern\xE9s intacts.`
15135
15420
  );
15421
+ } else if (command === "verify-vocabulary") {
15422
+ const problems = verifyVocabulary({ projectRoot: process.cwd() });
15423
+ if (problems.length > 0) {
15424
+ for (const problem of problems) printError(problem);
15425
+ console.error(`
15426
+ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
15427
+ process.exit(1);
15428
+ }
15429
+ console.log("ogygie verify-vocabulary \u2014 OK, le vocabulaire d\xE9clar\xE9 est respect\xE9.");
15136
15430
  } else {
15137
15431
  printError(
15138
15432
  ogygieError({
15139
15433
  code: "OGYGIE_CLI_USAGE",
15140
15434
  message: `Unknown command ${JSON.stringify(command ?? "")}.`,
15141
15435
  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`."
15436
+ cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary (flags: --json, --yes)`,
15437
+ suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check` or `ogygie verify-vocabulary`."
15144
15438
  })
15145
15439
  );
15146
15440
  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.3.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.3.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>