diff --git a/.claude/skills/code-style/SKILL.md b/.claude/skills/code-style/SKILL.md index 2afb3eafb..1db975a9b 100644 --- a/.claude/skills/code-style/SKILL.md +++ b/.claude/skills/code-style/SKILL.md @@ -156,34 +156,26 @@ implementing that algorithm — forcing list verbs onto textbook terms hides the ## Comments -Two comment jobs, two locations. A JSDoc block on a declaration carries the caller-facing contract: -what a reader needs to use the thing without opening its body — the guarantee, the invariants a -caller must uphold, the failure modes. A `//` at a statement carries the implementation note: why -that line does the non-obvious thing. A body's mechanism — how the algorithm walks, which step does -what — is never narrated from the top; it lives at the lines, or nowhere when the code already shows -it. - -- Prefer the enforceable form. Before writing a comment, put the fact where a machine holds it: - encode an outcome set as a discriminated union, a bound as a named constant, a caller rule as a - type; protect a frozen wire or draw layout with a golden test. Comment only the residue neither a - type nor a test can hold, and where a test enforces an invariant, point at it rather than - restating the consequence. -- Comment the decision, not the code. A comment states an invariant, cross-file or runtime behavior, - or why a non-obvious choice was made. One that restates the name, the signature, or the next - line's mechanics is a defect — delete it. -- A long JSDoc block is a placement smell, not a prose exercise. When a declaration's comment runs - long because it narrates the body, relocate: the mechanism to `//` at the lines, the enforceable - parts into types or tests, leaving the block at the contract. A genuinely irreducible multi-point - contract stays — render it as structured prose (one point per paragraph, led by its topic - sentence; one fact per sentence; an outcome map or state-to-action table as a bullet list) and - load the `docs-writing` skill for its wording. -- JSDoc blocks are always multi-line (`/**` alone, one `*`-prefixed line per point, `*/` alone — - never single-line `/** … */`), attached directly to the declaration they describe. +Two lint rules own comment shape, and neither has an exception for authored code or a baseline +marker (generated output gets a per-file override in `.oxlintrc.json`): `zgeoff/no-jsdoc` bans every +`/** … */` block, and `zgeoff/max-consecutive-line-comments` bans a run of more than three +consecutive `//` lines. A fact a reader needs has a home that stays true: the code, a type, a named +constant, a test whose name states the rule, or the subsystem's doc under `docs/architecture/`. A +comment holds only the residue none of those can hold. + +- Write a `//` comment for one thing: the reason the obvious alternative is wrong, when the cause + lives outside the file. A library quirk, a runtime or platform behavior, a parser or compiler + rule, a production incident. Place it at the line or declaration it explains, three lines at most. +- Never write what the code shows: what a function does, its parameters, its outcomes, an invariant + a test already states, which step does what. An agent derives those from the code, its tests, and + its references; a comment that caches them goes stale. +- A caller-facing contract is a test whose name states it, or a sentence in the subsystem doc. A + design decision is a sentence in the subsystem doc. - Comments describe the code as it is now — no history ("previously", "now uses"), no project state (issue numbers, phase labels, "not wired yet"); those live in the commit message. -- Comments don't name other declarations — renames strand the reference. State the contract instead: - "callers must pass edits sorted last-to-first", not "(buildEditsFromAST's contract)". A - declaration's own parameters and signature types are fine to name. +- Comments don't name other declarations — renames strand the reference. State the fact instead: "an + append from any other session is rejected", not "(see appendCheckpoints)". A declaration's own + parameters and signature types are fine to name. ## Type-only modules diff --git a/.claude/skills/docs-writing/SKILL.md b/.claude/skills/docs-writing/SKILL.md index c01544650..e005b721a 100644 --- a/.claude/skills/docs-writing/SKILL.md +++ b/.claude/skills/docs-writing/SKILL.md @@ -1,8 +1,8 @@ --- name: docs-writing description: - Prose rules for everything committed to the repo — docs/, READMEs, AGENTS.md, skills, and doc - comments. Use when writing, editing, or reviewing any repo prose. + Prose rules for everything committed to the repo — docs/, READMEs, AGENTS.md, and skills. Use when + writing, editing, or reviewing any repo prose. --- # Docs writing diff --git a/.oxlintrc.json b/.oxlintrc.json index b58dd13dd..3a86eb1f4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -446,10 +446,11 @@ }, { "files": ["**/schema.generated.ts"], - // kysely-codegen column overrides emit inline `import()` type annotations; the file is - // generated, so the style rule has nothing to teach it + // kysely-codegen emits inline `import()` type annotations and a JSDoc header; the file is + // generated, so the style rules have nothing to teach it "rules": { - "typescript/consistent-type-imports": "off" + "typescript/consistent-type-imports": "off", + "zgeoff/no-jsdoc": "off" } }, { diff --git a/AGENTS.md b/AGENTS.md index dd65d6e41..297a62002 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ read the architecture doc for the subsystem in full before reasoning from its co | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `code-style` | writing, reviewing, or renaming any TypeScript — the function-verb taxonomy lives there | | `testing` | designing, writing, or reviewing tests | -| `docs-writing` | writing or editing any committed prose: `docs/`, READMEs, this file, skills, doc comments | +| `docs-writing` | writing or editing any committed prose: `docs/`, READMEs, this file, skills | | `delivery-lead` | triage, refinement, milestone, board, or "what's next" work — the increment model and the roles live there | | `game-lifecycle` | any work in the activity, replay, or idle packages — the assumptions to drop and the state machines | @@ -341,6 +341,9 @@ the mechanics and provisioning. `// oxlint-disable-next-line -- baseline(#236)`, never turned off in config. The unused-directive check is the ratchet: fixing a baselined site strands its comment, and lint fails until the comment is deleted. +- `zgeoff/no-jsdoc` and `zgeoff/max-consecutive-line-comments` are never baselined and never + disabled inline: a comment the rules reject is deleted or cut, and the fact it held moves to a + test name or the subsystem doc (the `code-style` skill owns the rule for what a comment holds). - `typescript/prefer-readonly-parameter-types` is never baselined: a function's own data/config/props/option types go `readonly` (React props `Readonly`), and framework handles with no readonly form (a `Kysely`/`Elysia`/`RPCHandler`/`Request` handle, a `Date`, …) are diff --git a/apps/web-e2e/benchmarks/drag-pan.spec.ts b/apps/web-e2e/benchmarks/drag-pan.spec.ts index 3eb06a213..4ca137efd 100644 --- a/apps/web-e2e/benchmarks/drag-pan.spec.ts +++ b/apps/web-e2e/benchmarks/drag-pan.spec.ts @@ -2,56 +2,19 @@ import { expect, test } from '../src/test'; import { waitForHoneypotWindow } from '../src/wait-for-honeypot-window'; import { waitForStableFrames } from '../src/wait-for-stable-frames'; -/** - * How many drag legs to walk. Each leg drags 60% of the canvas width in the same direction, so - * travel accumulates leg over leg — enough total ground to cross many chunk boundaries without - * reading the client's internal chunk-size constants, which would couple this black-box benchmark - * to worldmap-client's geometry. Direction never alternates: a there-and-back oscillation would - * revisit the same chunks, and once chunk generation is cached the benchmark would measure cache - * replays instead of the generation cost it exists to track. - */ +// legs never alternate direction: a there-and-back drag revisits chunks whose generation is already +// cached, so the benchmark would measure cache replays instead of the generation cost it tracks const DRAG_LEG_COUNT = 12; - -/** - * Intermediate pointer-move events per leg. Camera-controls reads a drag as a series of pointermove - * deltas, not a single teleport — too few steps understates a real drag's incremental chunk - * crossings. - */ const DRAG_STEPS_PER_LEG = 20; - -/** - * A frame gap past this many milliseconds — more than one missed vsync at 60fps — counts as a - * dropped frame. - */ const DROPPED_FRAME_THRESHOLD_MS = 32; -/** - * The frame-gap sampler's state, parked on the page's own `globalThis` so it survives across the - * two separate `page.evaluate` round trips that start and read it. - */ interface DragPanWindow { __dragPanFrameGaps: Array; __dragPanFrameLoopID: number; } -/** - * Repeatable drag-pan performance probe for the explore map. Reports peak frame gap and - * dropped-frame count for a multi-leg one-way drag across the world map to `console.log` and a - * test annotation; it makes no pass/fail claim about specific numbers, since headless-GPU - * throughput varies by machine. - * - * Excluded from every default run: this config's `testDir` is `./benchmarks`, never scanned by - * `playwright.config.ts`'s `./specs`, so neither `bun run e2e` nor CI ever picks it up. The app - * server serves the prebuilt artifact, so build first, then run on demand: - * - * ```sh - * bun run build --filter=@vers/web - * bun run --cwd apps/web-e2e e2e:bench -- --headed - * ``` - * - * `--headed` is recommended: headless Chromium's software WebGPU path measures compositor - * throughput this benchmark doesn't care about, not the app's own frame cost. - */ +// run with `--headed`: headless Chromium's software WebGPU path measures compositor throughput +// rather than the app's own frame cost test('it drag-pans across the explore map and reports peak frame gap and dropped frames', async ({ page, }) => { @@ -87,10 +50,9 @@ test('it drag-pans across the explore map and reports peak frame gap and dropped const leftX = box.x + box.width * 0.2; const rightX = box.x + box.width * 0.8; - // a throwaway warm-up leg, run before the sampler is installed, so the measured legs below don't - // pay for whatever a drag itself first warms up (pointer-event handler JIT, first-drag camera- - // controls allocations) on top of the scene readiness waitForStableFrames already gated on. Ends - // at rightX, where the first measured leg (below) starts, so no extra jump sits between them. + // a throwaway warm-up leg, run before the sampler is installed, so the measured legs don't pay + // for what a first drag itself warms up (pointer-event handler JIT, first-drag camera-controls + // allocations). Ends at rightX, where the first measured leg starts. await page.mouse.move(leftX, centerY); await page.mouse.down(); await page.mouse.move(rightX, centerY, { steps: DRAG_STEPS_PER_LEG }); diff --git a/apps/web-e2e/playwright.bench.config.ts b/apps/web-e2e/playwright.bench.config.ts index baad56538..6edfb7924 100644 --- a/apps/web-e2e/playwright.bench.config.ts +++ b/apps/web-e2e/playwright.bench.config.ts @@ -6,11 +6,6 @@ import type { E2EOptions } from './src/test'; // measuring the real WebGPU/R3F canvas, not the placeholder const environment = loadE2EEnvironment({}); -/** - * On-demand perf benchmarks: run manually against a real GPU, never picked up by `bun run e2e`'s - * default config. Its own `testDir` keeps every benchmark spec out of `playwright.config.ts`'s - * discovery entirely, so nothing here can ever land on the CI critical path. - */ export default defineConfig({ expect: { timeout: 10 * 1000, diff --git a/apps/web-e2e/playwright.stack.config.ts b/apps/web-e2e/playwright.stack.config.ts index 039df0fd6..6dc1874f9 100644 --- a/apps/web-e2e/playwright.stack.config.ts +++ b/apps/web-e2e/playwright.stack.config.ts @@ -3,12 +3,6 @@ import type { E2EOptions } from './src/test'; const baseURL = process.env['STACK_BASE_URL'] ?? 'http://localhost:3200'; -/** - * The full-stack suite: the whole converged spec set against the real service images the deploy - * pipeline is about to promote, booted by `docker-compose.stack.yml` before playwright runs — no - * webServer entries, the harness owns the stack lifecycle. Specs create their own unique accounts, - * so a retry never replays against state a failed attempt mutated. - */ export default defineConfig({ expect: { timeout: 10 * 1000, diff --git a/apps/web-e2e/specs/avatar-roster.spec.ts b/apps/web-e2e/specs/avatar-roster.spec.ts index eb17c4b9c..fa0ba1f27 100644 --- a/apps/web-e2e/specs/avatar-roster.spec.ts +++ b/apps/web-e2e/specs/avatar-roster.spec.ts @@ -1,11 +1,6 @@ import { expect, test } from '../src/test'; import { waitForHoneypotWindow } from '../src/wait-for-honeypot-window'; -/** - * The multi-avatar journey: create a second avatar from the roster (which auto-selects it), - * switch back to the first, and prove the choice survives a full reload — the selection is - * persisted server-side, not a client artifact. - */ test('it creates a second avatar, switches back, and keeps the choice across a reload', async ({ page, }) => { diff --git a/apps/web-e2e/specs/avatar-satellite.spec.ts b/apps/web-e2e/specs/avatar-satellite.spec.ts index 308a4465f..08c9fee44 100644 --- a/apps/web-e2e/specs/avatar-satellite.spec.ts +++ b/apps/web-e2e/specs/avatar-satellite.spec.ts @@ -1,11 +1,6 @@ import { expect, test } from '../src/test'; import { waitForHoneypotWindow } from '../src/wait-for-honeypot-window'; -/** - * `/avatar` mounts its own satellite canvas alongside the persistent world canvas: two `` - * elements are attached while the panel is up, and navigating away drops back to one as the - * satellite dies with the route (`keepAlive: false`) while the tagged world canvas survives. - */ test('it mounts a second canvas for the avatar satellite and drops it on navigation away', async ({ page, }) => { diff --git a/apps/web-e2e/specs/canvas-persistence.spec.ts b/apps/web-e2e/specs/canvas-persistence.spec.ts index dec621f10..70253b94b 100644 --- a/apps/web-e2e/specs/canvas-persistence.spec.ts +++ b/apps/web-e2e/specs/canvas-persistence.spec.ts @@ -1,11 +1,6 @@ import { expect, test } from '../src/test'; import { waitForHoneypotWindow } from '../src/wait-for-honeypot-window'; -/** - * The `_game` layout mounts its canvas once and never remounts it across child-route navigation: - * a client-side nav to another game route must leave the same `` element in the DOM, - * carrying whatever GPU state it already uploaded. - */ test('it keeps the same canvas element across client-side game navigation', async ({ page }) => { await page.setExtraHTTPHeaders({ 'x-forwarded-for': '127.0.0.1' }); await page.goto('/login'); diff --git a/apps/web-e2e/specs/checkpoint-hash-parity.spec.ts b/apps/web-e2e/specs/checkpoint-hash-parity.spec.ts index 738432586..f1628e20b 100644 --- a/apps/web-e2e/specs/checkpoint-hash-parity.spec.ts +++ b/apps/web-e2e/specs/checkpoint-hash-parity.spec.ts @@ -14,11 +14,6 @@ const CANONICAL_JSON = JSON.stringify([ 'server-key', ]); -/** - * Every party on the checkpoint hash chain — service, verifier, browser client — must derive - * byte-identical digests. This asserts the Node-context contract call and a real browser's - * WebCrypto both derive the frozen digest from the same canonical bytes. - */ test('it derives the same frozen digest from the contract call and from browser WebCrypto', async ({ page, }) => { diff --git a/apps/web-e2e/specs/home-smoke.spec.ts b/apps/web-e2e/specs/home-smoke.spec.ts index 0b757604c..c9d458215 100644 --- a/apps/web-e2e/specs/home-smoke.spec.ts +++ b/apps/web-e2e/specs/home-smoke.spec.ts @@ -1,13 +1,6 @@ import { expect, test } from '../src/test'; import { waitForHoneypotWindow } from '../src/wait-for-honeypot-window'; -/** - * Exercises the home route against a live server, past what `bun test` can drive (it resolves - * package exports without the `react-server` condition, and there's no live request's - * `AsyncLocalStorage` context). The hero's calls to action come from route loader data, so this - * checks both the raw served HTML (200, `text/html`, hero copy and links already present) and the - * hydrated page. - */ test('it serves the home page and renders the signed-out actions', async ({ page, request }) => { const rawResponse = await request.get('/'); diff --git a/apps/web-e2e/specs/production-boot-smoke.spec.ts b/apps/web-e2e/specs/production-boot-smoke.spec.ts index 59671e5e3..084d23470 100644 --- a/apps/web-e2e/specs/production-boot-smoke.spec.ts +++ b/apps/web-e2e/specs/production-boot-smoke.spec.ts @@ -1,9 +1,5 @@ import { expect, test } from '../src/test'; -/** - * Serving proof for the deployable artifact needing no signed-in state and no secrets: the health - * check answers and the anonymous home page renders. - */ test('it serves the production build health check and anonymous home page', async ({ request }) => { const health = await request.get('/health'); diff --git a/apps/web-e2e/specs/signup-journey.spec.ts b/apps/web-e2e/specs/signup-journey.spec.ts index 46f568d14..ac7322f22 100644 --- a/apps/web-e2e/specs/signup-journey.spec.ts +++ b/apps/web-e2e/specs/signup-journey.spec.ts @@ -88,11 +88,6 @@ interface SignUpJourney extends E2EOptions { readonly username: string; } -/** - * Drives the whole account-creation journey — signup, emailed-code verification, onboarding, and - * avatar creation — and lands signed in at `/explore`. Every form submit paces past the artifact's - * real honeypot window first. - */ async function runSignUpIntoGame(page: Page, journey: Readonly): Promise { await page.setExtraHTTPHeaders({ 'x-forwarded-for': '127.0.0.1' }); await page.goto('/signup'); @@ -133,11 +128,8 @@ async function runSignUpIntoGame(page: Page, journey: Readonly): const nameField = page.getByLabel('Name', { exact: true }); // the create-avatar form's client mount replaces the server-rendered markup, so a name typed - // before the swap passes a value assertion yet submits as an empty form — and gating on the - // root hydration marker instead would serialize on the whole game shell, which can take far - // longer than the form needs. So the whole fill-and-submit cycle retries on the navigation - // outcome: an empty submit is rejected server-side without creating anything, which makes the - // retry safe, and the URL guard keeps a slow success from being submitted twice. + // before the swap passes a value assertion yet submits as an empty form. An empty submit is + // rejected server-side without creating anything, so the whole fill-and-submit cycle retries. await expect(async () => { if (new URL(page.url()).pathname !== '/explore') { await nameField.fill(journey.avatarName); @@ -151,11 +143,6 @@ async function runSignUpIntoGame(page: Page, journey: Readonly): }).toPass({ timeout: 20_000 }); } -/** - * Reads the onboarding code from whichever backend the journey signed up against: the mock - * verification service's e2e lookup, or the resend stub that captured the real service-email's - * welcome message. - */ function waitForVerificationCode( options: Readonly, ): Promise { @@ -166,11 +153,6 @@ function waitForVerificationCode( const MockVerificationCodeSchema = z.object({ code: z.string() }); -/** - * Polls the mock backend's test-only verification-code endpoint, which answers 404 until - * `createVerification` has stored a row for the email — the welcome-email send that carries the - * same code is a fire-and-forget queue drain behind the signup response. - */ async function waitForMockVerificationCode( email: string, mockVerificationURL: string | undefined, @@ -203,10 +185,6 @@ async function waitForMockVerificationCode( const CapturedEmailSchema = z.object({ text: z.string() }); const CapturedEmailsSchema = z.object({ emails: z.array(CapturedEmailSchema) }); -/** - * Polls the resend stub's capture endpoint for the welcome email the real service-email handed it - * and pulls the onboarding code out of the verification URL its `code` query param carries. - */ async function waitForStackVerificationCode( email: string, resendStubURL: string | undefined, @@ -237,11 +215,6 @@ async function waitForStackVerificationCode( return code; } -/** - * A globally-unique letters-only avatar name: `AvatarNameSchema` accepts letters only, and the - * real stack's database persists across runs and enforces the same uniqueness the mock backend - * does. - */ function buildAvatarName(): string { const alphabet = 'abcdefghijklmnopqrstuvwxyz'; diff --git a/apps/web-e2e/specs/web-locks-fallback.spec.ts b/apps/web-e2e/specs/web-locks-fallback.spec.ts index 423d1cab5..db3dcd177 100644 --- a/apps/web-e2e/specs/web-locks-fallback.spec.ts +++ b/apps/web-e2e/specs/web-locks-fallback.spec.ts @@ -9,11 +9,7 @@ interface LockProbe { readonly pendingCount: number; } -/** - * Reads the origin's writer-lock state from a page. `navigator.locks.query()` sees locks held by - * the page's dedicated workers, so this is the observable proof of which tab's worker won the - * election. - */ +// `navigator.locks.query()` from the page also reports locks held by the page's dedicated workers function readWriterLock(page: Page, lockName: string): Promise { return page.evaluate(async (name) => { const state = await navigator.locks.query(); @@ -27,12 +23,6 @@ function readWriterLock(page: Page, lockName: string): Promise { }, lockName); } -/** - * The fallback transport for browsers without SharedWorker: every tab spawns a dedicated worker, - * the workers race the writer lock, and closing the writer's tab promotes the next waiter. Real - * tab-death lock release is the one behaviour no in-process test can exercise — this spec is its - * only coverage. - */ test('it elects one writer without SharedWorker and promotes a survivor when the writer tab closes', async ({ context, page, diff --git a/apps/web-e2e/src/create-stack-seed.ts b/apps/web-e2e/src/create-stack-seed.ts index 512026779..0094a6629 100644 --- a/apps/web-e2e/src/create-stack-seed.ts +++ b/apps/web-e2e/src/create-stack-seed.ts @@ -3,12 +3,6 @@ import { createDB } from '@vers/db'; import { DEMO_ACCOUNTS } from '@vers/mock-services'; import invariant from 'tiny-invariant'; -/** - * Seeds the full-stack postgres with the shared demo accounts so a spec's seeded login lands the - * same as it does against the mock backend. Passwords are argon2id-hashed to match the user - * service's own hasher; each account's avatar is a direct row insert, enough for the shell's - * active-avatar gate. Run once against the migrated stack database before playwright. - */ async function createStackSeed(): Promise { const databaseURL = process.env['DATABASE_URL']; diff --git a/apps/web-e2e/src/load-e2e-environment.ts b/apps/web-e2e/src/load-e2e-environment.ts index 1daa3c591..c72ba6e01 100644 --- a/apps/web-e2e/src/load-e2e-environment.ts +++ b/apps/web-e2e/src/load-e2e-environment.ts @@ -18,20 +18,9 @@ interface E2EEnvironment { } interface LoadE2EEnvironmentOptions { - /** - * Extra env keys for the app-web server beyond the shared set — the default config forces the - * placeholder canvas with `FEATURE_GAME_RENDERER: 'false'`; benchmark runs omit it to measure - * the real WebGPU/R3F canvas. - */ readonly appWebEnv?: Readonly>; } -/** - * The environment pieces every playwright config in this package shares: the resolved roots, the - * `.env` load, and the two web servers. Config-specific knobs (testDir, outputDir, timeouts, - * projects) stay in each config file; the server topology lives here once so the configs cannot - * drift apart. - */ export function loadE2EEnvironment(options: Readonly): E2EEnvironment { // resolve the project root without relying on `__dirname`, which is unreliable when a config is // parsed for the task graph rather than run from its own directory @@ -60,10 +49,9 @@ export function loadE2EEnvironment(options: Readonly) projectRoot, webServer: [ { - // the stateful mock backends as real HTTP listeners on the service dev ports the - // artifact's SERVICE_URLS defaults resolve. Never reuse an already-listening server: a - // service answering on these ports could be the real dev stack, and specs must never - // mutate it. + // the stateful mock backends as real HTTP listeners on the service dev ports the artifact's + // SERVICE_URLS defaults resolve. Never reuse an already-listening server: it could be the + // real dev stack, which specs must never mutate. command: 'bun src/serve-mock-services.ts', cwd: projectRoot, reuseExistingServer: false, @@ -75,13 +63,9 @@ export function loadE2EEnvironment(options: Readonly) url: `${process.env['USER_SERVICE_URL'] ?? 'http://localhost:3003'}/health`, }, { - // every spec runs against the deployable artifact, exactly as built — no mock backend - // in-process, no build-time env overrides. Serving it here must not rebuild it: the e2e - // turbo task depends on the app's build task, and any other entrypoint builds first. - // Downstream service calls leave the process over HTTP and land on the mock listeners - // above. Never reuse an already-listening server: whatever answers on this port (a - // leftover vite dev, another app) is not the artifact, and reusing it silently voids the - // production-build guarantee. + // the deployable artifact exactly as built, never rebuilt here: the e2e turbo task depends + // on the app's build task. Never reuse an already-listening server: whatever answers on + // this port is not the artifact, and reusing it voids the production-build guarantee. command: 'node ./server.mjs', cwd: appWebRoot, env: { diff --git a/apps/web-e2e/src/serve-mock-services.ts b/apps/web-e2e/src/serve-mock-services.ts index 2ed7f6d98..520545d42 100644 --- a/apps/web-e2e/src/serve-mock-services.ts +++ b/apps/web-e2e/src/serve-mock-services.ts @@ -13,13 +13,6 @@ import { userRouter } from '@vers/mock-services/user'; import { verificationRouter } from '@vers/mock-services/verification'; import type { ServiceName } from '@vers/service-auth'; -/** - * Serves the stateful mock backends over real HTTP, one listener per service on the same origins - * the production server's `SERVICE_URLS` defaults resolve, so `server.mjs` needs no env - * repointing. The vite-dev mock plugin hosts these same routers in-process; this entrypoint - * exists for runs against the built artifact, which can host no mock backend. Requests - * round-trip oRPC's real wire protocol; a procedure a router doesn't implement 404s. - */ const RPC_HANDLERS: Readonly>> = { activity: new RPCHandler(activityRouter), avatar: new RPCHandler(avatarRouter), @@ -78,11 +71,6 @@ for (const service of SERVICES) { console.log(`mock ${service} listening on ${url.origin}`); } -/** - * Answers the e2e-only lookup a spec polls for the code the mock verification service generated, - * since nothing else observes a mock code once `createVerification` stores it. Responds `{ code }` - * for a stored `(target, type)` row, 404 otherwise — a spec still waiting on delivery. - */ function serveVerificationCode(request: Request): Response { const searchParams = new URL(request.url).searchParams; diff --git a/apps/web-e2e/src/serve-resend-stub.ts b/apps/web-e2e/src/serve-resend-stub.ts index ef81a5618..735fcf053 100644 --- a/apps/web-e2e/src/serve-resend-stub.ts +++ b/apps/web-e2e/src/serve-resend-stub.ts @@ -24,11 +24,9 @@ const emails: Array = []; let nextID = 1; const port = Number(process.env['RESEND_STUB_PORT'] ?? 3020); -// A capture-only stand-in for the Resend HTTP API, for suites running the real service-email -// against a stack that must send no real mail. `POST /emails` accepts a send exactly like the -// Resend endpoint resend-node targets via `RESEND_BASE_URL` and records it in memory; specs pull -// captured sends back with `GET /emails?to=
` to read verification codes out of the -// bodies. State lives for the process lifetime — one stack boot, one mailbox. +// A capture-only stand-in for the Resend HTTP API: `POST /emails` accepts a send exactly like the +// endpoint resend-node targets via `RESEND_BASE_URL` and records it in memory; `GET /emails?to=` +// reads the captured sends back. State lives for the process lifetime. Bun.serve({ fetch: async (request) => { const url = new URL(request.url); diff --git a/apps/web-e2e/src/test.ts b/apps/web-e2e/src/test.ts index f39557482..73feb6e13 100644 --- a/apps/web-e2e/src/test.ts +++ b/apps/web-e2e/src/test.ts @@ -1,20 +1,11 @@ import { test as base } from '@playwright/test'; -/** - * The converged spec set runs against both backends off one directory; each config sets these in - * its `use` block. Only the signup journey reads them — to poll the backend it signed up against - * for the emailed code — so the setup specs' seeded logins ignore them. - */ export interface E2EOptions { readonly codeSource: 'mock' | 'stack'; readonly mockVerificationURL: string | undefined; readonly resendStubURL: string | undefined; } -/** - * The shared `test` with the backend options registered, so every spec imports one configured - * `test`/`expect` pair rather than reaching for the framework's directly. - */ export const test = base.extend({ codeSource: ['mock', { option: true }], mockVerificationURL: [undefined, { option: true }], diff --git a/apps/web-e2e/src/wait-for-honeypot-window.ts b/apps/web-e2e/src/wait-for-honeypot-window.ts index 96b8ca2eb..65ecfd8d7 100644 --- a/apps/web-e2e/src/wait-for-honeypot-window.ts +++ b/apps/web-e2e/src/wait-for-honeypot-window.ts @@ -1,11 +1,5 @@ import type { Page } from '@playwright/test'; -/** - * Waits until the page's honeypot valid-from timestamp has passed, so a form submit isn't flagged - * as bot-speed. The suite runs the production artifact with its real human-speed floor — no - * build-time override — so every spec that submits an auth form must pace itself past the window - * the freshly rendered form declares. - */ export async function waitForHoneypotWindow(page: Page): Promise { const validFrom = await page.locator('input[name="from__confirm"]').inputValue(); diff --git a/apps/web-e2e/src/wait-for-stable-frames.ts b/apps/web-e2e/src/wait-for-stable-frames.ts index 2f5c09999..9c4fa6e8c 100644 --- a/apps/web-e2e/src/wait-for-stable-frames.ts +++ b/apps/web-e2e/src/wait-for-stable-frames.ts @@ -1,34 +1,12 @@ import type { Page } from '@playwright/test'; -/** - * A frame gap under this many milliseconds counts as settled — loose enough that a single fast - * frame during mount doesn't pass it, tight enough that a still-loading scene's own long frames - * keep failing it. - */ const STABLE_FRAME_GAP_THRESHOLD_MS = 20; - -/** - * Consecutive settled frames required before the scene counts as ready. One fast frame can land by - * chance while world data is still loading, a field is still building, or first-frame WebGPU - * pipelines are still compiling; a run of them can't. - */ const STABLE_FRAME_COUNT = 5; - -/** - * How long the gate keeps trying before it fails with an explicit message. A machine whose scene - * never settles (software WebGPU, heavy contention) would otherwise hang the gate silently until - * the whole test times out, hiding which step hung. - */ const STABLE_GATE_BUDGET_MS = 60 * 1000; -/** - * Waits for a live rendering scene to settle into steady frames, rather than proceeding the instant - * its canvas mounts. A persistent canvas is often already visible from the previous route, so a - * `toBeVisible()` check returns before the scene has loaded its data, built its fields, or compiled - * its first-frame WebGPU pipelines — mount-cost frame gaps would otherwise dominate anything the - * caller measures next. Throws with an explicit message if the scene never settles within the gate - * budget, so a stuck scene names itself rather than failing later as an opaque test timeout. - */ +// a persistent canvas is often already visible from the previous route, so a visibility check +// passes before the scene has loaded its data, built its fields, or compiled its first-frame WebGPU +// pipelines export async function waitForStableFrames(page: Page): Promise { const settled = await page.evaluate( ({ budgetMs, stableFrameCount, thresholdMs }) => diff --git a/apps/web/augment-bun-test.ts b/apps/web/augment-bun-test.ts index 4d5e83651..08a0d0b98 100644 --- a/apps/web/augment-bun-test.ts +++ b/apps/web/augment-bun-test.ts @@ -1,10 +1,5 @@ import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'; -/** - * Brings `@testing-library/jest-dom`'s matcher types into `bun:test`'s own `expect`; only the - * types — the runtime matchers are registered separately via `expect.extend` in the preload. - * The jest-extended matcher types come from `@zgeoff/bun-test-extended` via tsconfig `types`. - */ declare module 'bun:test' { // oxlint-disable-next-line typescript/no-empty-interface -- module augmentation, emptiness is the point interface Matchers extends TestingLibraryMatchers {} diff --git a/apps/web/reset-zustand-stores.ts b/apps/web/reset-zustand-stores.ts index 3e2fd2485..b511a2ce8 100644 --- a/apps/web/reset-zustand-stores.ts +++ b/apps/web/reset-zustand-stores.ts @@ -1,11 +1,6 @@ import { registerZustandReset } from '@vers/client-test-utils'; -// its own preload entry, ahead of the main test setup: that file's local `register-*-mock` imports -// (`registerWorldmapSceneMock` among them) transitively import zustand-backed stores, and -// `registerZustandReset` must wrap zustand's `create` before any of those imports run or the -// stores they create are never tracked for reset — a same-file call after those imports is too -// late, since ES module imports are hoisted and evaluate before the importing module's own body. -// Only the wrapper installs this early: the reset itself runs from the teardown hook that also -// unmounts rendered trees, after the unmount, so no still-mounted tree writes the outgoing test's -// state back into the freshly reset stores. +// its own preload entry, ahead of the main test setup: the wrapper must replace zustand's `create` +// before any import that creates a store runs, and ES module imports are hoisted ahead of the +// importing module's own body, so a same-file call after those imports is too late. export const resetZustandStores = registerZustandReset(); diff --git a/apps/web/src/components/auth-layout.tsx b/apps/web/src/components/auth-layout.tsx index 04f1cfbf5..5a8c44c55 100644 --- a/apps/web/src/components/auth-layout.tsx +++ b/apps/web/src/components/auth-layout.tsx @@ -13,9 +13,6 @@ const layout = css({ width: 'full', }); -/** - * The shared frame for pre-auth and account pages: a horizontally centred, width-bounded column. - */ export function AuthLayout(props: Readonly<{ children: ReactNode }>) { return
{props.children}
; } diff --git a/apps/web/src/components/avatar-switched-notice.tsx b/apps/web/src/components/avatar-switched-notice.tsx index 8b01d1536..de6cc87a7 100644 --- a/apps/web/src/components/avatar-switched-notice.tsx +++ b/apps/web/src/components/avatar-switched-notice.tsx @@ -7,12 +7,6 @@ interface AvatarSwitchedNoticeProps { readonly testID?: string; } -/** - * Explains a start or catch-up rejected because the account's active avatar changed, naming the - * new one and offering the one remedy that re-runs every gate against it — a reload. Carries - * `attempts`/`levelUps` tallies when a fallback catch-up already ran for the new avatar before - * this notice rendered, rendering nothing further while they read zero. - */ export function AvatarSwitchedNotice(props: Readonly) { return ( <> diff --git a/apps/web/src/components/game-updated-notice.tsx b/apps/web/src/components/game-updated-notice.tsx index 595f57ff9..cbe00432e 100644 --- a/apps/web/src/components/game-updated-notice.tsx +++ b/apps/web/src/components/game-updated-notice.tsx @@ -4,12 +4,8 @@ interface GameUpdatedNoticeProps { readonly testID?: string; } -/** - * Explains a start rejected because the running build's engine no longer supports the current - * content, offering the one remedy that fetches a build that does — a reload. Deliberately a - * button rather than an automatic reload: a stale service worker or CDN cache can still serve the - * very bundle that failed, and an unconditional reload would loop. - */ +// a button, never an automatic reload: a stale service worker or CDN cache can serve the very +// bundle that failed again, and an unconditional reload would loop export function GameUpdatedNotice(props: Readonly) { return ( <> diff --git a/apps/web/src/components/hydration-marker.tsx b/apps/web/src/components/hydration-marker.tsx index 3b0afbefe..2a7aecf3a 100644 --- a/apps/web/src/components/hydration-marker.tsx +++ b/apps/web/src/components/hydration-marker.tsx @@ -1,10 +1,5 @@ import { useEffect } from 'react'; -/** - * Stamps `data-hydrated` on the root element once React has committed the hydrated tree, i.e. once - * event handlers are live. E2E specs wait for the attribute before driving forms — interacting - * earlier hits server-rendered markup whose submit falls back to a native GET. - */ export function HydrationMarker(): null { useEffect(() => { document.documentElement.dataset['hydrated'] = 'true'; diff --git a/apps/web/src/components/placeholder-grid.tsx b/apps/web/src/components/placeholder-grid.tsx index 7cdae9b22..d1d1681f4 100644 --- a/apps/web/src/components/placeholder-grid.tsx +++ b/apps/web/src/components/placeholder-grid.tsx @@ -14,10 +14,6 @@ const cell = css({ borderWidth: '[1px]', }); -/** - * A grid of empty cells standing in for an item or slot layout. Cells hold a fixed size; the column - * count follows the available width. - */ export function PlaceholderGrid(props: Readonly<{ count: number }>) { return (
diff --git a/apps/web/src/components/root-error-screen.tsx b/apps/web/src/components/root-error-screen.tsx index a95e96c50..b4a696383 100644 --- a/apps/web/src/components/root-error-screen.tsx +++ b/apps/web/src/components/root-error-screen.tsx @@ -4,11 +4,6 @@ import { Button, Heading, Text } from '@vers/design-system'; import { css } from '@vers/styled-system/css'; import { useEffect } from 'react'; -/** - * Root route error boundary: the last-resort screen for a render or loader error nothing below - * caught. Reports the error once on mount — render errors never pass through the query caches, so - * this is their only reporting point. - */ export function RootErrorScreen(props: ErrorComponentProps) { useEffect(() => { Sentry.captureException(props.error); diff --git a/apps/web/src/components/screen-layout.tsx b/apps/web/src/components/screen-layout.tsx index a74cf55e3..b433e9498 100644 --- a/apps/web/src/components/screen-layout.tsx +++ b/apps/web/src/components/screen-layout.tsx @@ -13,10 +13,6 @@ const layout = css({ width: 'full', }); -/** - * The shared frame for a game screen: a titled, padded column that fills its host — the ambient - * sheet for meta screens, the viewport for focus scenes. - */ export function ScreenLayout(props: Readonly<{ children: ReactNode; title: string }>) { return (
diff --git a/apps/web/src/components/screen-panel.tsx b/apps/web/src/components/screen-panel.tsx index 5f3a6c49b..17daf3a67 100644 --- a/apps/web/src/components/screen-panel.tsx +++ b/apps/web/src/components/screen-panel.tsx @@ -19,9 +19,6 @@ const panelLabel = css({ textTransform: 'uppercase', }); -/** - * A labelled section within a screen: the label names the area and children carry its content. - */ export function ScreenPanel(props: Readonly<{ children?: ReactNode; label: string }>) { return (
diff --git a/apps/web/src/components/step-up-challenge-form.tsx b/apps/web/src/components/step-up-challenge-form.tsx index 0d400928a..a4854019a 100644 --- a/apps/web/src/components/step-up-challenge-form.tsx +++ b/apps/web/src/components/step-up-challenge-form.tsx @@ -14,11 +14,6 @@ interface StepUpChallengeFormProps { const formStyles = css({ display: 'flex', flexDirection: 'column', gap: '4' }); -/** - * The step-up gate's inline client island: every 2FA-gated mutation (change email, change - * password, disable 2FA) renders this in place of its own form once its handler reports - * `step-up-required`, and hands the resulting token back to resubmit the original mutation with. - */ export function StepUpChallengeForm(props: StepUpChallengeFormProps) { const verifyStepUpFn = useServerFn(verifyStepUp); const [formError, setFormError] = useState(null); diff --git a/apps/web/src/lib/account/get-account-content.tsx b/apps/web/src/lib/account/get-account-content.tsx index fc68a1244..e92fd8011 100644 --- a/apps/web/src/lib/account/get-account-content.tsx +++ b/apps/web/src/lib/account/get-account-content.tsx @@ -4,10 +4,6 @@ import { AccountContent } from '../../routes/-account/account-content'; import { userClient } from '../rpc/clients/user-client'; import { verificationClient } from '../rpc/clients/verification-client'; -/** - * Runs fresh on every loader pass (no client-side cache layer of its own), so a mutation that - * redirects back to `/account` always lands on current data. - */ export const getAccountContent = createServerFn({ method: 'GET' }).handler(async () => { const user = await userClient.getCurrentUser({}); diff --git a/apps/web/src/lib/activity/build-activity-rewards-query-options.ts b/apps/web/src/lib/activity/build-activity-rewards-query-options.ts index 487410bdf..09ba3c449 100644 --- a/apps/web/src/lib/activity/build-activity-rewards-query-options.ts +++ b/apps/web/src/lib/activity/build-activity-rewards-query-options.ts @@ -1,9 +1,5 @@ import type { OrpcQueryUtils } from '../rpc/orpc'; -/** - * Query options for an activity's revealed reward-slot contents: refetched on a modest interval - * while the panel keeps it mounted, so newly verified rewards surface without a manual refresh. - */ export function buildActivityRewardsQueryOptions(orpc: OrpcQueryUtils, activityID: string) { return orpc.activity.getActivityRewards.queryOptions({ input: { activityID }, diff --git a/apps/web/src/lib/activity/build-avatar-progression-query-options.ts b/apps/web/src/lib/activity/build-avatar-progression-query-options.ts index e1c0923fd..4baff1360 100644 --- a/apps/web/src/lib/activity/build-avatar-progression-query-options.ts +++ b/apps/web/src/lib/activity/build-avatar-progression-query-options.ts @@ -1,30 +1,14 @@ import { orpc } from '../rpc/orpc'; const IDLE_REFETCH_INTERVAL_MS = 10_000; - -/** - * The cadence while an entry is still waiting on the verifier — short enough that a settled total - * lands close behind the run that earned it. - */ const SETTLING_REFETCH_INTERVAL_MS = 2000; -/** - * The one field the refetch cadence reads off a progression response. - */ interface ProgressionRefetchQuery { readonly state: { readonly data?: { readonly pending: ReadonlyArray } | null | undefined; }; } -/** - * Query options for an avatar's settled xp/level plus its pending terminal-but-unsettled xp - * deltas, or `null` when the avatar doesn't exist or isn't owned by the caller. Settlement lands - * server-side with no client signal, so polling is what firms a pending delta up into the settled - * row: the interval tightens while any entry is outstanding and relaxes once none is. A run in - * flight needs no tightening of its own — its live overlay and the settled total that overlay nets - * against advance together, so the figure on screen holds steady however far behind the read lags. - */ export function buildAvatarProgressionQueryOptions(avatarID: string) { return { ...orpc.activity.getAvatarProgression.queryOptions({ input: { avatarID } }), diff --git a/apps/web/src/lib/activity/build-current-activity-query-options.ts b/apps/web/src/lib/activity/build-current-activity-query-options.ts index 14de4f035..3f1fb2fea 100644 --- a/apps/web/src/lib/activity/build-current-activity-query-options.ts +++ b/apps/web/src/lib/activity/build-current-activity-query-options.ts @@ -1,8 +1,5 @@ import { orpc } from '../rpc/orpc'; -/** - * An avatar's current activity row, or `null` when none is active. - */ export function buildCurrentActivityQueryOptions(avatarID: string) { return orpc.activity.getCurrentActivity.queryOptions({ input: { avatarID } }); } diff --git a/apps/web/src/lib/activity/build-optimistic-progression.ts b/apps/web/src/lib/activity/build-optimistic-progression.ts index e9d6238c1..adcbf5e28 100644 --- a/apps/web/src/lib/activity/build-optimistic-progression.ts +++ b/apps/web/src/lib/activity/build-optimistic-progression.ts @@ -28,20 +28,11 @@ interface BuildOptimisticProgressionInput { } interface OptimisticProgression { - /** - * Whether the displayed total carries anything not yet on the settled row: a pending entry, a - * live sim overlay, or both. A screen renders its settling marker exactly when this is true. - */ readonly isSettling: boolean; readonly level: number; readonly xp: number; } -/** - * Derives the level/xp a screen renders from the settled progression read: the settled total plus - * every pending entry's delta, plus a live-sim overlay, net of whatever the settled total already - * carries for that run. - */ export function buildOptimisticProgression( input: Readonly, ): OptimisticProgression { @@ -56,10 +47,9 @@ export function buildOptimisticProgression( const liveActivityID = input.progression.active?.activityID; const simIsLive = liveActivityID === input.simActivity?.id; - // A run other than the sim's is live, so the sim is a stale worker snapshot of a run that has - // already been displaced — its total belongs to nothing the settled row is still tracking. No - // live run at all leaves the overlay standing, covering the window between a terminal append - // and the pending entry that replaces it. + // a live run other than the sim's means the sim is a stale snapshot of a displaced run; no live + // run at all keeps the overlay, covering the window between a terminal append and its pending + // entry. const simIsStale = liveActivityID !== undefined && !simIsLive; const overlayApplies = input.simActivity !== undefined && !simIsPending && !simIsStale; const settledForSim = simIsLive ? (input.progression.active?.settledXP ?? 0) : 0; diff --git a/apps/web/src/lib/activity/build-revealed-nodes-query-options.ts b/apps/web/src/lib/activity/build-revealed-nodes-query-options.ts index b065b235d..b7c701ab6 100644 --- a/apps/web/src/lib/activity/build-revealed-nodes-query-options.ts +++ b/apps/web/src/lib/activity/build-revealed-nodes-query-options.ts @@ -1,20 +1,8 @@ import type { Viewport } from '@vers/worldmap-core'; import { orpc } from '../rpc/orpc'; -/** - * Reveal state changes only when the avatar earns a new first-clear grant, far less often than the - * player pans — long enough that a re-mount or window refocus doesn't re-fetch a viewport nothing - * has changed for. - */ const REVEALED_NODES_STALE_TIME_MS = 30_000; -/** - * Query options for an avatar's revealed world-map cells inside a viewport. The query key carries - * both inputs: the avatar id, so no avatar ever reads another's cached reveal data, and the - * viewport. Callers pass an already chunk-aligned viewport, so for a given avatar the key changes - * only when the player pans across a chunk boundary rather than on every frame's cell-granular - * move. - */ export function buildRevealedNodesQueryOptions(avatarID: string, viewport: Viewport) { return orpc.activity.getRevealedNodes.queryOptions({ input: { avatarID, viewport }, diff --git a/apps/web/src/lib/activity/merge-revealed-rewards.ts b/apps/web/src/lib/activity/merge-revealed-rewards.ts index dba5d7d3d..62a66b4b8 100644 --- a/apps/web/src/lib/activity/merge-revealed-rewards.ts +++ b/apps/web/src/lib/activity/merge-revealed-rewards.ts @@ -1,11 +1,5 @@ import type { RevealedReward, RevealedRewardsPage } from './types'; -/** - * Accumulates a fresh keyset page of revealed rewards onto whatever the cache already holds, - * deduping on `(chainIndex, ordinal)` — the coordinate a reward slot is identified by — so a - * re-fetched overlap never double-counts. `verifiedHead` carries the higher of the two pages': the - * fresher fetch's view of the settled boundary never regresses the cache's own. - */ export function mergeRevealedRewards( previous: Readonly | undefined, page: Readonly, diff --git a/apps/web/src/lib/activity/require-active-activity.ts b/apps/web/src/lib/activity/require-active-activity.ts index 0a67bfbd5..3b1bb99b8 100644 --- a/apps/web/src/lib/activity/require-active-activity.ts +++ b/apps/web/src/lib/activity/require-active-activity.ts @@ -4,11 +4,6 @@ import { findActiveAvatar } from '../avatar/find-active-avatar'; import { activityClient } from '../rpc/clients/activity-client'; import { avatarClient } from '../rpc/clients/avatar-client'; -/** - * The per-screen gate for the engagement view: redirects a caller with no active avatar to the - * create sheet or roster, and a caller with no running activity back to explore, so the screen - * can assume a live activity to render. - */ export const requireActiveActivity = createServerFn({ method: 'GET' }).handler(async () => { const roster = await avatarClient.getAvatars({}); diff --git a/apps/web/src/lib/activity/types.ts b/apps/web/src/lib/activity/types.ts index ef41bdb50..00841d532 100644 --- a/apps/web/src/lib/activity/types.ts +++ b/apps/web/src/lib/activity/types.ts @@ -2,22 +2,12 @@ import type { ContractRouterClient } from '@orpc/contract'; import type { activityContract } from '@vers/contract-activity'; import type { DeepReadonly } from '../rpc/types'; -/** - * One revealed reward slot's wire shape, as `getActivityRewards` returns it — mutable, the form - * mock handlers and payload builders produce. - */ export type RevealedRewardData = Awaited< ReturnType['getActivityRewards']> >['items'][number]; -/** - * One revealed reward slot as consumers read it: the wire shape, deep-readonly. - */ export type RevealedReward = DeepReadonly; -/** - * A page of revealed rewards, keyed by the verified head it was read against. - */ export interface RevealedRewardsPage { readonly items: ReadonlyArray; readonly verifiedHead: number; diff --git a/apps/web/src/lib/activity/use-activity-rewards.ts b/apps/web/src/lib/activity/use-activity-rewards.ts index 36cab4892..749450be7 100644 --- a/apps/web/src/lib/activity/use-activity-rewards.ts +++ b/apps/web/src/lib/activity/use-activity-rewards.ts @@ -7,16 +7,6 @@ import { useIsActivityIngested } from './use-is-activity-ingested'; const REFETCH_INTERVAL_MS = 15_000; -/** - * Polls an activity's revealed rewards, merging each fresh keyset page onto whatever the cache - * already holds — no push channel names when a reward's verifying stream advances, so a short - * poll is the honest minimal trigger. Disabled with no `activityID`, and until the worker has landed - * the activity's start on the server — a run that exists only as a local mint has no rewards to - * read there. Every returned item is - * already settled; each item's `chainIndex` is chain-absolute while the page's `verifiedHead` - * counts from the activity's own start, so comparing the two needs the activity row's - * `startChainIndex` offset. - */ export function useActivityRewards(activityID: string | undefined) { const queryClient = useQueryClient(); const isActivityIngested = useIsActivityIngested(activityID); diff --git a/apps/web/src/lib/activity/use-is-activity-ingested.ts b/apps/web/src/lib/activity/use-is-activity-ingested.ts index 47302c5dd..7ed4cdd0b 100644 --- a/apps/web/src/lib/activity/use-is-activity-ingested.ts +++ b/apps/web/src/lib/activity/use-is-activity-ingested.ts @@ -1,25 +1,11 @@ import { readActivityStart, useLastIngestedActivityID } from '@vers/idle-client'; import { useEffect, useState } from 'react'; -/** - * The last derived answer paired with the activity it was derived for, so the held answer is never - * read against a different activity before that activity's own read lands. - */ interface IngestedActivity { readonly activityID: string | undefined; readonly isIngested: boolean; } -/** - * Whether the server holds the named activity, so a read keyed on it answers rather than reporting - * the activity missing. An activity starts as a local mint carried by a durable pending-start row, - * and the row is dropped once the worker lands the start on the server — so the absence of that - * row is what the answer reads, which survives a reload the way a broadcast alone would not. The - * ingest report is the trigger to read again, not the answer itself. - * - * `undefined` reads false, matching an avatar with no activity in flight. A local store that - * cannot be read reads true, since it says nothing about what the server holds. - */ export function useIsActivityIngested(activityID: string | undefined): boolean { const [ingested, setIngested] = useState({ activityID: undefined, diff --git a/apps/web/src/lib/auth/build-auth-session-config.ts b/apps/web/src/lib/auth/build-auth-session-config.ts index c613f1efd..e2648f766 100644 --- a/apps/web/src/lib/auth/build-auth-session-config.ts +++ b/apps/web/src/lib/auth/build-auth-session-config.ts @@ -1,19 +1,10 @@ import type { getSession } from '@tanstack/react-start/server'; import { readSessionSecret } from './read-session-secret'; -/** - * `getSession`'s config parameter type, without a direct dependency on its owning package. - */ export type SessionConfig = Parameters[0]; -/** - * A generous ceiling for reads: real expiry is enforced by comparing `data.expires` to now. - */ export const AUTH_SESSION_READ_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; -/** - * Builds the `en_session` cookie's session config; `maxAge` governs the emitted `Max-Age`. - */ export function buildAuthSessionConfig(maxAge: number): SessionConfig { const domain = process.env['COOKIE_DOMAIN']; diff --git a/apps/web/src/lib/auth/build-honeypot-valid-from.ts b/apps/web/src/lib/auth/build-honeypot-valid-from.ts index dda0d10d0..9752188b3 100644 --- a/apps/web/src/lib/auth/build-honeypot-valid-from.ts +++ b/apps/web/src/lib/auth/build-honeypot-valid-from.ts @@ -1,13 +1,5 @@ -/** - * A human takes at least this long to fill in a form; a faster submission is treated as a bot. - */ const HONEYPOT_MIN_FILL_TIME_MS = 1500; -/** - * Computes the valid-from timestamp for a freshly rendered form. The floor has no env override: - * e2e specs pace their submits past the window instead, so the artifact under test enforces the - * same timing it ships with. - */ export function buildHoneypotValidFrom(): string { return String(Date.now() + HONEYPOT_MIN_FILL_TIME_MS); } diff --git a/apps/web/src/lib/auth/build-verify-session-config.ts b/apps/web/src/lib/auth/build-verify-session-config.ts index e5bd1ea38..f62f2f6a9 100644 --- a/apps/web/src/lib/auth/build-verify-session-config.ts +++ b/apps/web/src/lib/auth/build-verify-session-config.ts @@ -1,14 +1,8 @@ import type { SessionConfig } from './build-auth-session-config'; import { readSessionSecret } from './read-session-secret'; -/** - * Every in-flight verification flow abandons its state after this long. - */ const VERIFY_SESSION_MAX_AGE_SECONDS = 60 * 10; -/** - * Builds the `en_verification` cookie's session config: a fixed 10-minute lifetime. - */ export function buildVerifySessionConfig(): SessionConfig { const domain = process.env['COOKIE_DOMAIN']; diff --git a/apps/web/src/lib/auth/check-honeypot.ts b/apps/web/src/lib/auth/check-honeypot.ts index 63c4ebeb1..3bc6590e0 100644 --- a/apps/web/src/lib/auth/check-honeypot.ts +++ b/apps/web/src/lib/auth/check-honeypot.ts @@ -2,12 +2,6 @@ import { logger } from '../../server/logger'; import { HONEYPOT_FIELD_NAME, HONEYPOT_VALID_FROM_FIELD_NAME } from './honeypot-field-names'; import { SpamError } from './spam-error'; -/** - * Throws {@link SpamError} for a filled-in honeypot field or a submission that arrived before its - * form's `valid-from` timestamp, logging the flag so spam pressure is visible. Skips the timing - * check under `NODE_ENV=test`: tests submit forms instantly, well inside the window a bot would - * be flagged for. - */ export function checkHoneypot(formData: FormData): void { const honeypotValue = formData.get(HONEYPOT_FIELD_NAME); diff --git a/apps/web/src/lib/auth/check-step-up.ts b/apps/web/src/lib/auth/check-step-up.ts index 2edfd5b70..a2359b835 100644 --- a/apps/web/src/lib/auth/check-step-up.ts +++ b/apps/web/src/lib/auth/check-step-up.ts @@ -18,13 +18,6 @@ interface CheckStepUpOptions { readonly token: string | undefined; } -/** - * Gates a mutation behind step-up: callers with no live 2FA never gate at all. A 2FA-enabled - * caller needs a transaction token minted from a completed step-up code check — an absent, forged, - * expired, mismatched-claim, or session-mismatched token starts a fresh pending transaction instead - * of trusting it. The session check stops a token minted under one auth session from redeeming - * under another, mirroring the pending-transaction consume path's own `sessionID` match. - */ export async function checkStepUp(opts: Readonly): Promise { const twoFactorVerification = await verificationClient.getVerification({ target: opts.target, @@ -53,9 +46,6 @@ export async function checkStepUp(opts: Readonly): Promise>, ): Promise { @@ -57,9 +45,6 @@ export async function createStepUpTransactionToken( return { expiresAt, jti, token }; } -/** - * Verifies a step-up transaction token's signature and expiry, returning its claims. - */ export async function verifyStepUpTransactionToken( token: string, ): Promise { @@ -113,11 +98,6 @@ function getSessionIDClaim(payload: jose.JWTPayload): string | null { let keyPairPromise: Promise<{ privateKey: jose.CryptoKey; publicKey: jose.CryptoKey }> | null = null; -/** - * Lazily generates this process's step-up signing keypair. Minting and verifying always happen in - * the same edge process a token was issued from, so a fresh in-memory keypair per process is - * enough. - */ function getStepUpTransactionKeyPair(): Promise<{ privateKey: jose.CryptoKey; publicKey: jose.CryptoKey; diff --git a/apps/web/src/lib/auth/find-step-up-token.ts b/apps/web/src/lib/auth/find-step-up-token.ts index e7d66281a..4e2837db1 100644 --- a/apps/web/src/lib/auth/find-step-up-token.ts +++ b/apps/web/src/lib/auth/find-step-up-token.ts @@ -1,6 +1,3 @@ -/** - * Reads the step-up transaction token a gated mutation's resubmission carries, if any. - */ export function findStepUpToken(formData: FormData): string | undefined { const raw = formData.get('stepUpToken'); diff --git a/apps/web/src/lib/auth/get-auth-session.ts b/apps/web/src/lib/auth/get-auth-session.ts index d7cc5d71d..9f93dceab 100644 --- a/apps/web/src/lib/auth/get-auth-session.ts +++ b/apps/web/src/lib/auth/get-auth-session.ts @@ -5,11 +5,6 @@ import { } from './build-auth-session-config'; import type { AuthSessionData } from './types'; -/** - * Reads the caller's auth session. Never throws for a missing or expired cookie — an absent - * `accessToken`/`sessionID` is how callers (`requireAuth`, `requireAnonymous`) observe "signed - * out". - */ export async function getAuthSession(): Promise { const session = await getSession( buildAuthSessionConfig(AUTH_SESSION_READ_MAX_AGE_SECONDS), diff --git a/apps/web/src/lib/auth/get-honeypot-valid-from.ts b/apps/web/src/lib/auth/get-honeypot-valid-from.ts index 52f5c7d08..a5eb10363 100644 --- a/apps/web/src/lib/auth/get-honeypot-valid-from.ts +++ b/apps/web/src/lib/auth/get-honeypot-valid-from.ts @@ -1,13 +1,9 @@ import { createServerFn } from '@tanstack/react-start'; import { buildHoneypotValidFrom } from './build-honeypot-valid-from'; -/** - * Issues a form's `valid-from` timestamp from the server, so the value a route ships and the value - * the submission check compares it against are read from the same clock. Routes call this in their - * loader and pass the result down as loader data: computing it while rendering would let hydration - * recompute it against the browser's clock, and a caller whose device clock runs ahead of the - * server would then be rejected as a bot on every auth form. - */ +// issued from a loader, never computed during render: hydration would recompute a render-time value +// against the browser's clock, and a device clock ahead of the server's would then fail every auth +// form as a bot export const getHoneypotValidFrom = createServerFn({ method: 'GET' }).handler(() => buildHoneypotValidFrom(), ); diff --git a/apps/web/src/lib/auth/get-login-path-with-redirect.ts b/apps/web/src/lib/auth/get-login-path-with-redirect.ts index 880b4e8d5..f77666052 100644 --- a/apps/web/src/lib/auth/get-login-path-with-redirect.ts +++ b/apps/web/src/lib/auth/get-login-path-with-redirect.ts @@ -1,14 +1,8 @@ -/** - * The part of a URL {@link getLoginPathWithRedirect} needs to rebuild the page a guard bounced. - */ interface RedirectSource { readonly pathname: string; readonly search: string; } -/** - * Builds `/login?redirect=`, so a completed login returns the caller to where it left off. - */ export function getLoginPathWithRedirect(source: RedirectSource): string { const redirectTo = `${source.pathname}${source.search}`; diff --git a/apps/web/src/lib/auth/get-verify-session.ts b/apps/web/src/lib/auth/get-verify-session.ts index ef8425ca5..92cad157f 100644 --- a/apps/web/src/lib/auth/get-verify-session.ts +++ b/apps/web/src/lib/auth/get-verify-session.ts @@ -2,9 +2,6 @@ import { getSession } from '@tanstack/react-start/server'; import { buildVerifySessionConfig } from './build-verify-session-config'; import type { VerifySessionData } from './types'; -/** - * Reads the caller's in-flight verification state; empty once the 10-minute window lapses. - */ export async function getVerifySession(): Promise { const session = await getSession(buildVerifySessionConfig()); diff --git a/apps/web/src/lib/auth/honeypot-field-names.ts b/apps/web/src/lib/auth/honeypot-field-names.ts index 140e3365a..97235105e 100644 --- a/apps/web/src/lib/auth/honeypot-field-names.ts +++ b/apps/web/src/lib/auth/honeypot-field-names.ts @@ -1,9 +1,2 @@ -/** - * The hidden field a real caller never fills in; any value here marks the submission as spam. - */ export const HONEYPOT_FIELD_NAME = 'name__confirm'; - -/** - * Encodes the earliest timestamp (ms epoch) a submission of this render counts as human-paced. - */ export const HONEYPOT_VALID_FROM_FIELD_NAME = 'from__confirm'; diff --git a/apps/web/src/lib/auth/honeypot-inputs.tsx b/apps/web/src/lib/auth/honeypot-inputs.tsx index 9112ffeb9..95b485212 100644 --- a/apps/web/src/lib/auth/honeypot-inputs.tsx +++ b/apps/web/src/lib/auth/honeypot-inputs.tsx @@ -1,19 +1,9 @@ import { HONEYPOT_FIELD_NAME, HONEYPOT_VALID_FROM_FIELD_NAME } from './honeypot-field-names'; interface HoneypotInputsProps { - /** - * Epoch-ms timestamp the submission must arrive after. Callers pass a server-issued value from - * loader data rather than computing one here: a value built while rendering is rebuilt again - * during hydration, against the browser's clock instead of the server's. - */ readonly validFrom: string; } -/** - * Renders a form-bearing route's hidden anti-spam fields. A submission arriving before `validFrom`, - * or carrying a non-empty honeypot field, is treated as spam by the server-side check these fields - * feed. - */ export function HoneypotInputs(props: Readonly) { return (