@ogygie/core 0.6.0 → 0.7.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.6.0",
3
+ "version": "0.7.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",
@@ -456,3 +456,45 @@ describe("manifest — packages du produit (spec manifest-packages)", () => {
456
456
  expect(parsed.success).toBe(false);
457
457
  });
458
458
  });
459
+
460
+ describe("manifest — bloc tenancy (spec ogygie-standalone-repo v3)", () => {
461
+ const socle = featureNamed("espaces-et-comptes");
462
+
463
+ it("un bloc tenancy valide est accepté, rawDbAllowed a un défaut vide", () => {
464
+ const parsed = OgygieManifestSchema.safeParse(
465
+ manifestWithPackages(undefined, [socle]),
466
+ );
467
+ expect(parsed.success).toBe(true);
468
+ const withTenancy = OgygieManifestSchema.safeParse({
469
+ ...manifestWithPackages(undefined, [socle]),
470
+ tenancy: { column: "workspace_id" },
471
+ });
472
+ expect(withTenancy.success && withTenancy.data.tenancy?.rawDbAllowed).toEqual([]);
473
+ });
474
+
475
+ it("le socle peut nommer les features autorisées à la base brute", () => {
476
+ const parsed = OgygieManifestSchema.safeParse({
477
+ ...manifestWithPackages(undefined, [socle]),
478
+ tenancy: { column: "workspace_id", rawDbAllowed: ["espaces-et-comptes"] },
479
+ });
480
+ expect(parsed.success).toBe(true);
481
+ });
482
+
483
+ it("MUTATION : un socle nommant une feature INEXISTANTE est refusé", () => {
484
+ const parsed = OgygieManifestSchema.safeParse({
485
+ ...manifestWithPackages(undefined, [socle]),
486
+ tenancy: { column: "workspace_id", rawDbAllowed: ["fantome"] },
487
+ });
488
+ expect(parsed.success).toBe(false);
489
+ const issue = parsed.success ? undefined : parsed.error.issues[0];
490
+ expect(issue?.message).toContain("matches no declared feature");
491
+ });
492
+
493
+ it("MUTATION : une colonne tenant vide est refusée — elle ne garderait rien", () => {
494
+ const parsed = OgygieManifestSchema.safeParse({
495
+ ...manifestWithPackages(undefined, [socle]),
496
+ tenancy: { column: "" },
497
+ });
498
+ expect(parsed.success).toBe(false);
499
+ });
500
+ });
@@ -183,6 +183,34 @@ export const PackageManifestSchema = z.object({
183
183
  });
184
184
  export type PackageManifest = z.infer<typeof PackageManifestSchema>;
185
185
 
186
+ /**
187
+ * The two product values the framework's own gates need to do their job.
188
+ *
189
+ * They used to live as constants INSIDE the gate scripts, which is what kept
190
+ * those scripts out of governance: a governed file is identical in every
191
+ * Ogygie project, and these are not. Declared here, the scripts become
192
+ * identical everywhere and can finally be governed — so a corrected gate
193
+ * reaches every project through `ogygie upgrade` instead of waiting for each
194
+ * one to recopy it (spec ogygie-standalone-repo v3).
195
+ *
196
+ * Absent, the checks that depend on them simply do not run: a project without
197
+ * tenancy has nothing to guard, and saying so explicitly beats guarding
198
+ * nothing while pretending otherwise.
199
+ */
200
+ export const TenancyManifestSchema = z.object({
201
+ column: z
202
+ .string()
203
+ .min(1)
204
+ .describe("SQL name of the tenant column, e.g. workspace_id — the one AGENTS.product.md announces"),
205
+ rawDbAllowed: z
206
+ .array(FeatureNameSchema)
207
+ .default([])
208
+ .describe(
209
+ "Features allowed to reach the raw database: the tenancy socle, which BUILDS the contexts and therefore predates them. Every other feature goes through tenantDb",
210
+ ),
211
+ });
212
+ export type TenancyManifest = z.infer<typeof TenancyManifestSchema>;
213
+
186
214
  export const OgygieManifestSchema = z.object({
187
215
  version: z.literal(1),
188
216
  product: z.object({
@@ -213,6 +241,9 @@ export const OgygieManifestSchema = z.object({
213
241
  }),
214
242
  ),
215
243
  features: z.array(FeatureManifestSchema),
244
+ tenancy: TenancyManifestSchema.optional().describe(
245
+ "Product values the framework gates need — absent, the tenancy checks do not run",
246
+ ),
216
247
  packages: z
217
248
  .array(PackageManifestSchema)
218
249
  .optional()
@@ -280,6 +311,18 @@ export const OgygieManifestSchema = z.object({
280
311
  checkPackageDependencies(packageEntry.dependsOn, ["packages", index, "dependsOn"]);
281
312
  });
282
313
 
314
+ // A socle that names no existing feature guards nothing, and nobody would
315
+ // notice: the lint would simply allow one fewer folder.
316
+ (manifest.tenancy?.rawDbAllowed ?? []).forEach((featureName, index) => {
317
+ if (!featureNames.has(featureName)) {
318
+ context.addIssue({
319
+ code: "custom",
320
+ path: ["tenancy", "rawDbAllowed", index],
321
+ message: `"${featureName}" matches no declared feature`,
322
+ });
323
+ }
324
+ });
325
+
283
326
  // Vocabulary coherence (B2): one concept = one term. A canonical declared
284
327
  // twice, an alias claimed by two entries, or an alias that IS someone's
285
328
  // canonical would make the gate's suggested fix ambiguous — refused here,