← Focus Timer history

test(examples): add failing behavioral tests before app implementation

Actual project-only Git commit. Email addresses are redacted; source bytes and recorded timestamps are preserved. This is a code record, not human acceptance.

commit 0e48e06b78618b9a8ced1aa78787fef20dcc8651
Author:     0xLeif <redacted@invalid>
AuthorDate: Sun Nov 8 19:26:05 2026 -0700
Commit:     0xLeif <redacted@invalid>
CommitDate: Sun Nov 8 19:26:05 2026 -0700

    test(examples): add failing behavioral tests before app implementation

diff --git a/apps/focus-timer/LICENSE b/apps/focus-timer/LICENSE
new file mode 100644
index 0000000..812de8f
--- /dev/null
+++ b/apps/focus-timer/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 CorvidLabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/focus-timer/package.json b/apps/focus-timer/package.json
new file mode 100644
index 0000000..1e1ed6b
--- /dev/null
+++ b/apps/focus-timer/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "focus-timer",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "license": "MIT",
+  "scripts": {
+    "test": "bun test",
+    "typecheck": "tsc --noEmit -p tsconfig.json",
+    "build": "bun build.ts",
+    "start": "bun serve.ts"
+  },
+  "devDependencies": {
+    "typescript": "5.9.3"
+  }
+}
diff --git a/apps/focus-timer/src/model.ts b/apps/focus-timer/src/model.ts
new file mode 100644
index 0000000..90b021a
--- /dev/null
+++ b/apps/focus-timer/src/model.ts
@@ -0,0 +1,23 @@
+// Red-phase API stubs. Root commits these failure inputs before implementation.
+export const DEFAULT_DURATION_MS = 750_000;
+export type TimerPhase = "idle" | "running" | "paused" | "completed";
+export interface TimerState {
+    readonly phase: TimerPhase;
+    readonly remainingMs: number;
+    readonly deadlineMs: number | null;
+}
+export function createTimer(): TimerState {
+    throw new Error("Not implemented: createTimer");
+}
+export function startTimer(state: TimerState, nowMs: number): TimerState {
+    throw new Error("Not implemented: startTimer");
+}
+export function pauseTimer(state: TimerState, nowMs: number): TimerState {
+    throw new Error("Not implemented: pauseTimer");
+}
+export function advanceTimer(state: TimerState, nowMs: number): TimerState {
+    throw new Error("Not implemented: advanceTimer");
+}
+export function resetTimer(): TimerState {
+    throw new Error("Not implemented: resetTimer");
+}
diff --git a/apps/focus-timer/tests/model.test.ts b/apps/focus-timer/tests/model.test.ts
new file mode 100644
index 0000000..496830a
--- /dev/null
+++ b/apps/focus-timer/tests/model.test.ts
@@ -0,0 +1,70 @@
+import { describe, expect, test } from "bun:test";
+import { DEFAULT_DURATION_MS, createTimer, startTimer, pauseTimer, advanceTimer, resetTimer, type TimerState } from "../src/model";
+const idle: TimerState = Object.freeze({ phase: "idle", remainingMs: 750_000, deadlineMs: null });
+const running: TimerState = Object.freeze({ phase: "running", remainingMs: 750_000, deadlineMs: 751_000 });
+
+describe("REQ-focus-timer-001", () => {
+    test("default and reset are idle twelve minutes thirty seconds", () => {
+        expect(DEFAULT_DURATION_MS).toBe(750_000);
+        expect(createTimer()).toEqual(idle);
+        expect(resetTimer()).toEqual(idle);
+    });
+    test("start sets a deadline; repeated start never extends it", () => {
+        const started = Object.freeze(startTimer(idle, 1_000));
+        expect(started).toEqual({ phase: "running", remainingMs: 750_000, deadlineMs: 751_000 });
+        expect(startTimer(started, 2_000)).toEqual({ phase: "running", remainingMs: 749_000, deadlineMs: 751_000 });
+        expect(idle.phase).toBe("idle");
+        expect(started.remainingMs).toBe(750_000);
+    });
+    test("elapsed time, pause and resume preserve exact subsecond budget", () => {
+        expect(advanceTimer(running, 101_000).remainingMs).toBe(650_000);
+        const paused = Object.freeze(pauseTimer(running, 126_000));
+        expect(paused).toEqual({ phase: "paused", remainingMs: 625_000, deadlineMs: null });
+        expect(advanceTimer(paused, 499_000)).toEqual(paused);
+        expect(pauseTimer(paused, 499_001)).toEqual(paused);
+        const resumed = Object.freeze(startTimer(paused, 500_000));
+        expect(resumed.deadlineMs).toBe(1_125_000);
+        expect(resumed.remainingMs).toBe(625_000);
+        expect(advanceTimer(resumed, 500_500).remainingMs).toBe(624_500);
+        expect(paused.remainingMs).toBe(625_000);
+        expect(running.remainingMs).toBe(750_000);
+    });
+    test("exact deadline completes once and clamps late observations to zero", () => {
+        expect(advanceTimer(running, 750_999)).toEqual({ phase: "running", remainingMs: 1, deadlineMs: 751_000 });
+        const done = Object.freeze(advanceTimer(running, 751_000));
+        expect(done).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+        expect(advanceTimer(done, 9_000_000)).toEqual(done);
+        expect(startTimer(done, 9_000_001)).toEqual(done);
+        expect(pauseTimer(done, 9_000_002)).toEqual(done);
+        expect(advanceTimer(running, 9_000_000)).toEqual(done);
+        expect(running.phase).toBe("running");
+    });
+    test("pause at or beyond expiry cannot resurrect a completed timer", () => {
+        for (const time of [751_000, 751_001, 9_000_000]) {
+            expect(pauseTimer(running, time)).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+        }
+    });
+    test("reset after each phase permits a fresh full deadline", () => {
+        const states = [running, pauseTimer(running, 126_000), advanceTimer(running, 751_000)];
+        for (const state of states) {
+            const snapshot = structuredClone(state);
+            const reset = Object.freeze(resetTimer());
+            expect(reset).toEqual(idle);
+            expect(startTimer(reset, 2_000_000).deadlineMs).toBe(2_750_000);
+            expect(state).toEqual(snapshot);
+        }
+        expect(advanceTimer(idle, 3_000_000)).toEqual(idle);
+        expect(pauseTimer(idle, 3_000_001)).toEqual(idle);
+    });
+    test("one background gap completes without counting callback ticks", () => {
+        const started = startTimer(idle, 10);
+        expect(advanceTimer(started, 750_011)).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+    });
+    test("invalid clock values require RangeError rather than generic stub error", () => {
+        for (const value of [-1, NaN, Infinity]) {
+            expect(() => startTimer(idle, value)).toThrow(RangeError);
+            expect(() => pauseTimer(running, value)).toThrow(RangeError);
+            expect(() => advanceTimer(running, value)).toThrow(RangeError);
+        }
+    });
+});
diff --git a/apps/focus-timer/tsconfig.json b/apps/focus-timer/tsconfig.json
new file mode 100644
index 0000000..3a66cf3
--- /dev/null
+++ b/apps/focus-timer/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": [
+      "ES2022",
+      "DOM"
+    ],
+    "strict": true,
+    "noEmit": true,
+    "types": [],
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": [
+    "src/**/*.ts"
+  ]
+}