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