@ogygie/cli 0.11.1 → 0.12.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);
15009
+ const parsed = PinnedVersionSchema.safeParse(
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);
14932
15016
  const parsed = PinnedVersionSchema.safeParse(
14933
- JSON.parse(readFileSync(manifestPath, "utf8"))
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",
@@ -15659,6 +15800,30 @@ var governedChecksumsByVersion = {
15659
15800
  "scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
15660
15801
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15661
15802
  },
15803
+ "0.11.2": {
15804
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15805
+ "AGENTS.md": "816a00d69720702aa343a5010cc1ddcea8756f227e961785ab5a9f32962ce155",
15806
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15807
+ "scripts/lint-interdits.ts": "20c32a6f3af4aa68b24fe37b25a2989f58e77fd96808c3853dc6fd98f4c9af84",
15808
+ "scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
15809
+ "scripts/ogygie-impact.ts": "f06cc8b4a46ddfbcfb32ba747c2454548ca49918123d04ab13d210b4a89c1fb9",
15810
+ "scripts/verify-contracts.ts": "9cd64c88dcc3b54b65bf1e1b7a68eb00353ee16f2a9ce8bca3c49ba063c2a08b",
15811
+ "scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
15812
+ "scripts/verify-specs.ts": "04289dbeeb029703779757a2d8b58852c6b16414ab7503bf3223cfe4e9a4d45e",
15813
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
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
+ },
15662
15827
  "0.2.0": {
15663
15828
  ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15664
15829
  "AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
@@ -15752,23 +15917,23 @@ var governedChecksumsByVersion = {
15752
15917
  };
15753
15918
 
15754
15919
  // src/paths.ts
15755
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
15756
- import { dirname as dirname3, join as join8 } from "node:path";
15920
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
15921
+ import { dirname as dirname4, join as join10 } from "node:path";
15757
15922
  import { fileURLToPath } from "node:url";
15758
- var PACKAGE_ROOT = join8(dirname3(fileURLToPath(import.meta.url)), "..");
15923
+ var PACKAGE_ROOT = join10(dirname4(fileURLToPath(import.meta.url)), "..");
15759
15924
  var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
15760
15925
  function cliVersion() {
15761
15926
  return PackageVersionSchema2.parse(
15762
- JSON.parse(readFileSync7(join8(PACKAGE_ROOT, "package.json"), "utf8"))
15927
+ JSON.parse(readFileSync9(join10(PACKAGE_ROOT, "package.json"), "utf8"))
15763
15928
  ).version;
15764
15929
  }
15765
15930
  function templatesDir() {
15766
- const embedded = join8(PACKAGE_ROOT, "templates");
15767
- if (existsSync6(embedded)) return embedded;
15931
+ const embedded = join10(PACKAGE_ROOT, "templates");
15932
+ if (existsSync8(embedded)) return embedded;
15768
15933
  return repoTemplatesDir();
15769
15934
  }
15770
15935
  function repoTemplatesDir() {
15771
- return join8(PACKAGE_ROOT, "..", "..", "templates");
15936
+ return join10(PACKAGE_ROOT, "..", "..", "templates");
15772
15937
  }
15773
15938
 
15774
15939
  // src/cli.ts
@@ -15788,7 +15953,7 @@ function printError(error51) {
15788
15953
  console.error(` fix : ${error51.suggestedFix}`);
15789
15954
  }
15790
15955
  async function confirmOverwrite(fullDiff) {
15791
- console.log("Diff des fichiers gouvern\xE9s (version install\xE9e \u2192 version cible) :\n");
15956
+ console.log("Diff des fichiers gouvern\xE9s (\xE9tat actuel \u2192 version cible) :\n");
15792
15957
  console.log(fullDiff === "" ? "(aucune diff\xE9rence de contenu)" : fullDiff);
15793
15958
  if (skipConfirmation) return true;
15794
15959
  const readline = createInterface({ input: process.stdin, output: process.stdout });
@@ -15809,6 +15974,14 @@ try {
15809
15974
  },
15810
15975
  productName ?? ""
15811
15976
  );
15977
+ } else if (command === "adopt") {
15978
+ await adoptProject({
15979
+ projectRoot: process.cwd(),
15980
+ templatesDir: templatesDir(),
15981
+ cliVersion: cliVersion(),
15982
+ confirm: confirmOverwrite,
15983
+ log: (line) => console.log(line)
15984
+ });
15812
15985
  } else if (command === "upgrade") {
15813
15986
  await upgradeProject({
15814
15987
  projectRoot: process.cwd(),
@@ -15859,8 +16032,8 @@ ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
15859
16032
  code: "OGYGIE_CLI_USAGE",
15860
16033
  message: `Unknown command ${JSON.stringify(command ?? "")}.`,
15861
16034
  file: null,
15862
- cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
15863
- suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie mcp`."
16035
+ cause: `expected one of: init <product-name>, adopt, upgrade, check, verify-vocabulary, studio, mcp (flags: --json, --yes)`,
16036
+ suggestedFix: "Run `ogygie init <product-name>`, `ogygie adopt`, `ogygie upgrade`, `ogygie check`, `ogygie verify-vocabulary`, `ogygie studio` or `ogygie mcp`."
15864
16037
  })
15865
16038
  );
15866
16039
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.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.1",
20
+ "@ogygie/core": "0.12.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
@@ -9,7 +9,8 @@
9
9
  * ogygie.manifest.json through @ogygie/core/shared-tables (mayUseRawDb), so a
10
10
  * feature earns the right to touch the database directly only by declaring, in
11
11
  * the manifest, that every table it owns is shared (AGENTS.md §Multi-tenant
12
- * rule 4). The policy itself is unit-tested with its mutations (D22) — this
12
+ * rule 4). The policy itself is unit-tested with its mutations (AGENTS.md
13
+ * workflow 7) — this
13
14
  * script only reports.
14
15
  */
15
16
  import { readdirSync, readFileSync, statSync } from "node:fs";
@@ -3,7 +3,7 @@
3
3
  * impact-analyzer-core): prints the blast radius of a change to delivered
4
4
  * code — who depends on it, which symbols they use, its routes, the tables it
5
5
  * owns and any outside reader of them, the tests to re-run, and WHAT THE
6
- * ANALYSIS COULD NOT SEE (D12).
6
+ * ANALYSIS COULD NOT SEE.
7
7
  *
8
8
  * The judgement lives in `@ogygie/core/impact`, where it is tested against an
9
9
  * in-memory repo and callable by other tools; this script is its collecting
@@ -54,7 +54,7 @@ function printSummary(report: ImpactReport): void {
54
54
  );
55
55
  console.log(`Tests à relancer : ${report.testCommand}`);
56
56
  // Always printed, ESPECIALLY when nothing was found: "no dependents" and
57
- // "I did not look there" read the same without it (D12).
57
+ // "I did not look there" read the same without it.
58
58
  console.log(`\nPérimètre — dossiers scannés : ${report.scope.scannedFolders.length}`);
59
59
  console.log(`Hors de portée de cette analyse :`);
60
60
  for (const limit of report.scope.outOfReach) console.log(` · ${limit}`);
@@ -1,14 +1,15 @@
1
1
  /**
2
- * @intent CI gate `pnpm ogygie:verify-contracts` (DECISIONS.md D29): from every
3
- * feature's `schemas.ts`, walk the WHOLE import graph and prove that nothing
4
- * with a runtime effect is reachable — the transitivity is the point, since
5
- * that is how the server came back into the browser bundle
6
- * (`orders/schemas.ts` → a sibling feature's `index.ts` → its data loader →
7
- * `node:fs`, FAILURES.md).
2
+ * @intent CI gate `pnpm ogygie:verify-contracts` (AGENTS.md §Structure, the
3
+ * CONTRACTS exception): from every feature's `schemas.ts`, walk the WHOLE
4
+ * import graph and prove that nothing with a runtime effect is reachable —
5
+ * the transitivity is the point, since that is how the server came back into
6
+ * the browser bundle (`orders/schemas.ts` → a sibling feature's `index.ts` →
7
+ * its data loader → `node:fs`, FAILURES.md).
8
8
  *
9
9
  * The decision logic lives in @ogygie/core/contract-purity, born with its
10
- * mutation tests (D22) — this script only reads the manifest, the disk, and
11
- * reports as OgygieError so an agent can fix without guessing.
10
+ * mutation tests (AGENTS.md workflow 7) — this script only reads the
11
+ * manifest, the disk, and reports as OgygieError so an agent can fix without
12
+ * guessing.
12
13
  */
13
14
  import { existsSync, readFileSync, statSync } from "node:fs";
14
15
  import { join } from "node:path";
@@ -24,7 +25,7 @@ import { ogygieError } from "@ogygie/core/errors";
24
25
  const ROOT = fileURLToPath(new URL("..", import.meta.url));
25
26
  const MANIFEST_PATH = "ogygie.manifest.json";
26
27
 
27
- /** One stable error code per rule broken (D11 — machine-readable = English). */
28
+ /** One stable error code per rule broken (English: machine-readable). */
28
29
  const ERROR_CODES: Record<ContractImpurityKind, string> = {
29
30
  RUNTIME_PACKAGE: "CONTRACT_IMPURE_PACKAGE",
30
31
  SERVER_SURFACE: "CONTRACT_REACHES_SERVER_SURFACE",
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * @intent CI gate `pnpm ogygie:verify-specs`: every `specs/<feature>.spec.md`
3
3
  * must carry a valid frontmatter (SpecFrontmatterSchema), the canonical FRENCH
4
- * sections in order (specs are human-facing documents — DECISIONS.md D11), and
5
- * respect the lifecycle rule (draft → no code, implemented → manifest says
6
- * "delivered"). Frontmatter is deliberately restricted to scalars and inline
7
- * arrays — the format is owned by @ogygie/core/spec (parseSpecFrontmatter),
8
- * which REFUSES what it cannot read (D22 hardening, born with its mutation
9
- * tests in packages/ogygie-core/src/spec.schema.test.ts) — this gate only
10
- * reports.
4
+ * sections in order (specs are human-facing documents, AGENTS.md principle 8
5
+ * keeps the machine-readable half in English), and respect the lifecycle rule
6
+ * (draft → no code, implemented → manifest says "delivered"). Frontmatter is
7
+ * deliberately restricted to scalars and inline arrays — the format is owned
8
+ * by @ogygie/core/spec (parseSpecFrontmatter), which REFUSES what it cannot
9
+ * read (a hardening born with its mutation tests in
10
+ * packages/ogygie-core/src/spec.schema.test.ts) — this gate only reports.
11
11
  * It also refuses any reference to a decision `D<n>` absent from the decision
12
- * files — DECISIONS.md (framework) and DECISIONS.product.md (product), one
13
- * GLOBAL numbering since the Phase 3 split — in a spec (frontmatter `refs` or
12
+ * files — DECISIONS.md (framework) and DECISIONS.product.md (product), which
13
+ * share ONE global numbering — in a spec (frontmatter `refs` or
14
14
  * body), in AGENTS.md or in AGENTS.product.md: a rule founded on a decision
15
15
  * nobody took is silent documentary drift. A decision id declared in BOTH
16
16
  * files is refused too (ambiguous citation). Logic and mutation tests in
@@ -34,7 +34,7 @@ import { ogygieError, type OgygieError } from "@ogygie/core/errors";
34
34
 
35
35
  const ROOT = new URL("..", import.meta.url).pathname;
36
36
  const SPECS_FOLDER = "specs";
37
- /** One global D<n> numbering across the two files (Phase 3 split, D10 rule 4). */
37
+ /** One global D<n> numbering across the two decision files. */
38
38
  const DECISIONS_FILES = ["DECISIONS.md", "DECISIONS.product.md"];
39
39
  /** Documents whose decision citations are checked, besides the specs. */
40
40
  const AGENTS_FILES = ["AGENTS.md", "AGENTS.product.md"];
@@ -77,7 +77,7 @@ for (const fileName of specFiles) {
77
77
  file: specPath,
78
78
  cause: parsedFrontmatter.problems.join(" ; "),
79
79
  suggestedFix:
80
- "Use `key: value` lines with scalars or INLINE arrays only, balanced quotes (D13) — see parseSpecFrontmatter in packages/ogygie-core/src/spec.schema.ts.",
80
+ "Use `key: value` lines with scalars or INLINE arrays only, balanced quotes — see parseSpecFrontmatter in packages/ogygie-core/src/spec.schema.ts.",
81
81
  });
82
82
  continue;
83
83
  }
@@ -146,7 +146,7 @@ for (const fileName of specFiles) {
146
146
  }
147
147
 
148
148
  if (frontmatter.status === "implemented") {
149
- // D13: an implemented spec has nothing left pending — deferred must be empty.
149
+ // An implemented spec has nothing left pending — deferred must be empty.
150
150
  if (frontmatter.deferred.length > 0) {
151
151
  report({
152
152
  code: "SPEC_IMPLEMENTED_WITH_DEFERRED",
@@ -172,7 +172,7 @@ for (const fileName of specFiles) {
172
172
  }
173
173
  }
174
174
 
175
- // --------------------------------------------- decision references (D28 lot)
175
+ // -------------------------------------------------- decision references
176
176
  // Independent of the frontmatter checks above: a spec whose format is broken
177
177
  // may also cite a phantom decision, and both deserve to be said in one run.
178
178
  const decisionIdsPerFile = DECISIONS_FILES.map((fileName) =>
@@ -4,7 +4,7 @@ status: draft
4
4
  depends_on: []
5
5
  priority: P1
6
6
  refs: []
7
- # Optionnel (D13) : comportements décrits mais bloqués par une feature à venir.
7
+ # Optionnel : comportements décrits mais bloqués par une feature à venir.
8
8
  # Une spec ne peut passer `implemented` que si cette liste est vide.
9
9
  # deferred: ["CODE_ERREUR (résumé) blockedBy:nom-de-la-feature"]
10
10
  ---
@@ -15,7 +15,8 @@ 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 ORDER_REFERENCE_ALREADY_USED »)
18
+ directement en anglais — AGENTS.md principe 8, ex. « ALORS OgygieError
19
+ ORDER_REFERENCE_ALREADY_USED »)
19
20
  - ÉTANT DONNÉ <contexte>
20
21
  QUAND <action>
21
22
  ALORS <résultat observable et vérifiable>