Commit2316901unknown_key
fix: polish pass — kill 50+ TS errors and the 1 failing test
fix: polish pass — kill 50+ TS errors and the 1 failing test
Baseline before this session: 1 failing test (broken refactor leftover),
50+ TypeScript errors across the tree. None blocked tests at runtime
because Bun runs TS leniently, but the compiler surface was noisy enough
to hide real bugs and slow the next polish pass.
Real bugs found and fixed:
- src/__tests__/crontech-deploy.test.ts — a refactor left
`installFetchCapture` (never defined) plus shorthand `after, before`
references with no in-scope bindings. Rewritten to use the in-file
`captureFetch()` helper and declare the SHA fixtures inline.
- src/routes/pulls.tsx — `getRepoPath` was imported twice (named in two
separate blocks). TS flagged the duplicate identifier; runtime was
surviving on hoisting. Removed the orphan import.
- src/lib/workflow-runner.ts — `_coerceExtParsed` was called but never
defined. Workflows that successfully parsed via the v2 extended parser
would have crashed the runner. Added the coercion helper that maps an
`ExtendedParseResult` to the v1 `ParsedWorkflow` shape this runner
consumes; falls back to null on malformed input so callers degrade to
the locked v1 parser.
- src/lib/mcp-tools.ts — `codebase_explanations` schema has
`generatedAt`, not `createdAt`. MCP `gluecron_repo_explain_codebase`
tool was selecting + ordering by a non-existent column, which would
have thrown at runtime once a caller invoked it.
- src/lib/follows.ts — `listFollowers` / `listFollowing` selected an
explicit subset of `users` columns but missed `isAdmin`. The return
type `User[]` requires `isAdmin`, so callers consuming `.isAdmin` on
the result would have read `undefined`.
- src/lib/auto-repair.ts — `commitSha: result.sha` could be `undefined`
but `recordRepair` expects `string | null`. Coerced via `?? null`.
Type-system polish:
- src/middleware/auth.ts — `AuthEnv.Variables` was missing entries for
context vars actually set by other middleware: `csrfToken`,
`requestId`, `requestStart`, `repository`, `repoAccess`, `authMethod`,
`tokenScopes`. Declared them as optionals so `c.get(...)` resolves.
- src/app.tsx — top-level `new Hono()` is now typed `<AuthEnv>` so the
global `onError` can read `requestId` without a never-cast.
- src/db/index.ts — the `AnyDb` union narrowed `.returning({...})` to
the most-restrictive overload set (which has 0 args), causing 20+
errors at call sites. Both drivers expose identical PG query-builder
surfaces at runtime, so the exported proxy is now cast to
`NeonHttpDatabase<typeof schema>` for ergonomics without changing
runtime behaviour.
- src/middleware/repo-access.ts — programmatic `jsx(...)` calls had
array-children shapes that didn't match Hono's stricter JSX child
typing. Switched to spread-children + small `as any` casts since the
helper is rendered only on the 403 path.
- src/views/ui.tsx — `Form` component now accepts `id` and `encType`
(used by `specs.tsx` and would-be file-upload forms).
- src/routes/import.tsx, src/routes/import-bulk.tsx — `method="POST"`
→ `method="post"` (HTML attribute is lowercase per spec).
- src/views/live-feed.tsx, src/views/log-tail.tsx — dropped explicit
`JSX.Element` return annotation; the namespace isn't in scope and
Hono's JSX runtime infers correctly without it.
After: bunx tsc --noEmit → 0 errors. bun test → 1256 pass / 0 fail
(was 1255/1).
https://claude.ai/code/session_01RzRiNtuiTJYnSCP8r6Xj5z15 files changed+112−2523169010fae41536590a61a90de49757be1d35a7
15 changed files+112−25
Modifiedsrc/__tests__/crontech-deploy.test.ts+4−2View fileUnifiedSplit
@@ -143,9 +143,11 @@ describe("hooks/post-receive — triggerCrontechDeploy (BLK-016 sender)", () =>
143143 expect(calls[0]!.init.method).toBe("POST");
144144 });
145145
146 it("sends Authorization: Bearer <secret> when GLUECRON_WEBHOOK_SECRET is set", async () => {
146 it("sends the GitHub-style push payload (event/repo/ref/sha/pusher/commits)", async () => {
147147 process.env.GLUECRON_WEBHOOK_SECRET = "webhook-test-value";
148 const calls = installFetchCapture();
148 const { calls, fn } = captureFetch();
149 const after = "b".repeat(40);
150 const before = "c".repeat(40);
149151
150152 await triggerCrontechDeploy(
151153 makeArgs({
Modifiedsrc/app.tsx+3−1View fileUnifiedSplit
@@ -104,7 +104,9 @@ import workflowSecretsRoutes from "./routes/workflow-secrets";
104104import { authRateLimit, gitRateLimit, searchRateLimit } from "./middleware/rate-limit";
105105import { csrfToken, csrfProtect } from "./middleware/csrf";
106106
107const app = new Hono();
107import type { AuthEnv } from "./middleware/auth";
108
109const app = new Hono<AuthEnv>();
108110
109111// Request context (request ID, start time) runs before everything else
110112app.use("*", requestContext);
Modifiedsrc/db/index.ts+8−1View fileUnifiedSplit
@@ -60,8 +60,15 @@ export function getDb(): AnyDb {
6060
6161// Re-export as `db` for convenience — proxies to the chosen driver lazily.
6262// Will throw on first access if DATABASE_URL is unset.
63//
64// We type the proxy as `NeonHttpDatabase<typeof schema>` for ergonomics: the
65// two driver wrappers expose identical query-builder APIs at runtime (both are
66// `PgDatabase` underneath), but the union type narrows TypeScript's overload
67// resolution and makes `.returning({...})` etc. show as "0 args expected".
68// Casting to the Neon variant exposes the full drizzle PG query-builder
69// surface to call sites without changing runtime behaviour.
6370export const db = new Proxy({} as AnyDb, {
6471 get(_target, prop, receiver) {
6572 return Reflect.get(getDb(), prop, receiver) as unknown;
6673 },
67});
74}) as NeonHttpDatabase<typeof schema>;
Modifiedsrc/lib/auto-repair.ts+1−1View fileUnifiedSplit
@@ -533,7 +533,7 @@ ${parsed.patches.map((p) => `- ${p.path}: ${p.reason}`).join("\n")}
533533 tier: "ai-sonnet",
534534 patchSummary: parsed.summary,
535535 filesChanged: result.filesChanged,
536 commitSha: result.sha,
536 commitSha: result.sha ?? null,
537537 outcome: "success",
538538 }).catch(() => {});
539539 return {
Modifiedsrc/lib/follows.ts+2−0View fileUnifiedSplit
@@ -91,6 +91,7 @@ export async function listFollowers(userId: string): Promise<User[]> {
9191 notifyEmailOnGateFail: users.notifyEmailOnGateFail,
9292 notifyEmailDigestWeekly: users.notifyEmailDigestWeekly,
9393 lastDigestSentAt: users.lastDigestSentAt,
94 isAdmin: users.isAdmin,
9495 createdAt: users.createdAt,
9596 updatedAt: users.updatedAt,
9697 })
@@ -115,6 +116,7 @@ export async function listFollowing(userId: string): Promise<User[]> {
115116 notifyEmailOnGateFail: users.notifyEmailOnGateFail,
116117 notifyEmailDigestWeekly: users.notifyEmailDigestWeekly,
117118 lastDigestSentAt: users.lastDigestSentAt,
119 isAdmin: users.isAdmin,
118120 createdAt: users.createdAt,
119121 updatedAt: users.updatedAt,
120122 })
Modifiedsrc/lib/mcp-tools.ts+3−3View fileUnifiedSplit
@@ -287,18 +287,18 @@ const repoExplain: McpToolHandler = {
287287 .select({
288288 commitSha: codebaseExplanations.commitSha,
289289 markdown: codebaseExplanations.markdown,
290 createdAt: codebaseExplanations.createdAt,
290 generatedAt: codebaseExplanations.generatedAt,
291291 })
292292 .from(codebaseExplanations)
293293 .where(eq(codebaseExplanations.repositoryId, r.id))
294 .orderBy(desc(codebaseExplanations.createdAt))
294 .orderBy(desc(codebaseExplanations.generatedAt))
295295 .limit(1);
296296 if (!row) {
297297 return { explanation: null };
298298 }
299299 return {
300300 commitSha: row.commitSha,
301 generatedAt: row.createdAt,
301 generatedAt: row.generatedAt,
302302 markdown: row.markdown,
303303 };
304304 },
Modifiedsrc/lib/workflow-runner.ts+55−0View fileUnifiedSplit
@@ -132,6 +132,61 @@ function parseWorkflow(parsed: string): ParsedWorkflow | null {
132132 return null;
133133}
134134
135/**
136 * Coerce an `ExtendedParseResult` (from `workflow-parser-ext`) into the
137 * permissive `ParsedWorkflow` shape used by this v1 runner. The extended
138 * parser produces richer per-step/per-job metadata (needs, strategy, if,
139 * uses, env, outputs) — v1 only consumes `name`, `runsOn`, and `steps[].run`
140 * so the extras are tolerated via the index signature on `ParsedJob`.
141 *
142 * Returns null on parse failure or malformed input so callers fall back to
143 * the locked v1 parser.
144 */
145function _coerceExtParsed(result: unknown): ParsedWorkflow | null {
146 if (!result || typeof result !== "object") return null;
147 const r = result as { ok?: unknown; workflow?: unknown };
148 if (r.ok !== true) return null;
149 const wf = r.workflow;
150 if (!wf || typeof wf !== "object") return null;
151 const w = wf as {
152 name?: unknown;
153 on?: unknown;
154 jobs?: unknown;
155 };
156 if (!Array.isArray(w.jobs)) return null;
157 const jobs: ParsedJob[] = [];
158 for (const j of w.jobs) {
159 if (!j || typeof j !== "object") continue;
160 const job = j as Record<string, unknown>;
161 const steps: ParsedStep[] = [];
162 if (Array.isArray(job.steps)) {
163 for (const s of job.steps) {
164 if (!s || typeof s !== "object") continue;
165 const step = s as Record<string, unknown>;
166 steps.push({
167 name: typeof step.name === "string" ? step.name : undefined,
168 run: typeof step.run === "string" ? step.run : undefined,
169 });
170 }
171 }
172 jobs.push({
173 name: typeof job.name === "string" ? job.name : undefined,
174 runsOn:
175 typeof job.runsOn === "string"
176 ? job.runsOn
177 : typeof job["runs-on"] === "string"
178 ? (job["runs-on"] as string)
179 : undefined,
180 steps,
181 });
182 }
183 return {
184 name: typeof w.name === "string" ? w.name : undefined,
185 on: w.on,
186 jobs,
187 };
188}
189
135190// ---------------------------------------------------------------------------
136191// Terminal-state helpers — all wrap DB calls in try/catch.
137192// ---------------------------------------------------------------------------
Modifiedsrc/middleware/auth.ts+14−0View fileUnifiedSplit
@@ -22,6 +22,20 @@ export type AuthEnv = {
2222 /** When the caller authenticated via an OAuth bearer token, these are set. */
2323 oauthScopes?: string[];
2424 oauthAppId?: string;
25 /** Set by `csrf` middleware on GET requests; required field on POST forms. */
26 csrfToken?: string;
27 /** Set by `requestContext` middleware on every request — opaque request id. */
28 requestId?: string;
29 /** Wall-clock ms when the request entered the app — used by metrics. */
30 requestStart?: number;
31 /** Set by `requireRepoAccess` middleware — the resolved repository row. */
32 repository?: unknown;
33 /** Set by `requireRepoAccess` — the caller's access level ('read'|'write'|'admin'|'owner'). */
34 repoAccess?: string;
35 /** Set by API-auth middleware — how the caller authenticated. */
36 authMethod?: "token" | "session" | "none";
37 /** Set by API-auth middleware — scopes carried by the PAT or session. */
38 tokenScopes?: string[];
2539 };
2640};
2741
Modifiedsrc/middleware/repo-access.ts+6−8View fileUnifiedSplit
@@ -195,23 +195,21 @@ export function requireRepoAccess(
195195 import("hono/jsx"),
196196 import("../views/layout"),
197197 ]);
198 const body = jsx(
198 const body = (jsx as any)(
199199 "div",
200200 {
201201 style:
202202 "max-width: 600px; margin: 80px auto; padding: 24px; text-align: center;",
203203 },
204 [
205 jsx("h1", { style: "margin-bottom: 12px" }, ["403 — Access denied"]),
206 jsx("p", { style: "color: var(--muted, #8b949e)" }, [reason]),
207 ]
204 (jsx as any)("h1", { style: "margin-bottom: 12px" }, "403 — Access denied"),
205 (jsx as any)("p", { style: "color: var(--muted, #8b949e)" }, reason)
208206 );
209 const page = jsx(
207 const page = (jsx as any)(
210208 Layout as any,
211209 { title: "Access denied", user },
212 [body]
210 body
213211 );
214 return c.html(page, 403);
212 return c.html(page as any, 403);
215213 }
216214
217215 c.set("repoAccess", access);
Modifiedsrc/routes/import-bulk.tsx+1−1View fileUnifiedSplit
@@ -125,7 +125,7 @@ importBulkRoutes.get("/import/bulk", requireAuth, async (c) => {
125125 </ul>
126126 </div>
127127
128 <form method="POST" action="/import/bulk">
128 <form method="post" action="/import/bulk">
129129 <div class="form-group">
130130 <label style="display:block; margin-bottom:4px; font-size:13px">
131131 GitHub org
Modifiedsrc/routes/import.tsx+3−3View fileUnifiedSplit
@@ -129,7 +129,7 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
129129 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
130130 Import all public repositories from a GitHub user or organization.
131131 </p>
132 <form method="POST" action="/import/github/user" data-import-form>
132 <form method="post" action="/import/github/user" data-import-form>
133133 <div style="display: flex; gap: 8px">
134134 <input
135135 type="text"
@@ -151,7 +151,7 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
151151 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
152152 Import a specific repository by URL (https, ssh, or owner/repo).
153153 </p>
154 <form method="POST" action="/import/github/repo" data-import-form>
154 <form method="post" action="/import/github/repo" data-import-form>
155155 <div style="display: flex; gap: 8px">
156156 <input
157157 type="text"
@@ -174,7 +174,7 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
174174 Use a GitHub personal access token to import private repositories too.
175175 Generate one at github.com → Settings → Developer settings → Personal access tokens.
176176 </p>
177 <form method="POST" action="/import/github/user" data-import-form>
177 <form method="post" action="/import/github/user" data-import-form>
178178 <div class="form-group">
179179 <input
180180 type="text"
Modifiedsrc/routes/pulls.tsx+0−1View fileUnifiedSplit
@@ -27,7 +27,6 @@ import { isAiReviewEnabled, triggerAiReview } from "../lib/ai-review";
2727import { triggerPrTriage } from "../lib/pr-triage";
2828import { generatePrSummary } from "../lib/ai-generators";
2929import { isAiAvailable } from "../lib/ai-client";
30import { getRepoPath } from "../git/repository";
3130import { runAllGateChecks } from "../lib/gate";
3231import type { GateCheckResult } from "../lib/gate";
3332import {
Modifiedsrc/views/live-feed.tsx+1−1View fileUnifiedSplit
@@ -13,7 +13,7 @@ import { liveSubscribeScript } from "../lib/sse-client";
1313export function LiveFeed(props: {
1414 topic: string;
1515 title?: string;
16}): JSX.Element {
16}) {
1717 const title = props.title ?? "Live activity";
1818 const listId = "live-feed";
1919
Modifiedsrc/views/log-tail.tsx+1−1View fileUnifiedSplit
@@ -19,7 +19,7 @@ export function LogTail(props: {
1919 fallbackLogs?: string | null;
2020 height?: string;
2121 reloadOnRunDone?: boolean;
22}): JSX.Element {
22}) {
2323 const elementId = `log-tail-${props.runId}${props.jobId ? "-" + props.jobId : ""}`;
2424 const topic = `workflow-run-${props.runId}`;
2525 const script = liveLogTailScript({
Modifiedsrc/views/ui.tsx+10−2View fileUnifiedSplit
@@ -116,9 +116,17 @@ export const Form: FC<
116116 method?: string;
117117 csrfToken?: string;
118118 class?: string;
119 id?: string;
120 encType?: string;
119121 }>
120> = ({ children, action, method = "POST", csrfToken, class: cls }) => (
121 <form method={method.toLowerCase() as any} action={action} class={cls || ""}>
122> = ({ children, action, method = "POST", csrfToken, class: cls, id, encType }) => (
123 <form
124 method={method.toLowerCase() as any}
125 action={action}
126 class={cls || ""}
127 id={id}
128 enctype={encType as any}
129 >
122130 {csrfToken && <input type="hidden" name="_csrf" value={csrfToken} />}
123131 {children}
124132 </form>
125133