@ogygie/core 0.11.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/core",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Ogygie framework core: manifest/spec/error schemas, generic tenancy and gate tooling. Ships TypeScript sources — Ogygie products run tsx/vitest by construction (D1).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@ import { describe, expect, it } from "vitest";
9
9
  import {
10
10
  auditProductVocabulary,
11
11
  canonicalByAlias,
12
+ derogatedFeatureIdsOf,
12
13
  vocabularyRootsOf,
13
14
  } from "./vocabulary-audit.js";
14
15
 
@@ -198,3 +199,101 @@ describe("dérivations du bloc vocabulary", () => {
198
199
  ]);
199
200
  });
200
201
  });
202
+
203
+ describe("auditProductVocabulary — B3 : la dérogation des ids de features (D16)", () => {
204
+ /** The product's features are named `widgets` — the very forbidden alias. */
205
+ const FEATURE_IDS = ["widgets", "gadget-catalog"];
206
+
207
+ it("ÉTANT DONNÉ un id de feature qui EST un alias interdit QUAND le code en dérive ses identifiants ALORS rien n'est signalé", () => {
208
+ const violations = auditProductVocabulary({
209
+ vocabulary: VOCABULARY,
210
+ sourceFiles: [
211
+ {
212
+ path: "apps/api/src/features/widgets/routes.ts",
213
+ content: [
214
+ 'import { listWidgets } from "../features/widgets/service.js";',
215
+ "export function registerWidgetsRoutes() {}",
216
+ ].join("\n"),
217
+ },
218
+ ],
219
+ governedFiles: [],
220
+ featureIds: FEATURE_IDS,
221
+ });
222
+ expect(violations).toEqual([]);
223
+ });
224
+
225
+ it("MUTATION — sans la dérogation, ces mêmes identifiants dérivés sont TOUS signalés", () => {
226
+ const violations = auditProductVocabulary({
227
+ vocabulary: VOCABULARY,
228
+ sourceFiles: [
229
+ {
230
+ path: "apps/api/src/features/widgets/routes.ts",
231
+ content: [
232
+ 'import { listWidgets } from "../features/widgets/service.js";',
233
+ "export function registerWidgetsRoutes() {}",
234
+ ].join("\n"),
235
+ },
236
+ ],
237
+ governedFiles: [],
238
+ });
239
+ expect(violations.map((violation) => violation.match.identifier).sort()).toEqual([
240
+ "listWidgets",
241
+ "registerWidgetsRoutes",
242
+ "widgets",
243
+ ]);
244
+ });
245
+
246
+ it("la dérogation couvre la DÉRIVATION, jamais un nom seulement préfixé par la racine", () => {
247
+ const violations = auditProductVocabulary({
248
+ vocabulary: VOCABULARY,
249
+ sourceFiles: [
250
+ { path: "apps/api/src/features/widgets/service.ts", content: "const widgetCode = 1;" },
251
+ ],
252
+ governedFiles: [],
253
+ featureIds: FEATURE_IDS,
254
+ });
255
+ // `widgetCode` carries the root but NOT the whole feature id `widgets`.
256
+ expect(violations).toHaveLength(1);
257
+ expect(violations[0]?.match.identifier).toBe("widgetCode");
258
+ });
259
+
260
+ it("un id de feature multi-segments couvre ses dérivés, et lui seul", () => {
261
+ const violations = auditProductVocabulary({
262
+ vocabulary: VOCABULARY,
263
+ sourceFiles: [
264
+ {
265
+ path: "apps/api/src/features/gadget-catalog/service.ts",
266
+ content: ["const gadgetCatalogRows = [];", "const gadgetCatalogue = [];"].join("\n"),
267
+ },
268
+ ],
269
+ governedFiles: [],
270
+ featureIds: FEATURE_IDS,
271
+ });
272
+ expect(violations.map((violation) => violation.match.identifier)).toEqual([
273
+ "gadgetCatalogue",
274
+ ]);
275
+ });
276
+
277
+ it("la dérogation ne touche PAS la règle des fichiers gouvernés (B4)", () => {
278
+ const violations = auditProductVocabulary({
279
+ vocabulary: VOCABULARY,
280
+ sourceFiles: [],
281
+ governedFiles: [{ path: "AGENTS.md", content: "le gadget du produit" }],
282
+ featureIds: ["gadget"],
283
+ });
284
+ expect(violations).toHaveLength(1);
285
+ expect(violations[0]?.kind).toBe("GOVERNED_FILE");
286
+ });
287
+ });
288
+
289
+ describe("derogatedFeatureIdsOf — ce que la dérogation excuse VRAIMENT", () => {
290
+ it("ne retient que les ids de features qui portent un alias interdit", () => {
291
+ expect(
292
+ derogatedFeatureIdsOf(VOCABULARY, ["widgets", "invoices", "gadget-catalog"]),
293
+ ).toEqual(["widgets", "gadget-catalog"]);
294
+ });
295
+
296
+ it("sans vocabulaire déclaré, aucune dérogation à annoncer", () => {
297
+ expect(derogatedFeatureIdsOf(undefined, ["widgets"])).toEqual([]);
298
+ });
299
+ });
@@ -30,6 +30,7 @@ import {
30
30
  findProductVocabulary,
31
31
  stripComments,
32
32
  stripProseStrings,
33
+ tokenCarriesExactName,
33
34
  type VocabularyMatch,
34
35
  } from "./vocabulary.js";
35
36
 
@@ -61,6 +62,14 @@ export interface VocabularyAuditInput {
61
62
  readonly sourceFiles: readonly ScannedFile[];
62
63
  /** Framework-governed files as they exist in the project (B4). */
63
64
  readonly governedFiles: readonly ScannedFile[];
65
+ /**
66
+ * The manifest's feature ids. Identifiers DERIVED from one of them are not
67
+ * judged against the forbidden aliases (B3 derogation, D16): a feature id
68
+ * names a document and follows the project's language, while the code
69
+ * derives its folders, import paths and function names from it. Absent or
70
+ * empty, nothing is excused.
71
+ */
72
+ readonly featureIds?: readonly string[];
64
73
  }
65
74
 
66
75
  /**
@@ -81,6 +90,23 @@ export function canonicalByAlias(vocabulary: ProductVocabulary): ReadonlyMap<str
81
90
  return byAlias;
82
91
  }
83
92
 
93
+ /**
94
+ * The feature ids the derogation actually covers (D16): those whose own name
95
+ * carries a forbidden root. A feature id that collides with nothing excuses
96
+ * nothing, and must NOT be announced — a derogation list padded with harmless
97
+ * entries is a list nobody reads, which is how a derogation gets forgotten.
98
+ */
99
+ export function derogatedFeatureIdsOf(
100
+ vocabulary: ProductVocabulary | undefined,
101
+ featureIds: readonly string[],
102
+ ): readonly string[] {
103
+ if (vocabulary === undefined || vocabulary.entries.length === 0) return [];
104
+ const forbiddenAliases = [...canonicalByAlias(vocabulary).keys()];
105
+ return featureIds.filter(
106
+ (featureId) => findProductVocabulary(featureId, forbiddenAliases).length > 0,
107
+ );
108
+ }
109
+
84
110
  export function auditProductVocabulary(
85
111
  input: VocabularyAuditInput,
86
112
  ): readonly VocabularyViolation[] {
@@ -92,9 +118,14 @@ export function auditProductVocabulary(
92
118
  const canonicalTerms = vocabulary.entries.map((entry) => entry.canonical);
93
119
  const violations: VocabularyViolation[] = [];
94
120
 
121
+ const featureIds = input.featureIds ?? [];
122
+ const derivesFromAFeatureId = (identifier: string): boolean =>
123
+ featureIds.some((featureId) => tokenCarriesExactName(identifier, featureId));
124
+
95
125
  for (const file of input.sourceFiles) {
96
126
  const judged = stripProseStrings(stripComments(file.content));
97
127
  for (const match of findProductVocabulary(judged, forbiddenAliases)) {
128
+ if (derivesFromAFeatureId(match.identifier)) continue;
98
129
  violations.push({
99
130
  kind: "FORBIDDEN_ALIAS",
100
131
  filePath: file.path,
package/src/vocabulary.ts CHANGED
@@ -75,6 +75,30 @@ function tokenCarriesRoot(
75
75
  return false;
76
76
  }
77
77
 
78
+ /**
79
+ * Does this token carry that whole name as an EXACT run of segments? Unlike
80
+ * `tokenCarriesRoot`, the last segment must match in full: this serves the
81
+ * feature-id derogation (D16), where the point is to excuse what DERIVES from
82
+ * a declared name — `registerOrdersRoutes` for the feature `orders` — and
83
+ * never a name that merely starts the same way (`orderCode` stays judged).
84
+ */
85
+ export function tokenCarriesExactName(token: string, name: string): boolean {
86
+ const tokenSegments = segmentsOfIdentifier(token);
87
+ const nameSegments = segmentsOfIdentifier(name);
88
+ if (nameSegments.length === 0) return false;
89
+ for (let start = 0; start + nameSegments.length <= tokenSegments.length; start += 1) {
90
+ let allMatch = true;
91
+ for (let offset = 0; offset < nameSegments.length; offset += 1) {
92
+ if (tokenSegments[start + offset] !== nameSegments[offset]) {
93
+ allMatch = false;
94
+ break;
95
+ }
96
+ }
97
+ if (allMatch) return true;
98
+ }
99
+ return false;
100
+ }
101
+
78
102
  /**
79
103
  * Every forbidden root the text names, in order of appearance, deduplicated by
80
104
  * (root, identifier): one finding per distinct faulty identifier, however many