meat/.agents/history/jerklint-01/review.md
2026-08-04 10:30:39 +02:00

8.4 KiB

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.

  3. 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.

  4. 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<string>, 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.