aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-04-22 07:04:29 +0530
committerBobby <[email protected]>2026-04-22 07:04:29 +0530
commit9f0c3b0341d3ad29f3201d56fff5f19662a6f0b6 (patch)
treee53e1adb1a87b8a0faa4ee9a808155c21c0ab83c
parent4dfbf7a13120b3b64521fce6de7d5d3f76cd2084 (diff)
downloadhollowdark-9f0c3b0341d3ad29f3201d56fff5f19662a6f0b6.tar.xz
hollowdark-9f0c3b0341d3ad29f3201d56fff5f19662a6f0b6.zip
Use relative imports for same-directory siblings; split utils into folders
Two related cleanups landed together because they touch the same pattern: 1. Intra-module imports now use relative paths rather than the module's own path alias. rng/seeded.ts imports from './derive', not 'rng/derive'; time/gameTime.ts from './calendar', not 'time/calendar'; the utils barrel from './result' etc. This matches rules/01-code-style.md: "Relative imports only for same-directory siblings." The alias form is reserved for imports *between* modules — how external callers refer to a module's public surface. (The TS language server in the IDE had trouble resolving the self- aliased form even though svelte-check accepted it; this change removes the ambiguity.) 2. utils/ is now folder-per-concept. Each utility owns a directory with its files broken up into small pieces: utils/result/ types, constructors, predicates, map, unwrap utils/assert/ assert, assert-never, assert-defined utils/equal/ deep utils/types/ brand, deep-readonly, element-of, json, non-empty-array utils/log/ log utils/index.ts re-exports from each subfolder via the barrel. External callers import from 'utils' unchanged; internal references are relative. One reason to prefer folders over flat files here is headroom — when a concept grows, new files sit alongside their siblings inside the concept's folder rather than crowding the utils/ root. No API changes. All 128 tests still green.
-rw-r--r--rng/index.ts4
-rw-r--r--rng/seeded.ts2
-rw-r--r--time/gameTime.ts2
-rw-r--r--time/index.ts8
-rw-r--r--utils/assert.ts36
-rw-r--r--utils/assert/assert-defined.ts7
-rw-r--r--utils/assert/assert-never.ts16
-rw-r--r--utils/assert/assert.ts10
-rw-r--r--utils/assert/index.ts3
-rw-r--r--utils/equal/deep.ts (renamed from utils/equal.ts)4
-rw-r--r--utils/equal/index.ts1
-rw-r--r--utils/index.ts26
-rw-r--r--utils/log/index.ts1
-rw-r--r--utils/log/log.ts (renamed from utils/log.ts)14
-rw-r--r--utils/result.ts53
-rw-r--r--utils/result/constructors.ts9
-rw-r--r--utils/result/index.ts5
-rw-r--r--utils/result/map.ts12
-rw-r--r--utils/result/predicates.ts9
-rw-r--r--utils/result/types.ts14
-rw-r--r--utils/result/unwrap.ts17
-rw-r--r--utils/types.ts37
-rw-r--r--utils/types/brand.ts11
-rw-r--r--utils/types/deep-readonly.ts10
-rw-r--r--utils/types/element-of.ts2
-rw-r--r--utils/types/index.ts5
-rw-r--r--utils/types/json.ts8
-rw-r--r--utils/types/non-empty-array.ts2
28 files changed, 165 insertions, 163 deletions
diff --git a/rng/index.ts b/rng/index.ts
index fbc4251..b01e0dd 100644
--- a/rng/index.ts
+++ b/rng/index.ts
@@ -1,2 +1,2 @@
-export { createRNG, type SeededRNG } from 'rng/seeded'
-export { hashString, deriveSeed } from 'rng/derive'
+export { createRNG, type SeededRNG } from './seeded'
+export { hashString, deriveSeed } from './derive'
diff --git a/rng/seeded.ts b/rng/seeded.ts
index 8aff2fb..52c4719 100644
--- a/rng/seeded.ts
+++ b/rng/seeded.ts
@@ -1,4 +1,4 @@
-import { deriveSeed, hashString } from 'rng/derive'
+import { deriveSeed, hashString } from './derive'
/**
* Seeded PRNG. All gameplay randomness routes through this interface —
diff --git a/time/gameTime.ts b/time/gameTime.ts
index 7e56260..2f2fed7 100644
--- a/time/gameTime.ts
+++ b/time/gameTime.ts
@@ -8,7 +8,7 @@ import {
REGULAR_MONTH_COUNT,
daysInMonth,
monthName
-} from 'time/calendar'
+} from './calendar'
/**
* A position in game time. Immutable — all arithmetic returns a new value.
diff --git a/time/index.ts b/time/index.ts
index 34a518d..e111b0d 100644
--- a/time/index.ts
+++ b/time/index.ts
@@ -13,7 +13,7 @@ export {
monthSeason,
type MonthName,
type Season
-} from 'time/calendar'
+} from './calendar'
export {
addDays,
@@ -32,7 +32,7 @@ export {
toAbsoluteDays,
weeksBetween,
type GameTime
-} from 'time/gameTime'
+} from './gameTime'
export {
TICK_UNIT_BY_LIFE_STAGE,
@@ -40,6 +40,6 @@ export {
tickUnitForAge,
type LifeStage,
type TickUnit
-} from 'time/granularity'
+} from './granularity'
-export { SPEEDS, type Speed } from 'time/speed'
+export { SPEEDS, type Speed } from './speed'
diff --git a/utils/assert.ts b/utils/assert.ts
deleted file mode 100644
index 05de651..0000000
--- a/utils/assert.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * assert — throw if a condition is false, narrow the type otherwise.
- * For programmer errors (invariants that should never fail); use
- * Result<T, E> for recoverable failures.
- */
-export function assert(condition: unknown, message?: string): asserts condition {
- if (!condition) {
- throw new Error(message ?? 'Assertion failed')
- }
-}
-
-/**
- * Mark a branch as unreachable in an exhaustive switch. If the compiler
- * ever allows this to be reached, `value` stops being `never` and the
- * call site fails to type-check.
- *
- * Usage:
- * switch (kind) {
- * case 'a': ...
- * case 'b': ...
- * default: assertNever(kind)
- * }
- */
-export function assertNever(value: never, message?: string): never {
- throw new Error(message ?? `Unhandled case: ${String(value)}`)
-}
-
-/**
- * Throw if a value is null or undefined; return the narrowed value.
- */
-export function assertDefined<T>(value: T, message?: string): NonNullable<T> {
- if (value === null || value === undefined) {
- throw new Error(message ?? 'Expected value to be defined')
- }
- return value as NonNullable<T>
-}
diff --git a/utils/assert/assert-defined.ts b/utils/assert/assert-defined.ts
new file mode 100644
index 0000000..87af0e5
--- /dev/null
+++ b/utils/assert/assert-defined.ts
@@ -0,0 +1,7 @@
+/** Throw if a value is null or undefined; return the narrowed value. */
+export function assertDefined<T>(value: T, message?: string): NonNullable<T> {
+ if (value === null || value === undefined) {
+ throw new Error(message ?? 'Expected value to be defined')
+ }
+ return value as NonNullable<T>
+}
diff --git a/utils/assert/assert-never.ts b/utils/assert/assert-never.ts
new file mode 100644
index 0000000..406eca9
--- /dev/null
+++ b/utils/assert/assert-never.ts
@@ -0,0 +1,16 @@
+/**
+ * Mark a branch as unreachable in an exhaustive switch. If the compiler
+ * ever allows this line to be reached, `value` stops being `never` and
+ * the call site fails to type-check — making exhaustive checks load-bearing
+ * at compile time.
+ *
+ * Usage:
+ * switch (kind) {
+ * case 'a': ...
+ * case 'b': ...
+ * default: assertNever(kind)
+ * }
+ */
+export function assertNever(value: never, message?: string): never {
+ throw new Error(message ?? `Unhandled case: ${String(value)}`)
+}
diff --git a/utils/assert/assert.ts b/utils/assert/assert.ts
new file mode 100644
index 0000000..1fd70c8
--- /dev/null
+++ b/utils/assert/assert.ts
@@ -0,0 +1,10 @@
+/**
+ * Throw if a condition is false, narrow the type to truthy otherwise.
+ * For programmer errors (invariants that should never fail); use
+ * Result<T, E> for recoverable failures.
+ */
+export function assert(condition: unknown, message?: string): asserts condition {
+ if (!condition) {
+ throw new Error(message ?? 'Assertion failed')
+ }
+}
diff --git a/utils/assert/index.ts b/utils/assert/index.ts
new file mode 100644
index 0000000..a3df835
--- /dev/null
+++ b/utils/assert/index.ts
@@ -0,0 +1,3 @@
+export { assert } from './assert'
+export { assertDefined } from './assert-defined'
+export { assertNever } from './assert-never'
diff --git a/utils/equal.ts b/utils/equal/deep.ts
index 8e12216..d2c6ce2 100644
--- a/utils/equal.ts
+++ b/utils/equal/deep.ts
@@ -1,8 +1,8 @@
/**
* Structural deep equality, used by determinism tests to compare
* simulation snapshots. Handles plain objects, arrays, Maps, Sets, Dates,
- * and primitives. Does not handle class instances with custom equality
- * or cyclic object graphs — simulation state is a tree, not a graph.
+ * and primitives. Does not handle class instances with custom equality or
+ * cyclic object graphs — simulation state is a tree, not a graph.
*/
export function deepEqual(a: unknown, b: unknown): boolean {
if (Object.is(a, b)) return true
diff --git a/utils/equal/index.ts b/utils/equal/index.ts
new file mode 100644
index 0000000..3a937a8
--- /dev/null
+++ b/utils/equal/index.ts
@@ -0,0 +1 @@
+export { deepEqual } from './deep'
diff --git a/utils/index.ts b/utils/index.ts
index 4449e3e..436365f 100644
--- a/utils/index.ts
+++ b/utils/index.ts
@@ -1,21 +1,5 @@
-export {
- err,
- isErr,
- isOk,
- mapErr,
- mapResult,
- ok,
- unwrap,
- unwrapOr,
- type Err,
- type Ok,
- type Result
-} from 'utils/result'
-
-export { assert, assertDefined, assertNever } from 'utils/assert'
-
-export { deepEqual } from 'utils/equal'
-
-export type { Brand, DeepReadonly, ElementOf, JsonValue, NonEmptyArray } from 'utils/types'
-
-export { log } from 'utils/log'
+export * from './assert'
+export * from './equal'
+export * from './log'
+export * from './result'
+export * from './types'
diff --git a/utils/log/index.ts b/utils/log/index.ts
new file mode 100644
index 0000000..62d96a1
--- /dev/null
+++ b/utils/log/index.ts
@@ -0,0 +1 @@
+export { log } from './log'
diff --git a/utils/log.ts b/utils/log/log.ts
index 01e23f3..1b2cfec 100644
--- a/utils/log.ts
+++ b/utils/log/log.ts
@@ -11,16 +11,16 @@ function emit(level: LogLevel, message: string, context?: Record<string, unknown
const payload = context ? `${message} ${JSON.stringify(context)}` : message
switch (level) {
case 'debug':
- console.log(payload)
+ console.log(payload)
return
case 'info':
- console.info(payload)
+ console.info(payload)
return
case 'warn':
- console.warn(payload)
+ console.warn(payload)
return
case 'error':
- console.error(payload)
+ console.error(payload)
return
}
}
@@ -28,8 +28,10 @@ function emit(level: LogLevel, message: string, context?: Record<string, unknown
export const log = {
debug: (message: string, context?: Record<string, unknown>): void =>
emit('debug', message, context),
- info: (message: string, context?: Record<string, unknown>): void => emit('info', message, context),
- warn: (message: string, context?: Record<string, unknown>): void => emit('warn', message, context),
+ info: (message: string, context?: Record<string, unknown>): void =>
+ emit('info', message, context),
+ warn: (message: string, context?: Record<string, unknown>): void =>
+ emit('warn', message, context),
error: (message: string, context?: Record<string, unknown>): void =>
emit('error', message, context)
} as const
diff --git a/utils/result.ts b/utils/result.ts
deleted file mode 100644
index cfe487f..0000000
--- a/utils/result.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Result<T, E> — a recoverable-error return type for functions that can
- * fail without throwing. Used at system boundaries (save/load, content
- * validation, manifest fetch) where the caller needs to decide whether a
- * failure is fatal. Internal pure logic uses plain returns and throws
- * Error for programmer errors (rules/01-code-style.md).
- */
-
-export type Ok<T> = { readonly ok: true; readonly value: T }
-export type Err<E> = { readonly ok: false; readonly error: E }
-export type Result<T, E = Error> = Ok<T> | Err<E>
-
-export function ok<T>(value: T): Ok<T> {
- return { ok: true, value }
-}
-
-export function err<E>(error: E): Err<E> {
- return { ok: false, error }
-}
-
-export function isOk<T, E>(r: Result<T, E>): r is Ok<T> {
- return r.ok
-}
-
-export function isErr<T, E>(r: Result<T, E>): r is Err<E> {
- return !r.ok
-}
-
-/** Map the value of an ok result; pass errors through unchanged. */
-export function mapResult<T, U, E>(r: Result<T, E>, f: (value: T) => U): Result<U, E> {
- return r.ok ? ok(f(r.value)) : r
-}
-
-/** Map the error of a failed result; pass values through unchanged. */
-export function mapErr<T, E, F>(r: Result<T, E>, f: (error: E) => F): Result<T, F> {
- return r.ok ? r : err(f(r.error))
-}
-
-/**
- * Extract the value from an ok result; throw on error. Reserve for tests
- * and assertion contexts — in production code prefer branching on isOk.
- */
-export function unwrap<T, E>(r: Result<T, E>): T {
- if (!r.ok) {
- throw new Error(`unwrap() called on error result: ${String(r.error)}`)
- }
- return r.value
-}
-
-/** Extract the value or return a default. */
-export function unwrapOr<T, E>(r: Result<T, E>, fallback: T): T {
- return r.ok ? r.value : fallback
-}
diff --git a/utils/result/constructors.ts b/utils/result/constructors.ts
new file mode 100644
index 0000000..b0ed958
--- /dev/null
+++ b/utils/result/constructors.ts
@@ -0,0 +1,9 @@
+import type { Err, Ok } from './types'
+
+export function ok<T>(value: T): Ok<T> {
+ return { ok: true, value }
+}
+
+export function err<E>(error: E): Err<E> {
+ return { ok: false, error }
+}
diff --git a/utils/result/index.ts b/utils/result/index.ts
new file mode 100644
index 0000000..931b638
--- /dev/null
+++ b/utils/result/index.ts
@@ -0,0 +1,5 @@
+export type { Err, Ok, Result } from './types'
+export { err, ok } from './constructors'
+export { isErr, isOk } from './predicates'
+export { mapErr, mapResult } from './map'
+export { unwrap, unwrapOr } from './unwrap'
diff --git a/utils/result/map.ts b/utils/result/map.ts
new file mode 100644
index 0000000..9cf44a1
--- /dev/null
+++ b/utils/result/map.ts
@@ -0,0 +1,12 @@
+import { err, ok } from './constructors'
+import type { Result } from './types'
+
+/** Map the value of an ok result; pass errors through unchanged. */
+export function mapResult<T, U, E>(r: Result<T, E>, f: (value: T) => U): Result<U, E> {
+ return r.ok ? ok(f(r.value)) : r
+}
+
+/** Map the error of a failed result; pass values through unchanged. */
+export function mapErr<T, E, F>(r: Result<T, E>, f: (error: E) => F): Result<T, F> {
+ return r.ok ? r : err(f(r.error))
+}
diff --git a/utils/result/predicates.ts b/utils/result/predicates.ts
new file mode 100644
index 0000000..315eb0d
--- /dev/null
+++ b/utils/result/predicates.ts
@@ -0,0 +1,9 @@
+import type { Err, Ok, Result } from './types'
+
+export function isOk<T, E>(r: Result<T, E>): r is Ok<T> {
+ return r.ok
+}
+
+export function isErr<T, E>(r: Result<T, E>): r is Err<E> {
+ return !r.ok
+}
diff --git a/utils/result/types.ts b/utils/result/types.ts
new file mode 100644
index 0000000..ee95c08
--- /dev/null
+++ b/utils/result/types.ts
@@ -0,0 +1,14 @@
+/** A successful result carrying a value. */
+export type Ok<T> = { readonly ok: true; readonly value: T }
+
+/** A failed result carrying an error. */
+export type Err<E> = { readonly ok: false; readonly error: E }
+
+/**
+ * Result<T, E> — a recoverable-error return type for functions that can
+ * fail without throwing. Used at system boundaries (save/load, content
+ * validation, manifest fetch) where the caller needs to decide whether a
+ * failure is fatal. Internal pure logic uses plain returns and throws
+ * Error for programmer errors (rules/01-code-style.md).
+ */
+export type Result<T, E = Error> = Ok<T> | Err<E>
diff --git a/utils/result/unwrap.ts b/utils/result/unwrap.ts
new file mode 100644
index 0000000..84e658e
--- /dev/null
+++ b/utils/result/unwrap.ts
@@ -0,0 +1,17 @@
+import type { Result } from './types'
+
+/**
+ * Extract the value from an ok result; throw on error. Reserve for tests
+ * and assertion contexts — in production code prefer branching on isOk.
+ */
+export function unwrap<T, E>(r: Result<T, E>): T {
+ if (!r.ok) {
+ throw new Error(`unwrap() called on error result: ${String(r.error)}`)
+ }
+ return r.value
+}
+
+/** Extract the value or return a fallback. */
+export function unwrapOr<T, E>(r: Result<T, E>, fallback: T): T {
+ return r.ok ? r.value : fallback
+}
diff --git a/utils/types.ts b/utils/types.ts
deleted file mode 100644
index a9629c8..0000000
--- a/utils/types.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/** A non-empty readonly array; the type system guarantees index 0 exists. */
-export type NonEmptyArray<T> = readonly [T, ...T[]]
-
-/** Values that round-trip through JSON.stringify / JSON.parse. */
-export type JsonValue =
- | string
- | number
- | boolean
- | null
- | readonly JsonValue[]
- | { readonly [key: string]: JsonValue | undefined }
-
-/** Element type of a readonly array (e.g., `ElementOf<typeof MONTH_NAMES>`). */
-export type ElementOf<T extends readonly unknown[]> = T extends readonly (infer E)[] ? E : never
-
-/** Deep-readonly version of a structural type. */
-export type DeepReadonly<T> = T extends (infer U)[]
- ? ReadonlyArray<DeepReadonly<U>>
- : T extends Map<infer K, infer V>
- ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
- : T extends Set<infer U>
- ? ReadonlySet<DeepReadonly<U>>
- : T extends object
- ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
- : T
-
-/**
- * Nominal / branded type: compile-time-only tag to distinguish otherwise
- * structurally-equal types.
- *
- * type PersonId = Brand<string, 'PersonId'>
- * type RelId = Brand<string, 'RelationshipId'>
- *
- * PersonId and RelId are still strings at runtime but aren't assignable
- * to each other without an explicit cast.
- */
-export type Brand<T, B extends string> = T & { readonly __brand: B }
diff --git a/utils/types/brand.ts b/utils/types/brand.ts
new file mode 100644
index 0000000..d1b7977
--- /dev/null
+++ b/utils/types/brand.ts
@@ -0,0 +1,11 @@
+/**
+ * Nominal / branded type: compile-time-only tag to distinguish otherwise
+ * structurally-equal types.
+ *
+ * type PersonId = Brand<string, 'PersonId'>
+ * type RelId = Brand<string, 'RelationshipId'>
+ *
+ * PersonId and RelId are still strings at runtime but aren't assignable
+ * to each other without an explicit cast.
+ */
+export type Brand<T, B extends string> = T & { readonly __brand: B }
diff --git a/utils/types/deep-readonly.ts b/utils/types/deep-readonly.ts
new file mode 100644
index 0000000..dddb6d6
--- /dev/null
+++ b/utils/types/deep-readonly.ts
@@ -0,0 +1,10 @@
+/** Deep-readonly version of a structural type. */
+export type DeepReadonly<T> = T extends (infer U)[]
+ ? ReadonlyArray<DeepReadonly<U>>
+ : T extends Map<infer K, infer V>
+ ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
+ : T extends Set<infer U>
+ ? ReadonlySet<DeepReadonly<U>>
+ : T extends object
+ ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
+ : T
diff --git a/utils/types/element-of.ts b/utils/types/element-of.ts
new file mode 100644
index 0000000..fcb144d
--- /dev/null
+++ b/utils/types/element-of.ts
@@ -0,0 +1,2 @@
+/** Element type of a readonly array (e.g., `ElementOf<typeof MONTH_NAMES>`). */
+export type ElementOf<T extends readonly unknown[]> = T extends readonly (infer E)[] ? E : never
diff --git a/utils/types/index.ts b/utils/types/index.ts
new file mode 100644
index 0000000..cf55ea7
--- /dev/null
+++ b/utils/types/index.ts
@@ -0,0 +1,5 @@
+export type { Brand } from './brand'
+export type { DeepReadonly } from './deep-readonly'
+export type { ElementOf } from './element-of'
+export type { JsonValue } from './json'
+export type { NonEmptyArray } from './non-empty-array'
diff --git a/utils/types/json.ts b/utils/types/json.ts
new file mode 100644
index 0000000..0aa12c8
--- /dev/null
+++ b/utils/types/json.ts
@@ -0,0 +1,8 @@
+/** Values that round-trip through JSON.stringify / JSON.parse. */
+export type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | readonly JsonValue[]
+ | { readonly [key: string]: JsonValue | undefined }
diff --git a/utils/types/non-empty-array.ts b/utils/types/non-empty-array.ts
new file mode 100644
index 0000000..59a6160
--- /dev/null
+++ b/utils/types/non-empty-array.ts
@@ -0,0 +1,2 @@
+/** A non-empty readonly array; the type system guarantees index 0 exists. */
+export type NonEmptyArray<T> = readonly [T, ...T[]]