Skip to content

Weekly tech debt audit: windowstead - 2026-07-15 #279

Description

@itsmiso-ai

Weekly Tech Debt Audit — misospace/windowstead

Date: 2026-07-15
Overall Risk Level: Low-Medium

Summary

Windowstead's health is stable this week. Since the 2026-07-08 audit, several child issues have landed: the export CI build pipeline (Linux/Windows/macOS in release.yml), the tile_render and render_module extraction, worker_cap_logic integration, event log capacity increase to 20, WORKER_NAMES expansion to 10, food-bias sort deduplication, and reserved_resources persistence with scheck.

Two children from previous audits remain open: #236 (derive save schema grid sizes from LayoutMath) and #234 (full-cycle integration test for _on_tick) — both are status/in-progress with foreman coders.

This week's scan found 13 findings, mostly P2/P3 carry-forwards from previous audits that were implicitly re-checked and remain valid, plus a few new items.

Progress since 2026-07-08

Closed from #269's decomposition: items 3 (worker_collision_offset), 4 (double apply_loaded_dock_anchor), 5 (priority_order validation), 6 (sidebar popup), 7 (stray README trailer), 8 (labels.yaml), 9 (Focus Mode/Zoom widgets), 10 (stylebox caching), 11 (apply_anchor_layout test), 12 (persist guard), 13 (status_label overflow), 14 (render_worker_overlay per-frame cost), 15 (do_gather reservation drift) — all remained open as new issues were not spawned from #269's breakdown.

Actually closed from previous audits via PRs:

Still open from #221 decomposition: #236 (save schema grid sizes), #234 (full-cycle integration test).

Top findings

P1 — High

  1. main.gd remains at 2646 lines with 159 functions — extraction stalled mid-progress

    • Evidence: scripts/main.gd still contains apply_theme() (60+ lines of StyleBoxFlat creation), worker_texture() (pixel rendering), apply_anchor_layout() (full UI tree wiring), choose_task() (AI logic), _process() (edge snapping + overlay rendering), render_all() (8 render sub-functions), push_event(), persist(), _on_tick() orchestration — all mixed together.
    • Risk: 159 functions in one file means high cognitive load for any change. Every new feature increases the blast radius. Extracted modules exist (TileRender, WorkerCapLogic, etc.) but over 60 functions remain unextracted.
    • Evidence: wc -l scripts/main.gd = 2646 lines, grep -c "^func " scripts/main.gd = 159 functions.
  2. Worker texture pixel-pushing remains in main.gd

    • Evidence: worker_texture() at scripts/main.gd:~2215-2265 creates a new Image object per worker per frame (12x14 RGBA8), fills pixel-by-pixel, and creates ImageTexture. Cached via worker_texture_cache, but each unique cache key requires the full pixel-push pipeline.
    • Risk: CPU cost grows with worker count. Each worker at a unique (name, frame, carrying) combo generates a fresh Image texture on first encounter.
    • File: scripts/main.gd
  3. worker_collision_offset() hard-codes 6 offset positions — still not addressed

    • Evidence: scripts/main.gd:1943-1955 declares offsets as 6 elements and uses slot % 6. With cap 10+ and workers clustering around stockpile and foundations, the modulo wraps and some workers' visual positions overlap.
    • File: scripts/main.gd
  4. apply_theme() creates and .duplicate()s styleboxes for every button on every call — still not addressed

    • Evidence: scripts/main.gd:128-155 iterates 30+ buttons, creating new() StyleBoxFlat for each state then calling .duplicate() for each button individually. Called at least once per startup/hot-reload. Could cache as member variables.
    • File: scripts/main.gd

P2 — Medium

  1. persist() called from _on_tick() every tick even though inner _dirty short-circuit guards — still valid

    • Evidence: scripts/main.gd:2444-2458. The dirty-flag guard makes it cheap, but the contract should be enforced by callers. On a tick with no state changes (no events, no worker actions, no food upkeep), the persist call is wasted.
    • File: scripts/main.gd
  2. render_worker_overlay() runs every frame via _process(delta) — still valid

    • Evidence: scripts/main.gd:~1903-1940 called from _process() (~1400). Computes collision slots, progress, positions, and sets sprite texture for every worker every frame, even between ticks. Main render hot path.
    • Files: scripts/main.gd
  3. do_gather() reservation drift guard — still valid

    • Evidence: scripts/main.gd:1596-1617. Calls release_resource(String(task.resource)) after gather mutation. If the early-return at int(tile.amount) <= 0 triggers, the reservation counter can drift negative on edge paths.
    • File: scripts/main.gd
  4. render_event_drawer() creates string arrays every tick — new finding

    • Evidence: scripts/main.gd:2416-2436. Called from render_all() which is called from _on_tick(). Creates a lines := [] array and populates it with format strings for the last 6 events. On fast tick speeds this adds GC pressure from string allocations every ~0.6s.
    • File: scripts/main.gd
  5. settlement_status_text() unbounded overflow — still valid

    • Evidence: scripts/main.gd:2326-2371. Produces a one-line string with tick count, queued/building/idle/break counts, bottleneck hints, goal reward preview, and next-unlock. Overflows horizontally. Relies on clip_text = false + OVERRUN_NO_TRIMMING.
    • File: scripts/main.gd
  6. Stray test fix: trailer still present in README.md — still valid

  7. No test for apply_anchor_layout() bottom-vs-side orchestration — still valid

    • Evidence: Only tests/test_layout_math.gd covers pure-math helpers. The orchestration (scripts/main.gd:474-572) that re-parents resource_label, status_label, hud_row, header rows on dock switch is untested — and is the largest UX surface in the dock.
    • Files: tests/test_layout_math.gd, scripts/main.gd

P3 — Low

  1. apply_loaded_dock_anchor() called twice on startup — still valid

    • Evidence: Called from load_or_boot() and from load_saved_game(). The second call can double-rebuild the world.
    • File: scripts/main.gd
  2. labels.yaml does not include labels used on recent issues — still valid but improving

    • Evidence: Issues now use labels like tech-debt, agent/foreman-coder, type/chore, type/feature, type/bug — none appear in .github/labels.yaml. The label-sync workflow will prune them if delete-other-labels is ever enabled.
    • File: .github/labels.yaml

Recommended Issue Breakdown

[P1] Extract remaining main.gd subsystems (render_*, worker_texture, apply_theme, apply_anchor_layout)

Problem: main.gd is 2646 lines with 159 functions — the extraction that began with TileRender, WorkerCapLogic, LayoutMath, and ColonyStance is mid-progress. Over 60 functions remain including all rendering (15 render_* functions), worker_texture pixel-pushing (50 lines), apply_theme() stylebox generation (60+ lines), and apply_anchor_layout() UI tree wiring (100+ lines). Every new feature adds to this file.

Evidence: wc -l scripts/main.gd = 2646 lines; grep -c "^func " scripts/main.gd = 159 functions; remaining functions include render_all(), render_world(), render_worker_overlay(), render_goal(), render_sidebar(), render_build_buttons(), render_hud_row(), render_event_drawer(), worker_texture(), apply_theme(), apply_anchor_layout().

Acceptance:

  • worker_texture() moved into its own module (worker_renderer.gd) — Partially done via PR Extract worker_texture() into scripts/worker_renderer.gd (issue #232) #268 but verify scripts/main.gd:2215-2265 still has pixel-push code
  • render_*() functions extracted into a render_manager.gd or hud_renderer.gd module
  • apply_theme() styleboxes cached as class members, not created/duplicated per-call
  • apply_anchor_layout() extracted from main.gd into a dock_layout.gd module
  • Total main.gd line count reduced to <1000 lines

[P1] Cache apply_theme() styleboxes instead of creating and .duplicate()ing per button per call

Problem: apply_theme() in scripts/main.gd creates StyleBoxFlat.new() instances for four button states (normal/hover/pressed/disabled) then calls .duplicate() for each of 30+ buttons and 4+ build buttons every time it's called. Called at least once per startup/hot-reload.

Evidence: scripts/main.gd:128-155 iterates button_name list, creates make_panel_style(...) for each state, then .duplicate() for each button. The build_buttons section does the same.

Acceptance:

  • StyleBox references cached as script-level members
  • apply_theme() re-applies cached references without .new() or .duplicate() calls
  • Verify via adding a log: no StyleBoxFlat.new() calls in apply_theme()

[P2] Add apply_anchor_layout() orchestration test

Problem: The largest UX surface in the dock — apply_anchor_layout() that re-parents resource_label, status_label, hud_row, and header rows between bottom and side dock modes — is untested. Only pure-math helpers are tested in tests/test_layout_math.gd.

Evidence: scripts/main.gd:474-572 is a 100-line function that re-parents 10+ nodes depending on anchor family. Zero coverage in test files.

Acceptance:

  • Create tests/test_dock_layout.gd that instantiates the main scene or a test double
  • Test bottom mode: resource_label parented to bottom_header_row, hud_row parented to bottom_button_row, status_label in bottom_status_column
  • Test side mode: resource_label parented to side_header_row, hud_row parented to side_button_row
  • Test switching from bottom to side and back preserves correct parentage

[P2] Clean up stray test fix: trailer from README.md

Problem: README.md contains a stray # test fix: _assert_empty accepts Variant as the last line — leftover from a prior commit. First noticed in the 2026-07-08 audit.

Evidence: tail -1 README.md# test fix: _assert_empty accepts Variant

Acceptance:

  • Remove the stray trailer line
  • Verify README.md renders cleanly

[P2] Fix do_gather() reservation drift on edge paths

Problem: do_gather() in main.gd calls release_resource(String(task.resource)) unconditionally after gather mutation. If the early-return at int(tile.amount) <= 0 triggers, the reservation can be released twice or drift negative.

Evidence: scripts/main.gd:1596-1617 — the release_resource call is after the early-return check, but if a previous tick already released it via the normal path and the worker is assigned to the same task again, the counter can go negative.

Acceptance:

  • release_resource() clamps to non-negative (already does, via maxi(0, ...))
  • Ensure release_resource is called exactly once per gather task lifecycle
  • Add edge case tests to tests/test_reservations.gd

[P2] Validate labels.yaml against actual repo labels

Problem: .github/labels.yaml does not include tech-debt, agent/foreman-coder, type/chore, type/feature, type/bug, needs-gpt which are actively used on issues. The label-sync workflow (delete-other-labels: false) means they aren't pruned — but drift is a risk once delete-other-labels is ever enabled.

Evidence: gh api repos/misospace/windowstead/labels --jq '.[].name' shows these labels exist on GitHub but not in .github/labels.yaml.

Acceptance:

  • Add missing labels to .github/labels.yaml
  • Run label-sync workflow manually to verify

[P3] Reduce render_worker_overlay() per-frame cost

Problem: render_worker_overlay() is called from _process(delta) every frame (~60fps), computing collision slots, progress easing, and sprite positions for every worker, even between ticks. With 10+ workers, this runs pixel-position math 600+ times/second.

Evidence: scripts/main.gd:1903-1940 called from scripts/main.gd:~1400 (_process). Worker sprite positions recomputed each frame via tile_center() + lerp() + worker_collision_offset().

Acceptance:

  • Render only when _dirty or when a tick has advanced
  • Cache worker positions and only recompute when _dirty

[P3] Reduce render_event_drawer() allocation churn

Problem: render_event_drawer() creates a new lines := [] array and format strings every tick (called from render_all()). On fast tick speeds (~0.6s) this adds GC pressure.

Evidence: scripts/main.gd:2416-2436. Called from render_all(), which is called from _on_tick(). Creates new array and format strings every tick.

Acceptance:

  • Cache event log text and only recompute when events change
  • Pre-allocate lines array or use PackedStringArray

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.status/doneWork is complete.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions