@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 +1 -1
- package/src/vocabulary-audit.test.ts +99 -0
- package/src/vocabulary-audit.ts +31 -0
- package/src/vocabulary.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/core",
|
|
3
|
-
"version": "0.
|
|
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
|
+
});
|
package/src/vocabulary-audit.ts
CHANGED
|
@@ -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
|