chore(ts-migration): add generated entity types

Phase 3.5. Entity record shapes, generated from the backend's resource
registry rather than transcribed from it. Type-only: every touched file emits
byte-identical JavaScript, and the two type modules emit nothing at all.

`scripts/gen-entity-types.mjs` reads
`krow-backend/go-api/internal/domain/resources_gen.go` - itself generated out of
information_schema, so it cannot drift from the migrations - and writes
`src/types/entities.generated.ts`: 15 resources, 298 columns. It checks for
drift by default and rewrites with --write, the same arrangement seed-fixture.mjs
uses, and skips cleanly when the backend is not checked out beside this repo.

Field types come from `Column.SelectExpr()` in `domain/resource.go`, which is
what the read projection actually emits, not from the Postgres type. The two
differ: uuid and citext are cast to text, numeric to float8, dates and
timestamps to formatted strings, and - the case that justifies generating rather
than typing by hand - `user_activity.id` is an identity bigint cast to text, so
it arrives as a STRING. Written by hand it would have been called a number, and
nothing would have contradicted that until a comparison quietly stopped
matching.

The five Phase 3 leaf utilities swap their `any` placeholders for these records,
as `Partial<...>`: each takes `= {}` or guards every read because it renders
before the query resolves, and requiring the whole record would force those
defaults out - a behaviour change in a scoring path.

jsonb is where the generator stops. Thirteen columns across eight entities are
typed `unknown`, correctly: what sits inside a jsonb column is not in
information_schema and nothing on the backend declares it. Where a module reads
through one it is narrowed to `unknown[]`, `any[]` or `any` - what kind of value
it is, and nothing about its contents. An earlier draft declared the fields
these modules read off them; it was removed. That would have been inventing a
schema the database does not hold, with the compiler then defending the guess.

Not wired into skill-check.mjs: that file is being changed concurrently by
unrelated feature work. Adding `"types:check": "node scripts/gen-entity-types.mjs"`
beside the existing seed:check is the natural next step and is deliberately left
for when that file is quiet.

Verified in isolation from the parallel feature work (commit 1775395 plus these
eight files only): tsc 64 errors with an unchanged histogram, skill-check
1641/1642 with only the known stale-fixture failure, Owliver baseline 59/59,
lint 0 errors, production build succeeds with the API origin inlined.

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-15 11:49:56 +05:30
parent 1775395256
commit d1425f974c
8 changed files with 855 additions and 59 deletions

View File

@@ -8,17 +8,16 @@
* worker WANTS rather than what a posting OFFERS.
*/
import type { EmployeeRole } from '@/types/entities';
/**
* `draft` is a EmployeeRole record, typed `any` deliberately.
*
* The shared types in `src/types/` describe the transport and the signed-in
* user; they do not describe entity records yet. That shape is derivable — the
* backend generates a column registry from `information_schema` — but it
* belongs with the phase that converts the transport layer. A partial interface
* written here would be a guess that every later reader treats as settled.
* `any` states what is actually known today and is one annotation to replace.
* A draft is a partial record: the conversation fills it in a field at a time,
* and `defaultEmployeeRole()` supplies the rest. `EmployeeRole` has no `jsonb`
* columns, so every field here is checked against its real column type —
* `certifications` and `availability` are `text[]`, which is why they arrive as
* `string[]` rather than as something the registry could not describe.
*/
type EmployeeRoleDraft = any;
type EmployeeRoleDraft = Partial<EmployeeRole>;
/** English levels, in the order the schema declares them. */
export const EMPLOYEE_ROLE_STATUSES = ['seeking', 'placed', 'inactive'];

View File

@@ -1,23 +1,28 @@
import type { Course, JobPosting, LearningPath, WorkerProfile } from '@/types/entities';
/**
* The records this engine reads — WorkerProfile, JobPosting, Course and
* LearningPath — are typed `any` deliberately.
* The records this engine reads, from the generated registry.
*
* `src/types/` describes the transport and the signed-in user; it does not
* describe entity records yet. Those shapes are derivable from the backend's
* generated column registry, but they belong with the phase that converts the
* transport layer. Written here they would be a guess, and this is the file
* where a wrong guess is most expensive: these weights decide what a worker's
* score is and which jobs they are shown.
* `Partial` throughout: every function here guards each read (`|| 0`,
* `?.length`) because these run against records that may still be loading, and
* the seeded profiles do not all carry every column.
*
* `computeProfileCompletion` additionally indexes the profile by a field name
* held in a list (`profile[f]`), which no partial interface would permit
* without either widening it back to `any` or rewriting the loop — and
* rewriting it would be a behaviour change in a scoring path.
* The `jsonb` columns are narrowed to `any[]`. The registry types them
* `unknown`, which is the honest answer — what sits inside a jsonb column is
* not in `information_schema` and nothing on the backend declares it — but this
* module reads fields off the elements. `any[]` records exactly that state: it
* is an array, and its elements are unchecked. Writing a shape for the elements
* would be inventing one, and it would be the compiler defending a guess.
* Fields backed by real columns stay fully checked either way.
*/
type ProfileRecord = any;
type JobPostingRecord = any;
type CourseRecord = any;
type LearningPathRecord = any;
type ProfileRecord = Partial<WorkerProfile> & {
completed_courses?: any[];
earned_badges?: any[];
experience?: any[];
};
type JobPostingRecord = Partial<JobPosting>;
type CourseRecord = Partial<Course>;
type LearningPathRecord = Partial<LearningPath> & { steps?: any[] };
/** The score, its headline inputs, and the per-dimension breakdown a reader sees. */
export interface KrowScoreResult {

View File

@@ -1,13 +1,30 @@
import { base44 } from '@/api/base44Client';
import type { Course, WorkerProfile } from '@/types/entities';
/**
* Course, WorkerProfile and the conversation turns are typed `any`
* deliberately — `src/types/` does not describe entity records yet, and a
* partial interface written here would be a guess later readers treat as
* settled. See the same note in `krowScore.ts`.
* Course and WorkerProfile, from the generated registry.
*
* `Partial`, because `isUnlocked` takes `profile = {}` and every read is
* guarded — it runs on a course card before the profile has loaded.
*
* Three `jsonb` columns are narrowed, and the narrowing says only what kind of
* value it is. `challenge` and `unlock_requirements` are jsonb OBJECTS, so they
* become `any`; `earned_badges` is a jsonb ARRAY, so it becomes `any[]`. The
* registry types all three `unknown`, which is correct — the shape inside a
* jsonb column is not in `information_schema`. Declaring the fields this file
* reads off them (`min_shifts`, `rubric`, `prompt`, …) would be inventing a
* schema the database does not hold. Every field backed by a real column —
* `proof_skill`, `title`, `description`, `shifts_completed`,
* `reliability_score` — stays fully checked.
*/
type CourseRecord = any;
type ProfileRecord = any;
type CourseRecord = Partial<Course> & {
challenge?: any;
unlock_requirements?: any;
};
type ProfileRecord = Partial<WorkerProfile> & {
earned_badges?: any[];
};
/** One turn of the roleplay transcript, as the challenge runner stores it. */
interface ChallengeTurn {

View File

@@ -1,18 +1,29 @@
// Derives the Talent "credit score" dimensions and motivational metrics
// from a WorkerProfile. Keeps the UI honest by mapping to real fields only.
import type { WorkerProfile } from '@/types/entities';
/**
* `profile` is a WorkerProfile record, typed `any` deliberately.
* `Partial`, not the whole record, and the default is why.
*
* The shared types do not describe entity records yet — that shape is derivable
* from the backend's generated column registry and belongs with the phase that
* converts the transport layer, not with this one. Writing a partial
* WorkerProfile here would be a guess wearing an interface, and every reader
* after it would treat the guess as settled. `any` says what is actually known
* today, which is nothing, and it is one annotation to replace when the real
* record type exists.
* Every function here takes `profile = {}` and defends against absent fields
* with `|| 0`. That default is not decoration — these render on pages that
* mount before the profile query resolves. `WorkerProfile` outright would make
* `{}` unassignable and force the defaults to be removed, which is a behaviour
* change in a scoring path. `Partial` keeps the field NAMES and TYPES checked,
* which is the part worth having, while still describing what the callers pass.
*/
type WorkerProfileRecord = any;
type WorkerProfileRecord = Partial<WorkerProfile> & {
/**
* `completed_courses` is a `jsonb` column, so the registry types it
* `unknown` — correctly, because the shape inside a jsonb column is not in
* `information_schema` and nothing on the backend declares it. This module
* only ever asks it how long it is. Intersecting narrows that one field to an
* array without asserting anything about what the array holds, and leaves
* every other field checked against the real column type.
*/
completed_courses?: unknown[];
};
/**
* One row of the reputation breakdown.

View File

@@ -2,17 +2,21 @@
// client endorsements. Used by both the talent card and the talent detail
// modal so the two stay in sync.
import type { WorkerProfile } from '@/types/entities';
/**
* `profile` is a WorkerProfile record, typed `any` deliberately.
* `Partial`, because the callers render before the profile has loaded and every
* read here is already guarded (`profile?.experience || []`).
*
* The shared types in `src/types/` describe the transport and the signed-in
* user; they do not describe entity records yet. That shape is derivable — the
* backend generates a column registry from `information_schema` — but it
* belongs with the phase that converts the transport layer. A partial interface
* written here would be a guess that every later reader treats as settled.
* `any` states what is actually known today and is one annotation to replace.
* `experience` is a `jsonb` column. The registry types it `unknown`, correctly:
* what sits inside a jsonb column is not in `information_schema`. This module
* reads `.role` and `.company` off its entries, so it is narrowed to `any[]` —
* an array whose elements are unchecked. Declaring a shape for those elements
* would be inventing one, and the compiler would then defend the guess.
*/
type WorkerProfileRecord = any;
type WorkerProfileRecord = Partial<WorkerProfile> & {
experience?: any[];
};
/** One derived endorsement row, as the talent card and detail modal render it. */
export interface ClientEndorsement {

View File

@@ -0,0 +1,513 @@
/**
* GENERATED FILE — DO NOT EDIT BY HAND.
*
* Regenerate with: node scripts/gen-entity-types.mjs --write
* Source of truth: krow-backend/go-api/internal/domain/resources_gen.go
* (itself generated from information_schema)
*
* Field types follow `Column.SelectExpr()` in `domain/resource.go` — what the
* read projection emits — not the raw Postgres type. See the generator's header
* for the mapping and for why the two differ.
*
* 15 resources, 298 columns.
*/
/** `job-postings` — the `job_postings` table, every column the projection returns. */
export interface JobPosting {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** server-owned. */
created_by: string | null;
company: string;
/** required on create. */
title: string;
role_category: string;
description: string;
responsibilities: string[];
qualifications: string[];
nice_to_haves: string[];
custom_requirements: string;
physical_requirements: string;
leadership_expectations: string;
attendance_expectations: string;
min_experience_years: number;
english_required: 'basic' | 'conversational' | 'fluent' | 'native';
certifications_required: string[];
skill_requirements: unknown;
pay_range_min: number;
pay_range_max: number;
location: string;
status: 'draft' | 'active' | 'paused' | 'closed';
ai_generated: boolean;
headcount: number;
start_date: string | null;
duration_months: number | null;
priority: 'urgent' | 'high' | 'normal';
vetting_criteria: unknown;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `job-applications` — the `job_applications` table, every column the projection returns. */
export interface JobApplication {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
job_posting_id: string;
worker_profile_id: string | null;
job_title: string;
/** required on create. */
applicant_name: string;
/** required on create. */
email: string;
phone: string;
years_experience: number;
english_level: 'basic' | 'conversational' | 'fluent' | 'native';
certifications: string[];
availability: string[];
skills: string[];
companies_worked: string[];
client_rating: number;
professional_summary: string;
cover_letter: string;
selfie_url: string;
status: 'applied' | 'ai_screened' | 'shortlisted' | 'interview' | 'hired' | 'rejected' | 'assigned';
ai_score: number;
ai_match_label: string;
ai_summary: string;
ai_strengths: string[];
ai_gaps: string[];
ai_recommendation: string;
score_breakdown: unknown;
screened_at: string | null;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
interview_id: string | null;
}
/** `ai-interviews` — the `ai_interviews` table, every column the projection returns. */
export interface AIInterview {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
application_id: string;
/** required on create. */
job_posting_id: string;
job_title: string;
candidate_name: string;
messages: unknown;
overall_interview_score: number;
verdict: 'hire' | 'maybe' | 'no';
hire_recommendation: string;
integrity_score: number;
ai_flags: string[];
category_scores: unknown;
strengths: string[];
concerns: string[];
best_fit_roles: string[];
summary: string;
reasoning: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `staff` — the `staff` table, every column the projection returns. */
export interface Staff {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
application_id: string | null;
job_posting_id: string | null;
worker_profile_id: string | null;
/** required on create. */
name: string;
/** required on create. */
email: string;
phone: string;
role: string;
profile_tier: 'Beginner' | 'Cross-Trained' | 'Skilled';
/** required on create. */
hire_date: string;
ai_score: number;
status: 'onboarding' | 'active' | 'inactive';
client_rating: number;
endorsement_text: string;
endorsed_skills: string[];
review_date: string | null;
reviewer_name: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `worker-profiles` — the `worker_profiles` table, every column the projection returns. */
export interface WorkerProfile {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** server-owned. */
user_id: string | null;
/** required on create. */
full_name: string;
/** required on create. */
email: string;
phone: string;
address: string;
selfie_url: string;
languages: string[];
availability: string[];
transportation: string;
certifications: string[];
experience: unknown;
experience_years: number;
current_position: string;
desired_position: string;
career_goals: string;
skills: string[];
industries: string[];
personality: string;
strengths: string[];
weaknesses: string[];
communication_style: string;
salary_expectations: string;
leadership_potential: number;
ai_interview_score: number;
krow_score: number;
reliability_score: number;
profile_completion: number;
xp: number;
completed_courses: unknown;
earned_badges: unknown;
capabilities: unknown;
shifts_completed: number;
attendance_score: number;
performance_score: number;
client_rating: number;
supervisor_rating: number;
status: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
score_breakdown: unknown;
}
/** `courses` — the `courses` table, every column the projection returns. */
export interface Course {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string | null;
/** required on create. */
title: string;
description: string;
category: string;
difficulty: string;
xp: number;
estimated_minutes: number;
badge_reward: string | null;
proof_skill: string;
skill_id: string | null;
target_level: 'beginner' | 'intermediate' | 'advanced' | 'expert' | null;
required_level: 'beginner' | 'intermediate' | 'advanced' | 'expert' | null;
completion_criteria: string[];
verification_criteria: string[];
challenge: unknown;
unlock_requirements: unknown;
quiz: unknown;
pass_score: number;
status: 'active' | 'inactive';
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
training_outline: string[];
}
/** `learning-paths` — the `learning_paths` table, every column the projection returns. */
export interface LearningPath {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string | null;
/** required on create. */
name: string;
target_role: string;
description: string;
difficulty: string;
steps: unknown;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `role-categories` — the `role_categories` table, every column the projection returns. */
export interface RoleCategory {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
name: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `certifications` — the `certifications` table, every column the projection returns. */
export interface Certification {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
name: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `user-activity` — the `user_activity` table, every column the projection returns. */
export interface UserActivity {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
event_type: string;
/** server-owned. */
user_id: string | null;
/** server-owned. */
user_email: string;
/** server-owned. */
user_name: string;
/** server-owned. */
account_type: string;
details: string;
position_id: string | null;
application_id: string | null;
candidate_id: string | null;
interview_id: string | null;
worker_email: string | null;
metadata: unknown | null;
/** server-owned. */
created_date: string;
}
/** `evidence` — the `evidence` table, every column the projection returns. */
export interface Evidence {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
course_id: string | null;
worker_profile_id: string | null;
course_title: string;
skill: string;
/** required on create. */
worker_email: string;
worker_name: string;
/** required on create. */
type: 'roleplay' | 'video' | 'photo_identify';
media_url: string;
transcript: string;
ai_verdict: 'verified' | 'needs_work' | 'failed';
ai_score: number;
ai_rubric: unknown;
ai_feedback: string;
supervisor_verified: boolean;
supervisor_name: string;
verified_date: string | null;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `assignments` — the `assignments` table, every column the projection returns. */
export interface Assignment {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
job_posting_id: string;
application_id: string | null;
worker_profile_id: string | null;
/** required on create. */
worker_email: string;
worker_name: string;
/** required on create. */
starts_at: string;
ends_at: string | null;
status: 'active' | 'completed' | 'cancelled';
source: string;
match_score: number | null;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `shift-records` — the `shift_records` table, every column the projection returns. */
export interface ShiftRecord {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
staff_id: string | null;
assignment_id: string | null;
job_posting_id: string | null;
worker_name: string;
worker_email: string;
role: string;
role_category: string;
shift_date: string;
scheduled_start: string;
scheduled_end: string;
scheduled_hours: number;
actual_start: string | null;
actual_end: string | null;
actual_hours: number;
overtime_hours: number;
minutes_late: number;
status: 'present' | 'late' | 'absent' | 'no_show';
notes: string;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `employee-roles` — the `employee_roles` table, every column the projection returns. */
export interface EmployeeRole {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
worker_profile_id: string | null;
/** required on create. */
worker_email: string;
worker_name: string;
/** required on create. */
role_category: string;
experience_years: number;
english_level: 'basic' | 'conversational' | 'fluent' | 'native';
certifications: string[];
desired_pay_min: number;
desired_pay_max: number;
availability: string[];
notes: string;
status: 'seeking' | 'placed' | 'inactive';
/** server-owned. */
created_by: string | null;
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** `badges` — the `badges` table, every column the projection returns. */
export interface Badge {
/** server-owned. */
id: string;
/** server-owned. */
legacy_id: string | null;
/** server-owned. */
org_id: string;
/** required on create. */
name: string;
description: string;
image_url: string;
level: 'bronze' | 'silver' | 'gold' | 'platinum';
requirements: string;
expiration_months: number | null;
verification_status: 'pending' | 'verified' | 'expired';
/** server-owned. */
created_date: string;
/** server-owned. */
updated_date: string;
}
/** The entity names the generic registry serves. Three more exist — see `entities.ts`. */
export type GeneratedEntityName =
| 'JobPosting'
| 'JobApplication'
| 'AIInterview'
| 'Staff'
| 'WorkerProfile'
| 'Course'
| 'LearningPath'
| 'RoleCategory'
| 'Certification'
| 'UserActivity'
| 'Evidence'
| 'Assignment'
| 'ShiftRecord'
| 'EmployeeRole'
| 'Badge';
/** Entity name -> its record type, for looking a record up by name. */
export interface GeneratedEntityRecords {
JobPosting: JobPosting;
JobApplication: JobApplication;
AIInterview: AIInterview;
Staff: Staff;
WorkerProfile: WorkerProfile;
Course: Course;
LearningPath: LearningPath;
RoleCategory: RoleCategory;
Certification: Certification;
UserActivity: UserActivity;
Evidence: Evidence;
Assignment: Assignment;
ShiftRecord: ShiftRecord;
EmployeeRole: EmployeeRole;
Badge: Badge;
}

View File

@@ -26,6 +26,24 @@
* `httpserver/definitions.go`, and the user by `httpserver/me.go`.
*/
/**
* The record shapes, generated from the backend's resource registry.
*
* Re-exported here so that `@/types/entities` stays the one place to import an
* entity type from, whether or not that particular shape happens to be
* generated. See `entities.generated.ts` for how they are derived and
* `scripts/gen-entity-types.mjs` for the mapping from Postgres column to
* TypeScript field.
*/
export type {
AIInterview, Assignment, Badge, Certification, Course, EmployeeRole, Evidence,
JobApplication, JobPosting, LearningPath, RoleCategory, ShiftRecord, Staff,
UserActivity, WorkerProfile,
} from './entities.generated';
import type { GeneratedEntityRecords } from './entities.generated';
import type { User } from './user';
/**
* Every entity name the client knows, spelled as the frontend spells it.
*
@@ -104,17 +122,10 @@ export type EntityResourcePath =
* rely on them instead of passing their own, so a conversion that dropped them
* would change what those callers fetch.
*
* `T` is the record type, left open here. Phase 2 deliberately does not define
* the eighteen record shapes: they are derivable — `resources_gen.go` carries
* every column with its type, nullability and enum values — but nothing
* consumes them until the transport layer is converted, and transcribing some
* five hundred field declarations by hand ahead of a consumer is how a type
* becomes confidently wrong. They belong with Phase 4, generated from that
* registry rather than retyped from it.
*
* Until then `EntityClient<T>` describes the shape of the surface without
* asserting anything about the records that travel through it, which is exactly
* as much as is known today.
* `T` is the record type and defaults to `unknown`, so the surface can be
* described without asserting anything about what travels through it. Where the
* entity is known, prefer `EntityClientFor<'JobPosting'>` below, which fills `T`
* in from `EntityRecords`.
*/
export interface EntityClient<T = unknown> {
entityName: EntityName;
@@ -133,3 +144,30 @@ export interface EntityClient<T = unknown> {
*/
bulkCreate(records?: Partial<T>[]): Promise<T[]>;
}
/**
* Entity name -> the record that entity's endpoints return.
*
* Fifteen come from the generated registry. The other three are the entities
* the generic machinery does not serve, and they are recorded honestly rather
* than guessed:
*
* User typed, from `src/types/user.ts` — the `/me` projection is
* read out of `httpserver/me.go`. Note that no
* `/api/v1/users` route exists, so the entity client for
* this name would 404; nothing calls it.
* AgentDefinition `unknown`. Served by `httpserver/definitions.go`, whose
* SkillDefinition columns are not in the registry this file is generated
* from. They belong to the agent layer, which this phase of
* the migration does not describe. `unknown` forces a
* deliberate narrowing at the point of use instead of
* quietly permitting anything, which `any` would.
*/
export interface EntityRecords extends GeneratedEntityRecords {
User: User;
AgentDefinition: unknown;
SkillDefinition: unknown;
}
/** The client surface for one named entity, with its record type filled in. */
export type EntityClientFor<K extends EntityName> = EntityClient<EntityRecords[K]>;