@ogygie/studio 0.8.0 → 0.10.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,99 +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 SharedTableKindSchema = z2.enum(["reference", "ownerless"]);
119
- var DbTableDeclarationSchema = z2.union([
120
- z2.string(),
121
- z2.object({
122
- name: z2.string(),
123
- 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),
124
268
  sharedKind: SharedTableKindSchema.optional()
125
269
  })
126
270
  ]);
127
- var FeatureManifestSchema = z2.object({
271
+ var FeatureManifestSchema = z3.object({
128
272
  name: FeatureNameSchema,
129
- intent: z2.string().min(20).max(300).describe("The feature's 'why' in 1-3 sentences"),
130
- app: z2.string().optional().describe("Name of the owning app (apps[].name); defaults to the api app"),
131
- folder: z2.string().describe("Path relative to the app root, e.g. src/features/orders"),
132
- spec: z2.string().nullable().describe("Path to the spec, e.g. specs/orders.spec.md, null for technical features"),
133
- routes: z2.array(HttpRouteSchema),
134
- 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(
135
279
  "Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
136
280
  ),
137
- dependsOn: z2.array(z2.union([FeatureNameSchema, PackageDependencySchema])).describe(
281
+ dependsOn: z3.array(z3.union([FeatureNameSchema, PackageDependencySchema])).describe(
138
282
  "Features imported through their index.ts, and product packages as package:<name> \u2014 nothing else"
139
283
  ),
140
- status: z2.enum(["skeleton", "in-progress", "delivered"])
284
+ status: z3.enum(["skeleton", "in-progress", "delivered"])
141
285
  });
142
- var OgygieVersionSchema = z2.string().regex(/^\d+\.\d+\.\d+$/, "expected a plain x.y.z version");
143
- var FrameworkManifestSchema = z2.object({
144
- packages: z2.array(
145
- z2.object({
146
- name: z2.string().describe("Published npm name, e.g. @ogygie/core"),
147
- path: z2.string().describe("Folder relative to the repo root"),
148
- 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"])
149
307
  })
150
308
  ),
151
- templates: z2.object({
152
- project: z2.string().describe("Folder copied by `ogygie init`"),
153
- 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)")
154
312
  }),
155
- 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)")
156
314
  });
157
- var VocabularyEntrySchema = z2.object({
158
- 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"),
159
- forbiddenAliases: z2.array(
160
- 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")
161
319
  ).min(1).describe("Roots the code may never carry for this concept, e.g. widget"),
162
- 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")
163
321
  });
164
- var ProductVocabularySchema = z2.object({
165
- entries: z2.array(VocabularyEntrySchema)
322
+ var ProductVocabularySchema = z3.object({
323
+ entries: z3.array(VocabularyEntrySchema)
166
324
  });
167
- var PackageManifestSchema = z2.object({
325
+ var PackageManifestSchema = z3.object({
168
326
  name: FeatureNameSchema,
169
- path: z2.string().describe("Folder relative to the repo root, e.g. packages/solver"),
170
- intent: z2.string().min(20).max(300).describe("The package's 'why' in 1-3 sentences"),
171
- spec: z2.string().nullable(),
172
- 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"),
173
- 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"])
174
332
  });
175
- var TenancyManifestSchema = z2.object({
176
- column: z2.string().min(1).describe("SQL name of the tenant column, e.g. workspace_id \u2014 the one AGENTS.product.md announces"),
177
- 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(
178
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"
179
337
  )
180
338
  });
181
- var OgygieManifestSchema = z2.object({
182
- version: z2.literal(1),
183
- product: z2.object({
184
- name: z2.string(),
185
- 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)
186
344
  }),
187
345
  ogygieVersion: OgygieVersionSchema.optional(),
188
346
  framework: FrameworkManifestSchema.optional(),
189
- apps: z2.array(
190
- z2.object({
191
- name: z2.string(),
192
- path: z2.string(),
193
- type: z2.enum(["api", "front", "gateway"]),
194
- 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(),
195
353
  /**
196
354
  * Code the product HOSTS without maintaining it — imported as-is from
197
355
  * elsewhere, the framework's own dev tooling being the typical case.
@@ -204,21 +362,21 @@ var OgygieManifestSchema = z2.object({
204
362
  * ANNOUNCES it on every run — one that stops being visible is one that
205
363
  * gets forgotten.
206
364
  */
207
- vendored: z2.literal(true).optional()
365
+ vendored: z3.literal(true).optional()
208
366
  })
209
367
  ),
210
- features: z2.array(FeatureManifestSchema),
368
+ features: z3.array(FeatureManifestSchema),
211
369
  tenancy: TenancyManifestSchema.optional().describe(
212
370
  "Product values the framework gates need \u2014 absent, the tenancy checks do not run"
213
371
  ),
214
- 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"),
215
373
  vocabulary: ProductVocabularySchema.optional().describe(
216
374
  "The product's closed vocabulary \u2014 declared here, enforced by `ogygie verify-vocabulary`. Absent = gate silent"
217
375
  ),
218
- conventions: z2.object({
219
- agentsMd: z2.literal("AGENTS.md"),
220
- failuresMd: z2.literal("FAILURES.md"),
221
- 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/")
222
380
  })
223
381
  }).superRefine((manifest, context) => {
224
382
  const featureNames = new Set(manifest.features.map((feature) => feature.name));
@@ -240,6 +398,22 @@ var OgygieManifestSchema = z2.object({
240
398
  }
241
399
  packageNames.add(packageEntry.name);
242
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
+ });
243
417
  const checkPackageDependencies = (dependsOn, path) => {
244
418
  dependsOn.forEach((dependency, dependencyIndex) => {
245
419
  if (!dependency.startsWith("package:")) return;
@@ -352,17 +526,17 @@ var OgygieManifestSchema = z2.object({
352
526
  });
353
527
 
354
528
  // ../ogygie-core/src/spec.schema.ts
355
- var DeferredBehaviorSchema = z3.string().regex(
529
+ var DeferredBehaviorSchema = z4.string().regex(
356
530
  /^.+ blockedBy:[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
357
531
  'Expected "<behavior> blockedBy:<feature-name>" (feature name in kebab-case)'
358
532
  );
359
- var SpecFrontmatterSchema = z3.object({
533
+ var SpecFrontmatterSchema = z4.object({
360
534
  id: FeatureNameSchema.describe("Identical to the feature name in the manifest"),
361
- status: z3.enum(["draft", "validated", "implemented"]),
362
- depends_on: z3.array(FeatureNameSchema),
363
- priority: z3.enum(["P0", "P1", "P2"]),
364
- refs: z3.array(z3.string()).default([]).describe("References to the domain model, e.g. 'modele-gally \xA74.2'"),
365
- 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)")
366
540
  });
367
541
  var SECTIONS_SPEC_OBLIGATOIRES = [
368
542
  "# @intent",
@@ -578,15 +752,15 @@ function toReadableSummary(filePath, spec) {
578
752
  };
579
753
  }
580
754
  async function listSpecFileNames(deps) {
581
- const entries = await deps.storage.list(SPECS_DIR);
582
- 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();
583
757
  }
584
758
  async function listSpecSummaries(deps) {
585
759
  const fileNames = await listSpecFileNames(deps);
586
760
  const summaries = [];
587
761
  for (const fileName of fileNames) {
588
762
  const filePath = `${SPECS_DIR}/${fileName}`;
589
- const content = await deps.storage.read(filePath);
763
+ const content = await deps.storage.readFile(filePath);
590
764
  const parsed = parseSpec(filePath, fileName, content);
591
765
  const id = fileName.replace(/\.spec\.md$/, "");
592
766
  summaries.push(
@@ -611,7 +785,7 @@ async function readParsedSpec(deps, specId) {
611
785
  );
612
786
  }
613
787
  const filePath = specPathOf(specId);
614
- const content = await deps.storage.read(filePath);
788
+ const content = await deps.storage.readFile(filePath);
615
789
  const parsed = parseSpec(filePath, fileName, content);
616
790
  if (!parsed.ok) {
617
791
  throw new OgygieFailure(parsed.error, 422);
@@ -714,7 +888,7 @@ async function createSpec(deps, specId) {
714
888
  }
715
889
  let template;
716
890
  try {
717
- template = await deps.storage.read(TEMPLATE_PATH);
891
+ template = await deps.storage.readFile(TEMPLATE_PATH);
718
892
  } catch {
719
893
  return throwOgygieError({
720
894
  code: "SPEC_TEMPLATE_NOT_FOUND",
@@ -740,7 +914,7 @@ async function createSpec(deps, specId) {
740
914
  return toReadableSummary(filePath, parsed.spec);
741
915
  }
742
916
  async function getManifest(deps) {
743
- const content = await deps.storage.read(MANIFEST_PATH);
917
+ const content = await deps.storage.readFile(MANIFEST_PATH);
744
918
  let raw;
745
919
  try {
746
920
  raw = JSON.parse(content);
@@ -773,52 +947,52 @@ async function getManifest(deps) {
773
947
  }
774
948
 
775
949
  // src/server/schemas.ts
776
- import { z as z4 } from "zod";
950
+ import { z as z5 } from "zod";
777
951
  var SpecStatusSchema = SpecFrontmatterSchema.shape.status;
778
- var ReadableSpecSummarySchema = z4.object({
952
+ var ReadableSpecSummarySchema = z5.object({
779
953
  id: FeatureNameSchema,
780
954
  status: SpecStatusSchema,
781
955
  priority: SpecFrontmatterSchema.shape.priority,
782
- depends_on: z4.array(FeatureNameSchema),
783
- filePath: z4.string(),
784
- presentSections: z4.array(z4.string())
956
+ depends_on: z5.array(FeatureNameSchema),
957
+ filePath: z5.string(),
958
+ presentSections: z5.array(z5.string())
785
959
  });
786
- var UnreadableSpecSummarySchema = z4.object({
787
- id: z4.string(),
788
- filePath: z4.string(),
960
+ var UnreadableSpecSummarySchema = z5.object({
961
+ id: z5.string(),
962
+ filePath: z5.string(),
789
963
  error: OgygieErrorSchema
790
964
  });
791
- var SpecSummarySchema = z4.union([
965
+ var SpecSummarySchema = z5.union([
792
966
  ReadableSpecSummarySchema,
793
967
  UnreadableSpecSummarySchema
794
968
  ]);
795
- var ListSpecsOutputSchema = z4.object({
796
- specs: z4.array(SpecSummarySchema)
969
+ var ListSpecsOutputSchema = z5.object({
970
+ specs: z5.array(SpecSummarySchema)
797
971
  });
798
- var SpecParamsSchema = z4.object({
972
+ var SpecParamsSchema = z5.object({
799
973
  specId: FeatureNameSchema
800
974
  });
801
- var GetSpecOutputSchema = z4.object({
975
+ var GetSpecOutputSchema = z5.object({
802
976
  frontmatter: SpecFrontmatterSchema,
803
977
  /** Markdown body after the frontmatter block, byte-exact. */
804
- body: z4.string(),
805
- presentSections: z4.array(z4.string())
978
+ body: z5.string(),
979
+ presentSections: z5.array(z5.string())
806
980
  });
807
- var UpdateSpecInputSchema = z4.object({
981
+ var UpdateSpecInputSchema = z5.object({
808
982
  frontmatter: SpecFrontmatterSchema,
809
983
  /** Omitted ⇒ the existing markdown body is preserved byte-exact. */
810
- body: z4.string().max(2e5).optional()
984
+ body: z5.string().max(2e5).optional()
811
985
  });
812
- var UpdateSpecOutputSchema = z4.object({
986
+ var UpdateSpecOutputSchema = z5.object({
813
987
  spec: ReadableSpecSummarySchema
814
988
  });
815
- var CreateSpecInputSchema = z4.object({
989
+ var CreateSpecInputSchema = z5.object({
816
990
  id: FeatureNameSchema.max(80)
817
991
  });
818
- var CreateSpecOutputSchema = z4.object({
992
+ var CreateSpecOutputSchema = z5.object({
819
993
  spec: ReadableSpecSummarySchema
820
994
  });
821
- var GetManifestOutputSchema = z4.object({
995
+ var GetManifestOutputSchema = z5.object({
822
996
  manifest: OgygieManifestSchema
823
997
  });
824
998