@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 +310 -136
- package/dist-ui/assets/{index-WftTXKtR.js → index-mJUY1hdT.js} +10 -10
- 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,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
|
|
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 SharedTableKindSchema =
|
|
119
|
-
var DbTableDeclarationSchema =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
name:
|
|
123
|
-
shared:
|
|
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 =
|
|
271
|
+
var FeatureManifestSchema = z3.object({
|
|
128
272
|
name: FeatureNameSchema,
|
|
129
|
-
intent:
|
|
130
|
-
app:
|
|
131
|
-
folder:
|
|
132
|
-
spec:
|
|
133
|
-
routes:
|
|
134
|
-
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(
|
|
135
279
|
"Drizzle tables owned by this feature (one table = one owner). String = tenant-scoped; { name, shared: true } = shared referential"
|
|
136
280
|
),
|
|
137
|
-
dependsOn:
|
|
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:
|
|
284
|
+
status: z3.enum(["skeleton", "in-progress", "delivered"])
|
|
141
285
|
});
|
|
142
|
-
var OgygieVersionSchema =
|
|
143
|
-
var FrameworkManifestSchema =
|
|
144
|
-
packages:
|
|
145
|
-
|
|
146
|
-
name:
|
|
147
|
-
path:
|
|
148
|
-
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"])
|
|
149
307
|
})
|
|
150
308
|
),
|
|
151
|
-
templates:
|
|
152
|
-
project:
|
|
153
|
-
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)")
|
|
154
312
|
}),
|
|
155
|
-
governedFiles:
|
|
313
|
+
governedFiles: z3.string().describe("Module declaring the exhaustive list of files `upgrade` may overwrite (D3)")
|
|
156
314
|
});
|
|
157
|
-
var VocabularyEntrySchema =
|
|
158
|
-
canonical:
|
|
159
|
-
forbiddenAliases:
|
|
160
|
-
|
|
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:
|
|
320
|
+
reference: z3.string().optional().describe("Anchor into AGENTS.product.md where the concept is explained")
|
|
163
321
|
});
|
|
164
|
-
var ProductVocabularySchema =
|
|
165
|
-
entries:
|
|
322
|
+
var ProductVocabularySchema = z3.object({
|
|
323
|
+
entries: z3.array(VocabularyEntrySchema)
|
|
166
324
|
});
|
|
167
|
-
var PackageManifestSchema =
|
|
325
|
+
var PackageManifestSchema = z3.object({
|
|
168
326
|
name: FeatureNameSchema,
|
|
169
|
-
path:
|
|
170
|
-
intent:
|
|
171
|
-
spec:
|
|
172
|
-
dependsOn:
|
|
173
|
-
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"])
|
|
174
332
|
});
|
|
175
|
-
var TenancyManifestSchema =
|
|
176
|
-
column:
|
|
177
|
-
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(
|
|
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 =
|
|
182
|
-
version:
|
|
183
|
-
product:
|
|
184
|
-
name:
|
|
185
|
-
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)
|
|
186
344
|
}),
|
|
187
345
|
ogygieVersion: OgygieVersionSchema.optional(),
|
|
188
346
|
framework: FrameworkManifestSchema.optional(),
|
|
189
|
-
apps:
|
|
190
|
-
|
|
191
|
-
name:
|
|
192
|
-
path:
|
|
193
|
-
type:
|
|
194
|
-
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(),
|
|
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:
|
|
365
|
+
vendored: z3.literal(true).optional()
|
|
208
366
|
})
|
|
209
367
|
),
|
|
210
|
-
features:
|
|
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:
|
|
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:
|
|
219
|
-
agentsMd:
|
|
220
|
-
failuresMd:
|
|
221
|
-
specsFolder:
|
|
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 =
|
|
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 =
|
|
533
|
+
var SpecFrontmatterSchema = z4.object({
|
|
360
534
|
id: FeatureNameSchema.describe("Identical to the feature name in the manifest"),
|
|
361
|
-
status:
|
|
362
|
-
depends_on:
|
|
363
|
-
priority:
|
|
364
|
-
refs:
|
|
365
|
-
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)")
|
|
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.
|
|
582
|
-
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();
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
950
|
+
import { z as z5 } from "zod";
|
|
777
951
|
var SpecStatusSchema = SpecFrontmatterSchema.shape.status;
|
|
778
|
-
var ReadableSpecSummarySchema =
|
|
952
|
+
var ReadableSpecSummarySchema = z5.object({
|
|
779
953
|
id: FeatureNameSchema,
|
|
780
954
|
status: SpecStatusSchema,
|
|
781
955
|
priority: SpecFrontmatterSchema.shape.priority,
|
|
782
|
-
depends_on:
|
|
783
|
-
filePath:
|
|
784
|
-
presentSections:
|
|
956
|
+
depends_on: z5.array(FeatureNameSchema),
|
|
957
|
+
filePath: z5.string(),
|
|
958
|
+
presentSections: z5.array(z5.string())
|
|
785
959
|
});
|
|
786
|
-
var UnreadableSpecSummarySchema =
|
|
787
|
-
id:
|
|
788
|
-
filePath:
|
|
960
|
+
var UnreadableSpecSummarySchema = z5.object({
|
|
961
|
+
id: z5.string(),
|
|
962
|
+
filePath: z5.string(),
|
|
789
963
|
error: OgygieErrorSchema
|
|
790
964
|
});
|
|
791
|
-
var SpecSummarySchema =
|
|
965
|
+
var SpecSummarySchema = z5.union([
|
|
792
966
|
ReadableSpecSummarySchema,
|
|
793
967
|
UnreadableSpecSummarySchema
|
|
794
968
|
]);
|
|
795
|
-
var ListSpecsOutputSchema =
|
|
796
|
-
specs:
|
|
969
|
+
var ListSpecsOutputSchema = z5.object({
|
|
970
|
+
specs: z5.array(SpecSummarySchema)
|
|
797
971
|
});
|
|
798
|
-
var SpecParamsSchema =
|
|
972
|
+
var SpecParamsSchema = z5.object({
|
|
799
973
|
specId: FeatureNameSchema
|
|
800
974
|
});
|
|
801
|
-
var GetSpecOutputSchema =
|
|
975
|
+
var GetSpecOutputSchema = z5.object({
|
|
802
976
|
frontmatter: SpecFrontmatterSchema,
|
|
803
977
|
/** Markdown body after the frontmatter block, byte-exact. */
|
|
804
|
-
body:
|
|
805
|
-
presentSections:
|
|
978
|
+
body: z5.string(),
|
|
979
|
+
presentSections: z5.array(z5.string())
|
|
806
980
|
});
|
|
807
|
-
var UpdateSpecInputSchema =
|
|
981
|
+
var UpdateSpecInputSchema = z5.object({
|
|
808
982
|
frontmatter: SpecFrontmatterSchema,
|
|
809
983
|
/** Omitted ⇒ the existing markdown body is preserved byte-exact. */
|
|
810
|
-
body:
|
|
984
|
+
body: z5.string().max(2e5).optional()
|
|
811
985
|
});
|
|
812
|
-
var UpdateSpecOutputSchema =
|
|
986
|
+
var UpdateSpecOutputSchema = z5.object({
|
|
813
987
|
spec: ReadableSpecSummarySchema
|
|
814
988
|
});
|
|
815
|
-
var CreateSpecInputSchema =
|
|
989
|
+
var CreateSpecInputSchema = z5.object({
|
|
816
990
|
id: FeatureNameSchema.max(80)
|
|
817
991
|
});
|
|
818
|
-
var CreateSpecOutputSchema =
|
|
992
|
+
var CreateSpecOutputSchema = z5.object({
|
|
819
993
|
spec: ReadableSpecSummarySchema
|
|
820
994
|
});
|
|
821
|
-
var GetManifestOutputSchema =
|
|
995
|
+
var GetManifestOutputSchema = z5.object({
|
|
822
996
|
manifest: OgygieManifestSchema
|
|
823
997
|
});
|
|
824
998
|
|