Commit8ed88f2
fix(types): burn down all 49 TypeScript errors to 0
fix(types): burn down all 49 TypeScript errors to 0 Three real bug classes, not just type noise: - oauth.tsx/mcp.ts/well-known.ts: three handlers typed their Hono Context param via `Parameters<Parameters<typeof router.post>[1]>[0]`, reflecting off an overloaded function -- TS picked a no-handler overload and the type collapsed to `never`, cascading into ~22 errors across the three files. Replaced with a direct `Context<Env>` annotation in each. - components.tsx's RepoNav `active` prop union was missing "pipeline", "workspace", and "archaeology" even though the component's own JSX compared against those literals -- real drift between three routes and the nav component's type. pipeline.tsx/workspace-hub.tsx were also still calling RepoHeader with its pre-redesign prop shape (description/isPrivate/isFork/stars/forks, none of which the current component renders) and reading repositories.starsCount/forksCount, fields that don't exist (real ones: starCount/forkCount). - webhooks.tsx's injectable loadHooks() declared `secret: string` but the schema column is nullable; enqueueWebhookDelivery already accepted `string | null` correctly, only this one interface was wrong. Everything else was a smaller, self-contained issue: a null PgColumn comparison in oauth.tsx's DCR flow, admin.tsx indexing an already-destructured row as an array, automation-settings.tsx using camelCase onChange="..." (a raw-string handler needs the lowercase onchange intrinsic, matching every other route in this codebase), web.tsx passing a null user / a wrong live-feed type, a stale 2-arg-vs-1-arg test call, a missing tabindex number literal, an un-narrowed Bun.spawn stdout/stderr type, and a bare `JSX.Element` annotation this codebase otherwise deliberately avoids (see comments in dashboard.tsx/system-config.ts). Added a minimal ambient .d.ts for sanitize-html, which has no upstream type declarations. bun test: 3094 pass, same 4 pre-existing failures confirmed via `git stash` baseline (3 are cross-file flakiness, pass clean in isolation; 1 is a pre-existing /login test bug unrelated to any file touched here) -- nothing regressed.
15 files changed+64−388ed88f2abba06fc31f2a81db4e50a4647435ddb2
15 changed files+64−38
Modifiedsrc/__tests__/ai-review.test.ts+4−1View fileUnifiedSplit
@@ -175,7 +175,10 @@ describe("__test internals", () => {
175175 });
176176
177177 it("alreadyReviewed returns false for an unknown PR id (fail-open)", async () => {
178 const out = await __test.alreadyReviewed("00000000-0000-0000-0000-000000000000");
178 const out = await __test.alreadyReviewed(
179 "00000000-0000-0000-0000-000000000000",
180 "0000000000000000000000000000000000000000"
181 );
179182 expect(out).toBe(false);
180183 });
181184});
Modifiedsrc/git/repository.ts+7−2View fileUnifiedSplit
@@ -55,9 +55,14 @@ async function exec(
5555 // cwd doesn't exist or the binary is missing — treat as a non-zero exit
5656 return { stdout: "", stderr: "", exitCode: 128 };
5757 }
58 // `proc`'s static type is the generic Bun.spawn() overload (its exact type
59 // depends on the options object at the call site, which the try/catch's
60 // pre-declared `proc: ReturnType<typeof Bun.spawn>` loses) -- stdout/stderr
61 // are always real ReadableStreams here since we always pass
62 // `stdout: "pipe", stderr: "pipe"` above.
5863 const [stdout, stderr] = await Promise.all([
59 new Response(proc.stdout).text(),
60 new Response(proc.stderr).text(),
64 new Response(proc.stdout as ReadableStream<Uint8Array>).text(),
65 new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
6166 ]);
6267 const exitCode = await proc.exited;
6368 return { stdout, stderr, exitCode };
Modifiedsrc/routes/admin-command.tsx+1−1View fileUnifiedSplit
@@ -51,7 +51,7 @@ function statusPill(
5151 ok: boolean | null,
5252 label: string,
5353 detail?: string
54): JSX.Element {
54) {
5555 const cls = ok === null ? "cmd-pill cmd-pill-warn" : ok ? "cmd-pill cmd-pill-ok" : "cmd-pill cmd-pill-err";
5656 return (
5757 <span class={cls} title={detail ?? ""}>
Modifiedsrc/routes/admin.tsx+2−2View fileUnifiedSplit
@@ -2756,8 +2756,8 @@ admin.get("/admin", async (c) => {
27562756
27572757 const userCount = Number(uc?.n || 0);
27582758 const repoCount = Number(rc?.n || 0);
2759 const openPrCount = Number((prc as { n: number }[])[0]?.n || 0);
2760 const openIssueCount = Number((ic as { n: number }[])[0]?.n || 0);
2759 const openPrCount = Number(prc?.n || 0);
2760 const openIssueCount = Number(ic?.n || 0);
27612761 const adminCount = admins.length;
27622762
27632763 return c.html(
Modifiedsrc/routes/automation-settings.tsx+3−3View fileUnifiedSplit
@@ -284,7 +284,7 @@ automationSettings.get(
284284 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
285285 <input type="checkbox" name="auto_repair_quick" value="suggest"
286286 checked={settings.autoRepairMode !== "off"}
287 onChange="this.form.querySelector('[name=auto_repair_mode]').value = this.checked ? 'suggest' : 'off'"
287 onchange="this.form.querySelector('[name=auto_repair_mode]').value = this.checked ? 'suggest' : 'off'"
288288 style="width: 16px; height: 16px; cursor: pointer;" />
289289 <div>
290290 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-repair</div>
@@ -294,7 +294,7 @@ automationSettings.get(
294294 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
295295 <input type="checkbox" name="auto_issues_quick" value="suggest"
296296 checked={settings.autoIssuesMode !== "off"}
297 onChange="this.form.querySelector('[name=auto_issues_mode]').value = this.checked ? 'suggest' : 'off'"
297 onchange="this.form.querySelector('[name=auto_issues_mode]').value = this.checked ? 'suggest' : 'off'"
298298 style="width: 16px; height: 16px; cursor: pointer;" />
299299 <div>
300300 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Auto-issues</div>
@@ -304,7 +304,7 @@ automationSettings.get(
304304 <label style="display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 200px;">
305305 <input type="checkbox" name="doc_drift_quick" value="suggest"
306306 checked={settings.docDriftMode !== "off"}
307 onChange="this.form.querySelector('[name=doc_drift_mode]').value = this.checked ? 'suggest' : 'off'"
307 onchange="this.form.querySelector('[name=doc_drift_mode]').value = this.checked ? 'suggest' : 'off'"
308308 style="width: 16px; height: 16px; cursor: pointer;" />
309309 <div>
310310 <div style="font-size: 13px; font-weight: 600; color: var(--text-strong);">Doc-drift PRs</div>
Modifiedsrc/routes/mcp.ts+2−1View fileUnifiedSplit
@@ -17,6 +17,7 @@
1717 */
1818
1919import { Hono } from "hono";
20import type { Context } from "hono";
2021import { softAuth } from "../middleware/auth";
2122import type { AuthEnv } from "../middleware/auth";
2223import {
@@ -49,7 +50,7 @@ mcp.use("*", softAuth);
4950 * unaffected. GET /mcp discovery also stays open.
5051 */
5152function authChallenge(
52 c: Parameters<Parameters<typeof mcp.post>[1]>[0],
53 c: Context<AuthEnv>,
5354 id: unknown,
5455 opts?: { invalidToken?: boolean }
5556) {
Modifiedsrc/routes/oauth.tsx+10−7View fileUnifiedSplit
@@ -17,6 +17,7 @@
1717 */
1818
1919import { Hono } from "hono";
20import type { Context } from "hono";
2021import { and, eq, gt, isNull } from "drizzle-orm";
2122import { db } from "../db";
2223import { config } from "../lib/config";
@@ -691,12 +692,14 @@ oauth.get("/oauth/authorize", async (c) => {
691692 // Look up the app owner for the consent screen.
692693 let ownerName = "unknown";
693694 try {
694 const [ownerRow] = await db
695 .select({ username: users.username })
696 .from(users)
697 .where(eq(users.id, app.ownerId))
698 .limit(1);
699 if (ownerRow) ownerName = ownerRow.username;
695 if (app.ownerId) {
696 const [ownerRow] = await db
697 .select({ username: users.username })
698 .from(users)
699 .where(eq(users.id, app.ownerId))
700 .limit(1);
701 if (ownerRow) ownerName = ownerRow.username;
702 }
700703 } catch {
701704 /* non-fatal */
702705 }
@@ -1395,7 +1398,7 @@ oauth.post("/settings/authorizations/:appId/revoke", async (c) => {
13951398const DCR_MAX_REDIRECT_URIS = 8;
13961399
13971400function dcrError(
1398 c: Parameters<Parameters<typeof oauth.post>[1]>[0],
1401 c: Context<AuthEnv>,
13991402 error: string,
14001403 description: string,
14011404 status: 400 | 401 | 403 = 400
Modifiedsrc/routes/pipeline.tsx+3−6View fileUnifiedSplit
@@ -391,13 +391,10 @@ app.get("/:owner/:repo/pipeline", async (c) => {
391391 <RepoHeader
392392 owner={owner}
393393 repo={repo}
394 description={repoRow.repo.description ?? ""}
395 isPrivate={repoRow.repo.isPrivate}
396 isFork={!!repoRow.repo.forkedFromId}
397 stars={repoRow.repo.starsCount ?? 0}
398 forks={repoRow.repo.forksCount ?? 0}
394 starCount={repoRow.repo.starCount ?? 0}
395 forkCount={repoRow.repo.forkCount ?? 0}
399396 />
400 <RepoNav owner={owner} repo={repo} active="pipeline" isOwner={isOwner} />
397 <RepoNav owner={owner} repo={repo} active="pipeline" />
401398
402399 <div style="margin: var(--space-5) 0 var(--space-3); display:flex; align-items:baseline; gap:var(--space-3)">
403400 <h2 style="font-size:18px;font-weight:700;color:var(--text);margin:0">CI/CD Pipeline</h2>
Modifiedsrc/routes/web.tsx+9−5View fileUnifiedSplit
@@ -64,9 +64,9 @@ import { resolveRepoAccess, satisfiesAccess } from "../middleware/repo-access";
6464import { claudeSemanticSearch } from "../lib/claude-semantic-search";
6565import { isAiAvailable } from "../lib/ai-client";
6666import { trackByName } from "../lib/traffic";
67import { LandingPage, type LandingLiveFeed } from "../views/landing";
67import { LandingPage } from "../views/landing";
6868import { Landing2030Page } from "../views/landing-2030";
69import { LandingProPage } from "../views/landing-pro";
69import { LandingProPage, type LandingProLiveFeed } from "../views/landing-pro";
7070import { computePublicStats, type PublicStats } from "../lib/public-stats";
7171import {
7272 listQueuedAiBuildIssues,
@@ -1907,7 +1907,7 @@ web.get("/", async (c) => {
19071907 // Block M1 — initial SSR snapshot for the live-now demo feed.
19081908 // The helpers in lib/demo-activity.ts never throw, but we still wrap
19091909 // in try/catch so a freak module-level explosion can't take down /.
1910 let liveFeed: LandingLiveFeed | null = null;
1910 let liveFeed: LandingProLiveFeed | null = null;
19111911 try {
19121912 const [queued, merges, reviewList, reviewCount, feed] = await Promise.all([
19131913 listQueuedAiBuildIssues(3),
@@ -1921,7 +1921,7 @@ web.get("/", async (c) => {
19211921 repo: i.repo,
19221922 number: i.number,
19231923 title: i.title,
1924 createdAt: i.createdAt,
1924 createdAt: new Date(i.createdAt),
19251925 })),
19261926 merges: merges.map((m) => ({
19271927 repo: m.repo,
@@ -2201,7 +2201,11 @@ web.post("/new", requireAuth, async (c) => {
22012201// Daily brief — GET /brief
22022202web.get("/brief", (c) => {
22032203 const user = c.get("user");
2204 return c.html(<DailyBrief user={user} />);
2204 return c.html(
2205 <DailyBrief
2206 user={user ? { name: user.displayName || user.username } : undefined}
2207 />
2208 );
22052209});
22062210
22072211// Trust report — GET /trust (public)
Modifiedsrc/routes/webhooks.tsx+1−1View fileUnifiedSplit
@@ -810,7 +810,7 @@ export async function fireWebhooks(
810810 // requiredOwnersApproved).
811811 deps: {
812812 loadHooks: (repositoryId: string) => Promise<
813 Array<{ id: string; secret: string; isActive: boolean; events: string }>
813 Array<{ id: string; secret: string | null; isActive: boolean; events: string }>
814814 >;
815815 enqueueWebhookDelivery: typeof enqueueWebhookDelivery;
816816 drainPendingDeliveries: typeof drainPendingDeliveries;
Modifiedsrc/routes/well-known.ts+2−1View fileUnifiedSplit
@@ -21,6 +21,7 @@
2121 */
2222
2323import { Hono } from "hono";
24import type { Context } from "hono";
2425import { config } from "../lib/config";
2526import { SUPPORTED_SCOPES } from "../lib/oauth";
2627
@@ -76,7 +77,7 @@ wellKnown.get("/.well-known/oauth-authorization-server", (c) => {
7677 * MCP connector reads this to learn which authorization server guards the
7778 * `/mcp` resource and which scopes it accepts.
7879 */
79function protectedResourceDoc(c: Parameters<Parameters<typeof wellKnown.get>[1]>[0]) {
80function protectedResourceDoc(c: Context) {
8081 const b = base();
8182 return c.json(
8283 {
Modifiedsrc/routes/workspace-hub.tsx+3−6View fileUnifiedSplit
@@ -227,13 +227,10 @@ app.get("/:owner/:repo/workspace", async (c) => {
227227 <RepoHeader
228228 owner={owner}
229229 repo={repo}
230 description={repoRow.repo.description ?? ""}
231 isPrivate={repoRow.repo.isPrivate}
232 isFork={!!repoRow.repo.forkedFromId}
233 stars={repoRow.repo.starsCount ?? 0}
234 forks={repoRow.repo.forksCount ?? 0}
230 starCount={repoRow.repo.starCount ?? 0}
231 forkCount={repoRow.repo.forkCount ?? 0}
235232 />
236 <RepoNav owner={owner} repo={repo} active="workspace" isOwner={isOwner} />
233 <RepoNav owner={owner} repo={repo} active="workspace" />
237234
238235 {/* Hero intro */}
239236 <div class="ws-intro">
Addedsrc/types/sanitize-html.d.ts+12−0View fileUnifiedSplit
@@ -0,0 +1,12 @@
1// No @types/sanitize-html package exists on the registry; minimal ambient
2// declaration covering the surface src/lib/markdown.ts actually uses
3// (the default sanitize function + the IOptions bag passed to it).
4declare module "sanitize-html" {
5 namespace sanitizeHtml {
6 interface IOptions {
7 [key: string]: any;
8 }
9 }
10 function sanitizeHtml(html: string, options?: sanitizeHtml.IOptions): string;
11 export = sanitizeHtml;
12}
Modifiedsrc/views/components.tsx+4−1View fileUnifiedSplit
@@ -177,7 +177,10 @@ export const RepoNav: FC<{
177177 | "nl-search"
178178 | "contributors"
179179 | "pulse"
180 | "traffic";
180 | "traffic"
181 | "pipeline"
182 | "workspace"
183 | "archaeology";
181184 /** Current authenticated user — used for owner-only tab gating. */
182185 currentUser?: string | null;
183186 /** Repo owner username — used for owner-only tab gating. */
Modifiedsrc/views/pr-redesign.tsx+1−1View fileUnifiedSplit
@@ -232,7 +232,7 @@ export const PrRedesign: FC<PrRedesignProps> = (props) => {
232232
233233 {/* Merge Impact panel */}
234234 <div class="pr-card pr-impact-card" id="pr-impact-card">
235 <div class="pr-impact-header" id="pr-impact-toggle" role="button" tabindex="0" aria-expanded="false" aria-controls="pr-impact-body">
235 <div class="pr-impact-header" id="pr-impact-toggle" role="button" tabindex={0} aria-expanded="false" aria-controls="pr-impact-body">
236236 <span class="pr-impact-risk-badge">62</span>
237237 <strong class="pr-impact-title">Merge Impact</strong>
238238 <span class="pr-impact-summary">
239239