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:
@@ -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'];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user