@ogygie/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/cli.js +262 -9
- 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)
|
|
@@ -15044,6 +15096,189 @@ ${diff}`);
|
|
|
15044
15096
|
};
|
|
15045
15097
|
}
|
|
15046
15098
|
|
|
15099
|
+
// src/commands/verify-vocabulary.ts
|
|
15100
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
15101
|
+
import { join as join5, relative, sep } from "node:path";
|
|
15102
|
+
|
|
15103
|
+
// ../ogygie-core/src/vocabulary.ts
|
|
15104
|
+
var TOKEN_PATTERN = new RegExp("\\p{L}[\\p{L}\\p{N}]*(?:[-_$][\\p{L}\\p{N}]+)*", "gu");
|
|
15105
|
+
function segmentsOfIdentifier(token) {
|
|
15106
|
+
return token.split(/[-_$]+/).flatMap(
|
|
15107
|
+
(part) => part.replace(new RegExp("(?<=\\p{Ll}|\\p{N})(?=\\p{Lu})", "gu"), " ").replace(new RegExp("(?<=\\p{Lu})(?=\\p{Lu}\\p{Ll})", "gu"), " ").split(" ")
|
|
15108
|
+
).filter((segment) => segment.length > 0).map((segment) => segment.toLowerCase());
|
|
15109
|
+
}
|
|
15110
|
+
function tokenCarriesRoot(tokenSegments, rootSegments) {
|
|
15111
|
+
const lastIndex = rootSegments.length - 1;
|
|
15112
|
+
for (let start = 0; start + rootSegments.length <= tokenSegments.length; start += 1) {
|
|
15113
|
+
let leadingSegmentsMatch = true;
|
|
15114
|
+
for (let offset = 0; offset < lastIndex; offset += 1) {
|
|
15115
|
+
if (tokenSegments[start + offset] !== rootSegments[offset]) {
|
|
15116
|
+
leadingSegmentsMatch = false;
|
|
15117
|
+
break;
|
|
15118
|
+
}
|
|
15119
|
+
}
|
|
15120
|
+
const lastTokenSegment = tokenSegments[start + lastIndex];
|
|
15121
|
+
if (leadingSegmentsMatch && lastTokenSegment !== void 0 && lastTokenSegment.startsWith(rootSegments[lastIndex] ?? "")) {
|
|
15122
|
+
return true;
|
|
15123
|
+
}
|
|
15124
|
+
}
|
|
15125
|
+
return false;
|
|
15126
|
+
}
|
|
15127
|
+
function findProductVocabulary(sourceText, vocabularyRoots) {
|
|
15128
|
+
const roots = vocabularyRoots.map((root) => ({
|
|
15129
|
+
root,
|
|
15130
|
+
segments: segmentsOfIdentifier(root)
|
|
15131
|
+
}));
|
|
15132
|
+
const matches = [];
|
|
15133
|
+
const alreadyReported = /* @__PURE__ */ new Set();
|
|
15134
|
+
sourceText.split("\n").forEach((lineText, lineIndex) => {
|
|
15135
|
+
for (const token of lineText.match(TOKEN_PATTERN) ?? []) {
|
|
15136
|
+
const tokenSegments = segmentsOfIdentifier(token);
|
|
15137
|
+
for (const { root, segments } of roots) {
|
|
15138
|
+
if (segments.length === 0) continue;
|
|
15139
|
+
if (!tokenCarriesRoot(tokenSegments, segments)) continue;
|
|
15140
|
+
const reportKey = `${root} ${token}`;
|
|
15141
|
+
if (alreadyReported.has(reportKey)) continue;
|
|
15142
|
+
alreadyReported.add(reportKey);
|
|
15143
|
+
matches.push({ root, identifier: token, line: lineIndex + 1 });
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
15146
|
+
});
|
|
15147
|
+
return matches;
|
|
15148
|
+
}
|
|
15149
|
+
function stripComments(sourceText) {
|
|
15150
|
+
const blankKeepingNewlines = (text) => text.replace(/[^\n]/g, " ");
|
|
15151
|
+
return sourceText.replace(/\/\*[\s\S]*?\*\//g, blankKeepingNewlines).replace(/\/\/[^\n]*/g, blankKeepingNewlines);
|
|
15152
|
+
}
|
|
15153
|
+
|
|
15154
|
+
// ../ogygie-core/src/vocabulary-audit.ts
|
|
15155
|
+
function vocabularyRootsOf(vocabulary) {
|
|
15156
|
+
return vocabulary.entries.flatMap((entry) => [entry.canonical, ...entry.forbiddenAliases]);
|
|
15157
|
+
}
|
|
15158
|
+
function canonicalByAlias(vocabulary) {
|
|
15159
|
+
const byAlias = /* @__PURE__ */ new Map();
|
|
15160
|
+
for (const entry of vocabulary.entries) {
|
|
15161
|
+
for (const alias of entry.forbiddenAliases) byAlias.set(alias, entry.canonical);
|
|
15162
|
+
}
|
|
15163
|
+
return byAlias;
|
|
15164
|
+
}
|
|
15165
|
+
function auditProductVocabulary(input) {
|
|
15166
|
+
const { vocabulary } = input;
|
|
15167
|
+
if (vocabulary === void 0 || vocabulary.entries.length === 0) return [];
|
|
15168
|
+
const canonicalOfAlias = canonicalByAlias(vocabulary);
|
|
15169
|
+
const forbiddenAliases = [...canonicalOfAlias.keys()];
|
|
15170
|
+
const everyRoot = vocabularyRootsOf(vocabulary);
|
|
15171
|
+
const violations = [];
|
|
15172
|
+
for (const file2 of input.sourceFiles) {
|
|
15173
|
+
for (const match of findProductVocabulary(stripComments(file2.content), forbiddenAliases)) {
|
|
15174
|
+
violations.push({
|
|
15175
|
+
kind: "FORBIDDEN_ALIAS",
|
|
15176
|
+
filePath: file2.path,
|
|
15177
|
+
match,
|
|
15178
|
+
canonical: canonicalOfAlias.get(match.root) ?? null
|
|
15179
|
+
});
|
|
15180
|
+
}
|
|
15181
|
+
}
|
|
15182
|
+
for (const file2 of input.governedFiles) {
|
|
15183
|
+
for (const match of findProductVocabulary(file2.content, everyRoot)) {
|
|
15184
|
+
violations.push({
|
|
15185
|
+
kind: "GOVERNED_FILE",
|
|
15186
|
+
filePath: file2.path,
|
|
15187
|
+
match,
|
|
15188
|
+
canonical: null
|
|
15189
|
+
});
|
|
15190
|
+
}
|
|
15191
|
+
}
|
|
15192
|
+
return violations;
|
|
15193
|
+
}
|
|
15194
|
+
|
|
15195
|
+
// src/commands/verify-vocabulary.ts
|
|
15196
|
+
var CODE_EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"];
|
|
15197
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
15198
|
+
function readManifest(projectRoot) {
|
|
15199
|
+
const manifestPath = join5(projectRoot, MANIFEST_FILE_NAME);
|
|
15200
|
+
if (!existsSync5(manifestPath)) {
|
|
15201
|
+
throwOgygieError({
|
|
15202
|
+
code: "OGYGIE_CLI_USAGE",
|
|
15203
|
+
message: `No ${MANIFEST_FILE_NAME} here \u2014 this is not an Ogygie project root.`,
|
|
15204
|
+
file: MANIFEST_FILE_NAME,
|
|
15205
|
+
cause: `${manifestPath} does not exist`,
|
|
15206
|
+
suggestedFix: "Run the command from the project root."
|
|
15207
|
+
});
|
|
15208
|
+
}
|
|
15209
|
+
const parsed = OgygieManifestSchema.safeParse(
|
|
15210
|
+
JSON.parse(readFileSync5(manifestPath, "utf8"))
|
|
15211
|
+
);
|
|
15212
|
+
if (!parsed.success) {
|
|
15213
|
+
throwOgygieError({
|
|
15214
|
+
code: "MANIFEST_INVALID",
|
|
15215
|
+
message: `${MANIFEST_FILE_NAME} does not satisfy OgygieManifestSchema \u2014 the vocabulary cannot be read from it.`,
|
|
15216
|
+
file: MANIFEST_FILE_NAME,
|
|
15217
|
+
cause: parsed.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join(" ; "),
|
|
15218
|
+
suggestedFix: "Fix the listed fields, then run the gate again."
|
|
15219
|
+
});
|
|
15220
|
+
}
|
|
15221
|
+
return parsed.data;
|
|
15222
|
+
}
|
|
15223
|
+
function collectCodeFiles(projectRoot, folder) {
|
|
15224
|
+
const absoluteFolder = join5(projectRoot, folder);
|
|
15225
|
+
if (!existsSync5(absoluteFolder)) return [];
|
|
15226
|
+
const files = [];
|
|
15227
|
+
const walk = (directory) => {
|
|
15228
|
+
for (const entry of readdirSync2(directory)) {
|
|
15229
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
15230
|
+
const entryPath = join5(directory, entry);
|
|
15231
|
+
if (statSync2(entryPath).isDirectory()) {
|
|
15232
|
+
walk(entryPath);
|
|
15233
|
+
continue;
|
|
15234
|
+
}
|
|
15235
|
+
if (!CODE_EXTENSIONS.some((extension) => entry.endsWith(extension))) continue;
|
|
15236
|
+
files.push({
|
|
15237
|
+
path: relative(projectRoot, entryPath).split(sep).join("/"),
|
|
15238
|
+
content: readFileSync5(entryPath, "utf8")
|
|
15239
|
+
});
|
|
15240
|
+
}
|
|
15241
|
+
};
|
|
15242
|
+
walk(absoluteFolder);
|
|
15243
|
+
return files;
|
|
15244
|
+
}
|
|
15245
|
+
function productCodeFoldersOf(manifest) {
|
|
15246
|
+
return [
|
|
15247
|
+
...manifest.apps.map((app) => app.path),
|
|
15248
|
+
...(manifest.framework?.packages ?? []).map((packageEntry) => packageEntry.path)
|
|
15249
|
+
];
|
|
15250
|
+
}
|
|
15251
|
+
function verifyVocabulary(deps) {
|
|
15252
|
+
const manifest = readManifest(deps.projectRoot);
|
|
15253
|
+
if (manifest.vocabulary === void 0) return [];
|
|
15254
|
+
const sourceFiles = productCodeFoldersOf(manifest).flatMap(
|
|
15255
|
+
(folder) => collectCodeFiles(deps.projectRoot, folder)
|
|
15256
|
+
);
|
|
15257
|
+
const governed = governedFiles.filter((file2) => existsSync5(join5(deps.projectRoot, file2.targetPath))).map((file2) => ({
|
|
15258
|
+
path: file2.targetPath,
|
|
15259
|
+
content: readFileSync5(join5(deps.projectRoot, file2.targetPath), "utf8")
|
|
15260
|
+
}));
|
|
15261
|
+
return auditProductVocabulary({
|
|
15262
|
+
vocabulary: manifest.vocabulary,
|
|
15263
|
+
sourceFiles,
|
|
15264
|
+
governedFiles: governed
|
|
15265
|
+
}).map(
|
|
15266
|
+
(violation) => violation.kind === "FORBIDDEN_ALIAS" ? ogygieError({
|
|
15267
|
+
code: "OGYGIE_VOCABULARY_FORBIDDEN_ALIAS",
|
|
15268
|
+
message: `${violation.filePath}:${violation.match.line} uses "${violation.match.identifier}", built on the forbidden alias "${violation.match.root}".`,
|
|
15269
|
+
file: violation.filePath,
|
|
15270
|
+
cause: `identifier ${violation.match.identifier} carries the root "${violation.match.root}" declared forbidden in ${MANIFEST_FILE_NAME}`,
|
|
15271
|
+
suggestedFix: `Rename it around the canonical term: use "${violation.canonical ?? ""}" instead of "${violation.match.root}".`
|
|
15272
|
+
}) : ogygieError({
|
|
15273
|
+
code: "OGYGIE_VOCABULARY_IN_GOVERNED_FILE",
|
|
15274
|
+
message: `Governed file ${violation.filePath}:${violation.match.line} names the product term "${violation.match.identifier}".`,
|
|
15275
|
+
file: violation.filePath,
|
|
15276
|
+
cause: `a governed file is identical in every Ogygie project, so it may name no product domain \u2014 found the declared root "${violation.match.root}"`,
|
|
15277
|
+
suggestedFix: "Move the product-specific part to AGENTS.product.md (or to the product's own files), and restore the governed file to its pristine content."
|
|
15278
|
+
})
|
|
15279
|
+
);
|
|
15280
|
+
}
|
|
15281
|
+
|
|
15047
15282
|
// src/governed-checksums.ts
|
|
15048
15283
|
var governedChecksumsByVersion = {
|
|
15049
15284
|
"0.1.0": {
|
|
@@ -15051,24 +15286,33 @@ var governedChecksumsByVersion = {
|
|
|
15051
15286
|
"AGENTS.md": "de700f99aaf949b1d85c6ae6d199897736084f4c19adf132cdf5a3568af59f69",
|
|
15052
15287
|
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15053
15288
|
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15289
|
+
},
|
|
15290
|
+
"0.2.0": {
|
|
15291
|
+
".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
|
|
15292
|
+
"AGENTS.md": "ca73ab619d8c55fac11858d815bf5fbab6537ecebe7ae4123cbdeb3e3e36bf1a",
|
|
15293
|
+
"CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
|
|
15294
|
+
"tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
|
|
15054
15295
|
}
|
|
15055
15296
|
};
|
|
15056
15297
|
|
|
15057
15298
|
// src/paths.ts
|
|
15058
|
-
import { existsSync as
|
|
15059
|
-
import { dirname as dirname3, join as
|
|
15299
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
15300
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
15060
15301
|
import { fileURLToPath } from "node:url";
|
|
15061
|
-
var PACKAGE_ROOT =
|
|
15302
|
+
var PACKAGE_ROOT = join6(dirname3(fileURLToPath(import.meta.url)), "..");
|
|
15062
15303
|
var PackageVersionSchema2 = external_exports.object({ version: external_exports.string() });
|
|
15063
15304
|
function cliVersion() {
|
|
15064
15305
|
return PackageVersionSchema2.parse(
|
|
15065
|
-
JSON.parse(
|
|
15306
|
+
JSON.parse(readFileSync6(join6(PACKAGE_ROOT, "package.json"), "utf8"))
|
|
15066
15307
|
).version;
|
|
15067
15308
|
}
|
|
15068
15309
|
function templatesDir() {
|
|
15069
|
-
const embedded =
|
|
15070
|
-
if (
|
|
15071
|
-
return
|
|
15310
|
+
const embedded = join6(PACKAGE_ROOT, "templates");
|
|
15311
|
+
if (existsSync6(embedded)) return embedded;
|
|
15312
|
+
return repoTemplatesDir();
|
|
15313
|
+
}
|
|
15314
|
+
function repoTemplatesDir() {
|
|
15315
|
+
return join6(PACKAGE_ROOT, "..", "..", "templates");
|
|
15072
15316
|
}
|
|
15073
15317
|
|
|
15074
15318
|
// src/cli.ts
|
|
@@ -15133,14 +15377,23 @@ ogygie check \u2014 ${problems.length} probl\xE8me(s).`);
|
|
|
15133
15377
|
console.log(
|
|
15134
15378
|
`ogygie check \u2014 OK, lockstep ${cliVersion()} et fichiers gouvern\xE9s intacts.`
|
|
15135
15379
|
);
|
|
15380
|
+
} else if (command === "verify-vocabulary") {
|
|
15381
|
+
const problems = verifyVocabulary({ projectRoot: process.cwd() });
|
|
15382
|
+
if (problems.length > 0) {
|
|
15383
|
+
for (const problem of problems) printError(problem);
|
|
15384
|
+
console.error(`
|
|
15385
|
+
ogygie verify-vocabulary \u2014 ${problems.length} probl\xE8me(s).`);
|
|
15386
|
+
process.exit(1);
|
|
15387
|
+
}
|
|
15388
|
+
console.log("ogygie verify-vocabulary \u2014 OK, le vocabulaire d\xE9clar\xE9 est respect\xE9.");
|
|
15136
15389
|
} else {
|
|
15137
15390
|
printError(
|
|
15138
15391
|
ogygieError({
|
|
15139
15392
|
code: "OGYGIE_CLI_USAGE",
|
|
15140
15393
|
message: `Unknown command ${JSON.stringify(command ?? "")}.`,
|
|
15141
15394
|
file: null,
|
|
15142
|
-
cause: `expected one of: init <product-name>, upgrade, check (flags: --json, --yes)`,
|
|
15143
|
-
suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade` or `ogygie
|
|
15395
|
+
cause: `expected one of: init <product-name>, upgrade, check, verify-vocabulary (flags: --json, --yes)`,
|
|
15396
|
+
suggestedFix: "Run `ogygie init <product-name>`, `ogygie upgrade`, `ogygie check` or `ogygie verify-vocabulary`."
|
|
15144
15397
|
})
|
|
15145
15398
|
);
|
|
15146
15399
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"ogygie": "
|
|
8
|
+
"ogygie": "dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
-
"build": "tsx scripts/build.ts",
|
|
22
|
-
"record-checksums": "tsx scripts/record-governed-checksums.ts"
|
|
23
|
-
},
|
|
24
19
|
"devDependencies": {
|
|
25
|
-
"@ogygie/core": "workspace:*",
|
|
26
20
|
"@types/node": "^24.0.0",
|
|
27
21
|
"esbuild": "^0.28.0",
|
|
28
22
|
"tsx": "^4.20.0",
|
|
29
23
|
"typescript": "^5.8.0",
|
|
30
|
-
"zod": "^4.0.0"
|
|
24
|
+
"zod": "^4.0.0",
|
|
25
|
+
"@ogygie/core": "0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
29
|
+
"build": "tsx scripts/build.ts",
|
|
30
|
+
"record-checksums": "tsx scripts/record-governed-checksums.ts"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
|
@@ -240,6 +240,38 @@ throw ogygieError({
|
|
|
240
240
|
6. **Identifiants code en anglais, contenu en langue de l'utilisateur.** Les enums du
|
|
241
241
|
domaine sont en anglais ; leur affichage est traduit.
|
|
242
242
|
|
|
243
|
+
## Vocabulaire du produit — un concept, un terme
|
|
244
|
+
|
|
245
|
+
Le produit déclare son vocabulaire fermé dans le bloc `vocabulary` d'`ogygie.manifest.json` :
|
|
246
|
+
pour chaque concept, LE terme canonique (identifiant machine, anglais) et les
|
|
247
|
+
alias que le code n'a pas le droit de porter. La prose qui explique le métier
|
|
248
|
+
reste dans `AGENTS.product.md` et cite les termes canoniques : le manifeste
|
|
249
|
+
déclare, la prose explique, la partie machine n'existe qu'à un seul endroit.
|
|
250
|
+
|
|
251
|
+
Le gate `pnpm exec ogygie verify-vocabulary` (posé dans la CI gouvernée)
|
|
252
|
+
applique deux règles :
|
|
253
|
+
|
|
254
|
+
- **Le code du produit ne porte aucun alias interdit** — l'appariement se fait
|
|
255
|
+
sur les identifiants machine (découpage camelCase / snake_case / kebab-case,
|
|
256
|
+
par racine), commentaires exclus. La prose du métier garde ses mots ; le code
|
|
257
|
+
n'a qu'un terme par concept. Erreur `OGYGIE_VOCABULARY_FORBIDDEN_ALIAS`, dont
|
|
258
|
+
la correction suggérée est le terme canonique.
|
|
259
|
+
- **Aucun fichier gouverné ne nomme un terme du produit**, canonique compris :
|
|
260
|
+
un fichier gouverné est identique dans tous les projets Ogygie, il ne peut
|
|
261
|
+
donc connaître le domaine d'aucun. Erreur
|
|
262
|
+
`OGYGIE_VOCABULARY_IN_GOVERNED_FILE` — la spécificité locale se déplace vers
|
|
263
|
+
`AGENTS.product.md`, elle ne se négocie pas dans un fichier gouverné.
|
|
264
|
+
|
|
265
|
+
Tant qu'aucun bloc `vocabulary` n'est déclaré, le gate passe sans rien dire :
|
|
266
|
+
il devient actif le jour où le produit déclare, jamais avant. Renommer un
|
|
267
|
+
concept est donc une opération à un seul endroit — on change le canonique, le
|
|
268
|
+
gate liste tous les identifiants à corriger.
|
|
269
|
+
|
|
270
|
+
Choisir les racines est un acte de conception, pas une formalité : l'appariement
|
|
271
|
+
se fait PAR RACINE, donc une racine trop courte attrape des mots génériques
|
|
272
|
+
(déclarer `memo` interdit fait tomber `memory`, `createInMemoryDb`…). Une racine
|
|
273
|
+
se déclare assez longue pour ne nommer que le concept.
|
|
274
|
+
|
|
243
275
|
## Front (apps/web) — règles et templates canoniques
|
|
244
276
|
|
|
245
277
|
### Stack imposée (toute autre lib = amendement DECISIONS.md d'abord)
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
manifeste (`{ "name": "...", "shared": true }`) et s'ajoute ici avec sa
|
|
43
43
|
source et son script d'extraction.
|
|
44
44
|
|
|
45
|
+
## Vocabulaire du produit (§Vocabulaire)
|
|
46
|
+
|
|
47
|
+
- À définir, concept par concept : chaque terme canonique et ses alias
|
|
48
|
+
interdits se déclarent dans le bloc `vocabulary` d'`ogygie.manifest.json`
|
|
49
|
+
(la partie machine), et sa définition métier se rédige ICI, sous une ancre
|
|
50
|
+
que l'entrée du manifeste référence. Tant que le bloc est absent, le gate
|
|
51
|
+
`ogygie verify-vocabulary` passe sans rien dire.
|
|
52
|
+
|
|
45
53
|
## Règles produit
|
|
46
54
|
|
|
47
55
|
- (aucune pour l'instant — toute règle qui ne vaut que pour ce produit se
|
|
@@ -29,6 +29,7 @@ jobs:
|
|
|
29
29
|
- run: pnpm ogygie:verify-manifest # manifest ↔ actual code (routes, folders, tables)
|
|
30
30
|
- run: pnpm ogygie:verify-specs # frontmatter + sections + spec lifecycle + decision refs
|
|
31
31
|
- run: pnpm ogygie:verify-contracts # transitive purity of every schemas.ts
|
|
32
|
+
- run: pnpm exec ogygie verify-vocabulary # declared vocabulary: forbidden aliases + governed-file purity
|
|
32
33
|
- run: pnpm typecheck
|
|
33
34
|
- run: pnpm lint # any, ts-ignore, raw-db, dangerouslySetInnerHTML
|
|
34
35
|
- run: pnpm test
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"ogygie:verify-manifest": "tsx scripts/verify-manifest.ts",
|
|
14
14
|
"ogygie:verify-specs": "tsx scripts/verify-specs.ts",
|
|
15
15
|
"ogygie:verify-contracts": "tsx scripts/verify-contracts.ts",
|
|
16
|
+
"ogygie:verify-vocabulary": "ogygie verify-vocabulary",
|
|
16
17
|
"ogygie:impact": "tsx scripts/ogygie-impact.ts",
|
|
17
18
|
"ogygie:design-brief": "tsx scripts/ogygie-design-brief.ts",
|
|
18
|
-
"verify": "pnpm ogygie:verify-manifest && pnpm ogygie:verify-specs && pnpm ogygie:verify-contracts && pnpm typecheck && pnpm lint && pnpm test"
|
|
19
|
+
"verify": "pnpm ogygie:verify-manifest && pnpm ogygie:verify-specs && pnpm ogygie:verify-contracts && pnpm ogygie:verify-vocabulary && pnpm typecheck && pnpm lint && pnpm test"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@ogygie/core": "__OGYGIE_VERSION__"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Deliberate scope choices:
|
|
14
14
|
* - Depth 1 only (direct dependents) — spec §Non-objectifs.
|
|
15
|
-
* - The owned-tables scan ignores *.test.ts files:
|
|
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>
|