@ogygie/cli 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/dist/cli.js CHANGED
@@ -14588,6 +14588,42 @@ var governedFiles = [
14588
14588
  templatePath: "project/tsconfig.base.json",
14589
14589
  targetPath: "tsconfig.base.json",
14590
14590
  description: "Strict TypeScript options decided by the framework."
14591
+ },
14592
+ // The gate scripts (spec ogygie-standalone-repo v3). They decide nothing by
14593
+ // themselves: they apply the framework's rules to what the manifest
14594
+ // declares, so they are identical in every project — and a gate corrected
14595
+ // upstream must reach every project, which recopying by hand never achieved.
14596
+ // What used to be product-specific in them now lives in the manifest
14597
+ // (`tenancy`).
14598
+ {
14599
+ templatePath: "project/scripts/verify-manifest.ts",
14600
+ targetPath: "scripts/verify-manifest.ts",
14601
+ description: "Gate: the manifest tells the truth about the actual code."
14602
+ },
14603
+ {
14604
+ templatePath: "project/scripts/verify-specs.ts",
14605
+ targetPath: "scripts/verify-specs.ts",
14606
+ description: "Gate: spec frontmatter, canonical sections, lifecycle, decision refs."
14607
+ },
14608
+ {
14609
+ templatePath: "project/scripts/verify-contracts.ts",
14610
+ targetPath: "scripts/verify-contracts.ts",
14611
+ description: "Gate: transitive purity of every contract entry point."
14612
+ },
14613
+ {
14614
+ templatePath: "project/scripts/lint-interdits.ts",
14615
+ targetPath: "scripts/lint-interdits.ts",
14616
+ description: "Gate: the absolute bans of AGENTS.md, plus the raw-db rule derived from the manifest."
14617
+ },
14618
+ {
14619
+ templatePath: "project/scripts/ogygie-impact.ts",
14620
+ targetPath: "scripts/ogygie-impact.ts",
14621
+ description: "Tool: blast radius of a feature, derived from the manifest."
14622
+ },
14623
+ {
14624
+ templatePath: "project/scripts/ogygie-design-brief.ts",
14625
+ targetPath: "scripts/ogygie-design-brief.ts",
14626
+ description: "Tool: design brief derived from tokens, patterns, contracts and specs."
14591
14627
  }
14592
14628
  ];
14593
14629
 
@@ -14665,6 +14701,12 @@ var PackageManifestSchema = external_exports.object({
14665
14701
  dependsOn: external_exports.array(PackageDependencySchema).describe("Other PRODUCT packages, as package:<name> \u2014 never a feature: a package that depended on one would live under an app, and would be a feature"),
14666
14702
  status: external_exports.enum(["skeleton", "in-progress", "delivered"])
14667
14703
  });
14704
+ var TenancyManifestSchema = external_exports.object({
14705
+ column: external_exports.string().min(1).describe("SQL name of the tenant column, e.g. workspace_id \u2014 the one AGENTS.product.md announces"),
14706
+ rawDbAllowed: external_exports.array(FeatureNameSchema).default([]).describe(
14707
+ "Features allowed to reach the raw database: the tenancy socle, which BUILDS the contexts and therefore predates them. Every other feature goes through tenantDb"
14708
+ )
14709
+ });
14668
14710
  var OgygieManifestSchema = external_exports.object({
14669
14711
  version: external_exports.literal(1),
14670
14712
  product: external_exports.object({
@@ -14695,6 +14737,9 @@ var OgygieManifestSchema = external_exports.object({
14695
14737
  })
14696
14738
  ),
14697
14739
  features: external_exports.array(FeatureManifestSchema),
14740
+ tenancy: TenancyManifestSchema.optional().describe(
14741
+ "Product values the framework gates need \u2014 absent, the tenancy checks do not run"
14742
+ ),
14698
14743
  packages: external_exports.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
14699
14744
  vocabulary: ProductVocabularySchema.optional().describe(
14700
14745
  "The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
@@ -14743,6 +14788,15 @@ var OgygieManifestSchema = external_exports.object({
14743
14788
  (manifest.packages ?? []).forEach((packageEntry, index) => {
14744
14789
  checkPackageDependencies(packageEntry.dependsOn, ["packages", index, "dependsOn"]);
14745
14790
  });
14791
+ (manifest.tenancy?.rawDbAllowed ?? []).forEach((featureName, index) => {
14792
+ if (!featureNames.has(featureName)) {
14793
+ context.addIssue({
14794
+ code: "custom",
14795
+ path: ["tenancy", "rawDbAllowed", index],
14796
+ message: `"${featureName}" matches no declared feature`
14797
+ });
14798
+ }
14799
+ });
14746
14800
  const canonicalOwners = /* @__PURE__ */ new Map();
14747
14801
  const aliasOwners = /* @__PURE__ */ new Map();
14748
14802
  (manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
@@ -15503,6 +15557,18 @@ var governedChecksumsByVersion = {
15503
15557
  "AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
15504
15558
  "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15505
15559
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15560
+ },
15561
+ "0.7.0": {
15562
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15563
+ "AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
15564
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15565
+ "scripts/lint-interdits.ts": "d859e132b7020942a5211f5419b10045170dee6afba3f50ca5642570a5c4a683",
15566
+ "scripts/ogygie-design-brief.ts": "aba1585717b30808dd0bc8930a37a1ad2d83ee6869f969dbd06ca595e37a4a89",
15567
+ "scripts/ogygie-impact.ts": "0dd3ff8cdd57e6cff0dfec1c669fce53fde9ae43f0784ed114771c83d1f8b357",
15568
+ "scripts/verify-contracts.ts": "725cae269e78ecdaa1923553c3097d8d96277824b5bd79a9a40284c6246036b7",
15569
+ "scripts/verify-manifest.ts": "1adca9edee11e54ccaf0511012411516c52c68a32f686ca320c7e437afb22c7c",
15570
+ "scripts/verify-specs.ts": "ccf09c04155e5e88fa9acd4a5db33c2c79e85923b2d92e8e866fe9cbc4cdf342",
15571
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15506
15572
  }
15507
15573
  };
15508
15574
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Ogygie framework CLI: deterministic scaffolding (init), governed-file upgrades (upgrade) and lockstep verification (check) for Ogygie products.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "tsx": "^4.20.0",
23
23
  "typescript": "^5.8.0",
24
24
  "zod": "^4.0.0",
25
- "@ogygie/core": "0.6.0"
25
+ "@ogygie/core": "0.7.0"
26
26
  },
27
27
  "scripts": {
28
28
  "typecheck": "tsc -p tsconfig.json",
@@ -19,8 +19,7 @@
19
19
  - Tenant du produit : **à définir** (le mot du domaine qui possède les
20
20
  données). L'alias produit de `TenantContext` est **`<Tenant>Context`**
21
21
  (`apps/api/src/context.ts`) : clé TS de colonne `<tenant>Id`, colonne SQL
22
- **`<tenant>_id`** — à déclarer aussi dans `scripts/verify-manifest.ts`
23
- (`TENANT_COLUMN_NAMES`).
22
+ **`<tenant>_id`** — à déclarer aussi dans le bloc `tenancy` du manifeste).
24
23
  - Liste fermée du socle autorisé à toucher la base brute : **à définir** (la
25
24
  feature qui CONSTRUIT les contextes tenant) — déclarée dans
26
25
  `scripts/lint-interdits.ts` (`RAW_DB_SOCLE_ALLOWLIST`). Toute autre feature
@@ -22,6 +22,13 @@ const ROOT = fileURLToPath(new URL("..", import.meta.url));
22
22
  const SELF = fileURLToPath(import.meta.url);
23
23
  const SCANNED_DIRS = ["apps", "packages", "scripts"];
24
24
  const IGNORED_DIRS = new Set(["node_modules", "dist", ".git"]);
25
+ // Build artifacts of a FRAMEWORK repo: a published package may embed a copy of
26
+ // templates/ before publishing (generated and gitignored, like dist/). Derived
27
+ // from the manifest rather than hard-coded, so this file stays identical in
28
+ // every project — a consumer manifest has no framework block, and the set is
29
+ // simply empty.
30
+ const embeddedTemplateDirs = (frameworkPackagePaths: readonly string[]): Set<string> =>
31
+ new Set(frameworkPackagePaths.map((packagePath) => join(ROOT, packagePath, "templates")));
25
32
  const EXTENSIONS = [".ts", ".tsx", ".mts", ".mjs"];
26
33
 
27
34
  const TS_IGNORE_BAN = {
@@ -29,16 +36,12 @@ const TS_IGNORE_BAN = {
29
36
  rule: "ts-ignore interdit — @ts-expect-error avec justification si inévitable",
30
37
  };
31
38
 
32
- // Closed-list socle exception (AGENTS.md §Multi-tenant): the tenancy feature
33
- // BUILDS the contexts, so it predates them. Every other feature goes through
34
- // tenantDb — unless the manifest declares that every table it owns is SHARED,
35
- // which have no tenant column to filter on. Interim guard until the real
36
- // ESLint `ogygie/no-raw-db` rule lands.
39
+ // Socle exception (AGENTS.md §Multi-tenant): the tenancy feature BUILDS the
40
+ // contexts, so it predates them. Every other feature goes through tenantDb —
41
+ // unless the manifest declares that every table it owns is SHARED, which have
42
+ // no tenant column to filter on. The list is DECLARED at the manifest
43
+ // (`tenancy.rawDbAllowed`), never held here: this file is governed.
37
44
  const FEATURES_PREFIX = "apps/api/src/features/";
38
- // TODO(product): the ONE tenancy socle feature allowed to touch the raw
39
- // database (it BUILDS the tenant contexts) — declare it in AGENTS.product.md
40
- // and here. Empty until the socle exists.
41
- const RAW_DB_SOCLE_ALLOWLIST = new Set<string>([]);
42
45
  const RAW_DB_BAN = {
43
46
  pattern: /\bctx\.db\b/,
44
47
  rule: "ctx.db interdit hors du socle et des features à tables partagées — passer par tenantDb (AGENTS.md §Multi-tenant)",
@@ -53,11 +56,15 @@ const manifest = OgygieManifestSchema.parse(
53
56
  JSON.parse(readFileSync(join(ROOT, "ogygie.manifest.json"), "utf8")),
54
57
  );
55
58
  /** Feature folder names allowed to touch the raw database, derived — not held. */
59
+ const socleAllowlist = new Set(manifest.tenancy?.rawDbAllowed ?? []);
56
60
  const rawDbAllowedFolders = new Set(
57
61
  manifest.features
58
- .filter((feature) => mayUseRawDb(feature, RAW_DB_SOCLE_ALLOWLIST))
62
+ .filter((feature) => mayUseRawDb(feature, socleAllowlist))
59
63
  .map((feature) => feature.folder.split("/").at(-1) ?? feature.name),
60
64
  );
65
+ const ignoredBuildDirs = embeddedTemplateDirs(
66
+ (manifest.framework?.packages ?? []).map((packageEntry) => packageEntry.path),
67
+ );
61
68
 
62
69
  const violations: string[] = [];
63
70
 
@@ -75,6 +82,7 @@ function scan(dir: string): void {
75
82
  const entryPath = join(dir, entry);
76
83
  const stats = statSync(entryPath);
77
84
  if (stats.isDirectory()) {
85
+ if (ignoredBuildDirs.has(entryPath)) continue;
78
86
  scan(entryPath);
79
87
  continue;
80
88
  }
@@ -28,12 +28,35 @@ import {
28
28
  import { ogygieError, type OgygieError } from "@ogygie/core/errors";
29
29
 
30
30
  /**
31
- * Tenant column names of the PRODUCT (the core stays generic). A table
32
- * declared `shared: true` must carry none of them.
33
- * TODO(product): declare your tenant column here (the SQL name announced in
34
- * AGENTS.product.md, e.g. "workspace_id") — empty, this check guards nothing.
31
+ * Tenant column of the PRODUCT, read from the manifest (`tenancy.column`) —
32
+ * never a constant in this file, which is governed and therefore identical in
33
+ * every Ogygie project. A table declared `shared: true` must not carry it.
34
+ * No tenancy declared = nothing to guard, and the check stands down instead of
35
+ * pretending.
35
36
  */
36
- const TENANT_COLUMN_NAMES: readonly string[] = [];
37
+ const tenantColumnNames = (): readonly string[] =>
38
+ manifest.tenancy === undefined ? [] : [manifest.tenancy.column];
39
+
40
+ /**
41
+ * A project declaring SHARED tables without a tenant column is the one case
42
+ * where silence is dangerous: the check that guards those tables cannot run,
43
+ * and a passing gate would mean nothing. Said out loud rather than skipped —
44
+ * a gate that quietly guards nothing is worse than no gate.
45
+ */
46
+ function reportUndeclaredTenancy(): void {
47
+ if (manifest.tenancy !== undefined) return;
48
+ const shared = manifest.features.flatMap((feature) => sharedTableNamesOf(feature));
49
+ if (shared.length === 0) return;
50
+ report({
51
+ code: "MANIFEST_TENANCY_UNDECLARED",
52
+ message:
53
+ "Shared tables are declared but the manifest declares no tenant column — the shared-table check cannot guard anything.",
54
+ file: MANIFEST_PATH,
55
+ cause: `tables ${shared.join(", ")} declared shared, tenancy block absent`,
56
+ suggestedFix:
57
+ 'Declare `"tenancy": { "column": "<sql_tenant_column>", "rawDbAllowed": [...] }` in the manifest — the value AGENTS.product.md announces.',
58
+ });
59
+ }
37
60
 
38
61
  const ROOT = new URL("..", import.meta.url).pathname;
39
62
  const MANIFEST_PATH = "ogygie.manifest.json";
@@ -71,6 +94,7 @@ if (!parsed.success) {
71
94
  conclude();
72
95
  }
73
96
  const manifest = parsed.data;
97
+ reportUndeclaredTenancy();
74
98
 
75
99
  for (const app of manifest.apps) {
76
100
  if (!existsSync(join(ROOT, app.path))) {
@@ -84,6 +108,29 @@ for (const app of manifest.apps) {
84
108
  }
85
109
  }
86
110
 
111
+ // Framework self-description (spec ogygie-standalone-repo B1): every path the
112
+ // block advertises must exist — an agent trusts the manifest INSTEAD of
113
+ // exploring, so a phantom path here is a lie like any other.
114
+ if (manifest.framework !== undefined) {
115
+ const declaredPaths = [
116
+ ...manifest.framework.packages.map((pkg) => pkg.path),
117
+ manifest.framework.templates.project,
118
+ manifest.framework.templates.feature,
119
+ manifest.framework.governedFiles,
120
+ ];
121
+ for (const declaredPath of declaredPaths) {
122
+ if (!existsSync(join(ROOT, declaredPath))) {
123
+ report({
124
+ code: "MANIFEST_FRAMEWORK_PATH_NOT_FOUND",
125
+ message: `The framework block declares ${declaredPath}, which does not exist.`,
126
+ file: MANIFEST_PATH,
127
+ cause: `framework path ${declaredPath} absent from disk`,
128
+ suggestedFix: "Create the folder/module or fix the framework block.",
129
+ });
130
+ }
131
+ }
132
+ }
133
+
87
134
  // A declared package whose folder is absent is a lying map, exactly like a
88
135
  // phantom app (spec manifest-packages B1).
89
136
  for (const productPackage of manifest.packages ?? []) {
@@ -238,7 +285,7 @@ for (const feature of manifest.features) {
238
285
  for (const finding of findTenantColumnsInSharedTables(
239
286
  dbSchemaContent,
240
287
  sharedTableNamesOf(feature),
241
- TENANT_COLUMN_NAMES,
288
+ tenantColumnNames(),
242
289
  )) {
243
290
  report({
244
291
  code: "SHARED_TABLE_WITH_TENANT_COLUMN",