@ogygie/cli 0.11.2 → 0.13.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/dist/cli.js CHANGED
@@ -14555,12 +14555,90 @@ function throwOgygieError(input, httpStatus = 500) {
14555
14555
  throw new OgygieFailure(ogygieError(input), httpStatus);
14556
14556
  }
14557
14557
 
14558
- // src/commands/check.ts
14559
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
14560
- import { join as join2 } from "node:path";
14558
+ // src/commands/adopt.ts
14559
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
14560
+ import { dirname, join as join3 } from "node:path";
14561
14561
 
14562
- // src/checksum.ts
14563
- import { createHash } from "node:crypto";
14562
+ // src/diff.ts
14563
+ function diffLines(oldText, newText) {
14564
+ if (oldText === newText) return "";
14565
+ const oldLines = oldText.split("\n");
14566
+ const newLines = newText.split("\n");
14567
+ const lcs = Array.from(
14568
+ { length: oldLines.length + 1 },
14569
+ () => new Array(newLines.length + 1).fill(0)
14570
+ );
14571
+ for (let i2 = oldLines.length - 1; i2 >= 0; i2 -= 1) {
14572
+ for (let j2 = newLines.length - 1; j2 >= 0; j2 -= 1) {
14573
+ lcs[i2][j2] = oldLines[i2] === newLines[j2] ? (lcs[i2 + 1][j2 + 1] ?? 0) + 1 : Math.max(lcs[i2 + 1][j2] ?? 0, lcs[i2][j2 + 1] ?? 0);
14574
+ }
14575
+ }
14576
+ const output = [];
14577
+ let previousWasChange = false;
14578
+ let i = 0;
14579
+ let j = 0;
14580
+ while (i < oldLines.length || j < newLines.length) {
14581
+ if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
14582
+ previousWasChange = false;
14583
+ i += 1;
14584
+ j += 1;
14585
+ } else {
14586
+ if (!previousWasChange) output.push(`@@ line ${j + 1}`);
14587
+ previousWasChange = true;
14588
+ if (j >= newLines.length || i < oldLines.length && (lcs[i + 1]?.[j] ?? 0) >= (lcs[i]?.[j + 1] ?? 0)) {
14589
+ output.push(`- ${oldLines[i]}`);
14590
+ i += 1;
14591
+ } else {
14592
+ output.push(`+ ${newLines[j]}`);
14593
+ j += 1;
14594
+ }
14595
+ }
14596
+ }
14597
+ return output.join("\n");
14598
+ }
14599
+
14600
+ // src/gates-report.ts
14601
+ import { existsSync, readFileSync } from "node:fs";
14602
+ import { join } from "node:path";
14603
+
14604
+ // src/framework-gates.ts
14605
+ var frameworkGates = [
14606
+ {
14607
+ scriptName: "ogygie:verify-vocabulary",
14608
+ command: "ogygie verify-vocabulary",
14609
+ intent: "Le vocabulaire d\xE9clar\xE9 au manifeste : alias interdits dans les identifiants, puret\xE9 des fichiers gouvern\xE9s."
14610
+ }
14611
+ ];
14612
+ function missingFrameworkGates(scripts) {
14613
+ const commands = Object.values(scripts ?? {}).join(" ; ");
14614
+ return frameworkGates.filter((gate) => !commands.includes(gate.command));
14615
+ }
14616
+
14617
+ // src/gates-report.ts
14618
+ function reportMissingGates(projectRoot, log) {
14619
+ const packageJsonPath = join(projectRoot, "package.json");
14620
+ if (!existsSync(packageJsonPath)) return;
14621
+ let scripts;
14622
+ try {
14623
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
14624
+ const scriptsValue = typeof parsed === "object" && parsed !== null ? parsed.scripts : void 0;
14625
+ if (typeof scriptsValue === "object" && scriptsValue !== null) {
14626
+ scripts = scriptsValue;
14627
+ }
14628
+ } catch {
14629
+ return;
14630
+ }
14631
+ const missing = missingFrameworkGates(scripts);
14632
+ if (missing.length === 0) return;
14633
+ log("");
14634
+ log(
14635
+ `\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) :`
14636
+ );
14637
+ for (const gate of missing) {
14638
+ log(` "${gate.scriptName}": "${gate.command}" # ${gate.intent}`);
14639
+ }
14640
+ log(" puis appelle ces scripts depuis ton script `verify`.");
14641
+ }
14564
14642
 
14565
14643
  // src/governed-files.ts
14566
14644
  var GovernedFileSchema = external_exports.object({
@@ -14627,14 +14705,9 @@ var governedFiles = [
14627
14705
  }
14628
14706
  ];
14629
14707
 
14630
- // src/checksum.ts
14631
- function sha256Hex(content) {
14632
- return createHash("sha256").update(content, "utf8").digest("hex");
14633
- }
14634
-
14635
14708
  // src/project-manifest.ts
14636
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
14637
- import { join } from "node:path";
14709
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
14710
+ import { join as join2 } from "node:path";
14638
14711
 
14639
14712
  // ../ogygie-core/src/manifest.schema.ts
14640
14713
  var FeatureNameSchema = external_exports.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
@@ -14918,9 +14991,9 @@ var OgygieManifestSchema = external_exports.object({
14918
14991
  // src/project-manifest.ts
14919
14992
  var MANIFEST_FILE_NAME = "ogygie.manifest.json";
14920
14993
  var PinnedVersionSchema = external_exports.object({ ogygieVersion: OgygieVersionSchema.optional() });
14921
- function readPinnedOgygieVersion(projectRoot) {
14922
- const manifestPath = join(projectRoot, MANIFEST_FILE_NAME);
14923
- if (!existsSync(manifestPath)) {
14994
+ function requireManifestPath(projectRoot) {
14995
+ const manifestPath = join2(projectRoot, MANIFEST_FILE_NAME);
14996
+ if (!existsSync2(manifestPath)) {
14924
14997
  throwOgygieError({
14925
14998
  code: "OGYGIE_CLI_USAGE",
14926
14999
  message: `No ${MANIFEST_FILE_NAME} here \u2014 this is not an Ogygie project root.`,
@@ -14929,8 +15002,19 @@ function readPinnedOgygieVersion(projectRoot) {
14929
15002
  suggestedFix: "Run the command from the project root, or scaffold a project first with `pnpm dlx @ogygie/cli init <product-name>`."
14930
15003
  });
14931
15004
  }
15005
+ return manifestPath;
15006
+ }
15007
+ function readOptionalPinnedOgygieVersion(projectRoot) {
15008
+ const manifestPath = requireManifestPath(projectRoot);
14932
15009
  const parsed = PinnedVersionSchema.safeParse(
14933
- JSON.parse(readFileSync(manifestPath, "utf8"))
15010
+ JSON.parse(readFileSync2(manifestPath, "utf8"))
15011
+ );
15012
+ return parsed.success ? parsed.data.ogygieVersion ?? null : null;
15013
+ }
15014
+ function readPinnedOgygieVersion(projectRoot) {
15015
+ const manifestPath = requireManifestPath(projectRoot);
15016
+ const parsed = PinnedVersionSchema.safeParse(
15017
+ JSON.parse(readFileSync2(manifestPath, "utf8"))
14934
15018
  );
14935
15019
  const pinned = parsed.success ? parsed.data.ogygieVersion : void 0;
14936
15020
  if (pinned === void 0) {
@@ -14945,28 +15029,126 @@ function readPinnedOgygieVersion(projectRoot) {
14945
15029
  return pinned;
14946
15030
  }
14947
15031
  function writePinnedOgygieVersion(projectRoot, version2) {
14948
- const manifestPath = join(projectRoot, MANIFEST_FILE_NAME);
14949
- const content = readFileSync(manifestPath, "utf8");
15032
+ const manifestPath = join2(projectRoot, MANIFEST_FILE_NAME);
15033
+ const content = readFileSync2(manifestPath, "utf8");
14950
15034
  const bumped = content.replace(
14951
15035
  /"ogygieVersion"\s*:\s*"[^"]*"/,
14952
15036
  `"ogygieVersion": "${version2}"`
14953
15037
  );
14954
15038
  writeFileSync(manifestPath, bumped);
14955
15039
  }
15040
+ function addPinnedOgygieVersion(projectRoot, version2) {
15041
+ const manifestPath = requireManifestPath(projectRoot);
15042
+ const content = readFileSync2(manifestPath, "utf8");
15043
+ const openingBrace = content.indexOf("{");
15044
+ if (openingBrace === -1) {
15045
+ throwOgygieError({
15046
+ code: "OGYGIE_CLI_USAGE",
15047
+ message: `${MANIFEST_FILE_NAME} is not a JSON object \u2014 the pin cannot be added.`,
15048
+ file: MANIFEST_FILE_NAME,
15049
+ cause: "no opening brace found in the manifest",
15050
+ suggestedFix: "Fix the manifest so it is a valid JSON object, then retry."
15051
+ });
15052
+ }
15053
+ const indentation = /\n(\s+)"/.exec(content.slice(openingBrace))?.[1] ?? " ";
15054
+ const pinned = `${content.slice(0, openingBrace + 1)}
15055
+ ${indentation}"ogygieVersion": "${version2}",${content.slice(openingBrace + 1)}`;
15056
+ writeFileSync(manifestPath, pinned);
15057
+ }
15058
+
15059
+ // src/commands/adopt.ts
15060
+ async function adoptProject(deps) {
15061
+ const alreadyPinned = readOptionalPinnedOgygieVersion(deps.projectRoot);
15062
+ if (alreadyPinned !== null) {
15063
+ throwOgygieError({
15064
+ code: "OGYGIE_ALREADY_ADOPTED",
15065
+ message: `This project is already pinned to Ogygie ${alreadyPinned} \u2014 adoption is a one-way passage, it does not replay.`,
15066
+ file: MANIFEST_FILE_NAME,
15067
+ cause: `ogygieVersion ${alreadyPinned} already present in the manifest`,
15068
+ suggestedFix: "Use `ogygie upgrade` to move a governed project to a newer version; `adopt` only serves a repo that has never been governed."
15069
+ });
15070
+ }
15071
+ const planned = [];
15072
+ for (const governed of governedFiles) {
15073
+ const content = readFileSync3(
15074
+ join3(deps.templatesDir, governed.templatePath),
15075
+ "utf8"
15076
+ );
15077
+ const absolutePath = join3(deps.projectRoot, governed.targetPath);
15078
+ const occupied = existsSync3(absolutePath);
15079
+ const current = occupied ? readFileSync3(absolutePath, "utf8") : "";
15080
+ const diff = diffLines(current, content);
15081
+ if (diff === "") continue;
15082
+ planned.push({ targetPath: governed.targetPath, content, occupied, diff });
15083
+ }
15084
+ const occupiedWrites = planned.filter((write) => write.occupied);
15085
+ const freshWrites = planned.filter((write) => !write.occupied);
15086
+ if (occupiedWrites.length > 0) {
15087
+ deps.log(
15088
+ `\u26A0 ${occupiedWrites.length} chemin(s) gouvern\xE9(s) portent d\xE9j\xE0 un fichier de ce projet. L'adoption les \xC9CRASE \u2014 leur contenu actuel n'existera plus que dans l'historique git :`
15089
+ );
15090
+ for (const write of occupiedWrites) deps.log(` ${write.targetPath}`);
15091
+ deps.log(
15092
+ " Le partage produit/g\xE9n\xE9rique de ces fichiers est un travail humain : il se fait AVANT cette commande, pas par elle (B7)."
15093
+ );
15094
+ deps.log("");
15095
+ }
15096
+ const fullDiff = [...occupiedWrites, ...freshWrites].map(
15097
+ (write) => `=== ${write.targetPath}${write.occupied ? " (\xE9crase un fichier du projet)" : " (nouveau)"}
15098
+ ${write.diff}`
15099
+ ).join("\n\n");
15100
+ const confirmed = await deps.confirm(fullDiff);
15101
+ if (!confirmed) {
15102
+ deps.log("ogygie adopt \u2014 abandonn\xE9, aucun fichier modifi\xE9, projet non \xE9pingl\xE9.");
15103
+ return {
15104
+ adoptedVersion: deps.cliVersion,
15105
+ writtenFiles: [],
15106
+ overwrittenProductFiles: []
15107
+ };
15108
+ }
15109
+ for (const write of planned) {
15110
+ const absolutePath = join3(deps.projectRoot, write.targetPath);
15111
+ mkdirSync(dirname(absolutePath), { recursive: true });
15112
+ writeFileSync2(absolutePath, write.content);
15113
+ }
15114
+ addPinnedOgygieVersion(deps.projectRoot, deps.cliVersion);
15115
+ deps.log(
15116
+ `ogygie adopt \u2014 projet adopt\xE9 en ${deps.cliVersion}, ${planned.length} fichier(s) gouvern\xE9(s) pos\xE9(s) dont ${occupiedWrites.length} \xE9cras\xE9(s).`
15117
+ );
15118
+ deps.log(
15119
+ `Rappel lockstep (D2) : installer @ogygie/core ET @ogygie/cli en ${deps.cliVersion} \xE0 l'EXACT (\`pnpm add -D @ogygie/cli@${deps.cliVersion}\`, \`pnpm add @ogygie/core@${deps.cliVersion}\`), puis committer l'adoption SEULE \u2014 jamais m\xEAl\xE9e \xE0 une feature.`
15120
+ );
15121
+ reportMissingGates(deps.projectRoot, deps.log);
15122
+ return {
15123
+ adoptedVersion: deps.cliVersion,
15124
+ writtenFiles: planned.map((write) => write.targetPath),
15125
+ overwrittenProductFiles: occupiedWrites.map((write) => write.targetPath)
15126
+ };
15127
+ }
15128
+
15129
+ // src/commands/check.ts
15130
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
15131
+ import { join as join4 } from "node:path";
15132
+
15133
+ // src/checksum.ts
15134
+ import { createHash } from "node:crypto";
15135
+ function sha256Hex(content) {
15136
+ return createHash("sha256").update(content, "utf8").digest("hex");
15137
+ }
14956
15138
 
14957
15139
  // src/commands/check.ts
14958
15140
  var PackageVersionSchema = external_exports.object({ version: external_exports.string() });
14959
15141
  function installedCoreVersion(projectRoot) {
14960
- const packagePath = join2(
15142
+ const packagePath = join4(
14961
15143
  projectRoot,
14962
15144
  "node_modules",
14963
15145
  "@ogygie",
14964
15146
  "core",
14965
15147
  "package.json"
14966
15148
  );
14967
- if (!existsSync2(packagePath)) return null;
15149
+ if (!existsSync4(packagePath)) return null;
14968
15150
  const parsed = PackageVersionSchema.safeParse(
14969
- JSON.parse(readFileSync2(packagePath, "utf8"))
15151
+ JSON.parse(readFileSync4(packagePath, "utf8"))
14970
15152
  );
14971
15153
  return parsed.success ? parsed.data.version : null;
14972
15154
  }
@@ -15001,8 +15183,8 @@ function checkProject(deps) {
15001
15183
  for (const governed of governedFiles) {
15002
15184
  const expected = expectedChecksums[governed.targetPath];
15003
15185
  if (expected === void 0) continue;
15004
- const absolutePath = join2(deps.projectRoot, governed.targetPath);
15005
- const actual = existsSync2(absolutePath) ? sha256Hex(readFileSync2(absolutePath, "utf8")) : "<absent>";
15186
+ const absolutePath = join4(deps.projectRoot, governed.targetPath);
15187
+ const actual = existsSync4(absolutePath) ? sha256Hex(readFileSync4(absolutePath, "utf8")) : "<absent>";
15006
15188
  if (actual !== expected) {
15007
15189
  problems.push(
15008
15190
  ogygieError({
@@ -15020,23 +15202,23 @@ function checkProject(deps) {
15020
15202
 
15021
15203
  // src/commands/init.ts
15022
15204
  import {
15023
- existsSync as existsSync3,
15024
- mkdirSync,
15205
+ existsSync as existsSync5,
15206
+ mkdirSync as mkdirSync2,
15025
15207
  readdirSync,
15026
- readFileSync as readFileSync3,
15208
+ readFileSync as readFileSync5,
15027
15209
  statSync,
15028
- writeFileSync as writeFileSync2
15210
+ writeFileSync as writeFileSync3
15029
15211
  } from "node:fs";
15030
- import { dirname, join as join3 } from "node:path";
15212
+ import { dirname as dirname2, join as join5 } from "node:path";
15031
15213
  var PRODUCT_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
15032
15214
  var RENAMED_TEMPLATE_FILES = {
15033
15215
  gitignore: ".gitignore"
15034
15216
  };
15035
15217
  function listTemplateFiles(root, folder = "") {
15036
15218
  const files = [];
15037
- for (const entry of readdirSync(join3(root, folder)).sort()) {
15219
+ for (const entry of readdirSync(join5(root, folder)).sort()) {
15038
15220
  const relativePath = folder === "" ? entry : `${folder}/${entry}`;
15039
- if (statSync(join3(root, relativePath)).isDirectory()) {
15221
+ if (statSync(join5(root, relativePath)).isDirectory()) {
15040
15222
  files.push(...listTemplateFiles(root, relativePath));
15041
15223
  } else {
15042
15224
  files.push(relativePath);
@@ -15061,16 +15243,16 @@ function initProject(deps, productName2) {
15061
15243
  suggestedFix: "Pick a kebab-case name, e.g. `ogygie init order-desk`."
15062
15244
  });
15063
15245
  }
15064
- if (existsSync3(join3(deps.targetDir, MANIFEST_FILE_NAME))) {
15246
+ if (existsSync5(join5(deps.targetDir, MANIFEST_FILE_NAME))) {
15065
15247
  throwOgygieError({
15066
15248
  code: "OGYGIE_INIT_TARGET_NOT_EMPTY",
15067
15249
  message: `The target directory already hosts an Ogygie project (${MANIFEST_FILE_NAME} found).`,
15068
15250
  file: MANIFEST_FILE_NAME,
15069
- cause: `${join3(deps.targetDir, MANIFEST_FILE_NAME)} exists`,
15251
+ cause: `${join5(deps.targetDir, MANIFEST_FILE_NAME)} exists`,
15070
15252
  suggestedFix: "Run `pnpm ogygie upgrade` in that project instead of init."
15071
15253
  });
15072
15254
  }
15073
- const existingEntries = existsSync3(deps.targetDir) ? readdirSync(deps.targetDir).filter((entry) => entry !== ".git") : [];
15255
+ const existingEntries = existsSync5(deps.targetDir) ? readdirSync(deps.targetDir).filter((entry) => entry !== ".git") : [];
15074
15256
  if (existingEntries.length > 0) {
15075
15257
  throwOgygieError({
15076
15258
  code: "OGYGIE_INIT_TARGET_NOT_EMPTY",
@@ -15080,14 +15262,14 @@ function initProject(deps, productName2) {
15080
15262
  suggestedFix: "Start from an empty directory (only a .git folder is tolerated)."
15081
15263
  });
15082
15264
  }
15083
- const projectTemplates = join3(deps.templatesDir, "project");
15265
+ const projectTemplates = join5(deps.templatesDir, "project");
15084
15266
  const writtenFiles = [];
15085
15267
  for (const templateRelativePath of listTemplateFiles(projectTemplates)) {
15086
- const content = readFileSync3(join3(projectTemplates, templateRelativePath), "utf8").replaceAll("__PRODUCT_NAME__", productName2).replaceAll("__OGYGIE_VERSION__", deps.cliVersion);
15268
+ const content = readFileSync5(join5(projectTemplates, templateRelativePath), "utf8").replaceAll("__PRODUCT_NAME__", productName2).replaceAll("__OGYGIE_VERSION__", deps.cliVersion);
15087
15269
  const targetRelativePath = targetPathOf(templateRelativePath);
15088
- const absoluteTarget = join3(deps.targetDir, targetRelativePath);
15089
- mkdirSync(dirname(absoluteTarget), { recursive: true });
15090
- writeFileSync2(absoluteTarget, content);
15270
+ const absoluteTarget = join5(deps.targetDir, targetRelativePath);
15271
+ mkdirSync2(dirname2(absoluteTarget), { recursive: true });
15272
+ writeFileSync3(absoluteTarget, content);
15091
15273
  writtenFiles.push(targetRelativePath);
15092
15274
  }
15093
15275
  deps.log(`ogygie init \u2014 ${writtenFiles.length} fichiers \xE9crits (ogygieVersion ${deps.cliVersion}).`);
@@ -15103,11 +15285,11 @@ function initProject(deps, productName2) {
15103
15285
 
15104
15286
  // src/commands/studio.ts
15105
15287
  import { createRequire } from "node:module";
15106
- import { join as join4 } from "node:path";
15288
+ import { join as join6 } from "node:path";
15107
15289
  import { pathToFileURL } from "node:url";
15108
15290
  var PUBLISHED_STUDIO_SERVER = "@ogygie/studio/server";
15109
15291
  async function loadStudioFrom(projectRoot) {
15110
- const resolveFromProject = createRequire(join4(projectRoot, "package.json"));
15292
+ const resolveFromProject = createRequire(join6(projectRoot, "package.json"));
15111
15293
  const entryPoint = resolveFromProject.resolve(PUBLISHED_STUDIO_SERVER);
15112
15294
  return await import(pathToFileURL(entryPoint).href);
15113
15295
  }
@@ -15146,13 +15328,13 @@ async function runStudio(deps) {
15146
15328
  }
15147
15329
 
15148
15330
  // src/commands/mcp.ts
15149
- import { readFileSync as readFileSync4 } from "node:fs";
15331
+ import { readFileSync as readFileSync6 } from "node:fs";
15150
15332
  import { createRequire as createRequire2 } from "node:module";
15151
- import { join as join5 } from "node:path";
15333
+ import { join as join7 } from "node:path";
15152
15334
  import { pathToFileURL as pathToFileURL2 } from "node:url";
15153
15335
  var PUBLISHED_MCP_SERVER = "@ogygie/mcp/server";
15154
15336
  async function loadMcpFrom(projectRoot) {
15155
- const resolveFromProject = createRequire2(join5(projectRoot, "package.json"));
15337
+ const resolveFromProject = createRequire2(join7(projectRoot, "package.json"));
15156
15338
  const entryPoint = resolveFromProject.resolve(PUBLISHED_MCP_SERVER);
15157
15339
  return await import(pathToFileURL2(entryPoint).href);
15158
15340
  }
@@ -15189,64 +15371,13 @@ async function runMcp(deps) {
15189
15371
  cwd: deps.projectRoot,
15190
15372
  version: deps.cliVersion,
15191
15373
  token: process.env["OGYGIE_MCP_TOKEN"],
15192
- readConfigFile: (absolutePath) => readFileSync4(absolutePath, "utf8")
15374
+ readConfigFile: (absolutePath) => readFileSync6(absolutePath, "utf8")
15193
15375
  });
15194
15376
  }
15195
15377
 
15196
15378
  // src/commands/upgrade.ts
15197
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
15198
- import { dirname as dirname2, join as join6 } from "node:path";
15199
-
15200
- // src/diff.ts
15201
- function diffLines(oldText, newText) {
15202
- if (oldText === newText) return "";
15203
- const oldLines = oldText.split("\n");
15204
- const newLines = newText.split("\n");
15205
- const lcs = Array.from(
15206
- { length: oldLines.length + 1 },
15207
- () => new Array(newLines.length + 1).fill(0)
15208
- );
15209
- for (let i2 = oldLines.length - 1; i2 >= 0; i2 -= 1) {
15210
- for (let j2 = newLines.length - 1; j2 >= 0; j2 -= 1) {
15211
- lcs[i2][j2] = oldLines[i2] === newLines[j2] ? (lcs[i2 + 1][j2 + 1] ?? 0) + 1 : Math.max(lcs[i2 + 1][j2] ?? 0, lcs[i2][j2 + 1] ?? 0);
15212
- }
15213
- }
15214
- const output = [];
15215
- let previousWasChange = false;
15216
- let i = 0;
15217
- let j = 0;
15218
- while (i < oldLines.length || j < newLines.length) {
15219
- if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
15220
- previousWasChange = false;
15221
- i += 1;
15222
- j += 1;
15223
- } else {
15224
- if (!previousWasChange) output.push(`@@ line ${j + 1}`);
15225
- previousWasChange = true;
15226
- if (j >= newLines.length || i < oldLines.length && (lcs[i + 1]?.[j] ?? 0) >= (lcs[i]?.[j + 1] ?? 0)) {
15227
- output.push(`- ${oldLines[i]}`);
15228
- i += 1;
15229
- } else {
15230
- output.push(`+ ${newLines[j]}`);
15231
- j += 1;
15232
- }
15233
- }
15234
- }
15235
- return output.join("\n");
15236
- }
15237
-
15238
- // src/framework-gates.ts
15239
- var frameworkGates = [
15240
- {
15241
- scriptName: "ogygie:verify-vocabulary",
15242
- command: "ogygie verify-vocabulary",
15243
- intent: "Le vocabulaire d\xE9clar\xE9 au manifeste : alias interdits dans les identifiants, puret\xE9 des fichiers gouvern\xE9s."
15244
- }
15245
- ];
15246
- function missingFrameworkGates(scripts) {
15247
- const commands = Object.values(scripts ?? {}).join(" ; ");
15248
- return frameworkGates.filter((gate) => !commands.includes(gate.command));
15249
- }
15379
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
15380
+ import { dirname as dirname3, join as join8 } from "node:path";
15250
15381
 
15251
15382
  // src/version.ts
15252
15383
  function compareVersions(a, b) {
@@ -15262,30 +15393,6 @@ function compareVersions(a, b) {
15262
15393
  }
15263
15394
 
15264
15395
  // src/commands/upgrade.ts
15265
- function reportMissingGates(deps) {
15266
- const packageJsonPath = join6(deps.projectRoot, "package.json");
15267
- if (!existsSync4(packageJsonPath)) return;
15268
- let scripts;
15269
- try {
15270
- const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
15271
- const scriptsValue = typeof parsed === "object" && parsed !== null ? parsed.scripts : void 0;
15272
- if (typeof scriptsValue === "object" && scriptsValue !== null) {
15273
- scripts = scriptsValue;
15274
- }
15275
- } catch {
15276
- return;
15277
- }
15278
- const missing = missingFrameworkGates(scripts);
15279
- if (missing.length === 0) return;
15280
- deps.log("");
15281
- deps.log(
15282
- `\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) :`
15283
- );
15284
- for (const gate of missing) {
15285
- deps.log(` "${gate.scriptName}": "${gate.command}" # ${gate.intent}`);
15286
- }
15287
- deps.log(" puis appelle ces scripts depuis ton script `verify`.");
15288
- }
15289
15396
  async function upgradeProject(deps) {
15290
15397
  const installedVersion = readPinnedOgygieVersion(deps.projectRoot);
15291
15398
  if (compareVersions(deps.cliVersion, installedVersion) < 0) {
@@ -15318,8 +15425,8 @@ async function upgradeProject(deps) {
15318
15425
  for (const governed of governedFiles) {
15319
15426
  const expected = installedChecksums[governed.targetPath];
15320
15427
  if (expected === void 0) continue;
15321
- const absolutePath = join6(deps.projectRoot, governed.targetPath);
15322
- const actual = existsSync4(absolutePath) ? sha256Hex(readFileSync5(absolutePath, "utf8")) : "<absent>";
15428
+ const absolutePath = join8(deps.projectRoot, governed.targetPath);
15429
+ const actual = existsSync6(absolutePath) ? sha256Hex(readFileSync7(absolutePath, "utf8")) : "<absent>";
15323
15430
  if (actual !== expected) {
15324
15431
  throwOgygieError({
15325
15432
  code: "OGYGIE_GOVERNED_FILE_MODIFIED",
@@ -15333,12 +15440,12 @@ async function upgradeProject(deps) {
15333
15440
  const plannedWrites = [];
15334
15441
  const diffBlocks = [];
15335
15442
  for (const governed of governedFiles) {
15336
- const content = readFileSync5(
15337
- join6(deps.templatesDir, governed.templatePath),
15443
+ const content = readFileSync7(
15444
+ join8(deps.templatesDir, governed.templatePath),
15338
15445
  "utf8"
15339
15446
  );
15340
- const absolutePath = join6(deps.projectRoot, governed.targetPath);
15341
- const current = existsSync4(absolutePath) ? readFileSync5(absolutePath, "utf8") : "";
15447
+ const absolutePath = join8(deps.projectRoot, governed.targetPath);
15448
+ const current = existsSync6(absolutePath) ? readFileSync7(absolutePath, "utf8") : "";
15342
15449
  const diff = diffLines(current, content);
15343
15450
  if (diff !== "") {
15344
15451
  plannedWrites.push({ targetPath: governed.targetPath, content });
@@ -15358,9 +15465,9 @@ ${diff}`);
15358
15465
  };
15359
15466
  }
15360
15467
  for (const write of plannedWrites) {
15361
- const absolutePath = join6(deps.projectRoot, write.targetPath);
15362
- mkdirSync2(dirname2(absolutePath), { recursive: true });
15363
- writeFileSync3(absolutePath, write.content);
15468
+ const absolutePath = join8(deps.projectRoot, write.targetPath);
15469
+ mkdirSync3(dirname3(absolutePath), { recursive: true });
15470
+ writeFileSync4(absolutePath, write.content);
15364
15471
  }
15365
15472
  }
15366
15473
  writePinnedOgygieVersion(deps.projectRoot, deps.cliVersion);
@@ -15370,7 +15477,7 @@ ${diff}`);
15370
15477
  deps.log(
15371
15478
  `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.`
15372
15479
  );
15373
- reportMissingGates(deps);
15480
+ reportMissingGates(deps.projectRoot, deps.log);
15374
15481
  return {
15375
15482
  fromVersion: installedVersion,
15376
15483
  toVersion: deps.cliVersion,
@@ -15379,8 +15486,8 @@ ${diff}`);
15379
15486
  }
15380
15487
 
15381
15488
  // src/commands/verify-vocabulary.ts
15382
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
15383
- import { join as join7, relative, sep } from "node:path";
15489
+ import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
15490
+ import { join as join9, relative, sep } from "node:path";
15384
15491
 
15385
15492
  // ../ogygie-core/src/vocabulary.ts
15386
15493
  var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
@@ -15406,6 +15513,22 @@ function tokenCarriesRoot(tokenSegments, rootSegments) {
15406
15513
  }
15407
15514
  return false;
15408
15515
  }
15516
+ function tokenCarriesExactName(token, name) {
15517
+ const tokenSegments = segmentsOfIdentifier(token);
15518
+ const nameSegments = segmentsOfIdentifier(name);
15519
+ if (nameSegments.length === 0) return false;
15520
+ for (let start = 0; start + nameSegments.length <= tokenSegments.length; start += 1) {
15521
+ let allMatch = true;
15522
+ for (let offset = 0; offset < nameSegments.length; offset += 1) {
15523
+ if (tokenSegments[start + offset] !== nameSegments[offset]) {
15524
+ allMatch = false;
15525
+ break;
15526
+ }
15527
+ }
15528
+ if (allMatch) return true;
15529
+ }
15530
+ return false;
15531
+ }
15409
15532
  function findProductVocabulary(sourceText, vocabularyRoots) {
15410
15533
  const roots = vocabularyRoots.map((root) => ({
15411
15534
  root,
@@ -15452,6 +15575,13 @@ function canonicalByAlias(vocabulary) {
15452
15575
  }
15453
15576
  return byAlias;
15454
15577
  }
15578
+ function derogatedFeatureIdsOf(vocabulary, featureIds) {
15579
+ if (vocabulary === void 0 || vocabulary.entries.length === 0) return [];
15580
+ const forbiddenAliases = [...canonicalByAlias(vocabulary).keys()];
15581
+ return featureIds.filter(
15582
+ (featureId) => findProductVocabulary(featureId, forbiddenAliases).length > 0
15583
+ );
15584
+ }
15455
15585
  function auditProductVocabulary(input) {
15456
15586
  const { vocabulary } = input;
15457
15587
  if (vocabulary === void 0 || vocabulary.entries.length === 0) return [];
@@ -15459,9 +15589,12 @@ function auditProductVocabulary(input) {
15459
15589
  const forbiddenAliases = [...canonicalOfAlias.keys()];
15460
15590
  const canonicalTerms = vocabulary.entries.map((entry) => entry.canonical);
15461
15591
  const violations = [];
15592
+ const featureIds = input.featureIds ?? [];
15593
+ const derivesFromAFeatureId = (identifier) => featureIds.some((featureId) => tokenCarriesExactName(identifier, featureId));
15462
15594
  for (const file2 of input.sourceFiles) {
15463
15595
  const judged = stripProseStrings(stripComments(file2.content));
15464
15596
  for (const match of findProductVocabulary(judged, forbiddenAliases)) {
15597
+ if (derivesFromAFeatureId(match.identifier)) continue;
15465
15598
  violations.push({
15466
15599
  kind: "FORBIDDEN_ALIAS",
15467
15600
  filePath: file2.path,
@@ -15487,8 +15620,8 @@ function auditProductVocabulary(input) {
15487
15620
  var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
15488
15621
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
15489
15622
  function readManifest(projectRoot) {
15490
- const manifestPath = join7(projectRoot, MANIFEST_FILE_NAME);
15491
- if (!existsSync5(manifestPath)) {
15623
+ const manifestPath = join9(projectRoot, MANIFEST_FILE_NAME);
15624
+ if (!existsSync7(manifestPath)) {
15492
15625
  throwOgygieError({
15493
15626
  code: "OGYGIE_CLI_USAGE",
15494
15627
  message: `No ${MANIFEST_FILE_NAME} here \u2014 this is not an Ogygie project root.`,
@@ -15498,7 +15631,7 @@ function readManifest(projectRoot) {
15498
15631
  });
15499
15632
  }
15500
15633
  const parsed = OgygieManifestSchema.safeParse(
15501
- JSON.parse(readFileSync6(manifestPath, "utf8"))
15634
+ JSON.parse(readFileSync8(manifestPath, "utf8"))
15502
15635
  );
15503
15636
  if (!parsed.success) {
15504
15637
  throwOgygieError({
@@ -15512,13 +15645,13 @@ function readManifest(projectRoot) {
15512
15645
  return parsed.data;
15513
15646
  }
15514
15647
  function collectCodeFiles(projectRoot, folder) {
15515
- const absoluteFolder = join7(projectRoot, folder);
15516
- if (!existsSync5(absoluteFolder)) return [];
15648
+ const absoluteFolder = join9(projectRoot, folder);
15649
+ if (!existsSync7(absoluteFolder)) return [];
15517
15650
  const files = [];
15518
15651
  const walk = (directory) => {
15519
15652
  for (const entry of readdirSync2(directory)) {
15520
15653
  if (IGNORED_DIRS.has(entry)) continue;
15521
- const entryPath = join7(directory, entry);
15654
+ const entryPath = join9(directory, entry);
15522
15655
  if (statSync2(entryPath).isDirectory()) {
15523
15656
  walk(entryPath);
15524
15657
  continue;
@@ -15526,7 +15659,7 @@ function collectCodeFiles(projectRoot, folder) {
15526
15659
  if (!CODE_EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
15527
15660
  files.push({
15528
15661
  path: relative(projectRoot, entryPath).split(sep).join("/"),
15529
- content: readFileSync6(entryPath, "utf8")
15662
+ content: readFileSync8(entryPath, "utf8")
15530
15663
  });
15531
15664
  }
15532
15665
  };
@@ -15551,17 +15684,25 @@ function verifyVocabulary(deps) {
15551
15684
  `${vendored.length} app(s) hors du scan (vendored \u2014 code h\xE9berg\xE9, non maintenu par le produit) : ${vendored.map((app) => `${app.name} (${app.path})`).join(", ")}`
15552
15685
  );
15553
15686
  }
15687
+ const featureIds = manifest.features.map((feature) => feature.name);
15688
+ const derogated = derogatedFeatureIdsOf(manifest.vocabulary, featureIds);
15689
+ if (derogated.length > 0 && deps.log !== void 0) {
15690
+ deps.log(
15691
+ `${derogated.length} id(s) de feature hors du scan des alias, et les identifiants qui en D\xC9RIVENT (le manifeste les nomme, le code les suit) : ${derogated.join(", ")}`
15692
+ );
15693
+ }
15554
15694
  const sourceFiles = productCodeFoldersOf(manifest).flatMap(
15555
15695
  (folder) => collectCodeFiles(deps.projectRoot, folder)
15556
15696
  );
15557
- const governed = governedFiles.filter((file2) => existsSync5(join7(deps.projectRoot, file2.targetPath))).map((file2) => ({
15697
+ const governed = governedFiles.filter((file2) => existsSync7(join9(deps.projectRoot, file2.targetPath))).map((file2) => ({
15558
15698
  path: file2.targetPath,
15559
- content: readFileSync6(join7(deps.projectRoot, file2.targetPath), "utf8")
15699
+ content: readFileSync8(join9(deps.projectRoot, file2.targetPath), "utf8")
15560
15700
  }));
15561
15701
  return auditProductVocabulary({
15562
15702
  vocabulary: manifest.vocabulary,
15563
15703
  sourceFiles,
15564
- governedFiles: governed
15704
+ governedFiles: governed,
15705
+ featureIds
15565
15706
  }).map(
15566
15707
  (violation) => violation.kind === "FORBIDDEN_ALIAS" ? ogygieError({
15567
15708
  code: "OGYGIE_VOCABULARY_FORBIDDEN_ALIAS",
@@ -15671,6 +15812,30 @@ var governedChecksumsByVersion = {
15671
15812
  "scripts/verify-specs.ts": "04289dbeeb029703779757a2d8b58852c6b16414ab7503bf3223cfe4e9a4d45e",
15672
15813
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15673
15814
  },
15815
+ "0.12.0": {
15816
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15817
+ "AGENTS.md": "74725a71e531502d4d4898518bad68e610a370bd410058b58e11220532dc327f",
15818
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15819
+ "scripts/lint-interdits.ts": "20c32a6f3af4aa68b24fe37b25a2989f58e77fd96808c3853dc6fd98f4c9af84",
15820
+ "scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
15821
+ "scripts/ogygie-impact.ts": "f06cc8b4a46ddfbcfb32ba747c2454548ca49918123d04ab13d210b4a89c1fb9",
15822
+ "scripts/verify-contracts.ts": "9cd64c88dcc3b54b65bf1e1b7a68eb00353ee16f2a9ce8bca3c49ba063c2a08b",
15823
+ "scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
15824
+ "scripts/verify-specs.ts": "04289dbeeb029703779757a2d8b58852c6b16414ab7503bf3223cfe4e9a4d45e",
15825
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15826
+ },
15827
+ "0.13.0": {
15828
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15829
+ "AGENTS.md": "74725a71e531502d4d4898518bad68e610a370bd410058b58e11220532dc327f",
15830
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15831
+ "scripts/lint-interdits.ts": "20c32a6f3af4aa68b24fe37b25a2989f58e77fd96808c3853dc6fd98f4c9af84",
15832
+ "scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
15833
+ "scripts/ogygie-impact.ts": "f06cc8b4a46ddfbcfb32ba747c2454548ca49918123d04ab13d210b4a89c1fb9",
15834
+ "scripts/verify-contracts.ts": "9cd64c88dcc3b54b65bf1e1b7a68eb00353ee16f2a9ce8bca3c49ba063c2a08b",
15835
+ "scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
15836
+ "scripts/verify-specs.ts": "04289dbeeb029703779757a2d8b58852c6b16414ab7503bf3223cfe4e9a4d45e",
15837
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15838
+ },
15674
15839
  "0.2.0": {
15675
15840
  ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15676
15841
  "AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
@@ -15764,23 +15929,23 @@ var governedChecksumsByVersion = {
15764
15929
  };
15765
15930
 
15766
15931
  // src/paths.ts
15767
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
15768
- import { dirname as dirname3, join as join8 } from "node:path";
15932
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
15933
+ import { dirname as dirname4, join as join10 } from "node:path";
15769
15934
  import { fileURLToPath } from "node:url";
15770
- var PACKAGE_ROOT = join8(dirname3(fileURLToPath(import.meta.url)), "..");
15935
+ var PACKAGE_ROOT = join10(dirname4(fileURLToPath(import.meta.url)), "..");
15771
15936
  var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
15772
15937
  function cliVersion() {
15773
15938
  return PackageVersionSchema2.parse(
15774
- JSON.parse(readFileSync7(join8(PACKAGE_ROOT, "package.json"), "utf8"))
15939
+ JSON.parse(readFileSync9(join10(PACKAGE_ROOT, "package.json"), "utf8"))
15775
15940
  ).version;
15776
15941
  }
15777
15942
  function templatesDir() {
15778
- const embedded = join8(PACKAGE_ROOT, "templates");
15779
- if (existsSync6(embedded)) return embedded;
15943
+ const embedded = join10(PACKAGE_ROOT, "templates");
15944
+ if (existsSync8(embedded)) return embedded;
15780
15945
  return repoTemplatesDir();
15781
15946
  }
15782
15947
  function repoTemplatesDir() {
15783
- return join8(PACKAGE_ROOT, "..", "..", "templates");
15948
+ return join10(PACKAGE_ROOT, "..", "..", "templates");
15784
15949
  }
15785
15950
 
15786
15951
  // src/cli.ts
@@ -15800,7 +15965,7 @@ function printError(error51) {
15800
15965
  console.error(` fix : ${error51.suggestedFix}`);
15801
15966
  }
15802
15967
  async function confirmOverwrite(fullDiff) {
15803
- console.log("Diff des fichiers gouvern\xE9s (version install\xE9e \u2192 version cible) :\n");
15968
+ console.log("Diff des fichiers gouvern\xE9s (\xE9tat actuel \u2192 version cible) :\n");
15804
15969
  console.log(fullDiff === "" ? "(aucune diff\xE9rence de contenu)" : fullDiff);
15805
15970
  if (skipConfirmation) return true;
15806
15971
  const readline = createInterface({ input: process.stdin, output: process.stdout });
@@ -15821,6 +15986,14 @@ try {
15821
15986
  },
15822
15987
  productName ?? ""
15823
15988
  );
15989
+ } else if (command === "adopt") {
15990
+ await adoptProject({
15991
+ projectRoot: process.cwd(),
15992
+ templatesDir: templatesDir(),
15993
+ cliVersion: cliVersion(),
15994
+ confirm: confirmOverwrite,
15995
+ log: (line) => console.log(line)
15996
+ });
15824
15997
  } else if (command === "upgrade") {
15825
15998
  await upgradeProject({
15826
15999
  projectRoot: process.cwd(),
@@ -15871,8 +16044,8 @@ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
15871
16044
  code: "OGYGIE_CLI_USAGE",
15872
16045
  message: `Unknown command ${JSON.stringify(command ?? "")}.`,
15873
16046
  file: null,
15874
- cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
15875
- suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie mcp`."
16047
+ cause: `expected one of: init <product-name>, adopt, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
16048
+ suggestedFix: "Run `ogygie init <product-name>`, `ogygie adopt`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie mcp`."
15876
16049
  })
15877
16050
  );
15878
16051
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/cli",
3
- "version": "0.11.2",
3
+ "version": "0.13.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",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "devDependencies": {
20
- "@ogygie/core": "0.11.2",
20
+ "@ogygie/core": "0.13.0",
21
21
  "@types/node": "^24.0.0",
22
22
  "esbuild": "^0.28.0",
23
23
  "tsx": "^4.20.0",
@@ -267,6 +267,16 @@ il devient actif le jour où le produit déclare, jamais avant. Renommer un
267
267
  concept est donc une opération à un seul endroit — on change le canonique, le
268
268
  gate liste tous les identifiants à corriger.
269
269
 
270
+ **Les identifiants DÉRIVÉS d'un id de feature sortent du scan des alias.** Un
271
+ id de feature nomme un document et suit la langue du projet ; le code en dérive
272
+ mécaniquement son dossier `features/<id>/`, ses chemins d'import et ses
273
+ `register<Id>Routes`. Un token qui porte l'id de feature comme suite EXACTE de
274
+ segments n'est donc pas jugé, et le gate ANNONCE les ids concernés à chaque
275
+ exécution. Un identifiant qui ne fait que COMMENCER par la racine sans reprendre
276
+ l'id entier reste jugé : la dérogation couvre la dérivation, jamais le choix
277
+ d'un nom. Renommer le concept reste une opération à un seul endroit — on renomme
278
+ la feature, l'exclusion suit.
279
+
270
280
  Choisir les racines est un acte de conception, pas une formalité : l'appariement
271
281
  se fait PAR RACINE, donc une racine trop courte attrape des mots génériques
272
282
  (déclarer `memo` interdit fait tomber `memory`, `createInMemoryDb`…). Une racine