# Jerklint Review 01 Reviewed 2026-08-04. Scope: handwritten source under `engine/`, `app/`, `server/`, `shared/`, and `scripts/`; root runtime, build, lint, editor, and project configuration; `README.md`. Generated assets, build output, dependencies, and lockfile were excluded. **Jerklint Findings** 1. **High** `engine/render/Color.ts:37-43`, `engine/render/Texture.ts:46-56`, `engine/render/Rasterizer.ts:199-203`, `engine/render/RenderConfig.ts:104-114`, `scripts/gen-assets.ts:136-143`: `Color.lerp` promises to interpolate a `Color` but omits alpha, so `Color.rgb` silently supplies `255`. Every bilinear sample therefore becomes opaque. The `clean` preset enables linear filtering, while the NPC asset has a transparent background and alpha cutout happens after sampling. This is a false generic abstraction with a direct behavior change hidden behind an unrelated render knob. Smallest fix: interpolate alpha in `Color.lerp`; add an explicitly named RGB-only blend only if fog or sky ever needs one. 2. **High** `engine/render/RenderConfig.ts:16-58`, `engine/render/RenderConfig.ts:64-115`, `app/main.ts:19-29`, `engine/render/Framebuffer.ts:43-73`: live render configuration has no authoritative validity or application boundary. Public presets are mutable singleton objects, `useConfig` retains the supplied alias, dimensions are snapshotted into framebuffer resources, and documented numeric ranges are not enforced. A direct width change leaves resources stale; values such as `colorDepth = 0` make quantization divide by zero. This will become immediate pressure when planned live sliders arrive. Smallest fix: make presets readonly templates, create a fresh validated config for each update, and route updates through one app-side apply function that rebuilds resources when dimensions change. 4. **Medium** `app/level.ts:18-27`, `app/level.ts:66-105`, `app/player.ts:2`, `app/player.ts:19-31`, `app/player.ts:61-97`, `app/main.ts:48-52`: collision policy has no single owner. `Player.update` accepts the render-heavy `Level`, then reconstructs gameplay facts using a hardcoded ground height and NPC radius; NPC visual size lives in `main.ts`. `buildLevel` also constructs geometry, collision, NPC placement, and sky in one body. NPC or floor changes therefore require cross-file agreement and isolated player tests need unrelated render data. Smallest fix: let `Level` own a compact NPC descriptor and a narrow collision world containing ground, boxes, and circles; pass only that collision value to `Player.update`. 5. **Medium** `app/main.ts:54-69`, `app/player.ts:23-55`: browser key codes are the player simulation API. Physics owns WASD and Space bindings, receives a mutable `Set`, cannot represent a press edge, and therefore treats a held jump key as a fresh jump whenever landing. Lost `keyup` events also leave state stuck because no blur path clears the set. Rebinding, gamepad input, and deterministic tests all require physics edits. Smallest fix: map browser events in `main.ts` to a semantic `PlayerInput` value with movement axes and `jumpPressed`; clear raw input on blur. 6. **Medium** `engine/scene/Camera.ts:27-33`, `engine/math/Mat4.ts:23-34`, `engine/render/Rasterizer.ts:9-19`, `engine/render/Rasterizer.ts:58-68`, `engine/render/Rasterizer.ts:77-115`: projection limits and clipping have separate authorities. Camera projection declares near/far values of `0.05` and `100`, but `Rasterizer.project` discards clip-space `z` and clips only against unrelated `w >= 0.01`; far clipping is absent. `Rasterizer.draw` looks matrix-generic while relying on perspective-specific `w` semantics for clipping, depth, and fog. Smallest fix: retain clip-space `z` and clip against canonical near/far planes, or narrow and name the API so its perspective-matrix contract is explicit. 7. **Medium** `engine/render/RenderConfig.ts:48-54`, `engine/render/Texture.ts:36-38`, `engine/render/Rasterizer.ts:51-57`, `engine/render/Sky.ts:34-36`, `engine/render/Sky.ts:93-98`, `engine/render/Sky.ts:121-128`: closed option unions fail open. New texture filters silently become nearest, new lighting modes silently become unlit, and new cloud kinds silently become basic because each dispatcher uses a binary predicate plus fallback. `basicCumulus` accepts the full `CloudLayer` union, preventing TypeScript from exposing the missing branch. Smallest fix: use exhaustive switches with a `never` check and narrow cloud helpers to their concrete variant types. 11. **Low** `engine/scene/Sprite.ts:9-23`, `app/main.ts:48-52`, `app/main.ts:107-112`: `Sprite.texture` is assigned but never read; drawing separately reaches back to `textures.npc`. Two sources can diverge, and the type promises ownership the render path ignores. Smallest fix: draw with `npc.texture`, or remove the field if material binding is intentionally external. 12. **Low** `engine/scene/Camera.ts:4-13`, `engine/math/Mat4.ts:37-43`, `engine/render/Sky.ts:62-66`, `app/main.ts:73-79`: camera pitch validity is documented by `Camera` but enforced by one caller through the unexplained literal `1.4`. Both camera and sky basis construction rely on that ritual. Any second camera producer can create a degenerate basis. Smallest fix: own the pitch limit and look-delta/clamp operation in the `Camera` namespace. 14. **Low** `engine/math/Vec3.ts:3-4`, `engine/math/Vec3.ts:34-37`: `Vec3.normalize` violates its namespace-wide fresh-result guarantee only for zero vectors by returning the mutable input alias. Callers can safely mutate ordinary results but unexpectedly mutate source state at one edge. Smallest fix: return a fresh zero vector. **Scorecard** - DRY: concern - collision, release, and resource-update policy have split owners. - KISS: pass - core engine stays direct, data-oriented, and framework-free. - YAGNI: concern - two unused public placeholders remain. - SOC: concern - render config application and release artifacts mix concerns. - Cohesion: concern - player physics consumes unrelated level rendering data. - Coupling: fail - config/resources and projection/clipping rely on hidden cross-module rules. - Dependency Direction: pass - browser code points inward and engine remains DOM-free. - Law of Demeter: pass - shallow plain-data access; no traversal chains or service locators. - Immutability: concern - mutable preset aliases and zero-vector aliasing weaken boundaries. - Declarative Shape: concern - option unions and release phases are not exhaustively interpreted. - Implicit Contracts: fail - alpha, dimensions, pitch, clipping, checks, and publishing depend on rituals. - Abstraction Pressure: concern - important rules are under-centralized while stale exports remain. - Naming/API Clarity: fail - `Color.lerp`, `check`, `publish`, and `Sprite.texture` overpromise. - Locality: concern - NPC behavior and configuration changes require cross-file coordination. - Testability: fail - zero tests, false-green test command, and broad player fixtures. - File/API Shape: concern - type/namespace pattern is strong; ignored and stale exports weaken it. - Predicate Accuracy: concern - binary fallback dispatch silently accepts future union variants. - Construction Phase Separation: concern - level and release construction each hide multiple phases. **Strengths** - Engine/browser dependency boundary is clean and enforced by separate TypeScript libraries. - Domain types generally own behavior through matching namespaces and matching filenames. - Hot-loop mutation is local, explicit, and appropriate for a software rasterizer. - Rasterizer, sky, texture loading, and procedural mesh generation remain cohesive despite numeric code. - `main.ts` is still a reasonable composition root; splitting presentation or FPS code now would add ceremony. - No ECS, service locator, class hierarchy, utility bag, dependency cycle, or speculative plugin system appeared. **Validation** - `bun run build`: passed. - `bunx tsc --build --dry`: selected engine, app, and config only. - `bun run lint`: exited with one `typescript(array-type)` warning at `scripts/gen-assets.ts:162`. - `bun run test`: found zero tests and emitted Bun's tsconfig directory-mismatch internal error; `--pass-with-no-tests` kept the command green. **Verdict** Refactor soon. Keep the core architecture. Fix lying primitives and quality commands first, then centralize configuration, input, and collision contracts.