From 60039268074501471e00ffd42259c3171f4d36a9 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Mon, 27 Apr 2026 10:08:28 +0000 Subject: [PATCH] feat(schema/enums): define TypeBox categorical enum schemas and type aliases --- src/schema/enums.ts | 68 +++++++++- tasks/implementation/schema/enums.md | 31 +++-- test/schema.test.ts | 186 ++++++++++++++++++++++++++- 3 files changed, 266 insertions(+), 19 deletions(-) diff --git a/src/schema/enums.ts b/src/schema/enums.ts index acf3cd9..344e0ab 100644 --- a/src/schema/enums.ts +++ b/src/schema/enums.ts @@ -1 +1,67 @@ -// Enum definitions: TaskScope, TaskRisk, TaskImpact, TaskLevel, TaskStatus, TaskPriority \ No newline at end of file +import { Type, type Static, type TSchema } from "@alkdev/typebox"; + +// --- Nullable helper --- + +/** Wrap a schema to also accept `null`. */ +export const Nullable = (schema: T) => + Type.Union([schema, Type.Null()]); + +// --- Enum schemas (runtime) and type aliases (compile-time) --- + +export const TaskScopeEnum = Type.Union([ + Type.Literal("single"), + Type.Literal("narrow"), + Type.Literal("moderate"), + Type.Literal("broad"), + Type.Literal("system"), +]); +/** "single" | "narrow" | "moderate" | "broad" | "system" */ +export type TaskScope = Static; + +export const TaskRiskEnum = Type.Union([ + Type.Literal("trivial"), + Type.Literal("low"), + Type.Literal("medium"), + Type.Literal("high"), + Type.Literal("critical"), +]); +/** "trivial" | "low" | "medium" | "high" | "critical" */ +export type TaskRisk = Static; + +export const TaskImpactEnum = Type.Union([ + Type.Literal("isolated"), + Type.Literal("component"), + Type.Literal("phase"), + Type.Literal("project"), +]); +/** "isolated" | "component" | "phase" | "project" */ +export type TaskImpact = Static; + +export const TaskLevelEnum = Type.Union([ + Type.Literal("planning"), + Type.Literal("decomposition"), + Type.Literal("implementation"), + Type.Literal("review"), + Type.Literal("research"), +]); +/** "planning" | "decomposition" | "implementation" | "review" | "research" */ +export type TaskLevel = Static; + +export const TaskPriorityEnum = Type.Union([ + Type.Literal("low"), + Type.Literal("medium"), + Type.Literal("high"), + Type.Literal("critical"), +]); +/** "low" | "medium" | "high" | "critical" */ +export type TaskPriority = Static; + +export const TaskStatusEnum = Type.Union([ + Type.Literal("pending"), + Type.Literal("in-progress"), + Type.Literal("completed"), + Type.Literal("failed"), + Type.Literal("blocked"), +]); +/** "pending" | "in-progress" | "completed" | "failed" | "blocked" */ +export type TaskStatus = Static; \ No newline at end of file diff --git a/tasks/implementation/schema/enums.md b/tasks/implementation/schema/enums.md index 44e3511..f3f8a99 100644 --- a/tasks/implementation/schema/enums.md +++ b/tasks/implementation/schema/enums.md @@ -1,7 +1,7 @@ --- id: schema/enums name: Define TypeBox categorical enum schemas and type aliases -status: pending +status: completed depends_on: - setup/project-init scope: narrow @@ -18,17 +18,17 @@ The six enums: `TaskScopeEnum`, `TaskRiskEnum`, `TaskImpactEnum`, `TaskLevelEnum ## Acceptance Criteria -- [ ] `src/schema/enums.ts` exports all six enum schemas and their type aliases -- [ ] Each enum uses `Type.Union([Type.Literal("value"), ...])` pattern per [typebox-patterns.md](../../../docs/research/typebox-patterns.md) -- [ ] `TaskScopeEnum`: `"single" | "narrow" | "moderate" | "broad" | "system"` -- [ ] `TaskRiskEnum`: `"trivial" | "low" | "medium" | "high" | "critical"` -- [ ] `TaskImpactEnum`: `"isolated" | "component" | "phase" | "project"` -- [ ] `TaskLevelEnum`: `"planning" | "decomposition" | "implementation" | "review" | "research"` -- [ ] `TaskPriorityEnum`: `"low" | "medium" | "high" | "critical"` -- [ ] `TaskStatusEnum`: `"pending" | "in-progress" | "completed" | "failed" | "blocked"` -- [ ] Type aliases derived via `Static`: `TaskScope`, `TaskRisk`, `TaskImpact`, `TaskLevel`, `TaskPriority`, `TaskStatus` -- [ ] Naming convention matches spec: `Enum` suffix on schema constants only, never on type aliases -- [ ] `src/schema/index.ts` re-exports all schemas and types +- [x] `src/schema/enums.ts` exports all six enum schemas and their type aliases +- [x] Each enum uses `Type.Union([Type.Literal("value"), ...])` pattern per [typebox-patterns.md](../../../docs/research/typebox-patterns.md) +- [x] `TaskScopeEnum`: `"single" | "narrow" | "moderate" | "broad" | "system"` +- [x] `TaskRiskEnum`: `"trivial" | "low" | "medium" | "high" | "critical"` +- [x] `TaskImpactEnum`: `"isolated" | "component" | "phase" | "project"` +- [x] `TaskLevelEnum`: `"planning" | "decomposition" | "implementation" | "review" | "research"` +- [x] `TaskPriorityEnum`: `"low" | "medium" | "high" | "critical"` +- [x] `TaskStatusEnum`: `"pending" | "in-progress" | "completed" | "failed" | "blocked"` +- [x] Type aliases derived via `Static`: `TaskScope`, `TaskRisk`, `TaskImpact`, `TaskLevel`, `TaskPriority`, `TaskStatus` +- [x] Naming convention matches spec: `Enum` suffix on schema constants only, never on type aliases +- [x] `src/schema/index.ts` re-exports all schemas and types ## References @@ -37,8 +37,11 @@ The six enums: `TaskScopeEnum`, `TaskRiskEnum`, `TaskImpactEnum`, `TaskLevelEnum ## Notes -> To be filled by implementation agent +Also exported the `Nullable` helper generic (used by downstream schemas) and added JSDoc comments on each type alias. ## Summary -> To be filled on completion \ No newline at end of file +Implemented all six categorical enum schemas using `Type.Union([Type.Literal(...)])` pattern with `Static` type aliases. +- Created: `src/schema/enums.ts` (6 enum schemas + 6 type aliases + Nullable helper) +- Modified: `test/schema.test.ts` (21 enum-specific tests: Value.Check validation, Nullable helper, compile-time type alias verification) +- Tests: 21 enum tests + 4 placeholders, all passing; `tsc --noEmit` clean \ No newline at end of file diff --git a/test/schema.test.ts b/test/schema.test.ts index b2129f8..dc6f236 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -1,7 +1,185 @@ import { describe, it, expect } from 'vitest'; +import { Value } from '@alkdev/typebox/value'; +import { + TaskScopeEnum, + TaskRiskEnum, + TaskImpactEnum, + TaskLevelEnum, + TaskPriorityEnum, + TaskStatusEnum, + Nullable, +} from '../src/schema/enums.js'; -describe('Schema', () => { - it('placeholder — schema validation', () => { - expect(true).toBe(true); +describe('Enum schemas', () => { + // --- TaskScopeEnum --- + describe('TaskScopeEnum', () => { + const validValues = ['single', 'narrow', 'moderate', 'broad', 'system'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskScopeEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskScopeEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskScopeEnum, '')).toBe(false); + expect(Value.Check(TaskScopeEnum, 42)).toBe(false); + expect(Value.Check(TaskScopeEnum, null)).toBe(false); + }); }); -}); \ No newline at end of file + + // --- TaskRiskEnum --- + describe('TaskRiskEnum', () => { + const validValues = ['trivial', 'low', 'medium', 'high', 'critical'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskRiskEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskRiskEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskRiskEnum, '')).toBe(false); + expect(Value.Check(TaskRiskEnum, 42)).toBe(false); + expect(Value.Check(TaskRiskEnum, null)).toBe(false); + }); + }); + + // --- TaskImpactEnum --- + describe('TaskImpactEnum', () => { + const validValues = ['isolated', 'component', 'phase', 'project'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskImpactEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskImpactEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskImpactEnum, '')).toBe(false); + expect(Value.Check(TaskImpactEnum, 42)).toBe(false); + expect(Value.Check(TaskImpactEnum, null)).toBe(false); + }); + }); + + // --- TaskLevelEnum --- + describe('TaskLevelEnum', () => { + const validValues = ['planning', 'decomposition', 'implementation', 'review', 'research'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskLevelEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskLevelEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskLevelEnum, '')).toBe(false); + expect(Value.Check(TaskLevelEnum, 42)).toBe(false); + expect(Value.Check(TaskLevelEnum, null)).toBe(false); + }); + }); + + // --- TaskPriorityEnum --- + describe('TaskPriorityEnum', () => { + const validValues = ['low', 'medium', 'high', 'critical'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskPriorityEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskPriorityEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskPriorityEnum, '')).toBe(false); + expect(Value.Check(TaskPriorityEnum, 42)).toBe(false); + expect(Value.Check(TaskPriorityEnum, null)).toBe(false); + }); + }); + + // --- TaskStatusEnum --- + describe('TaskStatusEnum', () => { + const validValues = ['pending', 'in-progress', 'completed', 'failed', 'blocked'] as const; + + it('accepts each valid literal', () => { + for (const value of validValues) { + expect(Value.Check(TaskStatusEnum, value)).toBe(true); + } + }); + + it('rejects invalid values', () => { + expect(Value.Check(TaskStatusEnum, 'unknown')).toBe(false); + expect(Value.Check(TaskStatusEnum, '')).toBe(false); + expect(Value.Check(TaskStatusEnum, 42)).toBe(false); + expect(Value.Check(TaskStatusEnum, null)).toBe(false); + }); + }); +}); + +describe('Nullable helper', () => { + it('accepts valid enum values', () => { + const NullableScope = Nullable(TaskScopeEnum); + expect(Value.Check(NullableScope, 'single')).toBe(true); + expect(Value.Check(NullableScope, 'system')).toBe(true); + }); + + it('accepts null', () => { + const NullableScope = Nullable(TaskScopeEnum); + expect(Value.Check(NullableScope, null)).toBe(true); + }); + + it('rejects undefined and invalid strings', () => { + const NullableScope = Nullable(TaskScopeEnum); + expect(Value.Check(NullableScope, 'invalid')).toBe(false); + expect(Value.Check(NullableScope, undefined)).toBe(false); + expect(Value.Check(NullableScope, 42)).toBe(false); + }); +}); + +describe('Type alias correctness (compile-time)', () => { + // These tests verify that the type aliases resolve to the expected union types. + // We use type assertions to confirm the types are what we expect. + // If the types are wrong, TypeScript would fail to compile this file. + + it('TaskScope type accepts valid values', () => { + const scope: TaskScope = 'single'; + expect(scope).toBe('single'); + }); + + it('TaskRisk type accepts valid values', () => { + const risk: TaskRisk = 'critical'; + expect(risk).toBe('critical'); + }); + + it('TaskImpact type accepts valid values', () => { + const impact: TaskImpact = 'project'; + expect(impact).toBe('project'); + }); + + it('TaskLevel type accepts valid values', () => { + const level: TaskLevel = 'implementation'; + expect(level).toBe('implementation'); + }); + + it('TaskPriority type accepts valid values', () => { + const priority: TaskPriority = 'high'; + expect(priority).toBe('high'); + }); + + it('TaskStatus type accepts valid values', () => { + const status: TaskStatus = 'in-progress'; + expect(status).toBe('in-progress'); + }); +}); + +// Intentionally import type aliases to verify they exist at compile time +type TaskScope = import('../src/schema/enums.js').TaskScope; +type TaskRisk = import('../src/schema/enums.js').TaskRisk; +type TaskImpact = import('../src/schema/enums.js').TaskImpact; +type TaskLevel = import('../src/schema/enums.js').TaskLevel; +type TaskPriority = import('../src/schema/enums.js').TaskPriority; +type TaskStatus = import('../src/schema/enums.js').TaskStatus; \ No newline at end of file