Skip to content

Weekly tech debt audit: dispatch - 2026-07-15 #616

Description

@itsmiso-ai

Weekly tech debt audit: dispatch - 2026-07-15

Repo health snapshot

Met findings

Test health — CI gap

The test suite has 43 known failures across 11 test files, including unit tests for github.ts, version.ts, the Dockerfile, the PR follow-up webhook, and 7 component test files (layout.tsx, auth-controls, github-link, issue-card, kanban-board, kanban-column, login/page). The component failures are pre-existing (layout rendering, Tailwind CSS class mismatch, event handling). The unit failures are caused by a missing @vitest-environment node directive on 4 test files that use node:* built-in modules — they run under the jsdom environment by default and fail with "No such built-in module: node:".

These failures are silent in CI because the test job evades detection — work to make CI surface them has not been completed. The public test count (1913 total, 1870 passed) does not break CI builds.

Evidence:

  • src/lib/github.test.ts — imports node:crypto, no @vitest-environment node
  • src/lib/version.test.ts — imports node:fs via version.ts, no directive
  • Dockerfile.test.ts — imports node:fs, node:path, no directive
  • src/app/api/pr-followup/webhook/route.test.ts — imports node:crypto via prisma.ts, no directive
  • 7 component test files fail under jsdom: layout.test.tsx, auth-controls.test.tsx, github-link.test.tsx, issue-card.test.tsx, kanban-board.test.tsx, kanban-column.test.tsx, login/page.test.tsx

Relevant files (from merge #581 / commit de9a896):

  • src/lib/github.test.ts (line 1 — missing directive)
  • src/lib/version.test.ts (line 1 — missing directive)
  • Dockerfile.test.ts (line 1 — missing directive)
  • src/app/api/pr-followup/webhook/route.test.ts (line 1 — missing directive)

Acceptance: All 1913+ tests pass in a single vitest run without environment errors.


Rate limiter is in-memory only

The rate limiter in src/lib/rate-limit.ts is a module-level Map<string, WindowEntry> with no persistence. A process restart resets all rate limits. The limiter uses a fixed-window algorithm (not sliding window), which allows burst traffic at window boundaries. Expired key pruning only occurs when the map exceeds 1000 keys.

Evidence:

  • src/lib/rate-limit.ts line 44: const windows = new Map<string, WindowEntry>();
  • src/lib/rate-limit.ts line 56: simple fixed-window check: if (entry.count < limit)
  • src/lib/rate-limit.ts line 82: PRUNE_THRESHOLD = 1000 — no timer-based sweep

Acceptance: Document in accepted-risks.md as a single-node design tradeoff, or add a DB-backed sliding-window rate limiter.


Large module: src/lib/github.ts (1049 lines)

The GitHub client module now spans 1049 lines and mixes multiple responsibilities: GitHub App JWT auth, token management, issue CRUD, PR queries, CI job log fetching, code search, repository metadata, and check-run queries. This grew organically as new features were added (pr-followup CI logs, groomer features, automation sync). The module has one any-typed helper (fetchRepoJson returning Promise<any>) and a manual redirect-follow for log blob fetching.

Evidence:

  • src/lib/github.ts — 1049 lines total
  • src/lib/github.ts line 529: async function fetchRepoJson(repoFullName: string, errorPrefix: string): Promise<any>
  • Responsibilities: JWT signing (lines 34–100), token caching (lines 14–23), issue/PR CRUD (lines 150–500+), repo metadata (lines 987–1005), CI logs (lines 808–878), code search (lines 1006–1049)

Acceptance: Split into domain-specific files (e.g., github-auth.ts, github-issues.ts, github-ci.ts), or at minimum type the fetchRepoJson return with Record<string, unknown>.


projects/page.tsx uses pervasive as any casts

The projects page uses untyped any casts throughout the issue mapping and rendering, bypassing TypeScript strict mode checks.

Evidence:

  • src/app/projects/page.tsx line 76: (i: any) => getProjectIssueStatus(i as any) === column.id
  • src/app/projects/page.tsx line 87: (statusIssues as any).map((issue: any) => (

Acceptance: Define and use the proper issue type instead of any.


Pending TypeScript 7 upgrade (PR #598)

PR #598 attempts to upgrade from TypeScript 6.0.3 to 7.0.2 but is blocked: GitHub releases don't show v7.0.2 as published, Build and Lint CI checks fail, and the AI reviewer requested changes. This PR has been stale for 7 days.

Evidence:

  • gh pr view 598 --repo misospace/dispatch — mergeStateStatus: BLOCKED, reviewDecision: CHANGES_REQUESTED
  • CI: Build FAILURE, Lint FAILURE, Docker Build FAILURE, Typecheck SUCCESS, Tests SUCCESS
  • Dependencies modified: package.json (typescript 6.0.3 → 7.0.2), package-lock.json (+349/-8)

Acceptance: Either resolve the CI failures and verify the TypeScript v7 release exists, or close the PR with a note that v7 is not yet available on npm/GitHub Releases.


Groomer LLM prompt embedded as comments

The groomer LLM system prompt lives as a multi-line TypeScript comment string in src/lib/groomer/llm.ts rather than a dedicated prompt file or template. This makes prompt versioning, diffing, and review harder than necessary.

Evidence:

  • src/lib/groomer/llm.ts lines 18–205: multi-line template literal assigned to SYSTEM_PROMPT constant
  • No prompt version or template reference in the module exports

Acceptance: Extract the system prompt to a standalone file (e.g., src/lib/groomer/prompts/classify.ts) and reference it by path.


Unanalyzed: long-running webhook handler path

The PR follow-up webhook handler (src/app/api/pr-followup/webhook/route.ts) processes pull_request_review, pull_request, and check_run events sequentially, calling multiple GitHub API and Dispatch API endpoints per event. There's no timeout guard per sub-operation, and a single slow GitHub API call could hold the webhook response past typical HTTP gateway timeouts (10–30s).

Evidence:

  • src/app/api/pr-followup/webhook/route.ts — no per-operation timeout wrapper
  • The main handler calls ingestPrFollowupEvent, which performs GitHub API fetches and Dispatch internal mutations

Acceptance: Add per-operation timeouts or kick off async processing with an immediate 202 response.

Recommended Issue Breakdown

[P0] Fix 43 test failures across 11 test files

Problem: 43 tests fail in vitest run but do not break CI. Four unit test files are missing @vitest-environment node directives and fail with "No such built-in module: node:" errors. Seven component tests fail under jsdom due to rendering/styling/environment mismatches. This creates a false sense of test health — CI does not catch regressions in these files.

Evidence:

  • npx vitest run outputs: "Test Files 11 failed | 103 passed | Tests 43 failed | 1870 passed"
  • Missing directive files: src/lib/github.test.ts, src/lib/version.test.ts, Dockerfile.test.ts, src/app/api/pr-followup/webhook/route.test.ts
  • Failing component files: layout.test.tsx, auth-controls.test.tsx, github-link.test.tsx, issue-card.test.tsx, kanban-board.test.tsx, kanban-column.test.tsx, login/page.test.tsx

Acceptance: All 1913+ tests pass in npx vitest run. CI includes the full test suite in its success/failure check.

[P1] Extract and type the GitHub client module

Problem: src/lib/github.ts is 1049 lines spanning JWT auth, issue CRUD, PR queries, CI log fetching, code search, and repo metadata. One helper (fetchRepoJson) returns Promise<any>. Module bloat makes it hard to reason about, test in isolation, and change without risk.

Evidence:

  • src/lib/github.ts — 1049 lines
  • Line 529: async function fetchRepoJson(repoFullName: string, errorPrefix: string): Promise<any>
  • Mixed responsibilities: auth setup (lines 14–100), issue fetch (lines 150–250), PR logic (lines 260–520), log fetching (lines 808–878), code search (lines 1006–1049)

Acceptance: Split into github-auth.ts, github-issues.ts, github-ci.ts, github-code-search.ts (or equivalent domain files). fetchRepoJson typed with Record<string, unknown> instead of any. Each new file under 400 lines.

[P1] Resolve stale PR #598 (TypeScript 7.0.2 upgrade)

Problem: PR #598 (TypeScript 6.0.3 → 7.0.2) has blocked CI (Build, Lint, Docker Build fail), changes requested by AI reviewer, and references a version (v7.0.2) not confirmed published on GitHub Releases. The PR has been untouched for 7 days.

Evidence:

  • gh pr view 598 --repo misospace/dispatch — mergeStateStatus: BLOCKED, reviewDecision: CHANGES_REQUESTED
  • CI: Build FAILURE, Lint FAILURE, Docker Build FAILURE
  • GitHub Releases API shows no v7.0.2 tag for microsoft/TypeScript

Acceptance: Either the PR is fixed and merged, or closed with a documented reason. The Renovate dashboard is updated to skip v7 if it's not yet available.

[P2] Remove pervasive as any casts in projects page

Problem: src/app/projects/page.tsx uses untyped any casts for issue iteration and status projection, bypassing TypeScript strict mode on that component.

Evidence:

  • src/app/projects/page.tsx line 76: (i: any) => getProjectIssueStatus(i as any) === column.id
  • src/app/projects/page.tsx line 87: (statusIssues as any).map((issue: any) => (

Acceptance: Replace all any references with a properly typed IssueWithRepo or comparable interface, verified by npx tsc --noEmit.

[P2] Extract groomer LLM prompts to dedicated files

Problem: The groomer's LLM system prompt is embedded as a 188-line string in src/lib/groomer/llm.ts, making prompt versioning, diff review, and A/B testing harder than necessary.

Evidence:

  • src/lib/groomer/llm.ts lines 18–205: const SYSTEM_PROMPT = ... template literal

Acceptance: Move the prompt content to a dedicated file under src/lib/groomer/prompts/ and reference it from llm.ts. Prompt content should be reviewable as a diff when changed.

[P3] Document in-memory rate limiter tradeoff

Problem: The rate limiter (src/lib/rate-limit.ts) is an in-memory fixed-window Map with no persistence, meaning a process restart resets all limits. This is acceptable for a single-node internal ops tool but not documented as an architectural tradeoff.

Evidence:

  • src/lib/rate-limit.ts line 44: const windows = new Map<string, WindowEntry>();
  • src/lib/rate-limit.ts line 56: fixed-window check (allows burst at boundary)

Acceptance: Mention the single-node, in-memory rate-limiter tradeoff in docs/accepted-risks.md alongside the existing entries.

Decomposed into

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions