// server/typescript/packages/codegen-ts/src/templates/extractor.ts // // The strict `extract` tier — a generated `.extractor.ts` that sits OVER the existing tolerant // extractLenient and turns dirty LLM text into the STRICT typed payload graph (nested objects + // arrays-of-objects populated) in one call. // // Cross-port parity: this mirrors the Java ExtractorCodeGenerator (FOC Task 6). The Java port's // extract(loader, text) / extractLenient(loader, text) both take the loaded MetaDataLoader and delegate // to the Phase-B runtime extract (MetaObjectExtractor) so the WHOLE nested graph is assembled; the // returned flavored object IS the strict type there (the binding provider makes newInstance() // return it). TS has no flavored object-class — extractLenient returns an all-nullable `Extracted` // mirror and the strict payload is a separate `interface`, so the TS port adds the recursive // mirror→strict mapper (toStrict) that the Java/Kotlin ports get for free from the runtime. // // Why extract takes the MetaRoot: the SELF-CONTAINED extractLenient(text) leaves nested objects // null (the historical FR-010 gap). The nested-capable path is extractLenientWithLoader(root, text), // which delegates to the runtime extract. So the extract tier — like the Java port — is loader // (MetaRoot)-driven. extractLenientWithLoader is re-exposed here under the public name extractLenient. // // NO registry / binding provider / factory; codegen walks the whole type graph statically (the // same MetaObject walk the extract-schema / payload emitters use). import { type MetaData, type MetaField, TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_ENUM, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_REQUIRED, TEMPLATE_ATTR_RESPONSE_REF, resolveObjectRef, } from "@metaobjectsdev/metadata"; import { responseShape } from "./find-inbound.js"; import { fields, isArray } from "./fr010-field-mapping.js"; import { mirrorName } from "./extract-delegate-emitter.js"; import { enumUnionAliasName } from "./inferred-types.js"; import { enumValues } from "../enum-meta.js"; import type { RenderContext } from "../render-context.js"; // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident. // ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise. function findObject(root: MetaData, name: string, referrerPkg = ""): MetaData | undefined { return resolveObjectRef(root, name, referrerPkg).node; } // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident. function findTemplate(root: MetaData, name: string): MetaData | undefined { return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name); } /** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */ function refVo(field: MetaData, root: MetaData): MetaData | undefined { const ref = field.attr(FIELD_ATTR_OBJECT_REF); if (typeof ref !== "string") return undefined; // ADR-0042: resolve as authored — a bare ref binds the declaring VO's package, // an FQN exactly; NO bare-tail fallback (never bind a same-named VO elsewhere). return findObject(root, ref, field.parent?.package ?? field.parent?.fileDefaultPackage ?? ""); } function isObjectField(field: MetaData): boolean { return field.subType === FIELD_SUBTYPE_OBJECT; } /** * The union-alias type name for a `field.enum` with effective `@values`, or undefined when the * field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the entity * inferred-types emitter types the field as — so the cast target resolves to the exact alias * exported from the owning VO's entity module. `ownerName` is the owning value-object's interface name. */ function enumAlias(field: MetaData, ownerName: string): string | undefined { if (field.subType !== FIELD_SUBTYPE_ENUM) return undefined; const values = enumValues(field as MetaField); if (values === undefined) return undefined; return enumUnionAliasName(ownerName, field as MetaField); } /** * True iff the field is required IN THE STRICT PAYLOAD TYPE. The strict payload IS the VO's own * generated entity-module interface (`renderValueObjectInterface`), which types a required field * `f: T` and an optional one `f?: T` (i.e. `T | undefined` — NOT `T | null`). So the mapper's * optionality assumption (`m.f!` vs `m.f ?? undefined`) has to agree with THAT interface, and an * absent optional maps to `undefined`, never `null`. This predicate matches the interface's * required test (boolean `true` only) so the two never skew. */ function isFieldRequired(field: MetaData): boolean { return field.attr(FIELD_ATTR_REQUIRED) === true; } /** The mirror→strict mapper name for a value-object (`toStrict`). ADR-0044/#228: `ctx` * (optional) resolves the collision-scoped entity-domain emitted name (matches Task 3's * entity module), so the mapper name agrees with the strict payload type it targets under a * cross-package short-name collision (`toStrictAcmeAlphaNote`). Omitted → bare `vo.name`. */ function mapperName(vo: MetaData, ctx?: RenderContext): string { const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name; return `toStrict${name}`; } /** * The mapper-body initializer expression for one field, reading mirror member `m.` and * mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? undefined`). * The strict payload is the VO's generated entity-module interface, whose optional fields are * `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`. * Nested single/array objects recurse into their toStrict mapper, guarding when optional. */ function strictArg(field: MetaData, root: MetaData, ownerName: string, ctx?: RenderContext): string { const name = field.name; const required = isFieldRequired(field); if (isObjectField(field)) { const target = refVo(field, root); if (target === undefined) { // Unresolved @objectRef — the payload type would be `unknown`; pass through as-is. return required ? `m.${name}!` : `m.${name} ?? undefined`; } const fn = mapperName(target, ctx); if (isArray(field)) { // Required array-of-objects: each element mapped; element nulls dropped at the type level // via the non-null assertion (extract never yields null elements for a present array). if (required) return `m.${name}!.map((e) => ${fn}(e!))`; return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : undefined`; } // Single nested object. if (required) return `${fn}(m.${name}!)`; return `m.${name} ? ${fn}(m.${name}) : undefined`; } // Scalar ARRAY (e.g. `field.string` with isArray): the mirror types it `(T | null)[] | null` // but the strict payload types it `T[]` (required) / `T[]?` (optional). A bare `m.f!` // would leave the element type `T | null`, a `tsc --strict` TS2322 error. Filter out null // elements so the element type narrows to non-null (consistent with the lost-element DROP policy // already used for required arrays-of-objects above). An absent optional array maps to `undefined`. // // ENUM arrays: the mirror element is a plain `string`, but the strict payload types it as the // closed `[]` union. The null-filter alone narrows to `string[]`, not `[]` — a // `tsc --strict` TS2322 error. So the null-filtered result is CAST to `[]`. The cast is // sound: the engine validated each present element is a member of the closed set (else the field // is lost/MALFORMED and extract throws), so the runtime string IS a valid union member. const alias = enumAlias(field, ownerName); if (isArray(field)) { if (required) { const filtered = `(m.${name} ?? []).filter((x): x is NonNullable => x != null)`; return alias !== undefined ? `(${filtered}) as ${alias}[]` : filtered; } const filtered = `m.${name}.filter((x): x is NonNullable => x != null)`; const guarded = `m.${name} == null ? undefined : ${filtered}`; return alias !== undefined ? `m.${name} == null ? undefined : (${filtered}) as ${alias}[]` : guarded; } // Scalar / enum (single): the strict payload's optionality decides the shape. // Required → non-null assertion; optional → `?? undefined` (matches the entity-module `f?: T`). // // ENUM scalar: the mirror member is a plain `string`, but the strict payload types it as the // closed `` union — assigning `string` into `` is a `tsc --strict` TS2322 error. // So the value is CAST to ``. Sound for the same reason as enum arrays above: the engine // already validated membership (or extract throws on a lost required field). if (alias !== undefined) { return required ? `m.${name}! as ${alias}` : `(m.${name} ?? undefined) as ${alias} | undefined`; } return required ? `m.${name}!` : `m.${name} ?? undefined`; } /** * Emit one `toStrict(m)` mapper per value-object reachable from `vo` (payload + nested, * deduped, cycle-safe). Each maps the all-nullable `Extracted` mirror onto the strict `` * payload interface. The ROOT mapper reads the canonically-named root mirror (`