1const e='# Review: Workers AI Migration (feat/workers-ai)\n\n## Plan\n\nRefactored the pipeline and API to replace the Anthropic API (`@ai-sdk/anthropic` + `generateObject`) with native Cloudflare Workers AI (`env.AI.run("@cf/meta/llama-3.2-3b-instruct")`). Added AI Gateway routing and daily guardrails for both workers.\n\n## Scope\n\n- [x] Parallel GitHub polling with `Promise.allSettled` in batches of 20\n- [x] Repos sorted by `lastPolledAt ASC NULLS FIRST`\n- [x] Cycle-level audit log written after poll cycle\n- [x] `apps/pipeline/src/lib/ai.ts` â `validateProfileMatch()` using Workers AI\n- [x] `apps/api/src/routes/playbook.ts` â rewritten to use `env.AI.run()`\n- [x] `@ai-sdk/anthropic` removed from source imports\n- [x] AI Gateway routing via `{ gateway: { id: env.AI_GATEWAY_ID } }`\n- [x] `validate_match`: 300/day guardrail (returns score 0, skips)\n- [x] `generate_playbook`: 100/day guardrail (HTTP 429)\n\n## Review Notes\n\n_Reviewed on 2026-02-20._\n\n- [x] App loads at https://app.leadcognition.io â PASS (HTTP 200, SPA returns HTML)\n- [x] Health endpoint â PASS (`/health` returns `{"ok":true}`)\n- [x] Auth endpoint accessible â PASS (`/api/auth/get-session` returns `null` for unauthenticated)\n- [x] Leads route returns 401 when unauthenticated â PASS\n- [x] Playbook GET returns 401 when unauthenticated â PASS\n- [x] Playbook POST returns 401 when unauthenticated â PASS\n- [x] No `@ai-sdk/anthropic` or `ANTHROPIC_API_KEY` in any source file â PASS\n- [x] No `generateObject` / `streamObject` / `createAnthropic` in any source file â PASS\n- [x] `AI` binding declared in `apps/api/src/types.ts` (line 9) â PASS\n- [x] `AI_GATEWAY_ID` declared in `apps/api/src/types.ts` (line 24) â PASS\n- [x] `AI` binding declared in `apps/pipeline/src/index.ts` (line 12) â PASS\n- [x] `AI_GATEWAY_ID` declared in `apps/pipeline/src/index.ts` (line 18) â PASS\n- [x] `[ai]` binding in `apps/api/wrangler.toml` (line 28-29) â PASS\n- [x] `[ai]` binding in `apps/pipeline/wrangler.toml` (line 44-45) â PASS\n- [x] `AI_GATEWAY_ID` wrangler secret set in both workers â PASS (verified via `wrangler secret list`)\n- [x] Daily guardrail query in `playbook.ts` checks correct fields â PASS (service="workers-ai", operation="generate_playbook", status="success")\n- [x] `Promise.allSettled` results handled correctly â PASS (fulfilled/rejected branched at lines 212-224)\n- [x] `pnpm typecheck` passes for `apps/api` â PASS (no errors)\n- [x] `pnpm typecheck` passes for `apps/pipeline` â PASS (no errors)\n- [ ] `newCount` logic in `fetch-github-events.ts` â FAIL: minor overcounting bug (see Issues)\n\n### Issues Found\n\n#### 1. Leftover `"ai"` package in `apps/api/package.json` (suggestion, non-blocker)\n**File**: `/Users/wiselancer/dev/martech/leadcognition/apps/api/package.json`, line 20\nThe `"ai": "^6.0.91"` dependency remains in `package.json` even though it is no longer imported anywhere in the API source. This adds unused bundle weight and keeps `@ai-sdk/anthropic` as a transitive dependency in the lockfile. It can be removed with `pnpm remove ai --filter @leadcognition/api`.\n\n#### 2. `newCount` overcounts in `fetch-github-events.ts` (minor, non-blocker)\n**File**: `/Users/wiselancer/dev/martech/leadcognition/apps/pipeline/src/jobs/fetch-github-events.ts`, lines 82-99\n`newCount++` runs unconditionally after every successful `insert().onConflictDoNothing()` call, even when the row was a duplicate and no row was actually inserted. The correct approach is to check the row count from Drizzle\'s result (e.g. `result.rowCount` or using `returning()`) before incrementing. As a consequence, `newSignals` in the audit log and the user enrichment queue-gating logic (`if (newCount > 0)`) may trigger on a fully-cached response (even though the ETag 304 path is handled separately, the 200 path can still have all-duplicate events). This is cosmetic for logging but does cause spurious enrichment queue checks.\n\n#### 3. No auth check on `lead.id` ownership in `playbook.ts` (security note, non-blocker given DB design)\n**File**: `/Users/wiselancer/dev/martech/leadcognition/apps/api/src/routes/playbook.ts`, lines 81-87\nThe `POST /:leadId` handler fetches `leads` by `id` only â not filtered by `orgId`. Any authenticated user from any org can read and generate playbooks for leads belonging to other orgs, because the `leads` table appears to be a shared pool (leads are not org-scoped). This is a design-level concern: if `leads` are truly global/shared, this is fine. If they are intended to be org-private, a `and(eq(leads.id, leadId), eq(leads.orgId, orgId))` filter is needed. The `playbooks` upsert is already org-scoped correctly. Worth confirming intent.\n\n#### 4. `as any` casts on Workers AI response
1(low risk, cosmetic)\n**Files**: `/Users/wiselancer/dev/martech/leadcognition/apps/pipeline/src/lib/ai.ts` lines 41, 58; `/Users/wiselancer/dev/martech/leadcognition/apps/api/src/routes/playbook.ts` lines 162, 172\nBoth files use `as any` on the `ai.run()` call and response because the CF Workers Types for the AI binding do not yet expose a fully typed interface for arbitrary model invocations. This is expected given the current CF Workers Types state and is acceptable. The downstream Zod `safeParse` in `playbook.ts` provides a runtime safety net.\n\n### Console Errors\n- (none) â browser console not testable without Chrome extension; production HTTP responses confirm no crash-level errors\n\n### Network Errors\n- (none) â all tested endpoints returned expected HTTP codes (200 for health/SPA, 401 for protected routes without credentials)\n\n### Verdict: PASS (0 blockers, 1 suggestion, 2 minor issues, 1 design question)\n\nThe Workers AI migration is functionally correct. All Anthropic SDK imports are gone from source. Both workers have proper type declarations and wrangler bindings for `AI` and `AI_GATEWAY_ID`. Typechecks pass cleanly. The daily guardrail queries are correctly structured. `Promise.allSettled` is handled properly. The main actionable items are: removing the unused `"ai"` package from `apps/api/package.json`, and confirming whether leads need an org-scope filter in the playbook route.\n';export{e as default}; 2//# sourceMappingURL=workers-ai-review-SsAjCtuf.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.