From 64140c7add5cf444ca82c3701240c42924127fce Mon Sep 17 00:00:00 2001 From: Aravind Date: Thu, 17 Sep 2026 17:23:28 +0530 Subject: [PATCH] chore(ts-migration): migrate ai engine to TypeScript Phase 4b-i. Type-only, and the production bundle proves it: built from this commit's parent and from this commit, all six chunk hashes match. The plain emitted JavaScript is byte-identical at 34,095 bytes. Four annotation sites, no logic touched: - `ROLE_TITLES` and `AVAILABILITY_TOKENS` get `[RegExp, string][]`. Left to inference the element widens to `string | RegExp`, which has no `.test`. Explicit tuples rather than `as const`, which would also have worked and would additionally have made the arrays readonly - a change to the type this module publishes for no benefit it uses. - `ROUTES` gets `[RegExp, (prompt: string) => any, number][]`, because all three positions are used for what they are: `.test()` on the first, a call on the second, `think(ms)` on the third. - `invokeLLM` gets a real parameter type. It inferred `{ prompt?: string }` from its own destructuring defaults, and that single inference was responsible for nine errors in files this commit does not touch - eight in `krowAi.js`, one in `provingGround.ts` - every one of them a caller passing `response_json_schema` or `model`, which the real integration accepts. Naming the options type fixes all nine from here. - `uploadFile` gets `{ file?: File }`. The return type of `invokeLLM` stays `Promise`, deliberately. The ten handlers behind the router return ten different shapes, and `krowAi.js` branches on the result at run time - `typeof res === 'string' ? res : res.text || String(res)` - which a precise union would reject on every branch without a `.text`. The looseness is the contract, not an omission. `InvokeLLMOptions` is exported as a type only; the runtime exports are still exactly `invokeLLM` and `uploadFile`. Verified in isolation from the parallel feature work (f96f128 plus this file): tsc 53 -> 37, a set-difference against the baseline showing sixteen removed and none added; skill-check 1641/1642 with only the known stale-fixture failure; Owliver baseline 59/59; lint 0 errors; build succeeds with the API origin inlined; the nine Owliver baseline artifacts unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- src/api/{aiEngine.js => aiEngine.ts} | 49 ++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) rename src/api/{aiEngine.js => aiEngine.ts} (94%) diff --git a/src/api/aiEngine.js b/src/api/aiEngine.ts similarity index 94% rename from src/api/aiEngine.js rename to src/api/aiEngine.ts index 32268a6..fdd0f29 100644 --- a/src/api/aiEngine.js +++ b/src/api/aiEngine.ts @@ -547,7 +547,11 @@ function matchTalent(prompt) { /* ── 7. Owliver profile builder ────────────────────────────────────────── */ /** Job titles a worker might name for themselves, most specific first. */ -const ROLE_TITLES = [ +/* Explicit tuple, not `as const`: the entries are read as a pair — `.find(([re]) => re.test(…))` + then `role[1]` — and without it the element widens to `string | RegExp`, which has no `.test`. + `as const` would also work and would additionally make the array and its entries `readonly`, + a change to the type this module publishes for no benefit it uses. */ +const ROLE_TITLES: [RegExp, string][] = [ [/banquet captain|captain/, 'Banquet Captain'], [/executive chef/, 'Executive Chef'], [/sous chef/, 'Sous Chef'], @@ -563,7 +567,8 @@ const ROLE_TITLES = [ [/manager/, 'Manager'], ]; -const AVAILABILITY_TOKENS = [ +/** Same pairing, same reason — matched by regex, collected by label. */ +const AVAILABILITY_TOKENS: [RegExp, string][] = [ [/weekend|saturday|sunday|sábado|domingo/i, 'Weekends'], [/weekday|monday|tuesday|wednesday|thursday|friday|semana/i, 'Weekdays'], [/evening|night|noche|tarde/i, 'Evenings'], @@ -768,7 +773,15 @@ function evaluateChallenge(prompt) { * Recognizes each workflow by the phrase its prompt opens with. Order matters * only in that every branch is mutually exclusive by design. */ -const ROUTES = [ +/* `[pattern, handler, latencyMs]`. Spelled out because all three positions are used for what + they are: `.test()` on the first, a call on the second, and `think(ms)` on the third. Left to + inference the element becomes the union of all three and none of those work. + + The handler returns `any` deliberately. The ten handlers below return ten different shapes, + which is the point of a router — and `krowAi.js` relies on it: one caller does + `typeof res === 'string' ? res : res.text || String(res)`, which a precise union would + reject on every branch that has no `.text`. */ +const ROUTES: [RegExp, (prompt: string) => any, number][] = [ [/^You are KROW's AI screening engine/m, screenCandidate, 1100], [/^You are an expert hiring copywriter/m, generateJobDescription, 1400], [/Extract and infer a structured resume/m, buildResume, 1200], @@ -782,8 +795,32 @@ const ROUTES = [ () => 'Walk me through the exact order you would do that in — what happens first, and who do you tell?', 700], ]; -/** Drop-in replacement for `integrations.Core.InvokeLLM`. */ -export async function invokeLLM({ prompt = '', response_json_schema: schema } = {}) { +/** + * What a caller may send. Every field optional, because the destructuring default `= {}` + * says a call with no argument at all is legal, and it is one this module answers. + * + * `model` and `file_urls` are not read here — the local engine routes on the prompt and + * ignores both — but callers send them, and a parameter type that omitted them would reject + * eight call sites in `krowAi.js` and one in `provingGround.ts` for passing a field the real + * integration accepts. Declaring them describes the call this function is a drop-in for. + */ +export interface InvokeLLMOptions { + prompt?: string; + response_json_schema?: any; + model?: string; + file_urls?: string[]; +} + +/** + * Drop-in replacement for `integrations.Core.InvokeLLM`. + * + * The return stays `any` on purpose. See the note on `ROUTES`: the shape depends on which + * handler matched, and narrowing it here would break callers that branch on the result at + * run time rather than by type. + */ +export async function invokeLLM( + { prompt = '', response_json_schema: schema }: InvokeLLMOptions = {}, +): Promise { const route = ROUTES.find(([pattern]) => pattern.test(prompt)); if (!route) { @@ -800,7 +837,7 @@ export async function invokeLLM({ prompt = '', response_json_schema: schema } = } /** Drop-in replacement for `integrations.Core.UploadFile`. */ -export async function uploadFile({ file } = {}) { +export async function uploadFile({ file }: { file?: File } = {}) { await think(500); if (!file) return { file_url: '' }; // A blob URL keeps uploaded media viewable for the rest of the session