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"
+ ]
+}