@ogygie/studio 0.5.0 → 0.5.2

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.
Files changed (35) hide show
  1. package/dist/server.js +918 -0
  2. package/package.json +8 -8
  3. package/src/index.ts +0 -41
  4. package/src/server/app.ts +0 -84
  5. package/src/server/routes.test.ts +0 -164
  6. package/src/server/routes.ts +0 -87
  7. package/src/server/schemas.ts +0 -92
  8. package/src/server/service.test.ts +0 -305
  9. package/src/server/service.ts +0 -426
  10. package/src/server/start.ts +0 -80
  11. package/src/server/storage/local-adapter.test.ts +0 -52
  12. package/src/server/storage/local-adapter.ts +0 -67
  13. package/src/server/storage/memory-adapter.ts +0 -49
  14. package/src/server/storage/spec-storage.ts +0 -22
  15. package/src/ui/app.css +0 -3
  16. package/src/ui/features/manifest-view/ManifestView.test.tsx +0 -67
  17. package/src/ui/features/manifest-view/ManifestView.tsx +0 -80
  18. package/src/ui/features/spec-board/SpecBoard.test.tsx +0 -92
  19. package/src/ui/features/spec-board/SpecBoard.tsx +0 -198
  20. package/src/ui/features/spec-editor/MarkdownPreview.tsx +0 -49
  21. package/src/ui/features/spec-editor/SpecEditor.test.tsx +0 -79
  22. package/src/ui/features/spec-editor/SpecEditor.tsx +0 -237
  23. package/src/ui/features/spec-editor/frontmatter-fields.test.ts +0 -54
  24. package/src/ui/features/spec-editor/frontmatter-fields.ts +0 -85
  25. package/src/ui/features/spec-editor/markdown-blocks.test.ts +0 -37
  26. package/src/ui/features/spec-editor/markdown-blocks.ts +0 -82
  27. package/src/ui/index.html +0 -12
  28. package/src/ui/lib/fetchContract.ts +0 -40
  29. package/src/ui/lib/queries.ts +0 -69
  30. package/src/ui/main.tsx +0 -27
  31. package/src/ui/patterns/EmptyState.tsx +0 -24
  32. package/src/ui/patterns/ErrorBanner.tsx +0 -51
  33. package/src/ui/patterns/Modal.tsx +0 -38
  34. package/src/ui/router.tsx +0 -76
  35. package/src/ui/test-utils.tsx +0 -56
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ogygie/studio",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Ogygie framework dev studio: projects a repo's specs and manifest into a local UI. Ships its built UI; launched by `ogygie studio`.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "files": [
8
- "src",
8
+ "dist",
9
9
  "dist-ui",
10
10
  "README.md",
11
11
  "LICENSE"
@@ -14,15 +14,14 @@
14
14
  "access": "public"
15
15
  },
16
16
  "exports": {
17
- ".": "./src/index.ts",
18
- "./server": "./src/server/start.ts"
17
+ "./server": "./dist/server.js"
19
18
  },
20
19
  "dependencies": {
21
20
  "@fastify/static": "^10.1.3",
22
21
  "fastify": "^5.0.0",
23
22
  "fastify-type-provider-zod": "^6.0.0",
24
23
  "zod": "^4.0.0",
25
- "@ogygie/core": "0.5.0"
24
+ "@ogygie/core": "0.5.2"
26
25
  },
27
26
  "devDependencies": {
28
27
  "@tailwindcss/vite": "^4.3.3",
@@ -34,18 +33,19 @@
34
33
  "@types/react": "^19.2.18",
35
34
  "@types/react-dom": "^19.2.4",
36
35
  "@vitejs/plugin-react": "^5.2.0",
36
+ "esbuild": "^0.28.0",
37
37
  "jsdom": "^30.0.1",
38
38
  "react": "^19.2.8",
39
39
  "react-dom": "^19.2.8",
40
40
  "tailwindcss": "^4.3.3",
41
41
  "tsx": "^4.20.0",
42
42
  "typescript": "^5.8.0",
43
- "vitest": "^3.2.0",
44
- "vite": "^7.3.6"
43
+ "vite": "^7.3.6",
44
+ "vitest": "^3.2.0"
45
45
  },
46
46
  "scripts": {
47
47
  "dev": "vite",
48
- "build": "vite build",
48
+ "build": "vite build && tsx scripts/build-server.ts",
49
49
  "typecheck": "tsc -p tsconfig.json"
50
50
  }
51
51
  }
package/src/index.ts DELETED
@@ -1,41 +0,0 @@
1
- /**
2
- * @intent SOLE public export point of the ogygie-studio feature: the launcher,
3
- * the Fastify app factory (embedded/test use), the SpecStorage contract with
4
- * its two adapters, and the API contracts shared with the UI.
5
- */
6
- export { startStudio, type StartStudioOptions } from "./server/start.js";
7
- export { createStudioApp, type StudioAppOptions } from "./server/app.js";
8
- export type { FileEntry, SpecStorage } from "./server/storage/spec-storage.js";
9
- export { createLocalSpecStorage } from "./server/storage/local-adapter.js";
10
- export {
11
- createMemorySpecStorage,
12
- type MemorySpecStorage,
13
- } from "./server/storage/memory-adapter.js";
14
- export {
15
- CreateSpecInputSchema,
16
- CreateSpecOutputSchema,
17
- GetManifestOutputSchema,
18
- GetSpecOutputSchema,
19
- ListSpecsOutputSchema,
20
- ReadableSpecSummarySchema,
21
- SpecParamsSchema,
22
- SpecStatusSchema,
23
- SpecSummarySchema,
24
- UnreadableSpecSummarySchema,
25
- UpdateSpecInputSchema,
26
- UpdateSpecOutputSchema,
27
- } from "./server/schemas.js";
28
- export type {
29
- CreateSpecInput,
30
- CreateSpecOutput,
31
- GetManifestOutput,
32
- GetSpecOutput,
33
- ListSpecsOutput,
34
- ReadableSpecSummary,
35
- SpecParams,
36
- SpecStatus,
37
- SpecSummary,
38
- UnreadableSpecSummary,
39
- UpdateSpecInput,
40
- UpdateSpecOutput,
41
- } from "./server/schemas.js";
package/src/server/app.ts DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * @intent Fastify bootstrap of the studio server: Zod compilers, the OgygieError
3
- * handler (only contract fields ever reach the client — never a stack trace),
4
- * the studio API routes, and the built UI served statically with an SPA
5
- * fallback. No helmet/rate-limit/session on purpose: the studio binds
6
- * 127.0.0.1 only (see start.ts), is single-user by spec (non-objectifs), and
7
- * relays no product data — the product bootstrap rules target apps/api.
8
- */
9
- import Fastify, { type FastifyInstance } from "fastify";
10
- import {
11
- hasZodFastifySchemaValidationErrors,
12
- serializerCompiler,
13
- validatorCompiler,
14
- } from "fastify-type-provider-zod";
15
- import { OgygieFailure, ogygieError } from "@ogygie/core/errors";
16
- import type { StudioDeps } from "./service.js";
17
- import { registerStudioRoutes } from "./routes.js";
18
-
19
- export interface StudioAppOptions extends StudioDeps {
20
- /** Absolute path of the built UI (dist-ui); omitted in API-only tests. */
21
- uiDistDir?: string | undefined;
22
- }
23
-
24
- export async function createStudioApp(options: StudioAppOptions): Promise<FastifyInstance> {
25
- const app = Fastify({ logger: false });
26
- app.setValidatorCompiler(validatorCompiler);
27
- app.setSerializerCompiler(serializerCompiler);
28
-
29
- app.setErrorHandler((error, _request, reply) => {
30
- if (error instanceof OgygieFailure) {
31
- return reply.status(error.httpStatus).send(error.error);
32
- }
33
- if (hasZodFastifySchemaValidationErrors(error)) {
34
- return reply.status(400).send(
35
- ogygieError({
36
- code: "CONTRACT_VALIDATION_FAILED",
37
- message: "The request does not satisfy the route's input contract.",
38
- file: null,
39
- cause: error.validation
40
- .map((issue) => `${issue.instancePath || "(root)"}: ${issue.message ?? "invalid"}`)
41
- .join(" ; "),
42
- suggestedFix: "Fix the listed fields to match the route's Zod input schema.",
43
- }),
44
- );
45
- }
46
- // Unknown error: log server-side, leak nothing.
47
- app.log.error(error);
48
- return reply.status(500).send(
49
- ogygieError({
50
- code: "INTERNAL_ERROR",
51
- message: "Unexpected studio error — details are in the server logs only.",
52
- file: null,
53
- cause: "unhandled exception",
54
- suggestedFix: "Relaunch `pnpm ogygie studio`; record a FAILURES.md entry if it recurs.",
55
- }),
56
- );
57
- });
58
-
59
- await registerStudioRoutes(app, options);
60
-
61
- if (options.uiDistDir !== undefined) {
62
- const fastifyStatic = (await import("@fastify/static")).default;
63
- await app.register(fastifyStatic, { root: options.uiDistDir });
64
- // SPA fallback: every non-API GET renders the UI shell, whose router owns
65
- // the path (/specs/<id>, /manifest…).
66
- app.setNotFoundHandler((request, reply) => {
67
- if (request.method === "GET" && !request.url.startsWith("/api/")) {
68
- return reply.sendFile("index.html");
69
- }
70
- return reply.status(404).send(
71
- ogygieError({
72
- code: "ROUTE_NOT_FOUND",
73
- message: "No studio route matches this request.",
74
- file: null,
75
- cause: `${request.method} ${request.url}`,
76
- suggestedFix: "Use the routes documented in specs/ogygie-studio.spec.md.",
77
- }),
78
- );
79
- });
80
- }
81
-
82
- await app.ready();
83
- return app;
84
- }
@@ -1,164 +0,0 @@
1
- /**
2
- * @intent Proves the HTTP surface described in specs/ogygie-studio.spec.md:
3
- * every internal route with its contracts and error statuses, via
4
- * fastify.inject over the in-memory SpecStorage (no port, no fs).
5
- */
6
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
7
- import type { FastifyInstance } from "fastify";
8
- import { createStudioApp } from "./app.js";
9
- import { createMemorySpecStorage, type MemorySpecStorage } from "./storage/memory-adapter.js";
10
-
11
- const TEMPLATE = [
12
- "---",
13
- "id: nom-de-la-feature",
14
- "status: draft",
15
- "depends_on: []",
16
- "priority: P1",
17
- "refs: []",
18
- "---",
19
- "",
20
- "# @intent",
21
- "x",
22
- "",
23
- "# Comportements",
24
- "x",
25
- "",
26
- "# Contrats de données",
27
- "x",
28
- "",
29
- "# États UI",
30
- "x",
31
- "",
32
- "# Non-objectifs",
33
- "x",
34
- "",
35
- "# Budget",
36
- "x",
37
- "",
38
- ].join("\n");
39
-
40
- const DRAFT_SPEC = TEMPLATE.replace("id: nom-de-la-feature", "id: alpha");
41
-
42
- const MANIFEST = JSON.stringify({
43
- version: 1,
44
- product: { name: "starter", description: "Squelette de produit Ogygie pour les tests." },
45
- apps: [{ name: "api", path: "apps/api", type: "api", entryPoint: "apps/api/src/server.ts" }],
46
- features: [],
47
- conventions: { agentsMd: "AGENTS.md", failuresMd: "FAILURES.md", specsFolder: "specs/" },
48
- });
49
-
50
- describe("ogygie-studio — routes", () => {
51
- let app: FastifyInstance;
52
- let storage: MemorySpecStorage;
53
-
54
- beforeAll(async () => {
55
- storage = createMemorySpecStorage({
56
- "specs/_TEMPLATE.spec.md": TEMPLATE,
57
- "specs/alpha.spec.md": DRAFT_SPEC,
58
- "ogygie.manifest.json": MANIFEST,
59
- });
60
- app = await createStudioApp({ storage });
61
- });
62
-
63
- afterAll(async () => {
64
- await app.close();
65
- });
66
-
67
- it("ÉTANT DONNÉ un repo valide QUAND GET /api/specs ALORS 200 et le résumé de chaque spec", async () => {
68
- const response = await app.inject({ method: "GET", url: "/api/specs" });
69
- expect(response.statusCode).toBe(200);
70
- const payload = response.json();
71
- expect(payload.specs).toEqual([
72
- expect.objectContaining({ id: "alpha", status: "draft" }),
73
- ]);
74
- });
75
-
76
- it("ÉTANT DONNÉ une spec lisible QUAND GET /api/specs/:id ALORS 200 avec frontmatter, body et sections présentes", async () => {
77
- const response = await app.inject({ method: "GET", url: "/api/specs/alpha" });
78
- expect(response.statusCode).toBe(200);
79
- const payload = response.json();
80
- expect(payload.frontmatter).toMatchObject({ id: "alpha", status: "draft" });
81
- expect(payload.presentSections).toHaveLength(6);
82
- expect(payload.body).toContain("# Budget");
83
- });
84
-
85
- it("ÉTANT DONNÉ un id inconnu QUAND GET /api/specs/:id ALORS 404 SPEC_NOT_FOUND", async () => {
86
- const response = await app.inject({ method: "GET", url: "/api/specs/fantome" });
87
- expect(response.statusCode).toBe(404);
88
- expect(response.json().code).toBe("SPEC_NOT_FOUND");
89
- });
90
-
91
- it("ÉTANT DONNÉ un body hors contrat QUAND PUT /api/specs/:id ALORS 400 CONTRACT_VALIDATION_FAILED", async () => {
92
- const response = await app.inject({
93
- method: "PUT",
94
- url: "/api/specs/alpha",
95
- payload: { frontmatter: { id: "alpha" } },
96
- });
97
- expect(response.statusCode).toBe(400);
98
- expect(response.json().code).toBe("CONTRACT_VALIDATION_FAILED");
99
- });
100
-
101
- it("ÉTANT DONNÉ une spec draft complète QUAND PUT la valide ALORS 200, statut validated, message d'écriture \"spec: validate alpha\"", async () => {
102
- const current = (await app.inject({ method: "GET", url: "/api/specs/alpha" })).json();
103
- const response = await app.inject({
104
- method: "PUT",
105
- url: "/api/specs/alpha",
106
- payload: { frontmatter: { ...current.frontmatter, status: "validated" } },
107
- });
108
- expect(response.statusCode).toBe(200);
109
- expect(response.json().spec.status).toBe("validated");
110
- expect(storage.messages).toContain("spec: validate alpha");
111
- });
112
-
113
- it("ÉTANT DONNÉ une spec validated QUAND PUT force status implemented ALORS 409 STUDIO_TRANSITION_FORBIDDEN", async () => {
114
- const current = (await app.inject({ method: "GET", url: "/api/specs/alpha" })).json();
115
- const response = await app.inject({
116
- method: "PUT",
117
- url: "/api/specs/alpha",
118
- payload: { frontmatter: { ...current.frontmatter, status: "implemented" } },
119
- });
120
- expect(response.statusCode).toBe(409);
121
- expect(response.json().code).toBe("STUDIO_TRANSITION_FORBIDDEN");
122
- });
123
-
124
- it("ÉTANT DONNÉ le board QUAND POST /api/specs avec un id kebab-case ALORS 201 et la spec naît en draft ; un doublon répond 409 SPEC_ALREADY_EXISTS", async () => {
125
- const created = await app.inject({
126
- method: "POST",
127
- url: "/api/specs",
128
- payload: { id: "ma-feature" },
129
- });
130
- expect(created.statusCode).toBe(201);
131
- expect(created.json().spec).toMatchObject({ id: "ma-feature", status: "draft" });
132
-
133
- const duplicate = await app.inject({
134
- method: "POST",
135
- url: "/api/specs",
136
- payload: { id: "ma-feature" },
137
- });
138
- expect(duplicate.statusCode).toBe(409);
139
- expect(duplicate.json().code).toBe("SPEC_ALREADY_EXISTS");
140
- });
141
-
142
- it("ÉTANT DONNÉ un id non kebab-case QUAND POST /api/specs ALORS 400 CONTRACT_VALIDATION_FAILED", async () => {
143
- const response = await app.inject({
144
- method: "POST",
145
- url: "/api/specs",
146
- payload: { id: "Pas Kebab" },
147
- });
148
- expect(response.statusCode).toBe(400);
149
- expect(response.json().code).toBe("CONTRACT_VALIDATION_FAILED");
150
- });
151
-
152
- it("ÉTANT DONNÉ le manifeste QUAND GET /api/manifest ALORS 200 en lecture seule (aucune route d'écriture du manifeste n'existe)", async () => {
153
- const response = await app.inject({ method: "GET", url: "/api/manifest" });
154
- expect(response.statusCode).toBe(200);
155
- expect(response.json().manifest.product.name).toBe("starter");
156
-
157
- const write = await app.inject({
158
- method: "PUT",
159
- url: "/api/manifest",
160
- payload: {},
161
- });
162
- expect(write.statusCode).toBe(404);
163
- });
164
- });
@@ -1,87 +0,0 @@
1
- /**
2
- * @intent HTTP endpoints of the studio's internal localhost API.
3
- * No business logic here: schema validation + delegation to the service.
4
- * These routes are NEVER declared in ogygie.manifest.json nor mounted by a
5
- * product gateway — the studio is a dev tool, not a product surface.
6
- */
7
- import type { FastifyInstance } from "fastify";
8
- import type { ZodTypeProvider } from "fastify-type-provider-zod";
9
- import type { StudioDeps } from "./service.js";
10
- import {
11
- createSpec,
12
- getManifest,
13
- getSpec,
14
- listSpecSummaries,
15
- saveSpec,
16
- } from "./service.js";
17
- import {
18
- CreateSpecInputSchema,
19
- CreateSpecOutputSchema,
20
- GetManifestOutputSchema,
21
- GetSpecOutputSchema,
22
- ListSpecsOutputSchema,
23
- SpecParamsSchema,
24
- UpdateSpecInputSchema,
25
- UpdateSpecOutputSchema,
26
- } from "./schemas.js";
27
-
28
- export async function registerStudioRoutes(
29
- app: FastifyInstance,
30
- deps: StudioDeps,
31
- ): Promise<void> {
32
- const route = app.withTypeProvider<ZodTypeProvider>();
33
-
34
- route.route({
35
- method: "GET",
36
- url: "/api/specs",
37
- schema: { response: { 200: ListSpecsOutputSchema } },
38
- handler: async (_request, reply) => {
39
- return reply.code(200).send({ specs: await listSpecSummaries(deps) });
40
- },
41
- });
42
-
43
- route.route({
44
- method: "GET",
45
- url: "/api/specs/:specId",
46
- schema: { params: SpecParamsSchema, response: { 200: GetSpecOutputSchema } },
47
- handler: async (request, reply) => {
48
- return reply.code(200).send(await getSpec(deps, request.params.specId));
49
- },
50
- });
51
-
52
- route.route({
53
- method: "PUT",
54
- url: "/api/specs/:specId",
55
- schema: {
56
- params: SpecParamsSchema,
57
- body: UpdateSpecInputSchema,
58
- response: { 200: UpdateSpecOutputSchema },
59
- },
60
- handler: async (request, reply) => {
61
- const spec = await saveSpec(deps, request.params.specId, request.body);
62
- return reply.code(200).send({ spec });
63
- },
64
- });
65
-
66
- route.route({
67
- method: "POST",
68
- url: "/api/specs",
69
- schema: {
70
- body: CreateSpecInputSchema,
71
- response: { 201: CreateSpecOutputSchema },
72
- },
73
- handler: async (request, reply) => {
74
- const spec = await createSpec(deps, request.body.id);
75
- return reply.code(201).send({ spec });
76
- },
77
- });
78
-
79
- route.route({
80
- method: "GET",
81
- url: "/api/manifest",
82
- schema: { response: { 200: GetManifestOutputSchema } },
83
- handler: async (_request, reply) => {
84
- return reply.code(200).send({ manifest: await getManifest(deps) });
85
- },
86
- });
87
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * @intent Data contracts of the ogygie-studio feature: the studio's internal
3
- * localhost API (never mounted by a product gateway). Single source of truth
4
- * shared by the Fastify server and the studio UI: runtime validation + TS
5
- * types + documentation. The frontmatter contract itself is NOT redefined
6
- * here — the studio projects @ogygie/core/spec, it never forks it.
7
- */
8
- import { z } from "zod";
9
- import { FeatureNameSchema, OgygieManifestSchema } from "@ogygie/core/manifest";
10
- import { OgygieErrorSchema } from "@ogygie/core/errors";
11
- import { SpecFrontmatterSchema } from "@ogygie/core/spec";
12
-
13
- export const SpecStatusSchema = SpecFrontmatterSchema.shape.status;
14
- export type SpecStatus = z.infer<typeof SpecStatusSchema>;
15
-
16
- /**
17
- * Summary of one spec file for the board. A spec whose frontmatter cannot be
18
- * read has NO trustworthy status/priority: it becomes an error card carrying
19
- * the full OgygieError instead — never silently repaired nor hidden.
20
- */
21
- export const ReadableSpecSummarySchema = z.object({
22
- id: FeatureNameSchema,
23
- status: SpecStatusSchema,
24
- priority: SpecFrontmatterSchema.shape.priority,
25
- depends_on: z.array(FeatureNameSchema),
26
- filePath: z.string(),
27
- presentSections: z.array(z.string()),
28
- });
29
- export type ReadableSpecSummary = z.infer<typeof ReadableSpecSummarySchema>;
30
-
31
- export const UnreadableSpecSummarySchema = z.object({
32
- id: z.string(),
33
- filePath: z.string(),
34
- error: OgygieErrorSchema,
35
- });
36
- export type UnreadableSpecSummary = z.infer<typeof UnreadableSpecSummarySchema>;
37
-
38
- export const SpecSummarySchema = z.union([
39
- ReadableSpecSummarySchema,
40
- UnreadableSpecSummarySchema,
41
- ]);
42
- export type SpecSummary = z.infer<typeof SpecSummarySchema>;
43
-
44
- // ---------------------------------------------------------------- GET /api/specs
45
- export const ListSpecsOutputSchema = z.object({
46
- specs: z.array(SpecSummarySchema),
47
- });
48
- export type ListSpecsOutput = z.infer<typeof ListSpecsOutputSchema>;
49
-
50
- // ---------------------------------------------------------- GET /api/specs/:specId
51
- export const SpecParamsSchema = z.object({
52
- specId: FeatureNameSchema,
53
- });
54
- export type SpecParams = z.infer<typeof SpecParamsSchema>;
55
-
56
- export const GetSpecOutputSchema = z.object({
57
- frontmatter: SpecFrontmatterSchema,
58
- /** Markdown body after the frontmatter block, byte-exact. */
59
- body: z.string(),
60
- presentSections: z.array(z.string()),
61
- });
62
- export type GetSpecOutput = z.infer<typeof GetSpecOutputSchema>;
63
-
64
- // ---------------------------------------------------------- PUT /api/specs/:specId
65
- export const UpdateSpecInputSchema = z.object({
66
- frontmatter: SpecFrontmatterSchema,
67
- /** Omitted ⇒ the existing markdown body is preserved byte-exact. */
68
- body: z.string().max(200_000).optional(),
69
- });
70
- export type UpdateSpecInput = z.infer<typeof UpdateSpecInputSchema>;
71
-
72
- export const UpdateSpecOutputSchema = z.object({
73
- spec: ReadableSpecSummarySchema,
74
- });
75
- export type UpdateSpecOutput = z.infer<typeof UpdateSpecOutputSchema>;
76
-
77
- // ---------------------------------------------------------------- POST /api/specs
78
- export const CreateSpecInputSchema = z.object({
79
- id: FeatureNameSchema.max(80),
80
- });
81
- export type CreateSpecInput = z.infer<typeof CreateSpecInputSchema>;
82
-
83
- export const CreateSpecOutputSchema = z.object({
84
- spec: ReadableSpecSummarySchema,
85
- });
86
- export type CreateSpecOutput = z.infer<typeof CreateSpecOutputSchema>;
87
-
88
- // -------------------------------------------------------------- GET /api/manifest
89
- export const GetManifestOutputSchema = z.object({
90
- manifest: OgygieManifestSchema,
91
- });
92
- export type GetManifestOutput = z.infer<typeof GetManifestOutputSchema>;