@ogygie/core 0.2.0 → 0.4.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/manifest.schema.test.ts +24 -0
- package/src/manifest.schema.ts +13 -0
- package/src/vocabulary-audit.test.ts +18 -2
- package/src/vocabulary-audit.ts +18 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogygie/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -349,3 +349,27 @@ describe("manifest — bloc vocabulary (spec product-vocabulary-gate B2)", () =>
|
|
|
349
349
|
expect(issue?.path.join(".")).toBe("vocabulary.entries.1.canonical");
|
|
350
350
|
});
|
|
351
351
|
});
|
|
352
|
+
|
|
353
|
+
describe("manifest — apps vendored (spec product-vocabulary-gate B3, v3)", () => {
|
|
354
|
+
function manifestWithApp(app: Record<string, unknown>) {
|
|
355
|
+
return {
|
|
356
|
+
version: 1,
|
|
357
|
+
product: { name: "acme", description: "SaaS générique servant de fixture aux tests du manifeste." },
|
|
358
|
+
apps: [{ name: "tooling", path: "packages/tooling", type: "front", entryPoint: "packages/tooling/src/main.tsx", ...app }],
|
|
359
|
+
features: [],
|
|
360
|
+
conventions: { agentsMd: "AGENTS.md", failuresMd: "FAILURES.md", specsFolder: "specs/" },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
it("une app déclarée vendored: true est acceptée", () => {
|
|
365
|
+
expect(OgygieManifestSchema.safeParse(manifestWithApp({ vendored: true })).success).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("une app sans le champ reste la norme (le produit maintient son code)", () => {
|
|
369
|
+
expect(OgygieManifestSchema.safeParse(manifestWithApp({})).success).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("MUTATION : `vendored: false` est refusé — pas de troisième état", () => {
|
|
373
|
+
expect(OgygieManifestSchema.safeParse(manifestWithApp({ vendored: false })).success).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
});
|
package/src/manifest.schema.ts
CHANGED
|
@@ -159,6 +159,19 @@ export const OgygieManifestSchema = z.object({
|
|
|
159
159
|
path: z.string(),
|
|
160
160
|
type: z.enum(["api", "front", "gateway"]),
|
|
161
161
|
entryPoint: z.string(),
|
|
162
|
+
/**
|
|
163
|
+
* Code the product HOSTS without maintaining it — imported as-is from
|
|
164
|
+
* elsewhere, the framework's own dev tooling being the typical case.
|
|
165
|
+
* A vendored app is skipped by the vocabulary gate: a product's business
|
|
166
|
+
* vocabulary has no claim over code it will never rename.
|
|
167
|
+
*
|
|
168
|
+
* A literal `true`, like `shared` on a table: an app is either vendored
|
|
169
|
+
* (and says so) or the product's own (and stays silent) — no third
|
|
170
|
+
* state. The derogation is declared here, machine-readably, and the gate
|
|
171
|
+
* ANNOUNCES it on every run — one that stops being visible is one that
|
|
172
|
+
* gets forgotten.
|
|
173
|
+
*/
|
|
174
|
+
vendored: z.literal(true).optional(),
|
|
162
175
|
}),
|
|
163
176
|
),
|
|
164
177
|
features: z.array(FeatureManifestSchema),
|
|
@@ -121,9 +121,25 @@ describe("auditProductVocabulary — B4 : la pureté des fichiers gouvernés", (
|
|
|
121
121
|
const violations = auditProductVocabulary({
|
|
122
122
|
vocabulary: VOCABULARY,
|
|
123
123
|
sourceFiles: [],
|
|
124
|
-
governedFiles: [{ path: "tsconfig.base.json", content: "// tuned for the
|
|
124
|
+
governedFiles: [{ path: "tsconfig.base.json", content: "// tuned for the gadget pipeline" }],
|
|
125
125
|
});
|
|
126
|
-
expect(violations.map((violation) => violation.match.root)).toEqual(["
|
|
126
|
+
expect(violations.map((violation) => violation.match.root)).toEqual(["gadget"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("un ALIAS interdit dans un fichier gouverné ne déclenche RIEN (v2)", () => {
|
|
130
|
+
// An alias is by definition a word the product does NOT use, so finding it
|
|
131
|
+
// in a generic file proves no contamination — while the framework's own
|
|
132
|
+
// neutral domain and plain computing English produce such collisions in
|
|
133
|
+
// bulk (eight false positives out of eighteen on the first real migration).
|
|
134
|
+
expect(
|
|
135
|
+
auditProductVocabulary({
|
|
136
|
+
vocabulary: VOCABULARY,
|
|
137
|
+
sourceFiles: [],
|
|
138
|
+
governedFiles: [
|
|
139
|
+
{ path: "AGENTS.md", content: "const widgetId = 1; // gizmo everywhere" },
|
|
140
|
+
],
|
|
141
|
+
}),
|
|
142
|
+
).toEqual([]);
|
|
127
143
|
});
|
|
128
144
|
|
|
129
145
|
it("un fichier gouverné strictement générique passe", () => {
|
package/src/vocabulary-audit.ts
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
* - PRODUCT SOURCE (B3): only forbidden ALIASES are hunted, and only in the
|
|
9
9
|
* machine identifiers — comments are blanked out first. The business prose
|
|
10
10
|
* of the project keeps its own words; the code has one term per concept.
|
|
11
|
-
* - GOVERNED FILES (B4):
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - GOVERNED FILES (B4): the CANONICAL terms are hunted in the RAW text,
|
|
12
|
+
* comments and all. A governed file is identical in every Ogygie project, so
|
|
13
|
+
* it may know no product's domain at all, not even in a comment. That is the
|
|
14
|
+
* framework/product boundary made mechanical.
|
|
15
|
+
* Forbidden aliases are deliberately NOT hunted there (spec v2): an alias is
|
|
16
|
+
* by definition a word the product does NOT use, so finding one in a generic
|
|
17
|
+
* file proves no contamination — while the framework's own neutral domain
|
|
18
|
+
* (`orders`, `customerId`) and plain computing English (`level`, `register`)
|
|
19
|
+
* collide with it in bulk. Eight false positives out of eighteen on the first
|
|
20
|
+
* real migration, and a gate whose verdicts one learns to ignore protects
|
|
21
|
+
* nothing.
|
|
15
22
|
*
|
|
16
23
|
* The filesystem is injected (files come in as text), so the whole judgement
|
|
17
24
|
* is a pure function proven in memory with its mutations.
|
|
@@ -49,7 +56,11 @@ export interface VocabularyAuditInput {
|
|
|
49
56
|
readonly governedFiles: readonly ScannedFile[];
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Every root the vocabulary declares: canonical terms AND forbidden aliases.
|
|
61
|
+
* Kept for callers that need the full list; the governed-file rule uses the
|
|
62
|
+
* canonical terms alone (see above).
|
|
63
|
+
*/
|
|
53
64
|
export function vocabularyRootsOf(vocabulary: ProductVocabulary): readonly string[] {
|
|
54
65
|
return vocabulary.entries.flatMap((entry) => [entry.canonical, ...entry.forbiddenAliases]);
|
|
55
66
|
}
|
|
@@ -71,7 +82,7 @@ export function auditProductVocabulary(
|
|
|
71
82
|
|
|
72
83
|
const canonicalOfAlias = canonicalByAlias(vocabulary);
|
|
73
84
|
const forbiddenAliases = [...canonicalOfAlias.keys()];
|
|
74
|
-
const
|
|
85
|
+
const canonicalTerms = vocabulary.entries.map((entry) => entry.canonical);
|
|
75
86
|
const violations: VocabularyViolation[] = [];
|
|
76
87
|
|
|
77
88
|
for (const file of input.sourceFiles) {
|
|
@@ -86,7 +97,7 @@ export function auditProductVocabulary(
|
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
for (const file of input.governedFiles) {
|
|
89
|
-
for (const match of findProductVocabulary(file.content,
|
|
100
|
+
for (const match of findProductVocabulary(file.content, canonicalTerms)) {
|
|
90
101
|
violations.push({
|
|
91
102
|
kind: "GOVERNED_FILE",
|
|
92
103
|
filePath: file.path,
|