@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 +312 -133
- package/dist-ui/assets/{index-BFH0uwMD.js → index-mJUY1hdT.js} +11 -11
- package/dist-ui/index.html +1 -1
- package/package.json +2 -2
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,
|
|
43
|
-
import { dirname,
|
|
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
|
-
|
|
46
|
-
|
|
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: "
|
|
214
|
+
code: "PATH_OUTSIDE_REPO",
|
|
50
215
|
message: "The requested path escapes the repo root.",
|
|
51
|
-
file:
|
|
52
|
-
cause: `relative path "${relativePath}" resolves outside "${
|
|
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
|
-
|
|
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 =
|
|
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
|
|
246
|
+
import { z as z4 } from "zod";
|
|
103
247
|
|
|
104
248
|
// ../ogygie-core/src/manifest.schema.ts
|
|
105
|
-
import { z as
|
|
106
|
-
var FeatureNameSchema =
|
|
107
|
-
var PackageDependencySchema =
|
|
108
|
-
var HttpRouteSchema =
|
|
109
|
-
method:
|
|
110
|
-
path:
|
|
111
|
-
description:
|
|
112
|
-
schemaInput:
|
|
113
|
-
schemaOutput:
|
|
114
|
-
invalidates:
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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 =
|
|
271
|
+
var FeatureManifestSchema = z3.object({
|
|
123
272
|
name: FeatureNameSchema,
|
|
124
|
-
intent:
|
|
125
|
-
app:
|
|
126
|
-
folder:
|
|
127
|
-
spec:
|
|
128
|
-
routes:
|
|
129
|
-
dbTables:
|
|
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:
|
|
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:
|
|
284
|
+
status: z3.enum(["skeleton", "in-progress", "delivered"])
|
|
136
285
|
});
|
|
137
|
-
var OgygieVersionSchema =
|
|
138
|
-
var FrameworkManifestSchema =
|
|
139
|
-
packages:
|
|
140
|
-
|
|
141
|
-
name:
|
|
142
|
-
path:
|
|
143
|
-
intent:
|
|
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:
|
|
147
|
-
project:
|
|
148
|
-
feature:
|
|
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:
|
|
313
|
+
governedFiles: z3.string().describe("Module declaring the exhaustive list of files `upgrade` may overwrite (D3)")
|
|
151
314
|
});
|
|
152
|
-
var VocabularyEntrySchema =
|
|
153
|
-
canonical:
|
|
154
|
-
forbiddenAliases:
|
|
155
|
-
|
|
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:
|
|
320
|
+
reference: z3.string().optional().describe("Anchor into AGENTS.product.md where the concept is explained")
|
|
158
321
|
});
|
|
159
|
-
var ProductVocabularySchema =
|
|
160
|
-
entries:
|
|
322
|
+
var ProductVocabularySchema = z3.object({
|
|
323
|
+
entries: z3.array(VocabularyEntrySchema)
|
|
161
324
|
});
|
|
162
|
-
var PackageManifestSchema =
|
|
325
|
+
var PackageManifestSchema = z3.object({
|
|
163
326
|
name: FeatureNameSchema,
|
|
164
|
-
path:
|
|
165
|
-
intent:
|
|
166
|
-
spec:
|
|
167
|
-
dependsOn:
|
|
168
|
-
status:
|
|
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 =
|
|
171
|
-
column:
|
|
172
|
-
rawDbAllowed:
|
|
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 =
|
|
177
|
-
version:
|
|
178
|
-
product:
|
|
179
|
-
name:
|
|
180
|
-
description:
|
|
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:
|
|
185
|
-
|
|
186
|
-
name:
|
|
187
|
-
path:
|
|
188
|
-
type:
|
|
189
|
-
entryPoint:
|
|
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:
|
|
365
|
+
vendored: z3.literal(true).optional()
|
|
203
366
|
})
|
|
204
367
|
),
|
|
205
|
-
features:
|
|
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:
|
|
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:
|
|
214
|
-
agentsMd:
|
|
215
|
-
failuresMd:
|
|
216
|
-
specsFolder:
|
|
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 =
|
|
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 =
|
|
533
|
+
var SpecFrontmatterSchema = z4.object({
|
|
355
534
|
id: FeatureNameSchema.describe("Identical to the feature name in the manifest"),
|
|
356
|
-
status:
|
|
357
|
-
depends_on:
|
|
358
|
-
priority:
|
|
359
|
-
refs:
|
|
360
|
-
deferred:
|
|
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.
|
|
577
|
-
return entries.map((entry) => entry.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
950
|
+
import { z as z5 } from "zod";
|
|
772
951
|
var SpecStatusSchema = SpecFrontmatterSchema.shape.status;
|
|
773
|
-
var ReadableSpecSummarySchema =
|
|
952
|
+
var ReadableSpecSummarySchema = z5.object({
|
|
774
953
|
id: FeatureNameSchema,
|
|
775
954
|
status: SpecStatusSchema,
|
|
776
955
|
priority: SpecFrontmatterSchema.shape.priority,
|
|
777
|
-
depends_on:
|
|
778
|
-
filePath:
|
|
779
|
-
presentSections:
|
|
956
|
+
depends_on: z5.array(FeatureNameSchema),
|
|
957
|
+
filePath: z5.string(),
|
|
958
|
+
presentSections: z5.array(z5.string())
|
|
780
959
|
});
|
|
781
|
-
var UnreadableSpecSummarySchema =
|
|
782
|
-
id:
|
|
783
|
-
filePath:
|
|
960
|
+
var UnreadableSpecSummarySchema = z5.object({
|
|
961
|
+
id: z5.string(),
|
|
962
|
+
filePath: z5.string(),
|
|
784
963
|
error: OgygieErrorSchema
|
|
785
964
|
});
|
|
786
|
-
var SpecSummarySchema =
|
|
965
|
+
var SpecSummarySchema = z5.union([
|
|
787
966
|
ReadableSpecSummarySchema,
|
|
788
967
|
UnreadableSpecSummarySchema
|
|
789
968
|
]);
|
|
790
|
-
var ListSpecsOutputSchema =
|
|
791
|
-
specs:
|
|
969
|
+
var ListSpecsOutputSchema = z5.object({
|
|
970
|
+
specs: z5.array(SpecSummarySchema)
|
|
792
971
|
});
|
|
793
|
-
var SpecParamsSchema =
|
|
972
|
+
var SpecParamsSchema = z5.object({
|
|
794
973
|
specId: FeatureNameSchema
|
|
795
974
|
});
|
|
796
|
-
var GetSpecOutputSchema =
|
|
975
|
+
var GetSpecOutputSchema = z5.object({
|
|
797
976
|
frontmatter: SpecFrontmatterSchema,
|
|
798
977
|
/** Markdown body after the frontmatter block, byte-exact. */
|
|
799
|
-
body:
|
|
800
|
-
presentSections:
|
|
978
|
+
body: z5.string(),
|
|
979
|
+
presentSections: z5.array(z5.string())
|
|
801
980
|
});
|
|
802
|
-
var UpdateSpecInputSchema =
|
|
981
|
+
var UpdateSpecInputSchema = z5.object({
|
|
803
982
|
frontmatter: SpecFrontmatterSchema,
|
|
804
983
|
/** Omitted ⇒ the existing markdown body is preserved byte-exact. */
|
|
805
|
-
body:
|
|
984
|
+
body: z5.string().max(2e5).optional()
|
|
806
985
|
});
|
|
807
|
-
var UpdateSpecOutputSchema =
|
|
986
|
+
var UpdateSpecOutputSchema = z5.object({
|
|
808
987
|
spec: ReadableSpecSummarySchema
|
|
809
988
|
});
|
|
810
|
-
var CreateSpecInputSchema =
|
|
989
|
+
var CreateSpecInputSchema = z5.object({
|
|
811
990
|
id: FeatureNameSchema.max(80)
|
|
812
991
|
});
|
|
813
|
-
var CreateSpecOutputSchema =
|
|
992
|
+
var CreateSpecOutputSchema = z5.object({
|
|
814
993
|
spec: ReadableSpecSummarySchema
|
|
815
994
|
});
|
|
816
|
-
var GetManifestOutputSchema =
|
|
995
|
+
var GetManifestOutputSchema = z5.object({
|
|
817
996
|
manifest: OgygieManifestSchema
|
|
818
997
|
});
|
|
819
998
|
|