@ogygie/studio 0.7.0 → 0.9.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/server.js CHANGED
@@ -39,17 +39,182 @@ function throwOgygieError(input, httpStatus = 500) {
39
39
  }
40
40
 
41
41
  // src/server/storage/local-adapter.ts
42
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
43
- import { dirname, join, normalize } from "node:path";
42
+ import { mkdir, writeFile } from "node:fs/promises";
43
+ import { dirname, isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
44
+
45
+ // ../ogygie-core/src/repo-adapter.ts
46
+ import { readFile as readFileFromDisk, readdir, realpath, stat } from "node:fs/promises";
47
+ import { isAbsolute, join, resolve, sep } from "node:path";
48
+ import { z as z2 } from "zod";
49
+ var RepoFileEntrySchema = z2.object({
50
+ /** Path relative to the LISTED directory, "/" separated, never absolute. */
51
+ path: z2.string(),
52
+ sizeBytes: z2.number().int().nonnegative()
53
+ });
54
+ var NEVER_LISTED = /* @__PURE__ */ new Set(["node_modules"]);
55
+ function isSkippedDirectory(name) {
56
+ return name.startsWith(".") || NEVER_LISTED.has(name);
57
+ }
58
+ function refusePathOutsideRepo(relativePath, cause) {
59
+ return throwOgygieError(
60
+ {
61
+ code: "PATH_OUTSIDE_REPO",
62
+ message: "The requested path resolves outside the repo root.",
63
+ file: relativePath,
64
+ cause,
65
+ suggestedFix: "Address a path INSIDE the repo, written relative to its root and without `..`."
66
+ },
67
+ 400
68
+ );
69
+ }
44
70
  function resolveInsideRoot(rootDir, relativePath) {
45
- const absolute = normalize(join(rootDir, relativePath));
46
- if (!absolute.startsWith(normalize(rootDir))) {
71
+ if (isAbsolute(relativePath)) {
72
+ refusePathOutsideRepo(relativePath, `"${relativePath}" is an absolute path`);
73
+ }
74
+ const root = resolve(rootDir);
75
+ const absolute = resolve(root, relativePath);
76
+ if (absolute !== root && !absolute.startsWith(root + sep)) {
77
+ refusePathOutsideRepo(relativePath, `"${relativePath}" resolves to "${absolute}", outside "${root}"`);
78
+ }
79
+ return absolute;
80
+ }
81
+ async function assertRealPathInsideRoot(rootDir, absolute, relativePath) {
82
+ let realRoot;
83
+ let realTarget;
84
+ try {
85
+ realRoot = await realpath(rootDir);
86
+ realTarget = await realpath(absolute);
87
+ } catch {
88
+ return;
89
+ }
90
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep)) {
91
+ refusePathOutsideRepo(
92
+ relativePath,
93
+ `"${relativePath}" is a symbolic link leading to "${realTarget}", outside "${realRoot}"`
94
+ );
95
+ }
96
+ }
97
+ async function listFilesRecursively(absoluteDir, prefix) {
98
+ let entries;
99
+ try {
100
+ entries = await readdir(absoluteDir, { withFileTypes: true });
101
+ } catch {
102
+ return [];
103
+ }
104
+ const files = [];
105
+ for (const entry of entries) {
106
+ const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
107
+ if (entry.isDirectory()) {
108
+ if (isSkippedDirectory(entry.name)) continue;
109
+ files.push(...await listFilesRecursively(join(absoluteDir, entry.name), relativePath));
110
+ } else if (entry.isFile()) {
111
+ const stats = await stat(join(absoluteDir, entry.name));
112
+ files.push({ path: relativePath, sizeBytes: stats.size });
113
+ }
114
+ }
115
+ return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
116
+ }
117
+ async function resolveGitDirs(rootDir) {
118
+ const dotGit = join(rootDir, ".git");
119
+ let gitDir;
120
+ try {
121
+ const stats = await stat(dotGit);
122
+ if (stats.isDirectory()) {
123
+ gitDir = dotGit;
124
+ } else {
125
+ const pointer = (await readFileFromDisk(dotGit, "utf8")).trim();
126
+ const target = pointer.startsWith("gitdir:") ? pointer.slice("gitdir:".length).trim() : "";
127
+ if (target === "") return null;
128
+ gitDir = isAbsolute(target) ? target : resolve(rootDir, target);
129
+ }
130
+ } catch {
131
+ return null;
132
+ }
133
+ try {
134
+ const commonPointer = (await readFileFromDisk(join(gitDir, "commondir"), "utf8")).trim();
135
+ return { gitDir, commonDir: isAbsolute(commonPointer) ? commonPointer : resolve(gitDir, commonPointer) };
136
+ } catch {
137
+ return { gitDir, commonDir: gitDir };
138
+ }
139
+ }
140
+ async function readRefSha(dirs, refName) {
141
+ for (const base of [dirs.gitDir, dirs.commonDir]) {
142
+ try {
143
+ const sha = (await readFileFromDisk(join(base, refName), "utf8")).trim();
144
+ if (sha !== "") return sha;
145
+ } catch {
146
+ }
147
+ }
148
+ try {
149
+ const packed = await readFileFromDisk(join(dirs.commonDir, "packed-refs"), "utf8");
150
+ for (const line of packed.split("\n")) {
151
+ if (line.startsWith("#") || line.startsWith("^")) continue;
152
+ const [sha, name] = line.trim().split(" ");
153
+ if (name === refName && sha !== void 0 && sha !== "") return sha;
154
+ }
155
+ } catch {
156
+ return null;
157
+ }
158
+ return null;
159
+ }
160
+ function createFilesystemRepoAdapter(rootDir) {
161
+ return {
162
+ async readFile(relativePath) {
163
+ const absolute = resolveInsideRoot(rootDir, relativePath);
164
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
165
+ try {
166
+ return await readFileFromDisk(absolute, "utf8");
167
+ } catch (thrown) {
168
+ return throwOgygieError({
169
+ code: "REPO_FILE_NOT_FOUND",
170
+ message: "The requested file cannot be read from the repo.",
171
+ file: relativePath,
172
+ cause: `reading "${absolute}" failed: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
173
+ suggestedFix: "Check the path against what the map declares \u2014 the manifest is the only chart of a repo."
174
+ });
175
+ }
176
+ },
177
+ async listDirectory(relativePath) {
178
+ const absolute = resolveInsideRoot(rootDir, relativePath);
179
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
180
+ return listFilesRecursively(absolute, "");
181
+ },
182
+ async exists(relativePath) {
183
+ const absolute = resolveInsideRoot(rootDir, relativePath);
184
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
185
+ try {
186
+ await stat(absolute);
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ },
192
+ async currentRef() {
193
+ const dirs = await resolveGitDirs(rootDir);
194
+ if (dirs === null) return null;
195
+ let head;
196
+ try {
197
+ head = (await readFileFromDisk(join(dirs.gitDir, "HEAD"), "utf8")).trim();
198
+ } catch {
199
+ return null;
200
+ }
201
+ if (!head.startsWith("ref:")) return head === "" ? null : head;
202
+ return readRefSha(dirs, head.slice("ref:".length).trim());
203
+ }
204
+ };
205
+ }
206
+
207
+ // src/server/storage/local-adapter.ts
208
+ function resolveInsideRoot2(rootDir, relativePath) {
209
+ const root = resolve2(rootDir);
210
+ const absolute = resolve2(root, relativePath);
211
+ if (isAbsolute2(relativePath) || absolute !== root && !absolute.startsWith(root + sep2)) {
47
212
  throwOgygieError(
48
213
  {
49
- code: "STORAGE_PATH_OUTSIDE_ROOT",
214
+ code: "PATH_OUTSIDE_REPO",
50
215
  message: "The requested path escapes the repo root.",
51
- file: null,
52
- cause: `relative path "${relativePath}" resolves outside "${rootDir}"`,
216
+ file: relativePath,
217
+ cause: `relative path "${relativePath}" resolves to "${absolute}", outside "${root}"`,
53
218
  suggestedFix: "Only address files inside the repo (specs/, ogygie.manifest.json)."
54
219
  },
55
220
  400
@@ -58,32 +223,11 @@ function resolveInsideRoot(rootDir, relativePath) {
58
223
  return absolute;
59
224
  }
60
225
  function createLocalSpecStorage(rootDir) {
226
+ const reader = createFilesystemRepoAdapter(rootDir);
61
227
  return {
62
- async list(dir) {
63
- const absolute = resolveInsideRoot(rootDir, dir);
64
- try {
65
- const entries = await readdir(absolute, { withFileTypes: true });
66
- return entries.filter((entry) => entry.isFile()).map((entry) => ({ name: entry.name }));
67
- } catch {
68
- return [];
69
- }
70
- },
71
- async read(path) {
72
- const absolute = resolveInsideRoot(rootDir, path);
73
- try {
74
- return await readFile(absolute, "utf8");
75
- } catch {
76
- return throwOgygieError({
77
- code: "STORAGE_FILE_NOT_FOUND",
78
- message: "The requested file cannot be read from the repo.",
79
- file: path,
80
- cause: `readFile failed for "${absolute}"`,
81
- suggestedFix: "Check that the studio was launched from an Ogygie repo root (specs/ + ogygie.manifest.json)."
82
- });
83
- }
84
- },
228
+ ...reader,
85
229
  async write(path, content, _message) {
86
- const absolute = resolveInsideRoot(rootDir, path);
230
+ const absolute = resolveInsideRoot2(rootDir, path);
87
231
  await mkdir(dirname(absolute), { recursive: true });
88
232
  await writeFile(absolute, content, "utf8");
89
233
  }
@@ -99,94 +243,113 @@ import {
99
243
  } from "fastify-type-provider-zod";
100
244
 
101
245
  // ../ogygie-core/src/spec.schema.ts
102
- import { z as z3 } from "zod";
246
+ import { z as z4 } from "zod";
103
247
 
104
248
  // ../ogygie-core/src/manifest.schema.ts
105
- import { z as z2 } from "zod";
106
- var FeatureNameSchema = z2.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
107
- var PackageDependencySchema = z2.string().regex(/^package:[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "expected package:<kebab-case-name>");
108
- var HttpRouteSchema = z2.object({
109
- method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
110
- path: z2.string().startsWith("/api/"),
111
- description: z2.string().min(10),
112
- schemaInput: z2.string().describe("Exported name of the input Zod schema, e.g. CreateOrderInputSchema"),
113
- schemaOutput: z2.string().describe("Exported name of the output Zod schema"),
114
- invalidates: z2.array(z2.string()).optional().describe(
249
+ import { z as z3 } from "zod";
250
+ var FeatureNameSchema = z3.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "kebab-case required");
251
+ var PackageDependencySchema = z3.string().regex(/^package:[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "expected package:<kebab-case-name>");
252
+ var HttpRouteSchema = z3.object({
253
+ method: z3.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
254
+ path: z3.string().startsWith("/api/"),
255
+ description: z3.string().min(10),
256
+ schemaInput: z3.string().describe("Exported name of the input Zod schema, e.g. CreateOrderInputSchema"),
257
+ schemaOutput: z3.string().describe("Exported name of the output Zod schema"),
258
+ invalidates: z3.array(z3.string()).optional().describe(
115
259
  "Cache roots this mutation stales BEYOND its own path root (cross-feature dependency, read by the manifest-derived client \u2014 never guessed in a screen). Each value must be an existing feature name or route root"
116
260
  )
117
261
  });
118
- var DbTableDeclarationSchema = z2.union([
119
- z2.string(),
120
- z2.object({ name: z2.string(), shared: z2.literal(true) })
262
+ var SharedTableKindSchema = z3.enum(["reference", "ownerless"]);
263
+ var DbTableDeclarationSchema = z3.union([
264
+ z3.string(),
265
+ z3.object({
266
+ name: z3.string(),
267
+ shared: z3.literal(true),
268
+ sharedKind: SharedTableKindSchema.optional()
269
+ })
121
270
  ]);
122
- var FeatureManifestSchema = z2.object({
271
+ var FeatureManifestSchema = z3.object({
123
272
  name: FeatureNameSchema,
124
- intent: z2.string().min(20).max(300).describe("The feature's 'why' in 1-3 sentences"),
125
- app: z2.string().optional().describe("Name of the owning app (apps[].name); defaults to the api app"),
126
- folder: z2.string().describe("Path relative to the app root, e.g. src/features/orders"),
127
- spec: z2.string().nullable().describe("Path to the spec, e.g. specs/orders.spec.md, null for technical features"),
128
- routes: z2.array(HttpRouteSchema),
129
- dbTables: z2.array(DbTableDeclarationSchema).describe(
273
+ intent: z3.string().min(20).max(300).describe("The feature's 'why' in 1-3 sentences"),
274
+ app: z3.string().optional().describe("Name of the owning app (apps[].name); defaults to the api app"),
275
+ folder: z3.string().describe("Path relative to the app root, e.g. src/features/orders"),
276
+ spec: z3.string().nullable().describe("Path to the spec, e.g. specs/orders.spec.md, null for technical features"),
277
+ routes: z3.array(HttpRouteSchema),
278
+ dbTables: z3.array(DbTableDeclarationSchema).describe(
130
279
  "Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
131
280
  ),
132
- dependsOn: z2.array(z2.union([FeatureNameSchema, PackageDependencySchema])).describe(
281
+ dependsOn: z3.array(z3.union([FeatureNameSchema, PackageDependencySchema])).describe(
133
282
  "Features imported through their index.ts, and product packages as package:<name> \u2014 nothing else"
134
283
  ),
135
- status: z2.enum(["skeleton", "in-progress", "delivered"])
284
+ status: z3.enum(["skeleton", "in-progress", "delivered"])
136
285
  });
137
- var OgygieVersionSchema = z2.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
138
- var FrameworkManifestSchema = z2.object({
139
- packages: z2.array(
140
- z2.object({
141
- name: z2.string().describe("Published npm name, e.g. @ogygie/core"),
142
- path: z2.string().describe("Folder relative to the repo root"),
143
- intent: z2.string().min(20)
286
+ var OgygieVersionSchema = z3.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
287
+ var FrameworkManifestSchema = z3.object({
288
+ packages: z3.array(
289
+ z3.object({
290
+ name: z3.string().describe("Published npm name, e.g. @ogygie/core"),
291
+ path: z3.string().describe("Folder relative to the repo root"),
292
+ intent: z3.string().min(20),
293
+ /**
294
+ * The spec describing this package, when one does. Named EXPLICITLY
295
+ * because a published npm name (`@ogygie/mcp`) is not a spec id
296
+ * (`ogygie-mcp`) — the other two natures of entry match by name, this
297
+ * one cannot. Several packages may point at the same spec: a spec of
298
+ * the framework often spans the core and the CLI.
299
+ */
300
+ spec: z3.string().nullable(),
301
+ /**
302
+ * How far along, in the SAME words as the other natures. Required, and
303
+ * deliberately without default: a lifecycle gate that guesses a status
304
+ * confirms specs nobody delivered.
305
+ */
306
+ status: z3.enum(["skeleton", "in-progress", "delivered"])
144
307
  })
145
308
  ),
146
- templates: z2.object({
147
- project: z2.string().describe("Folder copied by `ogygie init`"),
148
- feature: z2.string().describe("Reserved for `ogygie generate feature` (not wired yet)")
309
+ templates: z3.object({
310
+ project: z3.string().describe("Folder copied by `ogygie init`"),
311
+ feature: z3.string().describe("Reserved for `ogygie generate feature` (not wired yet)")
149
312
  }),
150
- governedFiles: z2.string().describe("Module declaring the exhaustive list of files `upgrade` may overwrite (D3)")
313
+ governedFiles: z3.string().describe("Module declaring the exhaustive list of files `upgrade` may overwrite (D3)")
151
314
  });
152
- var VocabularyEntrySchema = z2.object({
153
- canonical: z2.string().regex(/^[a-z][a-zA-Z0-9]*$/, "camelCase English term required").describe("The one machine term for this concept, e.g. gadget"),
154
- forbiddenAliases: z2.array(
155
- z2.string().min(2).regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "lowercase root (kebab-case allowed) required")
315
+ var VocabularyEntrySchema = z3.object({
316
+ canonical: z3.string().regex(/^[a-z][a-zA-Z0-9]*$/, "camelCase English term required").describe("The one machine term for this concept, e.g. gadget"),
317
+ forbiddenAliases: z3.array(
318
+ z3.string().min(2).regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "lowercase root (kebab-case allowed) required")
156
319
  ).min(1).describe("Roots the code may never carry for this concept, e.g. widget"),
157
- reference: z2.string().optional().describe("Anchor into AGENTS.product.md where the concept is explained")
320
+ reference: z3.string().optional().describe("Anchor into AGENTS.product.md where the concept is explained")
158
321
  });
159
- var ProductVocabularySchema = z2.object({
160
- entries: z2.array(VocabularyEntrySchema)
322
+ var ProductVocabularySchema = z3.object({
323
+ entries: z3.array(VocabularyEntrySchema)
161
324
  });
162
- var PackageManifestSchema = z2.object({
325
+ var PackageManifestSchema = z3.object({
163
326
  name: FeatureNameSchema,
164
- path: z2.string().describe("Folder relative to the repo root, e.g. packages/solver"),
165
- intent: z2.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
166
- spec: z2.string().nullable(),
167
- dependsOn: z2.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"),
168
- status: z2.enum(["skeleton", "in-progress", "delivered"])
327
+ path: z3.string().describe("Folder relative to the repo root, e.g. packages/solver"),
328
+ intent: z3.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
329
+ spec: z3.string().nullable(),
330
+ dependsOn: z3.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"),
331
+ status: z3.enum(["skeleton", "in-progress", "delivered"])
169
332
  });
170
- var TenancyManifestSchema = z2.object({
171
- column: z2.string().min(1).describe("SQL name of the tenant column, e.g. workspace_id \u2014 the one AGENTS.product.md announces"),
172
- rawDbAllowed: z2.array(FeatureNameSchema).default([]).describe(
333
+ var TenancyManifestSchema = z3.object({
334
+ column: z3.string().min(1).describe("SQL name of the tenant column, e.g. workspace_id \u2014 the one AGENTS.product.md announces"),
335
+ rawDbAllowed: z3.array(FeatureNameSchema).default([]).describe(
173
336
  "Features allowed to reach the raw database: the tenancy socle, which BUILDS the contexts and therefore predates them. Every other feature goes through tenantDb"
174
337
  )
175
338
  });
176
- var OgygieManifestSchema = z2.object({
177
- version: z2.literal(1),
178
- product: z2.object({
179
- name: z2.string(),
180
- description: z2.string().min(20)
339
+ var OgygieManifestSchema = z3.object({
340
+ version: z3.literal(1),
341
+ product: z3.object({
342
+ name: z3.string(),
343
+ description: z3.string().min(20)
181
344
  }),
182
345
  ogygieVersion: OgygieVersionSchema.optional(),
183
346
  framework: FrameworkManifestSchema.optional(),
184
- apps: z2.array(
185
- z2.object({
186
- name: z2.string(),
187
- path: z2.string(),
188
- type: z2.enum(["api", "front", "gateway"]),
189
- entryPoint: z2.string(),
347
+ apps: z3.array(
348
+ z3.object({
349
+ name: z3.string(),
350
+ path: z3.string(),
351
+ type: z3.enum(["api", "front", "gateway"]),
352
+ entryPoint: z3.string(),
190
353
  /**
191
354
  * Code the product HOSTS without maintaining it — imported as-is from
192
355
  * elsewhere, the framework's own dev tooling being the typical case.
@@ -199,21 +362,21 @@ var OgygieManifestSchema = z2.object({
199
362
  * ANNOUNCES it on every run — one that stops being visible is one that
200
363
  * gets forgotten.
201
364
  */
202
- vendored: z2.literal(true).optional()
365
+ vendored: z3.literal(true).optional()
203
366
  })
204
367
  ),
205
- features: z2.array(FeatureManifestSchema),
368
+ features: z3.array(FeatureManifestSchema),
206
369
  tenancy: TenancyManifestSchema.optional().describe(
207
370
  "Product values the framework gates need \u2014 absent, the tenancy checks do not run"
208
371
  ),
209
- packages: z2.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
372
+ packages: z3.array(PackageManifestSchema).optional().describe("Product code living outside every app \u2014 libraries, engines. Absent = the project has none"),
210
373
  vocabulary: ProductVocabularySchema.optional().describe(
211
374
  "The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
212
375
  ),
213
- conventions: z2.object({
214
- agentsMd: z2.literal("AGENTS.md"),
215
- failuresMd: z2.literal("FAILURES.md"),
216
- specsFolder: z2.literal("specs/")
376
+ conventions: z3.object({
377
+ agentsMd: z3.literal("AGENTS.md"),
378
+ failuresMd: z3.literal("FAILURES.md"),
379
+ specsFolder: z3.literal("specs/")
217
380
  })
218
381
  }).superRefine((manifest, context) => {
219
382
  const featureNames = new Set(manifest.features.map((feature) => feature.name));
@@ -235,6 +398,22 @@ var OgygieManifestSchema = z2.object({
235
398
  }
236
399
  packageNames.add(packageEntry.name);
237
400
  });
401
+ const specIdOf = (specPath) => (specPath.split("/").pop() ?? specPath).replace(/\.spec\.md$/, "");
402
+ const ownedSpecIds = /* @__PURE__ */ new Set([
403
+ ...manifest.features.map((feature) => feature.name),
404
+ ...packageNames
405
+ ]);
406
+ (manifest.framework?.packages ?? []).forEach((packageEntry, index) => {
407
+ if (packageEntry.spec === null) return;
408
+ const specId = specIdOf(packageEntry.spec);
409
+ if (ownedSpecIds.has(specId)) {
410
+ context.addIssue({
411
+ code: "custom",
412
+ path: ["framework", "packages", index, "spec"],
413
+ message: `spec "${specId}" is already owned by a feature or a product package \u2014 one spec, one owner`
414
+ });
415
+ }
416
+ });
238
417
  const checkPackageDependencies = (dependsOn, path) => {
239
418
  dependsOn.forEach((dependency, dependencyIndex) => {
240
419
  if (!dependency.startsWith("package:")) return;
@@ -347,17 +526,17 @@ var OgygieManifestSchema = z2.object({
347
526
  });
348
527
 
349
528
  // ../ogygie-core/src/spec.schema.ts
350
- var DeferredBehaviorSchema = z3.string().regex(
529
+ var DeferredBehaviorSchema = z4.string().regex(
351
530
  /^.+ blockedBy:[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
352
531
  'Expected "<behavior> blockedBy:<feature-name>" (feature name in kebab-case)'
353
532
  );
354
- var SpecFrontmatterSchema = z3.object({
533
+ var SpecFrontmatterSchema = z4.object({
355
534
  id: FeatureNameSchema.describe("Identical to the feature name in the manifest"),
356
- status: z3.enum(["draft", "validated", "implemented"]),
357
- depends_on: z3.array(FeatureNameSchema),
358
- priority: z3.enum(["P0", "P1", "P2"]),
359
- refs: z3.array(z3.string()).default([]).describe("References to the domain model, e.g. 'modele-gally \xA74.2'"),
360
- deferred: z3.array(DeferredBehaviorSchema).default([]).describe("Behaviors deferred to a later feature (DECISIONS.md D13)")
535
+ status: z4.enum(["draft", "validated", "implemented"]),
536
+ depends_on: z4.array(FeatureNameSchema),
537
+ priority: z4.enum(["P0", "P1", "P2"]),
538
+ refs: z4.array(z4.string()).default([]).describe("References to the domain model, e.g. 'modele-gally \xA74.2'"),
539
+ deferred: z4.array(DeferredBehaviorSchema).default([]).describe("Behaviors deferred to a later feature (DECISIONS.md D13)")
361
540
  });
362
541
  var SECTIONS_SPEC_OBLIGATOIRES = [
363
542
  "# @intent",
@@ -573,15 +752,15 @@ function toReadableSummary(filePath, spec) {
573
752
  };
574
753
  }
575
754
  async function listSpecFileNames(deps) {
576
- const entries = await deps.storage.list(SPECS_DIR);
577
- return entries.map((entry) => entry.name).filter((name) => name.endsWith(".spec.md") && !name.startsWith("_")).sort();
755
+ const entries = await deps.storage.listDirectory(SPECS_DIR);
756
+ return entries.map((entry) => entry.path).filter((path) => !path.includes("/")).filter((name) => name.endsWith(".spec.md") && !name.startsWith("_")).sort();
578
757
  }
579
758
  async function listSpecSummaries(deps) {
580
759
  const fileNames = await listSpecFileNames(deps);
581
760
  const summaries = [];
582
761
  for (const fileName of fileNames) {
583
762
  const filePath = `${SPECS_DIR}/${fileName}`;
584
- const content = await deps.storage.read(filePath);
763
+ const content = await deps.storage.readFile(filePath);
585
764
  const parsed = parseSpec(filePath, fileName, content);
586
765
  const id = fileName.replace(/\.spec\.md$/, "");
587
766
  summaries.push(
@@ -606,7 +785,7 @@ async function readParsedSpec(deps, specId) {
606
785
  );
607
786
  }
608
787
  const filePath = specPathOf(specId);
609
- const content = await deps.storage.read(filePath);
788
+ const content = await deps.storage.readFile(filePath);
610
789
  const parsed = parseSpec(filePath, fileName, content);
611
790
  if (!parsed.ok) {
612
791
  throw new OgygieFailure(parsed.error, 422);
@@ -709,7 +888,7 @@ async function createSpec(deps, specId) {
709
888
  }
710
889
  let template;
711
890
  try {
712
- template = await deps.storage.read(TEMPLATE_PATH);
891
+ template = await deps.storage.readFile(TEMPLATE_PATH);
713
892
  } catch {
714
893
  return throwOgygieError({
715
894
  code: "SPEC_TEMPLATE_NOT_FOUND",
@@ -735,7 +914,7 @@ async function createSpec(deps, specId) {
735
914
  return toReadableSummary(filePath, parsed.spec);
736
915
  }
737
916
  async function getManifest(deps) {
738
- const content = await deps.storage.read(MANIFEST_PATH);
917
+ const content = await deps.storage.readFile(MANIFEST_PATH);
739
918
  let raw;
740
919
  try {
741
920
  raw = JSON.parse(content);
@@ -768,52 +947,52 @@ async function getManifest(deps) {
768
947
  }
769
948
 
770
949
  // src/server/schemas.ts
771
- import { z as z4 } from "zod";
950
+ import { z as z5 } from "zod";
772
951
  var SpecStatusSchema = SpecFrontmatterSchema.shape.status;
773
- var ReadableSpecSummarySchema = z4.object({
952
+ var ReadableSpecSummarySchema = z5.object({
774
953
  id: FeatureNameSchema,
775
954
  status: SpecStatusSchema,
776
955
  priority: SpecFrontmatterSchema.shape.priority,
777
- depends_on: z4.array(FeatureNameSchema),
778
- filePath: z4.string(),
779
- presentSections: z4.array(z4.string())
956
+ depends_on: z5.array(FeatureNameSchema),
957
+ filePath: z5.string(),
958
+ presentSections: z5.array(z5.string())
780
959
  });
781
- var UnreadableSpecSummarySchema = z4.object({
782
- id: z4.string(),
783
- filePath: z4.string(),
960
+ var UnreadableSpecSummarySchema = z5.object({
961
+ id: z5.string(),
962
+ filePath: z5.string(),
784
963
  error: OgygieErrorSchema
785
964
  });
786
- var SpecSummarySchema = z4.union([
965
+ var SpecSummarySchema = z5.union([
787
966
  ReadableSpecSummarySchema,
788
967
  UnreadableSpecSummarySchema
789
968
  ]);
790
- var ListSpecsOutputSchema = z4.object({
791
- specs: z4.array(SpecSummarySchema)
969
+ var ListSpecsOutputSchema = z5.object({
970
+ specs: z5.array(SpecSummarySchema)
792
971
  });
793
- var SpecParamsSchema = z4.object({
972
+ var SpecParamsSchema = z5.object({
794
973
  specId: FeatureNameSchema
795
974
  });
796
- var GetSpecOutputSchema = z4.object({
975
+ var GetSpecOutputSchema = z5.object({
797
976
  frontmatter: SpecFrontmatterSchema,
798
977
  /** Markdown body after the frontmatter block, byte-exact. */
799
- body: z4.string(),
800
- presentSections: z4.array(z4.string())
978
+ body: z5.string(),
979
+ presentSections: z5.array(z5.string())
801
980
  });
802
- var UpdateSpecInputSchema = z4.object({
981
+ var UpdateSpecInputSchema = z5.object({
803
982
  frontmatter: SpecFrontmatterSchema,
804
983
  /** Omitted ⇒ the existing markdown body is preserved byte-exact. */
805
- body: z4.string().max(2e5).optional()
984
+ body: z5.string().max(2e5).optional()
806
985
  });
807
- var UpdateSpecOutputSchema = z4.object({
986
+ var UpdateSpecOutputSchema = z5.object({
808
987
  spec: ReadableSpecSummarySchema
809
988
  });
810
- var CreateSpecInputSchema = z4.object({
989
+ var CreateSpecInputSchema = z5.object({
811
990
  id: FeatureNameSchema.max(80)
812
991
  });
813
- var CreateSpecOutputSchema = z4.object({
992
+ var CreateSpecOutputSchema = z5.object({
814
993
  spec: ReadableSpecSummarySchema
815
994
  });
816
- var GetManifestOutputSchema = z4.object({
995
+ var GetManifestOutputSchema = z5.object({
817
996
  manifest: OgygieManifestSchema
818
997
  });
819
998