From bba3f112692a3bcaa151c0144dedd5525c3aa96b Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 4 May 2026 19:38:52 +0200 Subject: [PATCH] lib: idea schemas + status mappers + transition guards (M12 T-493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/schemas/idea.ts: ideaCreateSchema, ideaUpdateSchema, ideaPlanMdFrontmatterSchema (yaml-frontmatter contract for materialize-step parser) - lib/idea-status.ts: bidirectional DBโ†”API mapping, canTransition state-machine guard, isIdeaEditable + isGrillMdEditable + isPlanMdEditable helpers - includes auto-regen docs/erd.svg from prisma generate Tests: 26 cases (status round-trip, transitions valid/invalid, schema validation edge-cases, priority bounds, verify-enum). Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/lib/idea-schemas.test.ts | 131 +++++++++++++++++++++++++++++ __tests__/lib/idea-status.test.ts | 99 ++++++++++++++++++++++ docs/erd.svg | 2 +- lib/idea-status.ts | 85 +++++++++++++++++++ lib/schemas/idea.ts | 53 ++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 __tests__/lib/idea-schemas.test.ts create mode 100644 __tests__/lib/idea-status.test.ts create mode 100644 lib/idea-status.ts create mode 100644 lib/schemas/idea.ts diff --git a/__tests__/lib/idea-schemas.test.ts b/__tests__/lib/idea-schemas.test.ts new file mode 100644 index 0000000..1514f5d --- /dev/null +++ b/__tests__/lib/idea-schemas.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect } from 'vitest' + +import { + ideaCreateSchema, + ideaUpdateSchema, + ideaPlanMdFrontmatterSchema, +} from '@/lib/schemas/idea' + +describe('ideaCreateSchema', () => { + it('accepts minimal valid input', () => { + const r = ideaCreateSchema.safeParse({ title: 'Plant-watering reminder' }) + expect(r.success).toBe(true) + }) + + it('trims and enforces non-empty title', () => { + const r = ideaCreateSchema.safeParse({ title: ' ' }) + expect(r.success).toBe(false) + }) + + it('rejects oversized title and description', () => { + expect(ideaCreateSchema.safeParse({ title: 'x'.repeat(201) }).success).toBe(false) + expect( + ideaCreateSchema.safeParse({ title: 'ok', description: 'x'.repeat(4001) }).success, + ).toBe(false) + }) + + it('accepts cuid-like product_id', () => { + const r = ideaCreateSchema.safeParse({ + title: 'Idee', + product_id: 'cmohrysyj0000rd17clnjy4tc', + }) + expect(r.success).toBe(true) + }) + + it('rejects non-cuid product_id', () => { + const r = ideaCreateSchema.safeParse({ title: 'Idee', product_id: 'not-a-cuid' }) + expect(r.success).toBe(false) + }) +}) + +describe('ideaUpdateSchema', () => { + it('allows empty object (no-op update)', () => { + expect(ideaUpdateSchema.safeParse({}).success).toBe(true) + }) + + it('allows partial title update', () => { + expect(ideaUpdateSchema.safeParse({ title: 'Updated' }).success).toBe(true) + }) +}) + +describe('ideaPlanMdFrontmatterSchema', () => { + const validPlan = { + pbi: { title: 'Test PBI', priority: 2 }, + stories: [ + { + title: 'Eerste flow', + priority: 2, + tasks: [ + { title: 'Setup', priority: 2, implementation_plan: '1. Doe X' }, + ], + }, + ], + } + + it('accepts a minimal valid plan', () => { + expect(ideaPlanMdFrontmatterSchema.safeParse(validPlan).success).toBe(true) + }) + + it('requires at least one story', () => { + const r = ideaPlanMdFrontmatterSchema.safeParse({ ...validPlan, stories: [] }) + expect(r.success).toBe(false) + }) + + it('requires at least one task per story', () => { + const r = ideaPlanMdFrontmatterSchema.safeParse({ + ...validPlan, + stories: [{ ...validPlan.stories[0], tasks: [] }], + }) + expect(r.success).toBe(false) + }) + + it('validates priority bounds 1-4', () => { + expect( + ideaPlanMdFrontmatterSchema.safeParse({ + ...validPlan, + pbi: { ...validPlan.pbi, priority: 5 }, + }).success, + ).toBe(false) + expect( + ideaPlanMdFrontmatterSchema.safeParse({ + ...validPlan, + pbi: { ...validPlan.pbi, priority: 0 }, + }).success, + ).toBe(false) + }) + + it('accepts optional verify_required + verify_only', () => { + const r = ideaPlanMdFrontmatterSchema.safeParse({ + ...validPlan, + stories: [ + { + ...validPlan.stories[0], + tasks: [ + { + title: 'Verify-only task', + priority: 2, + verify_required: 'ALIGNED_OR_PARTIAL', + verify_only: true, + }, + ], + }, + ], + }) + expect(r.success).toBe(true) + }) + + it('rejects invalid verify_required enum', () => { + const r = ideaPlanMdFrontmatterSchema.safeParse({ + ...validPlan, + stories: [ + { + ...validPlan.stories[0], + tasks: [ + { title: 't', priority: 2, verify_required: 'INVALID' }, + ], + }, + ], + }) + expect(r.success).toBe(false) + }) +}) diff --git a/__tests__/lib/idea-status.test.ts b/__tests__/lib/idea-status.test.ts new file mode 100644 index 0000000..0dfc3dc --- /dev/null +++ b/__tests__/lib/idea-status.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest' + +import { + ideaStatusToApi, + ideaStatusFromApi, + canTransition, + isIdeaEditable, + isGrillMdEditable, + isPlanMdEditable, + IDEA_STATUS_API_VALUES, +} from '@/lib/idea-status' + +describe('idea-status mappers', () => { + it('round-trips every API value', () => { + for (const api of IDEA_STATUS_API_VALUES) { + const db = ideaStatusFromApi(api) + expect(db).not.toBeNull() + expect(ideaStatusToApi(db!)).toBe(api) + } + }) + + it('returns null for invalid input', () => { + expect(ideaStatusFromApi('NOT_A_STATUS')).toBeNull() + }) + + it('is case-insensitive on the API side', () => { + expect(ideaStatusFromApi('PLAN_READY')).toBe('PLAN_READY') + expect(ideaStatusFromApi('Plan_Ready')).toBe('PLAN_READY') + }) +}) + +describe('canTransition', () => { + it('allows valid forward transitions', () => { + expect(canTransition('DRAFT', 'GRILLING')).toBe(true) + expect(canTransition('GRILLING', 'GRILLED')).toBe(true) + expect(canTransition('GRILLED', 'PLANNING')).toBe(true) + expect(canTransition('PLANNING', 'PLAN_READY')).toBe(true) + expect(canTransition('PLAN_READY', 'PLANNED')).toBe(true) + }) + + it('allows re-grill from GRILLED and PLAN_READY-ish states', () => { + expect(canTransition('GRILLED', 'GRILLING')).toBe(true) + expect(canTransition('PLAN_FAILED', 'PLANNING')).toBe(true) + }) + + it('allows fail-side transitions', () => { + expect(canTransition('GRILLING', 'GRILL_FAILED')).toBe(true) + expect(canTransition('PLANNING', 'PLAN_FAILED')).toBe(true) + }) + + it('allows recovery from failed states', () => { + expect(canTransition('GRILL_FAILED', 'GRILLING')).toBe(true) + expect(canTransition('PLAN_FAILED', 'GRILLED')).toBe(true) + }) + + it('only allows PLANNED โ†’ PLAN_READY (relink path)', () => { + expect(canTransition('PLANNED', 'PLAN_READY')).toBe(true) + expect(canTransition('PLANNED', 'GRILLING')).toBe(false) + expect(canTransition('PLANNED', 'DRAFT')).toBe(false) + }) + + it('rejects invalid jumps', () => { + expect(canTransition('DRAFT', 'PLANNED')).toBe(false) + expect(canTransition('DRAFT', 'PLAN_READY')).toBe(false) + expect(canTransition('GRILLING', 'PLANNED')).toBe(false) + }) +}) + +describe('isIdeaEditable', () => { + it('allows edit in non-running, non-PLANNED states', () => { + expect(isIdeaEditable('DRAFT')).toBe(true) + expect(isIdeaEditable('GRILLED')).toBe(true) + expect(isIdeaEditable('GRILL_FAILED')).toBe(true) + expect(isIdeaEditable('PLAN_FAILED')).toBe(true) + expect(isIdeaEditable('PLAN_READY')).toBe(true) + }) + + it('blocks edit while a job is running or after PLANNED', () => { + expect(isIdeaEditable('GRILLING')).toBe(false) + expect(isIdeaEditable('PLANNING')).toBe(false) + expect(isIdeaEditable('PLANNED')).toBe(false) + }) +}) + +describe('isGrillMdEditable / isPlanMdEditable', () => { + it('grill_md only editable in GRILLED or PLAN_READY', () => { + expect(isGrillMdEditable('GRILLED')).toBe(true) + expect(isGrillMdEditable('PLAN_READY')).toBe(true) + expect(isGrillMdEditable('DRAFT')).toBe(false) + expect(isGrillMdEditable('PLANNED')).toBe(false) + }) + + it('plan_md only editable in PLAN_READY', () => { + expect(isPlanMdEditable('PLAN_READY')).toBe(true) + expect(isPlanMdEditable('GRILLED')).toBe(false) + expect(isPlanMdEditable('PLAN_FAILED')).toBe(false) + expect(isPlanMdEditable('PLANNED')).toBe(false) + }) +}) diff --git a/docs/erd.svg b/docs/erd.svg index 12b3637..b31ab45 100644 --- a/docs/erd.svg +++ b/docs/erd.svg @@ -1 +1 @@ -

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

product

sprint

enum:status

enum:verify_required

user

product

task

enum:status

claimed_by_token

enum:verify_result

user

token

product

user

user

product

user

story

task

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

VerifyResult

ALIGNED

ALIGNED

PARTIAL

PARTIAL

EMPTY

EMPTY

DIVERGENT

DIVERGENT

VerifyRequired

ALIGNED

ALIGNED

ALIGNED_OR_PARTIAL

ALIGNED_OR_PARTIAL

ANY

ANY

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

users

String

id

๐Ÿ—๏ธ

String

username

String

email

โ“

String

password_hash

Boolean

is_demo

String

bio

โ“

String

bio_detail

โ“

Bytes

avatar_data

โ“

DateTime

created_at

DateTime

updated_at

user_roles

String

id

๐Ÿ—๏ธ

Role

role

api_tokens

String

id

๐Ÿ—๏ธ

String

token_hash

String

label

โ“

DateTime

created_at

DateTime

revoked_at

โ“

products

String

id

๐Ÿ—๏ธ

String

name

String

code

โ“

String

description

โ“

String

repo_url

โ“

String

definition_of_done

Boolean

auto_pr

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

Int

priority

Float

sort_order

PbiStatus

status

String

pr_url

โ“

DateTime

pr_merged_at

โ“

DateTime

created_at

DateTime

updated_at

stories

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

String

acceptance_criteria

โ“

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

๐Ÿ—๏ธ

LogType

type

String

content

TestStatus

status

โ“

String

commit_hash

โ“

String

commit_message

โ“

Json

metadata

โ“

DateTime

created_at

sprints

String

id

๐Ÿ—๏ธ

String

sprint_goal

SprintStatus

status

DateTime

start_date

โ“

DateTime

end_date

โ“

DateTime

created_at

DateTime

completed_at

โ“

tasks

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

String

implementation_plan

โ“

Int

priority

Float

sort_order

TaskStatus

status

Boolean

verify_only

VerifyRequired

verify_required

String

repo_url

โ“

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

๐Ÿ—๏ธ

ClaudeJobStatus

status

DateTime

claimed_at

โ“

DateTime

started_at

โ“

DateTime

finished_at

โ“

DateTime

pushed_at

โ“

VerifyResult

verify_result

โ“

String

plan_snapshot

โ“

String

branch

โ“

String

pr_url

โ“

String

summary

โ“

String

error

โ“

Int

retry_count

DateTime

created_at

DateTime

updated_at

claude_workers

String

id

๐Ÿ—๏ธ

String

product_id

โ“

DateTime

started_at

DateTime

last_seen_at

product_members

String

id

๐Ÿ—๏ธ

DateTime

created_at

todos

String

id

๐Ÿ—๏ธ

String

title

String

description

โ“

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

login_pairings

String

id

๐Ÿ—๏ธ

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

โ“

String

desktop_ip

โ“

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

โ“

DateTime

consumed_at

โ“

claude_questions

String

id

๐Ÿ—๏ธ

String

question

Json

options

โ“

String

status

String

answer

โ“

DateTime

answered_at

โ“

DateTime

created_at

DateTime

expires_at

\ No newline at end of file +

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

product

sprint

enum:status

enum:verify_required

user

product

task

idea

enum:kind

enum:status

claimed_by_token

enum:verify_result

user

token

product

user

user

product

user

product

pbi

enum:status

idea

enum:type

user

story

task

idea

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

VerifyResult

ALIGNED

ALIGNED

PARTIAL

PARTIAL

EMPTY

EMPTY

DIVERGENT

DIVERGENT

VerifyRequired

ALIGNED

ALIGNED

ALIGNED_OR_PARTIAL

ALIGNED_OR_PARTIAL

ANY

ANY

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

IdeaStatus

DRAFT

DRAFT

GRILLING

GRILLING

GRILL_FAILED

GRILL_FAILED

GRILLED

GRILLED

PLANNING

PLANNING

PLAN_FAILED

PLAN_FAILED

PLAN_READY

PLAN_READY

PLANNED

PLANNED

ClaudeJobKind

TASK_IMPLEMENTATION

TASK_IMPLEMENTATION

IDEA_GRILL

IDEA_GRILL

IDEA_MAKE_PLAN

IDEA_MAKE_PLAN

IdeaLogType

DECISION

DECISION

NOTE

NOTE

GRILL_RESULT

GRILL_RESULT

PLAN_RESULT

PLAN_RESULT

STATUS_CHANGE

STATUS_CHANGE

JOB_EVENT

JOB_EVENT

users

String

id

๐Ÿ—๏ธ

String

username

String

email

โ“

String

password_hash

Boolean

is_demo

String

bio

โ“

String

bio_detail

โ“

Bytes

avatar_data

โ“

Int

idea_code_counter

DateTime

created_at

DateTime

updated_at

user_roles

String

id

๐Ÿ—๏ธ

Role

role

api_tokens

String

id

๐Ÿ—๏ธ

String

token_hash

String

label

โ“

DateTime

created_at

DateTime

revoked_at

โ“

products

String

id

๐Ÿ—๏ธ

String

name

String

code

โ“

String

description

โ“

String

repo_url

โ“

String

definition_of_done

Boolean

auto_pr

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

Int

priority

Float

sort_order

PbiStatus

status

String

pr_url

โ“

DateTime

pr_merged_at

โ“

DateTime

created_at

DateTime

updated_at

stories

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

String

acceptance_criteria

โ“

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

๐Ÿ—๏ธ

LogType

type

String

content

TestStatus

status

โ“

String

commit_hash

โ“

String

commit_message

โ“

Json

metadata

โ“

DateTime

created_at

sprints

String

id

๐Ÿ—๏ธ

String

sprint_goal

SprintStatus

status

DateTime

start_date

โ“

DateTime

end_date

โ“

DateTime

created_at

DateTime

completed_at

โ“

tasks

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

String

implementation_plan

โ“

Int

priority

Float

sort_order

TaskStatus

status

Boolean

verify_only

VerifyRequired

verify_required

String

repo_url

โ“

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

๐Ÿ—๏ธ

ClaudeJobKind

kind

ClaudeJobStatus

status

DateTime

claimed_at

โ“

DateTime

started_at

โ“

DateTime

finished_at

โ“

DateTime

pushed_at

โ“

VerifyResult

verify_result

โ“

String

plan_snapshot

โ“

String

branch

โ“

String

pr_url

โ“

String

summary

โ“

String

error

โ“

Int

retry_count

DateTime

created_at

DateTime

updated_at

claude_workers

String

id

๐Ÿ—๏ธ

String

product_id

โ“

DateTime

started_at

DateTime

last_seen_at

product_members

String

id

๐Ÿ—๏ธ

DateTime

created_at

todos

String

id

๐Ÿ—๏ธ

String

title

String

description

โ“

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

ideas

String

id

๐Ÿ—๏ธ

String

code

String

title

String

description

โ“

String

grill_md

โ“

String

plan_md

โ“

IdeaStatus

status

Boolean

archived

DateTime

created_at

DateTime

updated_at

idea_logs

String

id

๐Ÿ—๏ธ

IdeaLogType

type

String

content

Json

metadata

โ“

DateTime

created_at

login_pairings

String

id

๐Ÿ—๏ธ

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

โ“

String

desktop_ip

โ“

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

โ“

DateTime

consumed_at

โ“

claude_questions

String

id

๐Ÿ—๏ธ

String

question

Json

options

โ“

String

status

String

answer

โ“

DateTime

answered_at

โ“

DateTime

created_at

DateTime

expires_at

\ No newline at end of file diff --git a/lib/idea-status.ts b/lib/idea-status.ts new file mode 100644 index 0000000..c972a38 --- /dev/null +++ b/lib/idea-status.ts @@ -0,0 +1,85 @@ +// Bidirectionele case-mapper voor IdeaStatus + transitie-guard helper. +// DB houdt UPPER_SNAKE; API exposeert lowercase. +// Patroon volgt lib/task-status.ts. + +import type { IdeaStatus } from '@prisma/client' + +const IDEA_DB_TO_API = { + DRAFT: 'draft', + GRILLING: 'grilling', + GRILL_FAILED: 'grill_failed', + GRILLED: 'grilled', + PLANNING: 'planning', + PLAN_FAILED: 'plan_failed', + PLAN_READY: 'plan_ready', + PLANNED: 'planned', +} as const satisfies Record + +const IDEA_API_TO_DB: Record = { + draft: 'DRAFT', + grilling: 'GRILLING', + grill_failed: 'GRILL_FAILED', + grilled: 'GRILLED', + planning: 'PLANNING', + plan_failed: 'PLAN_FAILED', + plan_ready: 'PLAN_READY', + planned: 'PLANNED', +} + +export type IdeaStatusApi = (typeof IDEA_DB_TO_API)[IdeaStatus] + +export function ideaStatusToApi(s: IdeaStatus): IdeaStatusApi { + return IDEA_DB_TO_API[s] +} + +export function ideaStatusFromApi(s: string): IdeaStatus | null { + return IDEA_API_TO_DB[s.toLowerCase()] ?? null +} + +export const IDEA_STATUS_API_VALUES = Object.values(IDEA_DB_TO_API) + +// --------------------------------------------------------------------------- +// State-machine transition table (zie docs/plans/M12-ideas.md state-machine). +// Server-actions gebruiken canTransition(from, to) als guard vรณรณr mutatie. +// +// Asymmetrisch: trek vanuit DRAFT alleen naar GRILLING; vanuit GRILLED kan +// re-grill (โ†’ GRILLING) of make-plan (โ†’ PLANNING) gebeuren. PLANNED is een +// terminal state; verlaat alleen via expliciete relink (PBI verwijderd โ†’ PLAN_READY). + +const ALLOWED_TRANSITIONS: Record> = { + DRAFT: ['GRILLING'], + GRILLING: ['GRILLED', 'GRILL_FAILED'], + GRILL_FAILED: ['GRILLING', 'DRAFT'], + GRILLED: ['GRILLING', 'PLANNING'], + PLANNING: ['PLAN_READY', 'PLAN_FAILED'], + PLAN_FAILED: ['PLANNING', 'GRILLED'], + PLAN_READY: ['PLANNING', 'PLANNED'], + PLANNED: ['PLAN_READY'], // alleen via relinkIdeaPlanAction (PBI deleted) +} + +export function canTransition(from: IdeaStatus, to: IdeaStatus): boolean { + return ALLOWED_TRANSITIONS[from].includes(to) +} + +// Statussen waarin een idee bewerkbaar is (form-input, niet md-velden). +const EDITABLE_STATUSES: ReadonlyArray = [ + 'DRAFT', + 'GRILL_FAILED', + 'GRILLED', + 'PLAN_FAILED', + 'PLAN_READY', +] + +export function isIdeaEditable(s: IdeaStatus): boolean { + return EDITABLE_STATUSES.includes(s) +} + +// Statussen waarin grill_md bewerkbaar is (handmatige finetuning). +export function isGrillMdEditable(s: IdeaStatus): boolean { + return s === 'GRILLED' || s === 'PLAN_READY' +} + +// Statussen waarin plan_md bewerkbaar is. +export function isPlanMdEditable(s: IdeaStatus): boolean { + return s === 'PLAN_READY' +} diff --git a/lib/schemas/idea.ts b/lib/schemas/idea.ts new file mode 100644 index 0000000..4be1553 --- /dev/null +++ b/lib/schemas/idea.ts @@ -0,0 +1,53 @@ +import { z } from 'zod' + +// Velden die de gebruiker invult bij create/edit. Status wordt door +// server-actions gezet (niet door client-input). +export const ideaCreateSchema = z.object({ + title: z.string().trim().min(1, 'Titel is verplicht').max(200, 'Maximaal 200 tekens'), + description: z.string().max(4000, 'Maximaal 4000 tekens').optional().nullable(), + product_id: z.string().cuid('Ongeldig product').optional().nullable(), +}) + +export const ideaUpdateSchema = ideaCreateSchema.partial() + +export type IdeaCreateInput = z.infer +export type IdeaUpdateInput = z.infer + +// --------------------------------------------------------------------------- +// plan_md frontmatter โ€” strict format dat door make-plan-job geproduceerd +// wordt en door materializeIdeaPlanAction wordt geparseerd. Zie +// docs/plans/M12-ideas.md "Plan-md formaat A". + +const verifyRequiredEnum = z.enum(['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY']) + +const planTaskSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().max(4000).optional(), + implementation_plan: z.string().max(8000).optional(), + priority: z.number().int().min(1).max(4), + verify_required: verifyRequiredEnum.optional(), + verify_only: z.boolean().optional(), +}) + +const planStorySchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().max(4000).optional(), + acceptance_criteria: z.string().max(4000).optional(), + priority: z.number().int().min(1).max(4), + tasks: z.array(planTaskSchema).min(1, 'Story moet minimaal 1 taak hebben'), +}) + +const planPbiSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().max(4000).optional(), + priority: z.number().int().min(1).max(4), +}) + +export const ideaPlanMdFrontmatterSchema = z.object({ + pbi: planPbiSchema, + stories: z.array(planStorySchema).min(1, 'Plan moet minimaal 1 story bevatten'), +}) + +export type IdeaPlanFrontmatter = z.infer +export type IdeaPlanStory = z.infer +export type IdeaPlanTask = z.infer