refactor(ts-migration): type src/api and the first of src/lib for noImplicitAny

Phase 12 step 1, in progress. The flag is not enabled yet — these are the
annotations it will require, landed first so the switch itself is a
one-line commit with a number that is already zero.

`noImplicitAny` projects 3496 errors across 242 files. This clears 179 of
them in 7 files: all of `src/api` (107 -> 0) and `workforce`,
`hiringRecords` in `src/lib`.

The leverage is real and worth recording, because it shapes the rest of
the step. Eight annotations on `aiEngine`'s prompt readers cleared 27
errors: where a value is `any`, every callback beneath it — `.map((c) =>
…)`, `.filter((r) => …)` — has no contextual type and errors on its own.
Typing the source fixes the callbacks for free, so this works bottom-up,
sources first.

Types are taken from what already exists wherever possible. The generated
entity types fit `workforce` and `hiringRecords` without a single
cascade: `JobPosting`, `JobApplication`, `Staff`, `AIInterview`,
`WorkerProfile`, `Assignment`, `Course`. `PreferencesUpdateResult` in
`src/types/user.ts` already described `updatePreferences`'s return.
`buildInsights` takes the other builders' outputs, so its parameters are
`ReturnType<typeof byDepartment>` and friends rather than a restatement
that could drift.

Two things are recorded rather than fixed:

`buildHires` probes four fields that are not `staff` columns —
`timeToHire`, `score`, `company` and `department` are absent from
`information_schema` and from the generated `Staff`. They are read as
fallbacks, so at run time they are always `undefined` and the other
branch always wins. `HireSourceRow` writes them down as optional so the
dead fallbacks are visible; removing the reads would be a behaviour
change. `Hire` likewise widens `profile_tier`, because the builder's
default `'skilled'` is lower-case where the column's check constraint
spells it `'Skilled'`.

`aiEngine`'s talent pool stays `any[]`. It is `JSON.parse` output from a
block embedded in a prompt, carrying computed fields like `match_score`
that no entity declares — typing it `WorkerProfile[]` would assert a
shape nothing validates.

One mistake worth keeping. I replaced an inline lookup with a hoisted
`const URGENCY = {…}`, and per-file esbuild said the output was
unchanged: with `--minify-syntax` it inlines a single-use const straight
back. The production bundle disagreed — `43e7f268` against `74d17e2d`.
Reverted to a type assertion, which erases. Hoisting reads as a tidy-up
and is a real change to the emitted code; the two checks disagreeing is
exactly why both are run.

  typecheck   0 under the committed config; 3317 under the probe, from 3496
  lint        exit 0
  npm test    1684/1691, the same 7 failures
  build       exit 0, bundle back to 74d17e2d…
  emitted JS  7/7 identical

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-18 17:18:18 +05:30
parent 21133a6064
commit e8207038dd
7 changed files with 228 additions and 149 deletions

View File

@@ -11,7 +11,7 @@
import { SHIFT_RECORDS } from './attendanceSeed';
import { DEMO_USER } from './demoUser';
const iso = (date) => new Date(`${date}T09:00:00.000Z`).toISOString();
const iso = (date: string) => new Date(`${date}T09:00:00.000Z`).toISOString();
/* ── Reference data ────────────────────────────────────────────────────── */
@@ -347,7 +347,7 @@ const JOB_POSTINGS = [
/* ── Applications ──────────────────────────────────────────────────────── */
const breakdown = (over) => ({
const breakdown = (over?: Record<string, number>) => ({
experience: 0, english: 0, reliability: 0, certifications: 0, availability: 0,
personality: 0, culture_fit: 0, communication_style: 0, attendance_expectations: 0,
physical_requirements: 0, leadership_expectations: 0, job_related_answers: 0,
@@ -356,13 +356,13 @@ const breakdown = (over) => ({
});
/** An applicant who has not been screened yet — every AI field stays empty. */
const unscreened = (over) => ({
const unscreened = (over?: Record<string, any>) => ({
status: 'applied',
ai_score: 0,
certifications: [],
availability: [],
skills: [],
companies_worked: [],
certifications: [] as string[],
availability: [] as string[],
skills: [] as string[],
companies_worked: [] as string[],
client_rating: 0,
created_date: iso('2026-08-04'),
updated_date: iso('2026-08-04'),
@@ -428,7 +428,7 @@ const JOB_APPLICATIONS = [
certifications: ['ServSafe', 'CPR / First Aid'],
availability: ['Weekdays', 'Weekends', 'Evenings'],
skills: ['Menu design', 'Brigade leadership', 'Food costing', 'Plated dinners', 'Offsite catering'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.9,
professional_summary:
'Executive chef with 12 years across hotel banquets and high-end offsite catering. Has run brigades of 20+ and holds plate standards at 400 covers.',
@@ -480,7 +480,7 @@ const JOB_APPLICATIONS = [
certifications: ['Guard Card', 'CPR / First Aid'],
availability: ['Weekends', 'Evenings', 'Overnight'],
skills: ['Crowd management', 'De-escalation', 'Incident reporting', 'Access control'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.8,
professional_summary:
'Licensed event security officer with five years across concerts and corporate activations. Known for defusing situations before they escalate.',
@@ -514,7 +514,7 @@ const JOB_APPLICATIONS = [
certifications: ['Guard Card'],
availability: ['Weekends', 'Evenings'],
skills: ['Access control', 'Incident reporting'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Venue security officer transitioning into private event work.',
job_posting_id: 'job_security',
@@ -562,7 +562,7 @@ const JOB_APPLICATIONS = [
certifications: ['RBS Alcohol Server', 'TIPS Certified'],
availability: ['Weekdays', 'Evenings'],
skills: ['Batch cocktails', 'Craft cocktails', 'Bar setup', 'Inventory control'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.7,
professional_summary:
'Corporate event bartender with six years on Bay Area tech campuses. Builds batched programs that hold quality at volume.',
@@ -605,7 +605,7 @@ const JOB_APPLICATIONS = [
certifications: ['Food Handler Card'],
availability: ['Weekends', 'Evenings'],
skills: ['Synchronized service', 'Tray service', 'Wine service', 'Guest relations'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.6,
professional_summary:
'Fine dining event server with four years of coursed banquet service and wine knowledge.',
@@ -639,7 +639,7 @@ const JOB_APPLICATIONS = [
certifications: ['Food Handler Card'],
availability: ['Weekends'],
skills: ['Banquet service', 'Tray service', 'Table resets'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.4,
professional_summary: 'Banquet server with three years of seated dinner service.',
selfie_url: 'https://i.pravatar.cc/240?img=44',
@@ -673,7 +673,7 @@ const JOB_APPLICATIONS = [
certifications: [],
availability: ['Weekends', 'Evenings'],
skills: ['Table service'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Restaurant server moving into event work.',
job_posting_id: 'job_server_fine',
@@ -695,7 +695,7 @@ const JOB_APPLICATIONS = [
certifications: ['TIPS Certified'],
availability: ['Weekends', 'Evenings'],
skills: ['Classic cocktails', 'Bar setup', 'Speed pouring'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.2,
professional_summary: 'Event bartender with three years of high-volume weekend service.',
job_posting_id: 'job_bartender',
@@ -728,7 +728,7 @@ const JOB_APPLICATIONS = [
certifications: ['TIPS Certified'],
availability: ['Evenings'],
skills: ['Bar setup', 'Wine service'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 4.0,
professional_summary: 'Barback stepping up to bartending at private events.',
job_posting_id: 'job_bartender',
@@ -761,7 +761,7 @@ const JOB_APPLICATIONS = [
certifications: [],
availability: ['Weekends'],
skills: [],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Looking for a first hospitality role.',
job_posting_id: 'job_bartender',
@@ -795,7 +795,7 @@ const JOB_APPLICATIONS = [
certifications: ['Forklift Operator'],
availability: ['Weekdays', 'Early mornings'],
skills: ['Order picking', 'Scanner operation', 'Pallet staging'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 3.9,
professional_summary: 'Warehouse picker with forklift certification.',
job_posting_id: 'job_picker',
@@ -827,7 +827,7 @@ const JOB_APPLICATIONS = [
certifications: [],
availability: ['Weekdays'],
skills: ['Order picking'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Fulfillment associate seeking steady warehouse shifts.',
job_posting_id: 'job_picker',
@@ -854,7 +854,7 @@ const JOB_APPLICATIONS = [
certifications: ['Food Handler Card'],
availability: ['Weekdays', 'Weekends'],
skills: ['Banquet leadership', 'BEO reading', 'Floor coordination'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Banquet server with lead-shift experience in hotel events.',
job_posting_id: 'job_banquet',
@@ -874,7 +874,7 @@ const JOB_APPLICATIONS = [
certifications: ['Food Handler Card'],
availability: ['Weekdays', 'Weekends', 'Evenings'],
skills: ['Floor leadership', 'Client relations', 'Event reporting'],
companies_worked: [],
companies_worked: [] as string[],
client_rating: 0,
professional_summary: 'Hotel banquet captain with six years directing event floors.',
job_posting_id: 'job_banquet',
@@ -900,7 +900,7 @@ const JOB_APPLICATIONS = [
/* ── AI interviews ─────────────────────────────────────────────────────── */
const transcript = (candidate, role) => [
const transcript = (candidate: string, role: string) => [
{ role: 'assistant', content: `Hi ${candidate.split(' ')[0]} — thanks for making time. Tell me about the busiest ${role} shift you've worked and how you handled it.`, timestamp: iso('2026-08-04') },
{ role: 'user', content: 'We were short two people on a 300-guest event. I re-sectioned the floor, moved the strongest server to the head tables, and pulled the rest into a runner rotation so nothing sat under the lamps.', timestamp: iso('2026-08-04'), response_time_seconds: 11 },
{ role: 'assistant', content: 'What broke first, and what did you do the moment you noticed?', timestamp: iso('2026-08-04') },
@@ -919,7 +919,7 @@ const AI_INTERVIEWS = [
verdict: 'maybe',
hire_recommendation: 'Worth a trial shift on a coursed dinner before committing to the season.',
integrity_score: 100,
ai_flags: [],
ai_flags: [] as string[],
category_scores: {
communication: 78, confidence: 71, experience_relevance: 74, culture_fit: 76,
problem_solving: 69, personality: 77, communication_style: 76,
@@ -947,11 +947,11 @@ const AI_INTERVIEWS = [
verdict: 'maybe',
hire_recommendation: '',
integrity_score: 100,
ai_flags: [],
ai_flags: [] as string[],
category_scores: {},
strengths: [],
concerns: [],
best_fit_roles: [],
strengths: [] as string[],
concerns: [] as string[],
best_fit_roles: [] as string[],
summary: '',
reasoning: '',
created_date: iso('2026-08-05'),
@@ -967,11 +967,11 @@ const AI_INTERVIEWS = [
verdict: 'maybe',
hire_recommendation: '',
integrity_score: 100,
ai_flags: [],
ai_flags: [] as string[],
category_scores: {},
strengths: [],
concerns: [],
best_fit_roles: [],
strengths: [] as string[],
concerns: [] as string[],
best_fit_roles: [] as string[],
summary: '',
reasoning: '',
created_date: iso('2026-08-05'),
@@ -987,11 +987,11 @@ const AI_INTERVIEWS = [
verdict: 'maybe',
hire_recommendation: '',
integrity_score: 100,
ai_flags: [],
ai_flags: [] as string[],
category_scores: {},
strengths: [],
concerns: [],
best_fit_roles: [],
strengths: [] as string[],
concerns: [] as string[],
best_fit_roles: [] as string[],
summary: '',
reasoning: '',
created_date: iso('2026-08-03'),
@@ -1015,7 +1015,7 @@ const STAFF = [
ai_score: 92,
status: 'onboarding',
client_rating: 0,
endorsed_skills: [],
endorsed_skills: [] as string[],
created_date: iso('2026-07-25'),
},
{
@@ -1031,7 +1031,7 @@ const STAFF = [
ai_score: 94,
status: 'onboarding',
client_rating: 0,
endorsed_skills: [],
endorsed_skills: [] as string[],
created_date: iso('2026-07-25'),
},
{
@@ -1047,7 +1047,7 @@ const STAFF = [
ai_score: 97,
status: 'onboarding',
client_rating: 0,
endorsed_skills: [],
endorsed_skills: [] as string[],
created_date: iso('2026-07-25'),
},
];
@@ -1085,7 +1085,7 @@ const COURSES = [
{ criterion: 'Protects the table experience', weight: 30 },
],
},
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] },
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] as string[] },
quiz: [
{
question: 'A guest flags an allergy mid-service. What happens first?',
@@ -1127,7 +1127,7 @@ const COURSES = [
{ criterion: 'Escalates appropriately', weight: 30 },
],
},
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] },
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] as string[] },
quiz: [
{
question: 'A guest shows clear signs of intoxication. What do you do?',
@@ -1151,7 +1151,7 @@ const COURSES = [
proof_skill: 'Hazard Identification',
skill_id: 'food_safety',
target_level: 'beginner',
required_level: null,
required_level: null as string | null,
completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'],
verification_criteria: ['Identifies temperature violations', 'Spots cross-contamination risk', 'Notes obstruction hazards'],
challenge: {
@@ -1164,7 +1164,7 @@ const COURSES = [
{ criterion: 'Notes obstruction hazards', weight: 20 },
],
},
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] },
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] as string[] },
quiz: [
{
question: 'What is the danger zone for cold-held food?',
@@ -1201,7 +1201,7 @@ const COURSES = [
],
},
unlock_requirements: { min_shifts: 5, min_reliability: 70, required_badges: ['Service Fundamentals'] },
quiz: [],
quiz: [] as unknown[],
pass_score: 80,
status: 'active',
created_date: iso('2026-06-16'),
@@ -1293,7 +1293,7 @@ const MODULE_TABLE: [string, string, string, number, string, string, string[]][]
['food_safety', 'advanced', 'Incident Response & Reporting', 18, 'roleplay', 'A suspected foodborne incident, in the hour it matters.', ['Isolates the product', 'Documents accurately', 'Notifies the right people']],
];
const SKILL_CATEGORY = {
const SKILL_CATEGORY: Record<string, string> = {
server: 'Service',
bartending: 'Bar',
leadership: 'Leadership',
@@ -1304,20 +1304,20 @@ const SKILL_CATEGORY = {
const LEVEL_ORDER = ['beginner', 'intermediate', 'advanced', 'expert'];
/** Level → the rung below it, which is what a module requires before it opens. */
const requiredLevelFor = (level) => LEVEL_ORDER[LEVEL_ORDER.indexOf(level) - 1] || null;
const requiredLevelFor = (level: string) => LEVEL_ORDER[LEVEL_ORDER.indexOf(level) - 1] || null;
/** Difficulty is the existing field; level drives it so the two never disagree. */
const DIFFICULTY_FOR = { beginner: 'beginner', intermediate: 'intermediate', advanced: 'advanced', expert: 'advanced' };
const DIFFICULTY_FOR: Record<string, string> = { beginner: 'beginner', intermediate: 'intermediate', advanced: 'advanced', expert: 'advanced' };
const XP_FOR = { beginner: 100, intermediate: 150, advanced: 220, expert: 300 };
const XP_FOR: Record<string, number> = { beginner: 100, intermediate: 150, advanced: 220, expert: 300 };
const PROMPT_FOR = {
const PROMPT_FOR: Record<string, (title: string) => string> = {
roleplay: (title) => `Work through a ${title.toLowerCase()} scenario with Owliver, start to finish.`,
video: (title) => `Record yourself demonstrating ${title.toLowerCase()} as you would on a live shift.`,
photo_identify: (title) => `Mark everything a ${title.toLowerCase()} check should catch in this photo.`,
};
const slug = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
const slug = (title: string) => title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
const TRAINING_MODULES = MODULE_TABLE.map(
([skill_id, target_level, title, minutes, type, description, criteria], i) => ({
@@ -1341,8 +1341,8 @@ const TRAINING_MODULES = MODULE_TABLE.map(
ai_persona: type === 'roleplay' ? 'A guest or teammate in a live service situation.' : undefined,
rubric: (Array.isArray(criteria) ? criteria : []).map((criterion, n) => ({ criterion, weight: n === 0 ? 40 : 30 })),
},
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] },
quiz: [],
unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] as string[] },
quiz: [] as unknown[],
pass_score: 70,
status: 'active',
created_date: iso('2026-06-20'),
@@ -1354,7 +1354,7 @@ const TRAINING_MODULES = MODULE_TABLE.map(
const COURSE_LIBRARY = [...COURSES, ...TRAINING_MODULES];
/** A module id from its skill and title, so profiles read like the ladder does. */
const mod = (skill, title) => `mod_${skill}_${slug(title)}`;
const mod = (skill: string, title: string) => `mod_${skill}_${slug(title)}`;
/**
* Completed training on a worker profile.
@@ -1362,7 +1362,7 @@ const mod = (skill, title) => `mod_${skill}_${slug(title)}`;
* Titles are looked up rather than repeated, so a renamed module cannot leave a
* profile claiming a course that no longer goes by that name.
*/
const completions = (ids, date, score = 88) =>
const completions = (ids: string[], date: string, score = 88) =>
ids.map((id) => ({
course_id: id,
title: COURSE_LIBRARY.find((c) => c.id === id)?.title || id,
@@ -1535,7 +1535,7 @@ const WORKER_PROFILES = [
earned_badges: [
{ name: 'Service Fundamentals', level: 'bronze', course_id: 'course_service_basics', earned_date: iso('2026-07-28') },
],
capabilities: [],
capabilities: [] as unknown[],
shifts_completed: 6,
attendance_score: 92,
performance_score: 0,
@@ -1605,8 +1605,8 @@ const WORKER_PROFILES = [
'2026-08-02',
89
),
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 38,
attendance_score: 96,
performance_score: 88,
@@ -1653,8 +1653,8 @@ const WORKER_PROFILES = [
'2026-07-30',
85
),
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 21,
attendance_score: 93,
performance_score: 81,
@@ -1701,8 +1701,8 @@ const WORKER_PROFILES = [
'2026-08-05',
76
),
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 4,
attendance_score: 100,
performance_score: 0,
@@ -1745,8 +1745,8 @@ const WORKER_PROFILES = [
},
xp: 0,
completed_courses: [],
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 0,
attendance_score: 100,
performance_score: 0,
@@ -1789,8 +1789,8 @@ const WORKER_PROFILES = [
},
xp: 0,
completed_courses: [],
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 0,
attendance_score: 100,
performance_score: 0,
@@ -1833,8 +1833,8 @@ const WORKER_PROFILES = [
},
xp: 0,
completed_courses: [],
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 0,
attendance_score: 100,
performance_score: 0,
@@ -1877,8 +1877,8 @@ const WORKER_PROFILES = [
},
xp: 0,
completed_courses: [],
earned_badges: [],
capabilities: [],
earned_badges: [] as unknown[],
capabilities: [] as unknown[],
shifts_completed: 0,
attendance_score: 100,
performance_score: 0,
@@ -2024,7 +2024,7 @@ const ASSIGNMENTS = [
worker_email: 'marco.rivera@email.com',
worker_name: 'Marco Rivera',
starts_at: iso('2026-07-01'),
ends_at: null,
ends_at: null as string | null,
status: 'active',
source: 'seed',
match_score: 92,