@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/core",
3
- "version": "0.2.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
+ });
@@ -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 gizmo pipeline" }],
124
+ governedFiles: [{ path: "tsconfig.base.json", content: "// tuned for the gadget pipeline" }],
125
125
  });
126
- expect(violations.map((violation) => violation.match.root)).toEqual(["gizmo"]);
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", () => {
@@ -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): EVERY declared root is hunted — canonical terms
12
- * included — in the RAW text, comments and all. A governed file is identical
13
- * in every Ogygie project, so it may know no product's domain at all, not
14
- * even in a comment. That is the framework/product boundary made mechanical.
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
- /** Every root the vocabulary declares: canonical terms AND forbidden aliases. */
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 everyRoot = vocabularyRootsOf(vocabulary);
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, everyRoot)) {
100
+ for (const match of findProductVocabulary(file.content, canonicalTerms)) {
90
101
  violations.push({
91
102
  kind: "GOVERNED_FILE",
92
103
  filePath: file.path,