← Target Tap 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 a83f0c7fbe7750002ceef66359c82b82f68f5dba
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/target-tap/LICENSE b/apps/target-tap/LICENSE
new file mode 100644
index 0000000..812de8f
--- /dev/null
+++ b/apps/target-tap/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/target-tap/package.json b/apps/target-tap/package.json
new file mode 100644
index 0000000..c785394
--- /dev/null
+++ b/apps/target-tap/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "target-tap",
+  "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/target-tap/src/model.ts b/apps/target-tap/src/model.ts
new file mode 100644
index 0000000..daa5e7f
--- /dev/null
+++ b/apps/target-tap/src/model.ts
@@ -0,0 +1,28 @@
+// Red-phase API stubs. Root commits these failure inputs before implementation.
+export const ROUND_DURATION_MS = 20_000;
+export type RandomSource = () => number;
+export type RoundPhase = "ready" | "running" | "finished";
+export interface Target {
+    readonly id: number;
+    readonly x: number;
+    readonly y: number;
+}
+export interface RoundState {
+    readonly phase: RoundPhase;
+    readonly score: number;
+    readonly deadlineMs: number | null;
+    readonly target: Target | null;
+    readonly targetSerial: number;
+}
+export function createRound(): RoundState {
+    throw new Error("Not implemented: createRound");
+}
+export function startRound(state: RoundState, nowMs: number, random: RandomSource): RoundState {
+    throw new Error("Not implemented: startRound");
+}
+export function hitTarget(state: RoundState, targetId: number, nowMs: number, random: RandomSource): RoundState {
+    throw new Error("Not implemented: hitTarget");
+}
+export function advanceRound(state: RoundState, nowMs: number): RoundState {
+    throw new Error("Not implemented: advanceRound");
+}
diff --git a/apps/target-tap/tests/model.test.ts b/apps/target-tap/tests/model.test.ts
new file mode 100644
index 0000000..e7729cb
--- /dev/null
+++ b/apps/target-tap/tests/model.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, test } from "bun:test";
+import { ROUND_DURATION_MS, createRound, startRound, hitTarget, advanceRound, type RoundState, type Target } from "../src/model";
+
+const random = (): number => 0.5;
+function target(state: RoundState): Target {
+    if (!state.target) throw new Error("Expected an eligible target");
+    return state.target;
+}
+function frozen(state: RoundState): RoundState {
+    if (state.target) Object.freeze(state.target);
+    return Object.freeze(state);
+}
+
+describe("REQ-target-tap-001", () => {
+    test("starts a fixed twenty-second round from exact ready values", () => {
+        const ready = frozen(createRound());
+        expect(ready).toEqual({ phase: "ready", score: 0, deadlineMs: null, target: null, targetSerial: 0 });
+        expect(ROUND_DURATION_MS).toBe(20_000);
+        const state = startRound(ready, 1_000, random);
+        expect(state.phase).toBe("running");
+        expect(state.deadlineMs).toBe(21_000);
+        expect(state.score).toBe(0);
+        expect(target(state).id).toBeGreaterThan(0);
+        expect(target(state).x).toBeCloseTo(0.5);
+        expect(target(state).y).toBeCloseTo(0.5);
+        expect(ready.phase).toBe("ready");
+    });
+    test("one eligible target scores once; its replacement remains hittable", () => {
+        let draws = 0;
+        const rng = (): number => { draws += 1; return 0.5; };
+        const running = frozen(startRound(createRound(), 1_000, rng));
+        const oldId = target(running).id;
+        const hit = frozen(hitTarget(running, oldId, 20_999, rng));
+        expect(hit.score).toBe(1);
+        expect(target(hit).id).not.toBe(oldId);
+        const drawsAfterHit = draws;
+        expect(hitTarget(hit, oldId, 20_999, rng)).toEqual(hit);
+        expect(draws).toBe(drawsAfterHit);
+        expect(hitTarget(hit, target(hit).id, 20_999, rng).score).toBe(2);
+        expect(running.score).toBe(0);
+    });
+    test("pre-start and unknown targets do not score or draw randomness", () => {
+        const ready = frozen(createRound());
+        let draws = 0;
+        const rng = (): number => { draws += 1; return 0; };
+        expect(hitTarget(ready, 123, 0, rng)).toEqual(ready);
+        expect(draws).toBe(0);
+        const running = frozen(startRound(ready, 1_000, rng));
+        const before = draws;
+        expect(hitTarget(running, -1, 1_001, rng)).toEqual(running);
+        expect(draws).toBe(before);
+    });
+    test("exact deadline rejects hits, finishes and never adds a grace frame", () => {
+        const running = frozen(startRound(createRound(), 1_000, random));
+        expect(advanceRound(running, 20_999).phase).toBe("running");
+        for (const time of [21_000, 21_001, 99_000]) {
+            const expired = hitTarget(running, target(running).id, time, random);
+            expect(expired.phase).toBe("finished");
+            expect(expired.score).toBe(0);
+            expect(expired.target).toBeNull();
+        }
+        expect(running.phase).toBe("running");
+    });
+    test("running start preserves score/deadline; finish and restart reject old identity", () => {
+        const first = startRound(createRound(), 1_000, random);
+        const scored = frozen(hitTarget(first, target(first).id, 2_000, random));
+        expect(startRound(scored, 3_000, random)).toEqual(scored);
+        const done = frozen(advanceRound(scored, 21_000));
+        expect(done.phase).toBe("finished");
+        expect(done.score).toBe(1);
+        expect(advanceRound(done, 40_000)).toEqual(done);
+        const restarted = frozen(startRound(done, 50_000, random));
+        expect(restarted.deadlineMs).toBe(70_000);
+        expect(restarted.score).toBe(0);
+        expect(target(restarted).id).toBeGreaterThan(target(scored).id);
+        expect(hitTarget(restarted, target(scored).id, 50_001, random)).toEqual(restarted);
+    });
+    test("injected randomness deterministically respects safe center bounds", () => {
+        const low = startRound(createRound(), 0, () => 0);
+        const high = startRound(createRound(), 0, () => 0.999999);
+        expect(target(low).x).toBeCloseTo(0.12);
+        expect(target(low).y).toBeCloseTo(0.12);
+        expect(target(high).x).toBeLessThan(0.88);
+        expect(target(high).y).toBeLessThan(0.88);
+        expect(startRound(createRound(), 0, random)).toEqual(startRound(createRound(), 0, random));
+    });
+    test("invalid random input specifically requires RangeError", () => {
+        const ready: RoundState = Object.freeze({ phase: "ready", score: 0, deadlineMs: null, target: null, targetSerial: 0 });
+        for (const value of [-1, 1, NaN, Infinity]) expect(() => startRound(ready, 0, () => value)).toThrow(RangeError);
+    });
+    test("invalid clock input specifically requires RangeError", () => {
+        const ready: RoundState = Object.freeze({ phase: "ready", score: 0, deadlineMs: null, target: null, targetSerial: 0 });
+        for (const value of [-1, NaN, Infinity]) {
+            expect(() => startRound(ready, value, random)).toThrow(RangeError);
+            expect(() => advanceRound(ready, value)).toThrow(RangeError);
+            expect(() => hitTarget(ready, 1, value, random)).toThrow(RangeError);
+        }
+    });
+});
diff --git a/apps/target-tap/tsconfig.json b/apps/target-tap/tsconfig.json
new file mode 100644
index 0000000..3a66cf3
--- /dev/null
+++ b/apps/target-tap/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"
+  ]
+}