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

@@ -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 {