From 8975d0a778fb0085a3790cda7826e17fc1a765ca Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Mon, 22 Jun 2026 08:34:07 +0200 Subject: [PATCH 1/2] feat(Schema/Class): facade `identifier` + add `StructFacade` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `.d.ts`-emit schema-facade compiler (effect-app/TypeScript, effect-app/tsgo) retypes faceted models to these facade types. Two changes so the emitted bare facade interface is complete on its own (the compiler should not hand-emit generic schema surface): - Add `readonly identifier: string` to OpaqueFacade / OpaqueClassFacade / OpaqueErrorFacadeClass. At construction the facade carried `identifier` via `& OpaqueFacadeStatics`, but the emitter writes the *bare* interface (the named namespace replaces SchemaS), so `identifier` — an `S.Class` static typed `string` — was lost. It is generic (same for every model), so it belongs on the interface, not per-model compiler output. - Add `StructFacade` = `Omit, ...> & { pinned type-level members }`. A top-level `const X = S.Struct(...)` model is retyped to it: still a real `S.Struct` (so `Workflow.AnyStructSchema`, the `Struct` reconstruction, `Union`, `.fields.x` keep working) while `Type`/`Encoded`/make/ services resolve to the named namespace interfaces. (Was scanner-local in `api/src/lib/StructFacade.ts`; this is its proper home.) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/effect-app/src/Schema/Class.ts | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/effect-app/src/Schema/Class.ts b/packages/effect-app/src/Schema/Class.ts index d0592a0e2..4f5a34383 100644 --- a/packages/effect-app/src/Schema/Class.ts +++ b/packages/effect-app/src/Schema/Class.ts @@ -490,6 +490,7 @@ export interface OpaqueFacade< { new(_: never): Brand readonly copy: ReturnType Self>> + readonly identifier: string // NOTE: `fields` / `mapFields` are intentionally NOT redeclared here. They are // carried (precise) from the underlying schema via `OpaqueFacadeStatics`. A wide // `mapFields(f: (fields: S.Struct.Fields) => To)` override would win overload @@ -519,6 +520,7 @@ export interface OpaqueClassFacade< { new(...args: OpaqueFacadeConstructorArgs): Brand readonly copy: ReturnType Self>> + readonly identifier: string } export function OpaqueFacade< @@ -600,6 +602,7 @@ export interface OpaqueErrorFacadeClass< // makes `yield* new X()` / `Effect.fail` / `instanceof` work without losing data. new(...args: OpaqueFacadeConstructorArgs): Cause.YieldableError & Brand readonly copy: ReturnType Self>> + readonly identifier: string } export function OpaqueErrorFacadeClass< @@ -627,3 +630,36 @@ export function OpaqueErrorFacadeClass< throw new TypeError("OpaqueErrorFacadeClass requires a class schema") } } + +/** + * Workflow-compatible struct facade. + * + * A top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` schema model whose `.d.ts` + * is produced by the schema-aware declaration emitter is retyped from the giant inline + * `S.Struct<{ ...fields... }>` to this facade. + * + * Unlike {@link OpaqueFacade}, `StructFacade` **extends `S.Struct`**, so the value is + * still a real struct schema: it satisfies `Workflow.AnyStructSchema` and the + * `Struct` reconstruction the workflow machinery performs, and `Union` / + * `.fields.x` keep working. At the same time the type-level members (`Type` / `Encoded` / + * make-input / services) are pinned to the named interfaces the emitter generates in + * `namespace X`, so consumers reference those by name instead of re-deriving them from + * `Fields` on every use. `Fields` is the model's own generated `X.Fields` interface — a single + * materialization of the (expensive) field shape that every reference site points at. + */ +export type StructFacade< + Self, + Encoded, + MakeIn, + DecodingServices, + EncodingServices, + Fields extends S.Struct.Fields +> = + & Omit, "Type" | "Encoded" | "~type.make.in" | "DecodingServices" | "EncodingServices"> + & { + readonly "Type": Self + readonly "Encoded": Encoded + readonly "~type.make.in": MakeIn + readonly "DecodingServices": DecodingServices + readonly "EncodingServices": EncodingServices + } From 481e70dcbbf4d70d0cfcc00245c2bc132354da43 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Mon, 22 Jun 2026 08:41:34 +0200 Subject: [PATCH 2/2] fix: identifier only on class facades (not OpaqueFacade) + add changeset S.Opaque / requests build on S.Bottom (not S.Class) so they have no identifier; remove it from OpaqueFacade, keep on OpaqueClassFacade + OpaqueErrorFacadeClass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/schema-facade-identifier-structfacade.md | 8 ++++++++ packages/effect-app/src/Schema/Class.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/schema-facade-identifier-structfacade.md diff --git a/.changeset/schema-facade-identifier-structfacade.md b/.changeset/schema-facade-identifier-structfacade.md new file mode 100644 index 000000000..d2c07836c --- /dev/null +++ b/.changeset/schema-facade-identifier-structfacade.md @@ -0,0 +1,8 @@ +--- +"effect-app": minor +--- + +Schema/Class facades: add `StructFacade` and carry `identifier` on the class facades. + +- Add `StructFacade` = `Omit, "Type" | "Encoded" | "~type.make.in" | "DecodingServices" | "EncodingServices"> & { pinned type-level members }`. A top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` model faceted by the `.d.ts`-emit compiler is retyped to it: still a real `S.Struct` (so `Workflow.AnyStructSchema`, the `Struct` reconstruction, `Union`, `.fields.x` keep working) while `Type`/`Encoded`/make/services resolve to the named namespace interfaces. +- Add `readonly identifier: string` to `OpaqueClassFacade` and `OpaqueErrorFacadeClass` (an `S.Class` static lost from the bare emitted interface, which the compiler otherwise had to hand-emit per model). `OpaqueFacade` is intentionally left untouched — `S.Opaque` and requests build on `S.Bottom`, not `S.Class`, so they have no `identifier`. diff --git a/packages/effect-app/src/Schema/Class.ts b/packages/effect-app/src/Schema/Class.ts index 4f5a34383..8058960f3 100644 --- a/packages/effect-app/src/Schema/Class.ts +++ b/packages/effect-app/src/Schema/Class.ts @@ -490,7 +490,8 @@ export interface OpaqueFacade< { new(_: never): Brand readonly copy: ReturnType Self>> - readonly identifier: string + // NOTE: no `identifier` here — `S.Opaque` (and requests) build on `S.Bottom`, not `S.Class`, + // so they have no `identifier`. It lives only on the class facades below. // NOTE: `fields` / `mapFields` are intentionally NOT redeclared here. They are // carried (precise) from the underlying schema via `OpaqueFacadeStatics`. A wide // `mapFields(f: (fields: S.Struct.Fields) => To)` override would win overload