Skip to main content

Facility Pipeline — implementation plan

The build checklist for VS Code Claude. Grounded in this repo's patterns. Migration number, component names, and tooling rules are verified against the tree.

0. Status legend

✅ done & verified · ⬜ to build. The pure core (Phase 1) is already in the tree.

1. File manifest

Migration & types

PathStatusPurpose
src/db/migrations/0053_facility_pipeline.sqlAdditive DDL (the repo already has 0050–0052). Parses against the PG grammar; includes review fixes. Not yet applied.
src/db/types.tsRegenerate after applying 0053 (supabase gen types typescript). Biome-ignored, but tsc still checks it.

Pure library (no I/O) — DONE

PathStatusPurpose
src/lib/pipeline/types.tsDomain types (RoleFamily, FacilityScoreInputs, MatchCandidate, …).
src/lib/pipeline/job-signal.tsclassifyRoleFamily, scoreUrgency, ROLE_WEIGHT.
src/lib/pipeline/score.tscomputeFacilityScore (revised model), deriveTier, DEFAULT_TIER_CUTOFFS.
src/lib/pipeline/match-facility.tsnameSimilarity, resolveFacility, tokens.
tests/lib/pipeline/{job-signal,score,match-facility}.test.tsVitest unit tests (run pnpm test).

Zod schemas, queries, actions

PathStatusPurpose
src/types/schemas/pipeline.tsZod for every action input; ids z.string().uuid(); export …Input.
src/types/schemas/facility.ts⬜ (edit)Widen facility_contacts.role enum to the new vocabulary (see §3).
src/db/queries/pipeline.tsDB I/O (client as first arg): upsertJobRaw, upsertJobSignal, listSignalsForInbox, listUnmatchedSignals, listPipelineFacilities, updateFacilityScore, listDueTasks, insertFollowUpTask, completeTask, listSequences.
src/server/actions/pipeline.ts'use server' actions (auth → Zod → company-scope → query → audit → revalidate).

AI, cron, script

PathStatusPurpose
src/server/ai/extractors/job-signal.tsextractJobSignalFromPosting(raw) — text orchestrator, Zod-validated; fallback for unstructured input.
src/server/ai/extractors/follow-up-email.tsdraftFollowUpEmail(ctx) — returns a draft only; tone guardrail in the prompt.
src/app/api/cron/job-signals/route.tsPOST, cron-secret gated (ADR-0024) → fetch → ingest.
src/app/api/cron/follow-ups/route.tsPOST, cron-secret gated → materialize due tasks (idempotent) → draft emails.
scripts/ingest-job-signals.mjsCLI mirror (--dry default, --live, --commit); CareerOneStop + schema.org; import-prospects.mjs pattern. A working prototype exists in the prior deliverables folder.

UI (extends /admin/crm)

PathStatusPurpose
src/app/admin/crm/page.tsx⬜ (edit)Add tabs: Signal Inbox · Pipeline Board (existing) · Today's Follow-ups · Sequences. Use Tabs.
src/app/admin/crm/loading.tsx / error.tsxSkeleton + error boundary (UX guidelines).
src/components/pipeline/SignalInbox.tsxTable of new/unmatched signals; row actions.
src/components/pipeline/TodayFollowUps.tsxDue follow_up_tasks grouped by channel.
src/components/pipeline/SequenceManager.tsxSequence templates + enrollments.
src/components/pipeline/ProspectDiscovery.tsxHigh-score facilities with no open posting.
src/components/pipeline/ScoreBadge.tsxTier chip + score + "as of" provenance.
src/components/admin/Facility360*.tsx⬜ (edit existing facility detail)Score breakdown, contacts-by-persona, timeline, signals, cadence.
Modals13 modals via Dialog/confirmDanger — see 04-ux-spec.md.
PathStatusPurpose
src/components/admin/AdminNav.tsx⬜ (edit)Rename the existing CRM item to "Pipeline"/"BD Pipeline"; do not add a competing item.
docs/adr/0040-facility-pipeline.mdDecision record.

2. Key signatures

Server actions follow the repo's crm.ts pattern — (_prev: State, formData: FormData) => Promise<State>, auth via getCurrentAdmin, Zod-validated, company-scoped, audited. (Confirm the exact ActionResult shape in src/server/actions/crm.ts before writing — match it.)

// src/server/actions/pipeline.ts
ingestJobSignals(payloads): summary // from cron route / script; upserts job_raw + job_signals
matchSignalToFacility(signalId, facilityId) // resolve a manual-queue signal; optional add facility_alias
raiseLeadFromSignal(signalId) // creates/refreshes a crm_lead; active-client → upsell track
moveLeadStage(leadId, stage|status) // reuse the existing CRM board action if present
logActivity(targetType, targetId, type, notes, followUpDate?)
enrollInSequence(sequenceId, facilityId|leadId, ownerId?) // blocks if contact-enrichment gate unmet
completeFollowUpTask(taskId, status, completedAt?) // done|skipped|snoozed(+until)
sendFollowUpEmailDraft(taskId, facilityId, templateKey) // drafts into outbound_emails; never sends
refreshFacilityScore(facilityId) // computeFacilityScore → optimistic update

Pure lib (already implemented — call these):

classifyRoleFamily(title, body?) => RoleFamily
scoreUrgency(posting) => 0|1|2|3|4
computeFacilityScore(inputs, cutoffs?) => { score, tier, breakdown, medicaidMultiplier }
resolveFacility(posting, candidates, opts?) => { facility|null, confidence, reason, runnerUp? }

3. Repo-specific gotchas (verified)

  1. Migration number is 005300500052 already exist. (Done.)
  2. Type-regen ordering: apply 0053supabase gen types → then write query/action code. Until then, new-table code won't typecheck. The pure lib has no DB types, so Phase 1 stands alone.
  3. exactOptionalPropertyTypes: true: for nullable DB columns use ?? null (as crm.ts does); for truly optional insert fields, spread-omit: ...(x != null && { field: x }). Never assign field: value ?? undefined.
  4. noUncheckedIndexedAccess: true: array access is T | undefined — guard (arr[0]?.x) or use .find(). (The pure lib already complies.)
  5. Biome: single quotes, semicolons, trailing commas, 100-col, import type for type-only imports. noConsoleLog is warn in src/ — use @/lib/log, not console.log (scripts are exempt).
  6. Guardrails (scripts/guardrails.mjs): never name an env NEXT_PUBLIC_*SECRET/SERVICE_KEY/…; never touch a Wise funding path. New secrets (CAREERONESTOP_API_KEY, …) are server-only in src/server/env.ts, .optional() so next build doesn't fail, documented in .env.example.
  7. Tests live in tests/ (not colocated); import { describe, expect, it } from 'vitest'; server-only is shimmed in vitest.config.ts.
  8. Component reuse (verified to exist in src/components/ui/): Table, Badge, Dialog, EmptyState, Toast (Toaster + notify), DangerConfirm (confirmDanger), PhoneInput, Tabs, Spinner, Alert, Card, Button, SlideOver, SortHeader, form.tsx. Shell: src/components/admin/{AdminShell,AdminNav}.tsx (white sidebar — don't reinvent it).
  9. facility_contacts.role Zod enum in src/types/schemas/ is currently ['billing','admin','clinical']; widen to administrator|don|mds_coordinator|regional_reimbursement|cfo|owner|admin|clinical|billing|other alongside the migration (the DB column is free text, so this is additive).

4. Build order

  • Phase 1 — Pure core + tests. ✅ Done (src/lib/pipeline/*, tests/lib/pipeline/*). pnpm test green.
  • Phase 2 — Schema + types. Apply 0053 (dev branch) → regen src/db/types.ts → widen contact-role schema → pnpm typecheck.
  • Phase 3 — Ingestion. src/types/schemas/pipeline.ts, env vars, src/db/queries/pipeline.ts, the job-signal extractor, ingestJobSignals/matchSignalToFacility/raiseLeadFromSignal, /api/cron/job-signals, scripts/ingest-job-signals.mjs (dry default). pnpm build.
  • Phase 4 — Scoring + Pipeline UI. refreshFacilityScore; SignalInbox, ScoreBadge, ProspectDiscovery, Facility 360; /admin/crm tabs + loading.tsx/error.tsx. Verify every component state (empty/loading/error) and the modal inventory.
  • Phase 5 — Follow-up engine + Sequences. enrollInSequence/completeFollowUpTask/sendFollowUpEmailDraft, /api/cron/follow-ups, TodayFollowUps, SequenceManager.
  • Phase 6 — CI green. pnpm check && pnpm typecheck && pnpm guardrails && pnpm test && pnpm build. Branch feat/facility-pipeline, conventional commits (feat:), PR to main.