@ogygie/cli 0.5.4 → 0.6.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
@@ -14602,6 +14602,7 @@ import { join } from "node:path";
14602
14602
 
14603
14603
  // ../ogygie-core/src/manifest.schema.ts
14604
14604
  var FeatureNameSchema = external_exports.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
14605
+ var PackageDependencySchema = external_exports.string().regex(/^package:[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "expected package:<kebab-case-name>");
14605
14606
  var HttpRouteSchema = external_exports.object({
14606
14607
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
14607
14608
  path: external_exports.string().startsWith("/api/"),
@@ -14626,7 +14627,9 @@ var FeatureManifestSchema = external_exports.object({
14626
14627
  dbTables: external_exports.array(DbTableDeclarationSchema).describe(
14627
14628
  "Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
14628
14629
  ),
14629
- dependsOn: external_exports.array(FeatureNameSchema).describe("Features imported through their index.ts \u2014 nothing else"),
14630
+ dependsOn: external_exports.array(external_exports.union([FeatureNameSchema, PackageDependencySchema])).describe(
14631
+ "Features imported through their index.ts, and product packages as package:<name> \u2014 nothing else"
14632
+ ),
14630
14633
  status: external_exports.enum(["skeleton", "in-progress", "delivered"])
14631
14634
  });
14632
14635
  var OgygieVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
@@ -14654,6 +14657,14 @@ var VocabularyEntrySchema = external_exports.object({
14654
14657
  var ProductVocabularySchema = external_exports.object({
14655
14658
  entries: external_exports.array(VocabularyEntrySchema)
14656
14659
  });
14660
+ var PackageManifestSchema = external_exports.object({
14661
+ name: FeatureNameSchema,
14662
+ path: external_exports.string().describe("Folder relative to the repo root, e.g. packages/solver"),
14663
+ intent: external_exports.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
14664
+ spec: external_exports.string().nullable(),
14665
+ 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
+ status: external_exports.enum(["skeleton", "in-progress", "delivered"])
14667
+ });
14657
14668
  var OgygieManifestSchema = external_exports.object({
14658
14669
  version: external_exports.literal(1),
14659
14670
  product: external_exports.object({
@@ -14684,6 +14695,7 @@ var OgygieManifestSchema = external_exports.object({
14684
14695
  })
14685
14696
  ),
14686
14697
  features: external_exports.array(FeatureManifestSchema),
14698
+ packages: external_exports.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
14687
14699
  vocabulary: ProductVocabularySchema.optional().describe(
14688
14700
  "The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
14689
14701
  ),
@@ -14693,6 +14705,44 @@ var OgygieManifestSchema = external_exports.object({
14693
14705
  specsFolder: external_exports.literal("specs/")
14694
14706
  })
14695
14707
  }).superRefine((manifest, context) => {
14708
+ const featureNames = new Set(manifest.features.map((feature) => feature.name));
14709
+ const packageNames = /* @__PURE__ */ new Set();
14710
+ (manifest.packages ?? []).forEach((packageEntry, index) => {
14711
+ if (featureNames.has(packageEntry.name)) {
14712
+ context.addIssue({
14713
+ code: "custom",
14714
+ path: ["packages", index, "name"],
14715
+ message: `"${packageEntry.name}" is already the name of a feature \u2014 one name designates one thing`
14716
+ });
14717
+ }
14718
+ if (packageNames.has(packageEntry.name)) {
14719
+ context.addIssue({
14720
+ code: "custom",
14721
+ path: ["packages", index, "name"],
14722
+ message: `package "${packageEntry.name}" is declared twice`
14723
+ });
14724
+ }
14725
+ packageNames.add(packageEntry.name);
14726
+ });
14727
+ const checkPackageDependencies = (dependsOn, path) => {
14728
+ dependsOn.forEach((dependency, dependencyIndex) => {
14729
+ if (!dependency.startsWith("package:")) return;
14730
+ const target = dependency.slice("package:".length);
14731
+ if (!packageNames.has(target)) {
14732
+ context.addIssue({
14733
+ code: "custom",
14734
+ path: [...path, dependencyIndex],
14735
+ message: `dependency "${dependency}" matches no declared package`
14736
+ });
14737
+ }
14738
+ });
14739
+ };
14740
+ manifest.features.forEach((feature, index) => {
14741
+ checkPackageDependencies(feature.dependsOn, ["features", index, "dependsOn"]);
14742
+ });
14743
+ (manifest.packages ?? []).forEach((packageEntry, index) => {
14744
+ checkPackageDependencies(packageEntry.dependsOn, ["packages", index, "dependsOn"]);
14745
+ });
14696
14746
  const canonicalOwners = /* @__PURE__ */ new Map();
14697
14747
  const aliasOwners = /* @__PURE__ */ new Map();
14698
14748
  (manifest.vocabulary?.entries ?? []).forEach((entry, index) => {
@@ -15447,6 +15497,12 @@ var governedChecksumsByVersion = {
15447
15497
  "AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
15448
15498
  "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15449
15499
  "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15500
+ },
15501
+ "0.6.0": {
15502
+ ".github/workflows/ogygie-gates.yml": "48c40efe9fe66dd0f9c3b94287026d80eae9afd6782b5833fdf8846d8662babf",
15503
+ "AGENTS.md": "c7d51a0cd97b67343b486bdb7c54914da68d96409eaa985959d2a525c65549fd",
15504
+ "CLAUDE.md": "785ee8d90e368806d8cf0b4d84a15f29d1dc961168bda86cf1a6c60a923c8c9b",
15505
+ "tsconfig.base.json": "0f8919e9ed8bbc88e0308fa2fa91471dabb18d1df721bd78c04076597eba89a2"
15450
15506
  }
15451
15507
  };
15452
15508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/cli",
3
- "version": "0.5.4",
3
+ "version": "0.6.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.5.4"
25
+ "@ogygie/core": "0.6.0"
26
26
  },
27
27
  "scripts": {
28
28
  "typecheck": "tsc -p tsconfig.json",
@@ -84,6 +84,20 @@ for (const app of manifest.apps) {
84
84
  }
85
85
  }
86
86
 
87
+ // A declared package whose folder is absent is a lying map, exactly like a
88
+ // phantom app (spec manifest-packages B1).
89
+ for (const productPackage of manifest.packages ?? []) {
90
+ if (!existsSync(join(ROOT, productPackage.path))) {
91
+ report({
92
+ code: "MANIFEST_PACKAGE_NOT_FOUND",
93
+ message: `Package "${productPackage.name}" declares folder ${productPackage.path} which does not exist.`,
94
+ file: MANIFEST_PATH,
95
+ cause: `packages[].path=${productPackage.path} absent from disk`,
96
+ suggestedFix: "Create the folder or fix the manifest entry.",
97
+ });
98
+ }
99
+ }
100
+
87
101
  const apiApp = manifest.apps.find((app) => app.type === "api");
88
102
  const featureNames = new Set(manifest.features.map((feature) => feature.name));
89
103
 
@@ -29,6 +29,7 @@ import {
29
29
  parseKnownDecisionIds,
30
30
  } from "@ogygie/core/decision-refs";
31
31
  import { OgygieManifestSchema } from "@ogygie/core/manifest";
32
+ import { findSpecOwner, SPEC_OWNER_FIX } from "@ogygie/core/manifest-owners";
32
33
  import { ogygieError, type OgygieError } from "@ogygie/core/errors";
33
34
 
34
35
  const ROOT = new URL("..", import.meta.url).pathname;
@@ -46,7 +47,6 @@ function report(input: Parameters<typeof ogygieError>[0]): void {
46
47
  const manifest = OgygieManifestSchema.parse(
47
48
  JSON.parse(readFileSync(join(ROOT, "ogygie.manifest.json"), "utf8")),
48
49
  );
49
- const apiApp = manifest.apps.find((app) => app.type === "api");
50
50
 
51
51
  const specFiles = readdirSync(join(ROOT, SPECS_FOLDER)).filter(
52
52
  (fileName) => fileName.endsWith(".spec.md") && !fileName.startsWith("_"),
@@ -124,18 +124,15 @@ for (const fileName of specFiles) {
124
124
  }
125
125
  }
126
126
 
127
- const manifestFeature = manifest.features.find(
128
- (feature) => feature.name === frontmatter.id,
129
- );
130
- const featureDir =
131
- apiApp !== undefined && manifestFeature !== undefined
132
- ? join(ROOT, apiApp.path, manifestFeature.folder)
133
- : null;
127
+ // The map confirms a spec through a feature OR a product package: the rule
128
+ // is unchanged, the natures able to confirm are two (spec manifest-packages).
129
+ const owner = findSpecOwner(manifest, frontmatter.id);
130
+ const ownerDir = owner?.folder === null || owner === undefined ? null : join(ROOT, owner.folder);
134
131
 
135
132
  if (frontmatter.status === "draft") {
136
133
  const implemented =
137
- (manifestFeature !== undefined && manifestFeature.status !== "skeleton") ||
138
- (featureDir !== null && existsSync(featureDir));
134
+ (owner !== undefined && owner.status !== "skeleton") ||
135
+ (ownerDir !== null && existsSync(ownerDir));
139
136
  if (implemented) {
140
137
  report({
141
138
  code: "SPEC_DRAFT_ALREADY_IMPLEMENTED",
@@ -160,14 +157,16 @@ for (const fileName of specFiles) {
160
157
  "Implement the deferred behaviors (their blocking features now exist?) and empty `deferred`, or keep the spec in validated.",
161
158
  });
162
159
  }
163
- if (manifestFeature === undefined || manifestFeature.status !== "delivered") {
160
+ if (owner === undefined || owner.status !== "delivered") {
164
161
  report({
165
162
  code: "SPEC_IMPLEMENTED_NOT_DELIVERED",
166
163
  message: `Spec ${fileName} is implemented but the manifest does not say status "delivered".`,
167
164
  file: specPath,
168
- cause: `manifest status=${manifestFeature?.status ?? "absent"}`,
169
- suggestedFix:
170
- "Set the feature's status to delivered in the manifest, or keep the spec in validated.",
165
+ cause:
166
+ owner === undefined
167
+ ? "no feature and no package of that name in the manifest"
168
+ : `${owner.kind} "${owner.name}" status=${owner.status}`,
169
+ suggestedFix: SPEC_OWNER_FIX,
171
170
  });
172
171
  }
173
172
  }