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
-
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.lerppromises to interpolate aColorbut omits alpha, soColor.rgbsilently supplies255. Every bilinear sample therefore becomes opaque. Thecleanpreset 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 inColor.lerp; add an explicitly named RGB-only blend only if fog or sky ever needs one. -
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,useConfigretains 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 ascolorDepth = 0make 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. -
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.updateaccepts the render-heavyLevel, then reconstructs gameplay facts using a hardcoded ground height and NPC radius; NPC visual size lives inmain.ts.buildLevelalso 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: letLevelown a compact NPC descriptor and a narrow collision world containing ground, boxes, and circles; pass only that collision value toPlayer.update. -
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 mutableSet<string>, cannot represent a press edge, and therefore treats a held jump key as a fresh jump whenever landing. Lostkeyupevents 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 inmain.tsto a semanticPlayerInputvalue with movement axes andjumpPressed; clear raw input on blur. -
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 of0.05and100, butRasterizer.projectdiscards clip-spacezand clips only against unrelatedw >= 0.01; far clipping is absent.Rasterizer.drawlooks matrix-generic while relying on perspective-specificwsemantics for clipping, depth, and fog. Smallest fix: retain clip-spacezand clip against canonical near/far planes, or narrow and name the API so its perspective-matrix contract is explicit. -
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.basicCumulusaccepts the fullCloudLayerunion, preventing TypeScript from exposing the missing branch. Smallest fix: use exhaustive switches with anevercheck and narrow cloud helpers to their concrete variant types. -
Low
engine/scene/Sprite.ts:9-23,app/main.ts:48-52,app/main.ts:107-112:Sprite.textureis assigned but never read; drawing separately reaches back totextures.npc. Two sources can diverge, and the type promises ownership the render path ignores. Smallest fix: draw withnpc.texture, or remove the field if material binding is intentionally external. -
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 byCamerabut enforced by one caller through the unexplained literal1.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 theCameranamespace. -
Low
engine/math/Vec3.ts:3-4,engine/math/Vec3.ts:34-37:Vec3.normalizeviolates 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, andSprite.textureoverpromise. - 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.tsis 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 onetypescript(array-type)warning atscripts/gen-assets.ts:162.bun run test: found zero tests and emitted Bun's tsconfig directory-mismatch internal error;--pass-with-no-testskept the command green.
Verdict
Refactor soon. Keep the core architecture. Fix lying primitives and quality commands first, then centralize configuration, input, and collision contracts.