From 83f637ddb34ef87ea81b0761eeeedcce49cedf2e Mon Sep 17 00:00:00 2001 From: p-sw Date: Sun, 7 Jun 2026 14:20:59 +0900 Subject: [PATCH] refactor: move types next to schema --- src/brain/index.test.ts | 29 ++++++++++++------- src/brain/index.ts | 18 ++++++------ src/brain/schedule.ts | 22 --------------- src/commands/debug/schedule.test.ts | 17 ++++++++---- src/commands/debug/schedule.ts | 10 +++---- src/openrouter/schema.ts | 43 +++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 53 deletions(-) diff --git a/src/brain/index.test.ts b/src/brain/index.test.ts index 0bddd30..c91e21d 100644 --- a/src/brain/index.test.ts +++ b/src/brain/index.test.ts @@ -16,6 +16,7 @@ let customDailySlots: Array<{ start: string; end: string; activity: string; + notes: string; }> | null = null; let customAvailability: Array<{ start: string; @@ -27,8 +28,14 @@ function build48Slots(): Array<{ start: string; end: string; activity: string; + notes: string; }> { - const slots: Array<{ start: string; end: string; activity: string }> = []; + const slots: Array<{ + start: string; + end: string; + activity: string; + notes: string; + }> = []; for (let i = 0; i < 48; i++) { const startHour = Math.floor(i / 2); const startMin = (i % 2) * 30; @@ -41,7 +48,7 @@ function build48Slots(): Array<{ const endMin = ((i + 1) % 2) * 30; end = `${String(endHour).padStart(2, "0")}:${String(endMin).padStart(2, "0")}`; } - slots.push({ start, end, activity: `slot-${i}` }); + slots.push({ start, end, activity: `slot-${i}`, notes: "" }); } return slots; } @@ -70,7 +77,7 @@ function buildAvailability(): Array<{ const mockCall = mock(async (model: unknown, options: any): Promise => { llmCalls.push({ model, options }); if (options.jsonSchemaName === "daily-schedule") { - return (customDailySlots ?? build48Slots()) as unknown as T; + return { items: customDailySlots ?? build48Slots() } as unknown as T; } if (options.jsonSchemaName === "monthly-schedule") { if (customMonthlyDays) return customMonthlyDays as unknown as T; @@ -150,16 +157,18 @@ describe("Brain.createDailySchedule", () => { const result = await brain.createDailySchedule(today, "focus on writing"); expect(result).not.toBeNull(); - expect(result).toHaveLength(48); - expect(result![0]).toEqual({ + expect(result!.items).toHaveLength(48); + expect(result!.items[0]).toEqual({ start: "00:00", end: "00:30", activity: "slot-0", + notes: "", }); - expect(result![47]).toEqual({ + expect(result!.items[47]).toEqual({ start: "23:30", end: "24:00", activity: "slot-47", + notes: "", }); const llmCall = llmCalls.find( @@ -177,7 +186,7 @@ describe("Brain.createDailySchedule", () => { }, ); expect(facts).toHaveLength(1); - expect(JSON.parse(facts[0]!.statement)).toHaveLength(48); + expect(JSON.parse(facts[0]!.statement).items).toHaveLength(48); }); test("S4: month wrap (June 30 -> July 1)", async () => { @@ -306,7 +315,7 @@ describe("Brain.getTodayScheduledAvailability", () => { const todayKey = formatDateKey(today); await brain.db.addFact({ spaceName: brain.space.name, - statement: JSON.stringify(build48Slots()), + statement: JSON.stringify({ items: build48Slots() }), summary: "test daily", source: "test", confidence: 1.0, @@ -357,7 +366,7 @@ describe("Brain.removeScheduledAvailability", () => { const todayKey = formatDateKey(today); await brain.db.addFact({ spaceName: brain.space.name, - statement: JSON.stringify(build48Slots()), + statement: JSON.stringify({ items: build48Slots() }), summary: "test daily", source: "test", confidence: 1.0, @@ -426,7 +435,7 @@ describe("Brain.createDebug", () => { const schedule = await brain.createDailySchedule(today, "msg"); expect(schedule).not.toBeNull(); - expect(schedule).toHaveLength(48); + expect(schedule!.items).toHaveLength(48); const facts = await brain.db.getTopicFacts(`daily-schedule:${tomorrowKey}`, { spaceName: brain.space.name, diff --git a/src/brain/index.ts b/src/brain/index.ts index 0cee18d..231d5dd 100644 --- a/src/brain/index.ts +++ b/src/brain/index.ts @@ -7,14 +7,14 @@ import { availabilitySchema, dailyScheduleSchema, monthlyScheduleSchema, + type Availability, + type DailySchedule, + type MonthlySchedule, } from "@/openrouter/schema"; import { logger } from "@/utils/logger"; import { factExtractor } from "./factExtractor"; import { brainManager, type BrainItem } from "./manager"; import { - type Availability, - type DailySchedule, - type MonthlySchedule, formatDateKey, formatMonthKey, nextDay, @@ -72,7 +72,7 @@ export class Brain { await this.db.addFact({ spaceName: this.space.name, statement: JSON.stringify(schedule), - summary: `Daily schedule for ${dateKey} (${schedule.length} slots)`, + summary: `Daily schedule for ${dateKey} (${schedule.items.length} slots)`, source: "createDailySchedule", confidence: 1.0, topics: [ @@ -291,11 +291,11 @@ export class Brain { "PERSONA_BASE_SYSTEM_PROMPT", ); const generatedBaseSystemPrompt = await llm.call( -llm.models.identity, -{ - instruction: personaSystemInstruction, - message: description, - }, + llm.models.identity, + { + instruction: personaSystemInstruction, + message: description, + }, ); const personaSystemFixed = await loadPrompt( diff --git a/src/brain/schedule.ts b/src/brain/schedule.ts index 37c80c5..b54b9d8 100644 --- a/src/brain/schedule.ts +++ b/src/brain/schedule.ts @@ -1,25 +1,3 @@ -export type DailySlot = { - start: string; - end: string; - activity: string; - notes?: string; -}; -export type DailySchedule = DailySlot[]; - -export type MonthlyDay = { - day: number; - summary: string; -}; -export type MonthlySchedule = MonthlyDay[]; - -export type AvailabilityStatus = "online" | "do-not-disturb" | "offline"; -export type Availability = { - start: string; - end: string; - status: AvailabilityStatus; -}; -export type AvailabilityWindows = Availability[]; - export function pad2(n: number): string { return n < 10 ? `0${n}` : `${n}`; } diff --git a/src/commands/debug/schedule.test.ts b/src/commands/debug/schedule.test.ts index b89a369..3279286 100644 --- a/src/commands/debug/schedule.test.ts +++ b/src/commands/debug/schedule.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import type { Availability, DailySchedule, MonthlySchedule } from "@/brain/schedule"; +import type { + Availability, + DailySlot, + MonthlySchedule, +} from "@/openrouter/schema"; interface RecordedCall { model: unknown; @@ -8,8 +12,8 @@ interface RecordedCall { const llmCalls: RecordedCall[] = []; -function build48Slots(): DailySchedule { - const slots: DailySchedule = []; +function build48Slots(): DailySlot[] { + const slots: DailySlot[] = []; for (let i = 0; i < 48; i++) { const startHour = Math.floor(i / 2); const startMin = (i % 2) * 30; @@ -22,7 +26,7 @@ function build48Slots(): DailySchedule { const endMin = ((i + 1) % 2) * 30; end = `${String(endHour).padStart(2, "0")}:${String(endMin).padStart(2, "0")}`; } - slots.push({ start, end, activity: `slot-${i}` }); + slots.push({ start, end, activity: `slot-${i}`, notes: "" }); } return slots; } @@ -44,7 +48,8 @@ function buildMonthly(): MonthlySchedule { const mockCall = mock(async (_model: unknown, options: any) => { llmCalls.push({ model: _model, options }); - if (options.jsonSchemaName === "daily-schedule") return build48Slots(); + if (options.jsonSchemaName === "daily-schedule") + return { items: build48Slots() }; if (options.jsonSchemaName === "monthly-schedule") { const match = (options.message as string).match(/\((\d+) days\)/); const days = match ? parseInt(match[1]!, 10) : 30; @@ -100,7 +105,7 @@ describe("runDebugScheduleDaily", () => { if (!result.ok) throw new Error("expected ok"); expect(result.kind).toBe("daily"); - expect(result.schedule).toHaveLength(48); + expect(result.schedule.items).toHaveLength(48); expect(result.availability.length).toBeGreaterThan(0); expect(result.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/); diff --git a/src/commands/debug/schedule.ts b/src/commands/debug/schedule.ts index a5f7d8d..d982445 100644 --- a/src/commands/debug/schedule.ts +++ b/src/commands/debug/schedule.ts @@ -1,15 +1,13 @@ import type { Command } from "commander"; import ora from "ora"; import { Brain } from "@/brain"; -import { logger } from "@/utils/logger"; import { type Availability, type DailySchedule, type MonthlySchedule, - formatDateKey, - nextMonth, - pad2, -} from "@/brain/schedule"; +} from "@/openrouter/schema"; +import { logger } from "@/utils/logger"; +import { formatDateKey, nextMonth, pad2 } from "@/brain/schedule"; export interface ScheduleOptions { message: string; @@ -59,7 +57,7 @@ export async function runDebugScheduleDaily( return { ok: false, error: "Daily schedule generation failed" }; } scheduleSpinner.succeed( - `Daily schedule generated (${schedule.length} slots)`, + `Daily schedule generated (${schedule.items.length} slots)`, ); printSection( diff --git a/src/openrouter/schema.ts b/src/openrouter/schema.ts index acd33cd..3620f71 100644 --- a/src/openrouter/schema.ts +++ b/src/openrouter/schema.ts @@ -98,3 +98,46 @@ export const availabilitySchema = { required: ["start", "end", "status"], }, }; + +// ---------------------------------------------------------------------------- +// Types — co-located with their schemas. +// ---------------------------------------------------------------------------- + +/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */ +export type DailySlot = { + start: string; + end: string; + activity: string; + notes: string; +}; + +/** + * A complete daily schedule: a wrapped object containing exactly 48 half-hour + * slots. Matches `dailyScheduleSchema` (the LLM is constrained to return the + * `{ items: [...] }` envelope). + */ +export type DailySchedule = { + items: DailySlot[]; +}; + +/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items`. */ +export type MonthlyDay = { + day: number; + summary: string; +}; + +/** A complete monthly schedule. Matches `monthlyScheduleSchema`. */ +export type MonthlySchedule = MonthlyDay[]; + +/** Reachability status for a single availability window. */ +export type AvailabilityStatus = "online" | "do-not-disturb" | "offline"; + +/** A single availability window. Matches `availabilitySchema.items`. */ +export type Availability = { + start: string; + end: string; + status: AvailabilityStatus; +}; + +/** The full set of availability windows for a day. Matches `availabilitySchema`. */ +export type AvailabilityWindows = Availability[];