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
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 nodedirective on 4 test files that usenode:*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— importsnode:crypto, no@vitest-environment nodesrc/lib/version.test.ts— importsnode:fsviaversion.ts, no directiveDockerfile.test.ts— importsnode:fs,node:path, no directivesrc/app/api/pr-followup/webhook/route.test.ts— importsnode:cryptoviaprisma.ts, no directivelayout.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.tsxRelevant 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 runwithout environment errors.Rate limiter is in-memory only
The rate limiter in
src/lib/rate-limit.tsis a module-levelMap<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.tsline 44:const windows = new Map<string, WindowEntry>();src/lib/rate-limit.tsline 56: simple fixed-window check:if (entry.count < limit)src/lib/rate-limit.tsline 82:PRUNE_THRESHOLD = 1000— no timer-based sweepAcceptance: 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 (fetchRepoJsonreturningPromise<any>) and a manual redirect-follow for log blob fetching.Evidence:
src/lib/github.ts— 1049 lines totalsrc/lib/github.tsline 529:async function fetchRepoJson(repoFullName: string, errorPrefix: string): Promise<any>Acceptance: Split into domain-specific files (e.g.,
github-auth.ts,github-issues.ts,github-ci.ts), or at minimum type thefetchRepoJsonreturn withRecord<string, unknown>.projects/page.tsxuses pervasiveas anycastsThe projects page uses untyped
anycasts throughout the issue mapping and rendering, bypassing TypeScript strict mode checks.Evidence:
src/app/projects/page.tsxline 76:(i: any) => getProjectIssueStatus(i as any) === column.idsrc/app/projects/page.tsxline 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_REQUESTEDpackage.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.tsrather than a dedicated prompt file or template. This makes prompt versioning, diffing, and review harder than necessary.Evidence:
src/lib/groomer/llm.tslines 18–205: multi-line template literal assigned toSYSTEM_PROMPTconstantAcceptance: 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 wrapperingestPrFollowupEvent, which performs GitHub API fetches and Dispatch internal mutationsAcceptance: 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 runbut do not break CI. Four unit test files are missing@vitest-environment nodedirectives 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 runoutputs: "Test Files 11 failed | 103 passed | Tests 43 failed | 1870 passed"src/lib/github.test.ts,src/lib/version.test.ts,Dockerfile.test.ts,src/app/api/pr-followup/webhook/route.test.tslayout.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.tsxAcceptance: 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.tsis 1049 lines spanning JWT auth, issue CRUD, PR queries, CI log fetching, code search, and repo metadata. One helper (fetchRepoJson) returnsPromise<any>. Module bloat makes it hard to reason about, test in isolation, and change without risk.Evidence:
src/lib/github.ts— 1049 linesasync function fetchRepoJson(repoFullName: string, errorPrefix: string): Promise<any>Acceptance: Split into
github-auth.ts,github-issues.ts,github-ci.ts,github-code-search.ts(or equivalent domain files).fetchRepoJsontyped withRecord<string, unknown>instead ofany. 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_REQUESTEDAcceptance: 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 anycasts in projects pageProblem:
src/app/projects/page.tsxuses untypedanycasts for issue iteration and status projection, bypassing TypeScript strict mode on that component.Evidence:
src/app/projects/page.tsxline 76:(i: any) => getProjectIssueStatus(i as any) === column.idsrc/app/projects/page.tsxline 87:(statusIssues as any).map((issue: any) => (Acceptance: Replace all
anyreferences with a properly typedIssueWithRepoor comparable interface, verified bynpx 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.tslines 18–205:const SYSTEM_PROMPT = ...template literalAcceptance: Move the prompt content to a dedicated file under
src/lib/groomer/prompts/and reference it fromllm.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-windowMapwith 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.tsline 44:const windows = new Map<string, WindowEntry>();src/lib/rate-limit.tsline 56: fixed-window check (allows burst at boundary)Acceptance: Mention the single-node, in-memory rate-limiter tradeoff in
docs/accepted-risks.mdalongside the existing entries.Decomposed into