+
+
diff --git a/eval/fixtures/sveltekit-pms/src/lib/SpaceForm.svelte b/eval/fixtures/sveltekit-pms/src/lib/SpaceForm.svelte
new file mode 100644
index 0000000..902c477
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/SpaceForm.svelte
@@ -0,0 +1,163 @@
+
+
+
+
+
diff --git a/eval/fixtures/sveltekit-pms/src/lib/format.ts b/eval/fixtures/sveltekit-pms/src/lib/format.ts
new file mode 100644
index 0000000..c384ad8
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/format.ts
@@ -0,0 +1,50 @@
+/**
+ * Nightly rate for display. Deterministic (no `Intl`/locale) so the
+ * server-rendered markup matches the client on hydration.
+ */
+export function formatRate(rateCents: number | null): string | null {
+ if (rateCents === null) return null
+ const whole = Math.floor(rateCents / 100)
+ const cents = rateCents % 100
+ const grouped = String(whole).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+ return cents === 0
+ ? `$${grouped}`
+ : `$${grouped}.${String(cents).padStart(2, '0')}`
+}
+
+const MONTHS = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+]
+
+/**
+ * Format an ISO `YYYY-MM-DD` date without `new Date`/locale so the
+ * server-rendered markup matches the client on hydration.
+ */
+export function formatDate(iso: string): string {
+ const [year, month, day] = iso.split('-').map(Number)
+ if (!year || !month || !day) return iso
+ return `${MONTHS[month - 1]} ${day}, ${year}`
+}
+
+/** Whole nights between two ISO dates (half-open interval). */
+export function nights(checkIn: string, checkOut: string): number {
+ const toUtc = (iso: string) => {
+ const [year, month, day] = iso.split('-').map(Number)
+ return Date.UTC(year, month - 1, day)
+ }
+ return Math.max(
+ 0,
+ Math.round((toUtc(checkOut) - toUtc(checkIn)) / 86_400_000),
+ )
+}
diff --git a/eval/fixtures/sveltekit-pms/src/lib/schemas.ts b/eval/fixtures/sveltekit-pms/src/lib/schemas.ts
new file mode 100644
index 0000000..5150b5c
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/schemas.ts
@@ -0,0 +1,68 @@
+import { z } from 'zod'
+
+import { SPACE_KINDS } from '$lib/space-kinds'
+
+/**
+ * Shared validation schemas. Kept free of any database import so they can be
+ * used both to shape `+page.server.ts` form actions and to re-validate the
+ * payloads those actions build from `FormData`.
+ */
+
+/** Shape of the public booking form. */
+export const bookingInput = z
+ .object({
+ guestName: z.string().trim().min(1, 'Please enter your name'),
+ email: z.string().trim().email('Enter a valid email'),
+ phone: z.string().trim().min(5, 'Enter a valid phone number'),
+ checkIn: z.string().min(1, 'Choose a check-in date'),
+ checkOut: z.string().min(1, 'Choose a check-out date'),
+ partySize: z.coerce.number().int().min(1).max(20),
+ notes: z.string().trim().max(1000).optional(),
+ /** null = let the front desk assign a space later. */
+ spaceId: z.number().int().positive().nullable().default(null),
+ })
+ .refine((value) => value.checkOut > value.checkIn, {
+ message: 'Check-out must be after check-in',
+ path: ['checkOut'],
+ })
+
+export type BookingInput = z.infer
+
+export const statusInput = z.object({
+ id: z.number().int().positive(),
+ status: z.enum(['pending', 'confirmed', 'cancelled']),
+})
+
+export const assignInput = z.object({
+ id: z.number().int().positive(),
+ spaceId: z.number().int().positive().nullable(),
+})
+
+export const idInput = z.object({ id: z.number().int().positive() })
+
+/** Shape of the space create / edit form. */
+export const spaceInput = z.object({
+ name: z.string().trim().min(1, 'Give the space a name').max(80),
+ kind: z.enum(SPACE_KINDS),
+ capacity: z.coerce.number().int().min(1, 'At least 1').max(40),
+ /** Nightly rate in whole currency units; blank means "no rate set". */
+ rate: z.coerce.number().min(0).max(1_000_000).nullable().default(null),
+ notes: z.string().trim().max(500).nullable().default(null),
+})
+
+export type SpaceInput = z.infer
+
+export const spaceUpdateInput = spaceInput.extend({
+ id: z.number().int().positive(),
+})
+
+export const setSpaceStatusInput = z.object({
+ id: z.number().int().positive(),
+ status: z.enum(['active', 'archived']),
+})
+
+export const availabilityInput = z.object({
+ checkIn: z.string().min(1),
+ checkOut: z.string().min(1),
+ partySize: z.coerce.number().int().min(1),
+})
diff --git a/eval/fixtures/sveltekit-pms/src/lib/server/actions.ts b/eval/fixtures/sveltekit-pms/src/lib/server/actions.ts
new file mode 100644
index 0000000..0b5121d
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/server/actions.ts
@@ -0,0 +1,231 @@
+import { db } from '$lib/server/db'
+import type { Reservation, Space } from '$lib/server/db'
+import { assertSpaceBookable, bookedSpaceIds } from '$lib/server/availability'
+import {
+ assignInput,
+ availabilityInput,
+ bookingInput,
+ idInput,
+ setSpaceStatusInput,
+ spaceInput,
+ spaceUpdateInput,
+ statusInput,
+} from '$lib/schemas'
+import type { BookingInput, SpaceInput } from '$lib/schemas'
+
+/**
+ * The write path (plus the availability read that booking depends on). Each
+ * `+page.server.ts` form action builds a plain object from `FormData` and hands
+ * it here; these functions re-validate and mutate the database. Keeping them in
+ * `$lib/server/` means the DB never leaks into the client bundle.
+ */
+
+const SPACE_COLUMNS = `
+ id, name, kind, capacity, rate_cents AS rateCents, status, notes,
+ created_at AS createdAt
+`
+
+const RESERVATION_COLUMNS = `
+ id, guest_name AS guestName, email, phone, check_in AS checkIn,
+ check_out AS checkOut, party_size AS partySize, notes, space_id AS spaceId,
+ status, created_at AS createdAt
+`
+
+export type SpaceAvailability = Space & {
+ available: boolean
+ /** Why it can't be booked, when `available` is false. */
+ reason: string | null
+}
+
+/** Active spaces annotated with whether they can take the given stay. */
+export function listSpaceAvailability(input: {
+ checkIn: string
+ checkOut: string
+ partySize: number
+}): Array {
+ const data = availabilityInput.parse(input)
+
+ const active = db
+ .prepare(
+ `SELECT ${SPACE_COLUMNS} FROM spaces WHERE status = 'active' ORDER BY name ASC`,
+ )
+ .all() as Array
+ const booked = bookedSpaceIds(data.checkIn, data.checkOut)
+
+ return active.map((space) => {
+ if (booked.has(space.id))
+ return { ...space, available: false, reason: 'Booked for these dates' }
+ if (data.partySize > space.capacity)
+ return { ...space, available: false, reason: `Sleeps ${space.capacity}` }
+ return { ...space, available: true, reason: null }
+ })
+}
+
+/** Create a reservation from the public booking form. */
+export function createReservation(input: BookingInput): Reservation {
+ const data = bookingInput.parse(input)
+
+ if (data.spaceId !== null) {
+ assertSpaceBookable({
+ spaceId: data.spaceId,
+ checkIn: data.checkIn,
+ checkOut: data.checkOut,
+ partySize: data.partySize,
+ })
+ }
+
+ return db
+ .prepare(
+ `INSERT INTO reservations
+ (guest_name, email, phone, check_in, check_out, party_size, notes, space_id)
+ VALUES (@guestName, @email, @phone, @checkIn, @checkOut, @partySize, @notes, @spaceId)
+ RETURNING ${RESERVATION_COLUMNS}`,
+ )
+ .get({
+ guestName: data.guestName,
+ email: data.email,
+ phone: data.phone,
+ checkIn: data.checkIn,
+ checkOut: data.checkOut,
+ partySize: data.partySize,
+ notes: data.notes || null,
+ spaceId: data.spaceId,
+ }) as Reservation
+}
+
+/** Update a reservation's status (front desk). */
+export function updateReservationStatus(input: {
+ id: number
+ status: 'pending' | 'confirmed' | 'cancelled'
+}): Reservation {
+ const data = statusInput.parse(input)
+
+ const current = db
+ .prepare(`SELECT ${RESERVATION_COLUMNS} FROM reservations WHERE id = ?`)
+ .get(data.id) as Reservation | undefined
+ if (!current) throw new Error('That reservation no longer exists.')
+
+ // Cancelling releases the space, so reviving a cancelled stay has to win its
+ // space back — someone else may have taken it in the meantime.
+ if (
+ current.status === 'cancelled' &&
+ data.status !== 'cancelled' &&
+ current.spaceId !== null
+ ) {
+ assertSpaceBookable({
+ spaceId: current.spaceId,
+ checkIn: current.checkIn,
+ checkOut: current.checkOut,
+ partySize: current.partySize,
+ excludeReservationId: current.id,
+ })
+ }
+
+ return db
+ .prepare(
+ `UPDATE reservations SET status = ? WHERE id = ? RETURNING ${RESERVATION_COLUMNS}`,
+ )
+ .get(data.status, data.id) as Reservation
+}
+
+/** Assign, move, or clear a reservation's space (front desk). */
+export function assignReservationSpace(input: {
+ id: number
+ spaceId: number | null
+}): Reservation {
+ const data = assignInput.parse(input)
+
+ const current = db
+ .prepare(`SELECT ${RESERVATION_COLUMNS} FROM reservations WHERE id = ?`)
+ .get(data.id) as Reservation | undefined
+ if (!current) throw new Error('That reservation no longer exists.')
+
+ if (data.spaceId !== null) {
+ assertSpaceBookable({
+ spaceId: data.spaceId,
+ checkIn: current.checkIn,
+ checkOut: current.checkOut,
+ partySize: current.partySize,
+ excludeReservationId: current.id,
+ })
+ }
+
+ return db
+ .prepare(
+ `UPDATE reservations SET space_id = ? WHERE id = ? RETURNING ${RESERVATION_COLUMNS}`,
+ )
+ .get(data.spaceId, data.id) as Reservation
+}
+
+/** Delete a reservation (front desk). */
+export function deleteReservation(input: { id: number }): { id: number } {
+ const data = idInput.parse(input)
+ db.prepare('DELETE FROM reservations WHERE id = ?').run(data.id)
+ return { id: data.id }
+}
+
+export function createSpace(input: SpaceInput): Space {
+ const data = spaceInput.parse(input)
+ try {
+ return db
+ .prepare(
+ `INSERT INTO spaces (name, kind, capacity, rate_cents, notes)
+ VALUES (@name, @kind, @capacity, @rateCents, @notes)
+ RETURNING ${SPACE_COLUMNS}`,
+ )
+ .get(toRow(data)) as Space
+ } catch (err) {
+ rethrowNameCollision(err, data.name)
+ }
+}
+
+export function updateSpace(input: SpaceInput & { id: number }): Space {
+ const { id, ...rest } = spaceUpdateInput.parse(input)
+ try {
+ return db
+ .prepare(
+ `UPDATE spaces
+ SET name = @name, kind = @kind, capacity = @capacity,
+ rate_cents = @rateCents, notes = @notes
+ WHERE id = @id
+ RETURNING ${SPACE_COLUMNS}`,
+ )
+ .get({ ...toRow(rest), id }) as Space
+ } catch (err) {
+ rethrowNameCollision(err, rest.name)
+ }
+}
+
+/**
+ * Archive or restore a space. Archiving keeps it out of the booking picker
+ * without touching the reservations that already reference it.
+ */
+export function setSpaceStatus(input: {
+ id: number
+ status: 'active' | 'archived'
+}): Space {
+ const data = setSpaceStatusInput.parse(input)
+ return db
+ .prepare(
+ `UPDATE spaces SET status = ? WHERE id = ? RETURNING ${SPACE_COLUMNS}`,
+ )
+ .get(data.status, data.id) as Space
+}
+
+function toRow(data: SpaceInput) {
+ return {
+ name: data.name,
+ kind: data.kind,
+ capacity: data.capacity,
+ rateCents: data.rate === null ? null : Math.round(data.rate * 100),
+ notes: data.notes || null,
+ }
+}
+
+/** Names are unique, so surface the collision instead of a raw SQLite error. */
+function rethrowNameCollision(err: unknown, name: string): never {
+ const message = err instanceof Error ? err.message : ''
+ if (message.includes('UNIQUE') && message.includes('name'))
+ throw new Error(`A space named “${name}” already exists.`)
+ throw err instanceof Error ? err : new Error(String(err))
+}
diff --git a/eval/fixtures/sveltekit-pms/src/lib/server/availability.ts b/eval/fixtures/sveltekit-pms/src/lib/server/availability.ts
new file mode 100644
index 0000000..832dec9
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/server/availability.ts
@@ -0,0 +1,76 @@
+import { db } from '$lib/server/db'
+import type { Space } from '$lib/server/db'
+
+/**
+ * Availability helpers shared by booking and the front desk.
+ *
+ * Reservations hold a space for the half-open interval [checkIn, checkOut), so
+ * a same-day turnover (one guest out, the next in) is not a conflict. Cancelled
+ * reservations release the space. ISO `YYYY-MM-DD` dates sort lexicographically,
+ * so a text compare is a date compare.
+ */
+
+/** Space ids already held for the given range, excluding one reservation. */
+export function bookedSpaceIds(
+ checkIn: string,
+ checkOut: string,
+ excludeReservationId?: number,
+): Set {
+ let sql = `
+ SELECT DISTINCT space_id AS spaceId
+ FROM reservations
+ WHERE space_id IS NOT NULL
+ AND status != 'cancelled'
+ AND check_in < ?
+ AND check_out > ?
+ `
+ const params: Array = [checkOut, checkIn]
+
+ if (excludeReservationId !== undefined) {
+ sql += ' AND id != ?'
+ params.push(excludeReservationId)
+ }
+
+ const held = db.prepare(sql).all(...params) as Array<{ spaceId: number }>
+ return new Set(held.map((row) => row.spaceId))
+}
+
+/**
+ * Assert a space can take a stay, throwing a guest-readable message if not.
+ * Used by both booking and front-desk reassignment.
+ */
+export function assertSpaceBookable({
+ spaceId,
+ checkIn,
+ checkOut,
+ partySize,
+ excludeReservationId,
+}: {
+ spaceId: number
+ checkIn: string
+ checkOut: string
+ partySize: number
+ excludeReservationId?: number
+}): Space {
+ const space = db
+ .prepare(
+ `SELECT id, name, kind, capacity, rate_cents AS rateCents, status, notes,
+ created_at AS createdAt
+ FROM spaces WHERE id = ?`,
+ )
+ .get(spaceId) as Space | undefined
+
+ if (!space) throw new Error('That space no longer exists.')
+ if (space.status !== 'active')
+ throw new Error(`${space.name} is archived and can't be booked.`)
+ if (partySize > space.capacity)
+ throw new Error(
+ `${space.name} sleeps ${space.capacity}, but this stay is for ${partySize}.`,
+ )
+
+ const booked = bookedSpaceIds(checkIn, checkOut, excludeReservationId)
+ if (booked.has(spaceId))
+ throw new Error(`${space.name} is already booked for those dates.`)
+
+ return space
+}
diff --git a/eval/fixtures/sveltekit-pms/src/lib/server/db.ts b/eval/fixtures/sveltekit-pms/src/lib/server/db.ts
new file mode 100644
index 0000000..ab57a0a
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/server/db.ts
@@ -0,0 +1,94 @@
+import Database from 'better-sqlite3'
+import { env } from '$env/dynamic/private'
+
+import type { SpaceKind } from '$lib/space-kinds'
+
+/**
+ * The single better-sqlite3 handle for the app. Everything that touches the
+ * database lives under `$lib/server/`, so SvelteKit keeps it out of the client
+ * bundle. The schema is created on import (see below) — a fresh clone can run
+ * `npm run dev` with no separate migrate step.
+ */
+
+export type SpaceStatus = 'active' | 'archived'
+export type ReservationStatus = 'pending' | 'confirmed' | 'cancelled'
+
+/** A bookable space. Column names are aliased to camelCase in every query. */
+export type Space = {
+ id: number
+ name: string
+ kind: SpaceKind
+ capacity: number
+ rateCents: number | null
+ status: SpaceStatus
+ notes: string | null
+ createdAt: number | null
+}
+
+/** A single reservation held against (optionally) a space. */
+export type Reservation = {
+ id: number
+ guestName: string
+ email: string
+ phone: string
+ checkIn: string
+ checkOut: string
+ partySize: number
+ notes: string | null
+ spaceId: number | null
+ status: ReservationStatus
+ createdAt: number | null
+}
+
+const databasePath = env.DATABASE_URL ?? 'dev.db'
+
+export const db = new Database(databasePath)
+db.pragma('journal_mode = WAL')
+db.pragma('foreign_keys = ON')
+db.pragma('busy_timeout = 10000')
+
+/**
+ * Spaces are archived rather than deleted so past reservations keep pointing at
+ * something real; a reservation's space is nullable (ON DELETE SET NULL) so a
+ * stay can be taken before the front desk has decided which space it gets.
+ */
+db.exec(`
+ CREATE TABLE IF NOT EXISTS spaces (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ kind TEXT NOT NULL DEFAULT 'room',
+ capacity INTEGER NOT NULL DEFAULT 2,
+ rate_cents INTEGER,
+ status TEXT NOT NULL DEFAULT 'active',
+ notes TEXT,
+ created_at INTEGER DEFAULT (unixepoch())
+ );
+
+ CREATE TABLE IF NOT EXISTS reservations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ guest_name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ phone TEXT NOT NULL,
+ check_in TEXT NOT NULL,
+ check_out TEXT NOT NULL,
+ party_size INTEGER NOT NULL DEFAULT 1,
+ notes TEXT,
+ space_id INTEGER REFERENCES spaces(id) ON DELETE SET NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ created_at INTEGER DEFAULT (unixepoch())
+ );
+`)
+
+// Seed a few spaces the first time so the booking form's availability picker
+// isn't empty on a fresh clone.
+const seeded = db.prepare('SELECT count(*) AS count FROM spaces').get() as {
+ count: number
+}
+if (seeded.count === 0) {
+ const insert = db.prepare(
+ 'INSERT INTO spaces (name, kind, capacity, rate_cents, notes) VALUES (?, ?, ?, ?, ?)',
+ )
+ insert.run('Seagrass Suite', 'suite', 4, 24_000, 'Ocean view, walk-in shower')
+ insert.run('Dune Cabin', 'cabin', 2, 16_000, null)
+ insert.run('Harbor Room 101', 'room', 2, 12_000, null)
+}
diff --git a/eval/fixtures/sveltekit-pms/src/lib/server/queries.ts b/eval/fixtures/sveltekit-pms/src/lib/server/queries.ts
new file mode 100644
index 0000000..56a038b
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/server/queries.ts
@@ -0,0 +1,91 @@
+import { db } from '$lib/server/db'
+import type { Reservation, Space } from '$lib/server/db'
+import type { SpaceKind } from '$lib/space-kinds'
+
+/**
+ * Server-only read queries, called from each route's `+page.server.ts` load.
+ */
+
+const SPACE_COLUMNS = `
+ id, name, kind, capacity, rate_cents AS rateCents, status, notes,
+ created_at AS createdAt
+`
+
+const RESERVATION_COLUMNS = `
+ id, guest_name AS guestName, email, phone, check_in AS checkIn,
+ check_out AS checkOut, party_size AS partySize, notes, space_id AS spaceId,
+ status, created_at AS createdAt
+`
+
+/** A reservation plus the display fields of its assigned space. */
+export type ReservationRow = Reservation & {
+ spaceName: string | null
+ spaceKind: SpaceKind | null
+}
+
+/** All reservations, newest first, with the assigned space joined in. */
+export function listReservations(): Array {
+ return db
+ .prepare(
+ `SELECT
+ reservations.id AS id,
+ reservations.guest_name AS guestName,
+ reservations.email AS email,
+ reservations.phone AS phone,
+ reservations.check_in AS checkIn,
+ reservations.check_out AS checkOut,
+ reservations.party_size AS partySize,
+ reservations.notes AS notes,
+ reservations.space_id AS spaceId,
+ reservations.status AS status,
+ reservations.created_at AS createdAt,
+ spaces.name AS spaceName,
+ spaces.kind AS spaceKind
+ FROM reservations
+ LEFT JOIN spaces ON spaces.id = reservations.space_id
+ ORDER BY reservations.created_at DESC, reservations.id DESC`,
+ )
+ .all() as Array
+}
+
+export type Guest = {
+ name: string
+ email: string
+ phone: string
+ reservationCount: number
+}
+
+/** Unique guests (deduped by email), with how many reservations each has. */
+export function listGuests(): Array {
+ const all = db
+ .prepare(
+ `SELECT ${RESERVATION_COLUMNS} FROM reservations
+ ORDER BY created_at DESC, id DESC`,
+ )
+ .all() as Array
+
+ const byEmail = new Map()
+ for (const reservation of all) {
+ const key = reservation.email.trim().toLowerCase()
+ const existing = byEmail.get(key)
+ if (existing) {
+ existing.reservationCount += 1
+ } else {
+ // rows are newest-first, so the first hit is the guest's latest details
+ byEmail.set(key, {
+ name: reservation.guestName,
+ email: reservation.email,
+ phone: reservation.phone,
+ reservationCount: 1,
+ })
+ }
+ }
+ return [...byEmail.values()]
+}
+
+/** Every space, active first then alphabetical. */
+export function listSpaces(): Array {
+ return db
+ .prepare(`SELECT ${SPACE_COLUMNS} FROM spaces ORDER BY status ASC, name ASC`)
+ .all() as Array
+}
diff --git a/eval/fixtures/sveltekit-pms/src/lib/space-kinds.ts b/eval/fixtures/sveltekit-pms/src/lib/space-kinds.ts
new file mode 100644
index 0000000..0e30160
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/lib/space-kinds.ts
@@ -0,0 +1,25 @@
+/**
+ * The kinds of bookable space a property can offer, plus their labels.
+ *
+ * Kept free of any database import so both `+page.svelte` views and the
+ * server-only query layer can share it.
+ */
+export const SPACE_KINDS = [
+ 'room',
+ 'suite',
+ 'cabin',
+ 'villa',
+ 'tent',
+ 'other',
+] as const
+
+export type SpaceKind = (typeof SPACE_KINDS)[number]
+
+export const SPACE_KIND_LABELS: Record = {
+ room: 'Room',
+ suite: 'Suite',
+ cabin: 'Cabin',
+ villa: 'Villa',
+ tent: 'Tent',
+ other: 'Space',
+}
diff --git a/eval/fixtures/sveltekit-pms/src/routes/+layout.svelte b/eval/fixtures/sveltekit-pms/src/routes/+layout.svelte
new file mode 100644
index 0000000..812a6c9
--- /dev/null
+++ b/eval/fixtures/sveltekit-pms/src/routes/+layout.svelte
@@ -0,0 +1,48 @@
+
+
+
+ Your reservation
+ #{form.reservation.id}
+ is pending confirmation.
+ {#if form.reservation.spaceName}
+ We're holding {form.reservation.spaceName} for you.
+ {/if}
+ We'll be in touch by email shortly.
+