@eventmodelers/cli 1.0.54 → 1.0.56
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/README.md +6 -0
- package/cli.js +24 -0
- package/package.json +1 -1
- package/shared/build-kit/lib/ralph.js +103 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +3 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +5 -1
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +90 -2
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +81 -3
- package/stacks/node/templates/build-kit/CLAUDE.md +6 -1
- package/stacks/node/templates/build-kit/lib/checks/60-openapi-annotation.cjs +72 -0
- package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +42 -0
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +43 -0
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +42 -0
- package/stacks/react/templates/build-kit/CLAUDE.md +62 -0
- package/stacks/react/templates/build-kit/README.md +79 -0
- package/stacks/react/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +135 -0
- package/stacks/react/templates/build-kit/lib/prompt.md +139 -0
- package/stacks/react/templates/build-kit/lib/ralph.js +508 -0
- package/stacks/react/templates/build-kit/package.json +9 -0
- package/stacks/react/templates/build-kit/ralph-claude.js +107 -0
- package/stacks/react/templates/build-kit/ralph-ollama.js +40 -0
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +5 -1
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +90 -2
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +80 -2
- package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +73 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +6 -1
- package/stacks/supabase/templates/build-kit/lib/checks/60-openapi-annotation.cjs +72 -0
- package/stacks/supabase/templates/root/src/swagger.ts +3 -1
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +305 -0
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +238 -0
- package/stacks/supabase-react/templates/.claude/skills/init-style-guide/SKILL.md +60 -0
- package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/SKILL.md +28 -0
- package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/references/README.md +5 -0
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +149 -0
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +139 -0
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +145 -0
- package/stacks/supabase-react/templates/root/.env.example +12 -0
- package/stacks/supabase-react/templates/root/.oxlintrc.json +9 -0
- package/stacks/supabase-react/templates/root/README.md +49 -0
- package/stacks/supabase-react/templates/root/index.html +13 -0
- package/stacks/supabase-react/templates/root/package.json +26 -0
- package/stacks/supabase-react/templates/root/public/favicon.svg +1 -0
- package/stacks/supabase-react/templates/root/public/icons.svg +24 -0
- package/stacks/supabase-react/templates/root/src/App.css +184 -0
- package/stacks/supabase-react/templates/root/src/App.tsx +122 -0
- package/stacks/supabase-react/templates/root/src/assets/hero.png +0 -0
- package/stacks/supabase-react/templates/root/src/assets/react.svg +1 -0
- package/stacks/supabase-react/templates/root/src/assets/vite.svg +1 -0
- package/stacks/supabase-react/templates/root/src/index.css +111 -0
- package/stacks/supabase-react/templates/root/src/lib/api.ts +127 -0
- package/stacks/supabase-react/templates/root/src/lib/supabase.ts +10 -0
- package/stacks/supabase-react/templates/root/src/main.tsx +10 -0
- package/stacks/supabase-react/templates/root/src/slices/.gitkeep +0 -0
- package/stacks/supabase-react/templates/root/src/vite-env.d.ts +13 -0
- package/stacks/supabase-react/templates/root/tsconfig.app.json +26 -0
- package/stacks/supabase-react/templates/root/tsconfig.json +7 -0
- package/stacks/supabase-react/templates/root/tsconfig.node.json +23 -0
- package/stacks/supabase-react/templates/root/vite.config.ts +7 -0
|
@@ -251,7 +251,7 @@ Skip (do not fabricate) a segment when the command beat has no immediately-follo
|
|
|
251
251
|
|
|
252
252
|
File: `src/slices/{context}/{SliceName}/routes.ts`
|
|
253
253
|
|
|
254
|
-
> **
|
|
254
|
+
> **Pattern reference**: the template below is the full pattern — auth, error mapping, and the mandatory `@openapi` annotation (Step 5a). If the project already has slices under `src/slices/`, open one of their `routes.ts` files first and match it.
|
|
255
255
|
|
|
256
256
|
```typescript
|
|
257
257
|
import {Request, Response, Router} from 'express';
|
|
@@ -261,6 +261,59 @@ import {{SliceName}Command, handle{SliceName}} from './{SliceName}Command';
|
|
|
261
261
|
|
|
262
262
|
export const api = (): WebApiSetup => (router: Router): void => {
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* @openapi
|
|
266
|
+
* /api/{slicename}/{id}:
|
|
267
|
+
* post:
|
|
268
|
+
* tags: [{Context}]
|
|
269
|
+
* summary: {slice title from slice.json}
|
|
270
|
+
* description: {slice.json description — plus any comments that explain the endpoint}
|
|
271
|
+
* security:
|
|
272
|
+
* - bearerAuth: []
|
|
273
|
+
* parameters:
|
|
274
|
+
* - in: path
|
|
275
|
+
* name: id
|
|
276
|
+
* required: true
|
|
277
|
+
* schema:
|
|
278
|
+
* type: string
|
|
279
|
+
* description: Stream id this command is applied to
|
|
280
|
+
* - in: header
|
|
281
|
+
* name: correlation_id
|
|
282
|
+
* required: false
|
|
283
|
+
* schema:
|
|
284
|
+
* type: string
|
|
285
|
+
* requestBody:
|
|
286
|
+
* required: true
|
|
287
|
+
* content:
|
|
288
|
+
* application/json:
|
|
289
|
+
* schema:
|
|
290
|
+
* type: object
|
|
291
|
+
* required: [{command fields without optional: true}]
|
|
292
|
+
* properties:
|
|
293
|
+
* {fieldName}:
|
|
294
|
+
* type: string
|
|
295
|
+
* example: {the field's own example from slice.json, if it has one}
|
|
296
|
+
* responses:
|
|
297
|
+
* '201':
|
|
298
|
+
* description: Accepted — {EmittedEventName} appended
|
|
299
|
+
* content:
|
|
300
|
+
* application/json:
|
|
301
|
+
* schema:
|
|
302
|
+
* type: object
|
|
303
|
+
* properties:
|
|
304
|
+
* ok:
|
|
305
|
+
* type: boolean
|
|
306
|
+
* next_expected_stream_version:
|
|
307
|
+
* type: string
|
|
308
|
+
* last_event_global_position:
|
|
309
|
+
* type: string
|
|
310
|
+
* '401':
|
|
311
|
+
* description: Not authenticated
|
|
312
|
+
* '409':
|
|
313
|
+
* description: {message errorMapping returns — one line per error code}
|
|
314
|
+
* '500':
|
|
315
|
+
* description: Server error
|
|
316
|
+
*/
|
|
264
317
|
router.post('/api/{slicename}/:id', async (req: Request, res: Response) => {
|
|
265
318
|
const auth = await requireUser(req, res);
|
|
266
319
|
if (auth.error) return;
|
|
@@ -313,6 +366,39 @@ const errorMapping = (code: string): string | null => {
|
|
|
313
366
|
|
|
314
367
|
---
|
|
315
368
|
|
|
369
|
+
### Step 5a — OpenAPI annotation (required)
|
|
370
|
+
|
|
371
|
+
`src/swagger.ts` builds the published OpenAPI document by scanning `./src/slices/**/routes.ts` for `@openapi` JSDoc blocks. A handler without one is **invisible** in Swagger UI (`/api-docs`) and in `/swagger.json` — the endpoint works, but nobody can find it. `src/swagger.ts` is shared infra and outside a slice's commit scope, so the block in this slice's own `routes.ts` is the only place the endpoint can be documented. The `openapi-annotation` commit check rejects a `routes.ts` whose handlers have no matching block.
|
|
372
|
+
|
|
373
|
+
Everything in the block comes from slice.json — same rule as the code: no invented fields, no guessed types.
|
|
374
|
+
|
|
375
|
+
| slice.json field `type` | OpenAPI schema |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `String` | `type: string` |
|
|
378
|
+
| `UUID` | `type: string`, `format: uuid` |
|
|
379
|
+
| `Int` | `type: integer`, `format: int32` |
|
|
380
|
+
| `Long` | `type: integer`, `format: int64` |
|
|
381
|
+
| `Double` | `type: number`, `format: double` |
|
|
382
|
+
| `Decimal` | `type: number` |
|
|
383
|
+
| `Boolean` | `type: boolean` |
|
|
384
|
+
| `Date` | `type: string`, `format: date` |
|
|
385
|
+
| `DateTime` | `type: string`, `format: date-time` |
|
|
386
|
+
| `Custom` | `type: object` |
|
|
387
|
+
|
|
388
|
+
Mapping rules:
|
|
389
|
+
|
|
390
|
+
- **path key** — the express path with `:param` rewritten as `{param}` (`/api/foo/:id` → `/api/foo/{id}`). If the two disagree, Swagger publishes a path that does not exist.
|
|
391
|
+
- **tags** — `[{Context}]`, the slice's context, so every slice of one context groups under one heading.
|
|
392
|
+
- **summary** — the slice title. **description** — slice.json `description`, plus any `comments[]` that explain what the endpoint does.
|
|
393
|
+
- **requestBody properties** — exactly `commands[].fields`, minus the ones taken from the URL path or a header. Types from the table above.
|
|
394
|
+
- **required** — every command field not marked `optional: true`.
|
|
395
|
+
- **example** — only from the field's own `example` in slice.json. A field with no example gets no `example:` line; do not invent one.
|
|
396
|
+
- **responses** — `'201'` with the body the handler actually returns; one `'409'` per error code in `errorMapping` (i.e. per failing specification); `'401'` whenever the handler enforces auth; `'500'`.
|
|
397
|
+
|
|
398
|
+
A placeholder left unreplaced ships straight into the published spec, and `npm run build` will not catch it — open `/api-docs` and look at the rendered endpoint before marking the slice `Done`.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
316
402
|
## Step 6 — Wire up the route
|
|
317
403
|
|
|
318
404
|
Find the application's router registration (usually `src/index.ts` or `src/app.ts`) and add:
|
|
@@ -360,4 +446,6 @@ Before marking this slice as `Done`, verify the implementation against slice.jso
|
|
|
360
446
|
- [ ] Every entry in `specifications[]` maps to a test case in `{SliceName}.test.ts`
|
|
361
447
|
- [ ] If `storylines[]` is present, each command-to-event transition relevant to this slice has a corresponding test (or a documented reason it was skipped)
|
|
362
448
|
- [ ] No business rules, defaults, or constraints were added that do not appear in slice.json `description` or `comments`
|
|
363
|
-
- [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
|
|
449
|
+
- [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
|
|
450
|
+
- [ ] `routes.ts` carries an `@openapi` JSDoc block above every handler, and its path key matches the registered route with `:param` written as `{param}`
|
|
451
|
+
- [ ] The documented request body is exactly `commands[].fields`, and every error code in `errorMapping` has a `'409'` line — no undocumented fields, no invented ones
|
|
@@ -327,7 +327,7 @@ Skip (do not fabricate) a segment when:
|
|
|
327
327
|
|
|
328
328
|
File: `src/slices/{context}/{SliceName}/routes.ts`
|
|
329
329
|
|
|
330
|
-
> **
|
|
330
|
+
> **Pattern reference**: the template below is the full pattern — auth, error mapping, and the mandatory `@openapi` annotation (Step 6a). If the project already has slices under `src/slices/`, open one of their `routes.ts` files first and match it.
|
|
331
331
|
|
|
332
332
|
```typescript
|
|
333
333
|
import {Request, Response, Router} from 'express';
|
|
@@ -339,6 +339,48 @@ import createClient from '../../../supabase/api';
|
|
|
339
339
|
|
|
340
340
|
export const api = (): WebApiSetup => (router: Router): void => {
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* @openapi
|
|
344
|
+
* /api/query/{slicename}-collection:
|
|
345
|
+
* get:
|
|
346
|
+
* tags: [{Context}]
|
|
347
|
+
* summary: {slice title from slice.json}
|
|
348
|
+
* description: {slice.json description — what this read model answers}
|
|
349
|
+
* security:
|
|
350
|
+
* - bearerAuth: []
|
|
351
|
+
* parameters:
|
|
352
|
+
* - in: query
|
|
353
|
+
* name: _id
|
|
354
|
+
* required: false
|
|
355
|
+
* schema:
|
|
356
|
+
* type: string
|
|
357
|
+
* description: When set, returns the single row with this id instead of the full collection
|
|
358
|
+
* responses:
|
|
359
|
+
* '200':
|
|
360
|
+
* description: The {SliceName} read model
|
|
361
|
+
* content:
|
|
362
|
+
* application/json:
|
|
363
|
+
* schema:
|
|
364
|
+
* oneOf:
|
|
365
|
+
* - $ref: '#/components/schemas/{SliceName}ReadModel'
|
|
366
|
+
* - type: array
|
|
367
|
+
* items:
|
|
368
|
+
* $ref: '#/components/schemas/{SliceName}ReadModel'
|
|
369
|
+
* '401':
|
|
370
|
+
* description: Not authenticated
|
|
371
|
+
* '500':
|
|
372
|
+
* description: Server error
|
|
373
|
+
* components:
|
|
374
|
+
* schemas:
|
|
375
|
+
* {SliceName}ReadModel:
|
|
376
|
+
* type: object
|
|
377
|
+
* properties:
|
|
378
|
+
* id:
|
|
379
|
+
* type: string
|
|
380
|
+
* {fieldName}:
|
|
381
|
+
* type: string
|
|
382
|
+
* example: {the field's own example from slice.json, if it has one}
|
|
383
|
+
*/
|
|
342
384
|
router.get('/api/query/{slicename}-collection', async (req: Request, res: Response) => {
|
|
343
385
|
try {
|
|
344
386
|
const principal = await requireUser(req, res, true);
|
|
@@ -369,6 +411,40 @@ export const api = (): WebApiSetup => (router: Router): void => {
|
|
|
369
411
|
|
|
370
412
|
---
|
|
371
413
|
|
|
414
|
+
### Step 6a — OpenAPI annotation (required)
|
|
415
|
+
|
|
416
|
+
`src/swagger.ts` builds the published OpenAPI document by scanning `./src/slices/**/routes.ts` for `@openapi` JSDoc blocks. A handler without one is **invisible** in Swagger UI (`/api-docs`) and in `/swagger.json` — the endpoint works, but nobody can find it. `src/swagger.ts` is shared infra and outside a slice's commit scope, so the block in this slice's own `routes.ts` is the only place the endpoint can be documented. The `openapi-annotation` commit check rejects a `routes.ts` whose handlers have no matching block.
|
|
417
|
+
|
|
418
|
+
Everything in the block comes from slice.json — same rule as the code: no invented fields, no guessed types.
|
|
419
|
+
|
|
420
|
+
| slice.json field `type` | OpenAPI schema |
|
|
421
|
+
|---|---|
|
|
422
|
+
| `String` | `type: string` |
|
|
423
|
+
| `UUID` | `type: string`, `format: uuid` |
|
|
424
|
+
| `Int` | `type: integer`, `format: int32` |
|
|
425
|
+
| `Long` | `type: integer`, `format: int64` |
|
|
426
|
+
| `Double` | `type: number`, `format: double` |
|
|
427
|
+
| `Decimal` | `type: number` |
|
|
428
|
+
| `Boolean` | `type: boolean` |
|
|
429
|
+
| `Date` | `type: string`, `format: date` |
|
|
430
|
+
| `DateTime` | `type: string`, `format: date-time` |
|
|
431
|
+
| `Custom` | `type: object` |
|
|
432
|
+
|
|
433
|
+
Mapping rules:
|
|
434
|
+
|
|
435
|
+
- **path key** — the express path exactly as registered (a query read model has no path params; if you do add one, rewrite `:param` as `{param}`).
|
|
436
|
+
- **tags** — `[{Context}]`, the slice's context, so every slice of one context groups under one heading.
|
|
437
|
+
- **summary** — the slice title. **description** — slice.json `description`, plus any `comments[]` that explain what the read model answers.
|
|
438
|
+
- **schema properties** — exactly the read model fields from slice.json, the same set as the migration columns and the `{SliceName}ReadModel` type. Keep the JSON field spelling the route returns, not the snake_case column name, when they differ.
|
|
439
|
+
- **required** — omit it unless slice.json marks fields as mandatory; a projection row can legitimately be sparse.
|
|
440
|
+
- **example** — only from the field's own `example` in slice.json. A field with no example gets no `example:` line; do not invent one.
|
|
441
|
+
- **`components.schemas`** — declaring the read model once and `$ref`-ing it keeps the single-row and collection responses in sync. swagger-jsdoc merges the `components` block from every scanned file, so name the schema `{SliceName}ReadModel` to avoid colliding with another slice's.
|
|
442
|
+
- **responses** — `'200'` with the shape above, `'401'` whenever the handler enforces auth, `'500'`.
|
|
443
|
+
|
|
444
|
+
A placeholder left unreplaced ships straight into the published spec, and `npm run build` will not catch it — open `/api-docs` and look at the rendered endpoint before marking the slice `Done`.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
372
448
|
## Step 7 — Wire up the route
|
|
373
449
|
|
|
374
450
|
Find the application's router registration (usually `src/index.ts` or `src/app.ts`) and add:
|
|
@@ -412,4 +488,6 @@ src/common/
|
|
|
412
488
|
- [ ] Every field in the read model definition in slice.json has a column in the migration and a field in the TypeScript type — no invented columns
|
|
413
489
|
- [ ] Every event type in `events[]` is listed in the projection's `canHandle` — no assumed events
|
|
414
490
|
- [ ] No extra columns or fields were added beyond what slice.json defines
|
|
415
|
-
- [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
|
|
491
|
+
- [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
|
|
492
|
+
- [ ] `routes.ts` carries an `@openapi` JSDoc block above every handler, and its path key matches the registered route
|
|
493
|
+
- [ ] The documented response schema lists exactly the read model fields from slice.json — same set as the migration columns and the `{SliceName}ReadModel` type
|
|
@@ -182,6 +182,48 @@ async function handle{SliceName}(id: string, command: {SliceName}Command) {
|
|
|
182
182
|
|
|
183
183
|
console.info('{function-name} started');
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* @openapi
|
|
187
|
+
* /functions/v1/{function-name}:
|
|
188
|
+
* post:
|
|
189
|
+
* tags: [{Context}]
|
|
190
|
+
* summary: {slice title from slice.json}
|
|
191
|
+
* description: {slice.json description — which external system calls this, and when}
|
|
192
|
+
* requestBody:
|
|
193
|
+
* required: true
|
|
194
|
+
* content:
|
|
195
|
+
* application/json:
|
|
196
|
+
* schema:
|
|
197
|
+
* type: object
|
|
198
|
+
* required: [{command fields without optional: true}]
|
|
199
|
+
* properties:
|
|
200
|
+
* id:
|
|
201
|
+
* type: string
|
|
202
|
+
* description: Aggregate id — generated when the caller omits it
|
|
203
|
+
* {fieldName}:
|
|
204
|
+
* type: string
|
|
205
|
+
* example: {the field's own example from slice.json, if it has one}
|
|
206
|
+
* responses:
|
|
207
|
+
* '201':
|
|
208
|
+
* description: Accepted — {EventName} appended
|
|
209
|
+
* content:
|
|
210
|
+
* application/json:
|
|
211
|
+
* schema:
|
|
212
|
+
* type: object
|
|
213
|
+
* properties:
|
|
214
|
+
* ok:
|
|
215
|
+
* type: boolean
|
|
216
|
+
* id:
|
|
217
|
+
* type: string
|
|
218
|
+
* nextExpectedStreamVersion:
|
|
219
|
+
* type: string
|
|
220
|
+
* lastEventGlobalPosition:
|
|
221
|
+
* type: string
|
|
222
|
+
* '409':
|
|
223
|
+
* description: {message thrown by decide — one line per error code}
|
|
224
|
+
* '500':
|
|
225
|
+
* description: Internal server error
|
|
226
|
+
*/
|
|
185
227
|
export default {
|
|
186
228
|
fetch: withSupabase({auth: ['publishable', 'secret']}, async (req, _ctx) => {
|
|
187
229
|
try {
|
|
@@ -233,6 +275,35 @@ export default {
|
|
|
233
275
|
|
|
234
276
|
---
|
|
235
277
|
|
|
278
|
+
### Step 3a — OpenAPI annotation (required)
|
|
279
|
+
|
|
280
|
+
`src/swagger.ts` scans `./supabase/functions/**/index.ts` alongside the express routes, so the webhook shows up in Swagger UI (`/api-docs`) and `/swagger.json` next to the rest of the API — but only if the function carries an `@openapi` JSDoc block. Without one the endpoint is undocumented, which matters more here than anywhere else: a webhook's only consumer is an external system whose integrator cannot read this codebase.
|
|
281
|
+
|
|
282
|
+
The block goes directly above the `export default {` handler (as shown in the template above). Everything in it comes from slice.json.
|
|
283
|
+
|
|
284
|
+
| slice.json field `type` | OpenAPI schema |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `String` | `type: string` |
|
|
287
|
+
| `UUID` | `type: string`, `format: uuid` |
|
|
288
|
+
| `Int` | `type: integer`, `format: int32` |
|
|
289
|
+
| `Long` | `type: integer`, `format: int64` |
|
|
290
|
+
| `Double` | `type: number`, `format: double` |
|
|
291
|
+
| `Decimal` | `type: number` |
|
|
292
|
+
| `Boolean` | `type: boolean` |
|
|
293
|
+
| `Date` | `type: string`, `format: date` |
|
|
294
|
+
| `DateTime` | `type: string`, `format: date-time` |
|
|
295
|
+
| `Custom` | `type: object` |
|
|
296
|
+
|
|
297
|
+
Mapping rules:
|
|
298
|
+
|
|
299
|
+
- **path key** — `/functions/v1/{function-name}`, the deployed URL, not the file path.
|
|
300
|
+
- **requestBody properties** — exactly the `{SliceName}Payload` fields, which are exactly `commands[].fields`. **required** — every one not marked `optional: true` (never `id`, which the handler generates when absent).
|
|
301
|
+
- **responses** — `'201'` with the body the handler returns; one `'409'` per error code thrown in `decide`; `'500'`.
|
|
302
|
+
- **security** — a `verify_jwt = false` webhook takes no bearer token, so omit the `security:` block. When the function verifies a provider signature header instead (Stripe et al.), document that header under `parameters:` so the integrator knows to send it.
|
|
303
|
+
- `example` only from the field's own `example` in slice.json; never invent one.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
236
307
|
## Step 4 — Verify event store schema migration
|
|
237
308
|
|
|
238
309
|
The Emmett schema must be migrated before the edge function can write events. This happens once in the **backend startup**. Confirm that `src/common/loadPostgresEventstore.ts` calls `schema.migrate()`:
|
|
@@ -286,6 +357,8 @@ curl -i -X POST http://localhost:54321/functions/v1/{function-name} \
|
|
|
286
357
|
- [ ] `supabase/config.toml` entry added with correct `verify_jwt` setting
|
|
287
358
|
- [ ] `schema.migrate()` confirmed in backend startup (or run manually)
|
|
288
359
|
- [ ] Local test with `supabase functions serve` passes
|
|
360
|
+
- [ ] An `@openapi` block sits above the `export default` handler, keyed on `/functions/v1/{function-name}`
|
|
361
|
+
- [ ] The documented request body is exactly the `{SliceName}Payload` fields, and every error code thrown in `decide` has a `'409'` line
|
|
289
362
|
|
|
290
363
|
---
|
|
291
364
|
|
|
@@ -20,7 +20,9 @@ Read Events in src/events to understand the global structure.
|
|
|
20
20
|
3. Follow TypeScript best practices for type definitions and interfaces
|
|
21
21
|
|
|
22
22
|
Only check src/slices/{slice}/*.ts, do not check subfolders unless explicitely tasked to.
|
|
23
|
-
If not tasked explicitely to change routes, ignore routes*.ts
|
|
23
|
+
If not tasked explicitely to change routes, ignore routes*.ts — except the `routes.ts` of the slice you are
|
|
24
|
+
building: the build skill owns that file, and its `@openapi` block has to stay in step with the slice's
|
|
25
|
+
fields (the `openapi-annotation` check blocks the commit otherwise).
|
|
24
26
|
|
|
25
27
|
Ignore case for files and slices in prompts. "CartItems" slice is the same as "cartitems"
|
|
26
28
|
|
|
@@ -76,6 +78,9 @@ and rejects the commit if any of them find a problem:
|
|
|
76
78
|
`.build-kit/.slices/{context}/{slice}/slice.json`
|
|
77
79
|
- **spec-coverage** — heuristic: the test file needs at least as many `it(...)` blocks as slice.json
|
|
78
80
|
has `specifications[]` entries
|
|
81
|
+
- **openapi-annotation** — every handler in a slice's `routes.ts` needs an `@openapi` JSDoc block
|
|
82
|
+
above it, keyed on the registered path (`:param` written as `{param}`); without it the endpoint
|
|
83
|
+
never reaches `/api-docs` or `/swagger.json`
|
|
79
84
|
- **tsc-build** — `npx tsc --noEmit` must still pass
|
|
80
85
|
|
|
81
86
|
If a commit is rejected, split it — commit the out-of-scope file separately from the slice work, or add
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Every HTTP handler in a slice's routes.ts must carry an `@openapi` JSDoc block
|
|
4
|
+
// directly above it. src/swagger.ts builds the published OpenAPI document by
|
|
5
|
+
// scanning ./src/slices/**/routes.ts for those blocks, so a handler without one
|
|
6
|
+
// is a working endpoint that never appears in Swagger UI (/api-docs) or in
|
|
7
|
+
// /swagger.json — and swagger.ts itself is shared infra a slice commit may not
|
|
8
|
+
// touch, so the block in routes.ts is the only place the endpoint can be
|
|
9
|
+
// documented. See the build-state-change / build-state-view SKILL.md files for
|
|
10
|
+
// the block template and the slice.json -> OpenAPI field mapping.
|
|
11
|
+
//
|
|
12
|
+
// Heuristic, not an OpenAPI parser: it checks that a block exists between the
|
|
13
|
+
// previous handler and this one, and that the block names this handler's own
|
|
14
|
+
// path (express `:param` rewritten as `{param}`). An invalid schema, or a
|
|
15
|
+
// placeholder left unreplaced, still slips through — the rendered /api-docs
|
|
16
|
+
// page is the real check.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROUTES_FILE = /^src\/slices\/[^/]+\/[^/]+\/routes\.ts$/;
|
|
22
|
+
const ROUTE_CALL = /\brouter\s*\.\s*(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2/g;
|
|
23
|
+
const OPENAPI_BLOCK = /\/\*\*(?:[\s\S]*?)@openapi(?:[\s\S]*?)\*\//g;
|
|
24
|
+
|
|
25
|
+
// /api/foo/:id -> /api/foo/{id}
|
|
26
|
+
const toOpenApiPath = (p) => p.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
name: 'openapi-annotation',
|
|
30
|
+
run(ctx) {
|
|
31
|
+
const violations = [];
|
|
32
|
+
|
|
33
|
+
for (const { path: p } of ctx.changes) {
|
|
34
|
+
if (!ROUTES_FILE.test(p)) continue;
|
|
35
|
+
|
|
36
|
+
let content;
|
|
37
|
+
try {
|
|
38
|
+
content = fs.readFileSync(path.join(ctx.repoRoot, p), 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
continue; // deleted — nothing to check
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ROUTE_CALL.lastIndex = 0;
|
|
44
|
+
let cursor = 0; // start of the text belonging to the handler being checked
|
|
45
|
+
let call;
|
|
46
|
+
while ((call = ROUTE_CALL.exec(content))) {
|
|
47
|
+
const [, method, , routePath] = call;
|
|
48
|
+
const preceding = content.slice(cursor, call.index);
|
|
49
|
+
cursor = ROUTE_CALL.lastIndex;
|
|
50
|
+
|
|
51
|
+
const blocks = preceding.match(OPENAPI_BLOCK) || [];
|
|
52
|
+
if (blocks.length === 0) {
|
|
53
|
+
violations.push({
|
|
54
|
+
path: p,
|
|
55
|
+
reason: `${method.toUpperCase()} ${routePath} has no @openapi JSDoc block above it — the endpoint would be missing from /api-docs and /swagger.json`,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const documented = toOpenApiPath(routePath);
|
|
61
|
+
if (!blocks[blocks.length - 1].includes(documented)) {
|
|
62
|
+
violations.push({
|
|
63
|
+
path: p,
|
|
64
|
+
reason: `the @openapi block above ${method.toUpperCase()} ${routePath} does not document "${documented}" — the path key must match the registered route, with express ":param" written as "{param}"`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return violations;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -28,7 +28,9 @@ const options = {
|
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
|
-
|
|
31
|
+
// Edge functions carry their own @openapi blocks (see the build-webhook skill),
|
|
32
|
+
// so a webhook shows up in the same document as the express routes.
|
|
33
|
+
apis: ['./src/slices/**/routes.ts', './supabase/functions/**/index.ts'],
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export const specs = swaggerJsdoc(options);
|