Spec 0010 — target profiles, declarations and split HTTP emit modes
spec-frozen. This is the compiler-only prerequisite for the Profile proving slice. It does not rewrite mono-web, exercise its browser/PostgreSQL journey, or authorize deployment. Gate 2A's client/Fol…
Frozen operator amendment — 2026-10-09
Stable Effect >=4.0.0 <5 is the only supported application target. This
amendment supersedes every RC target, module mapping, compatibility brand and
RC acceptance criterion below; those passages remain historical evidence.
Remove the effect-4.0-rc choice without a compatibility shim. Migrate the
isolated fixture to stable-v4 and native stable APIs while retaining split
emit identity, HTTP/RPC/CLI, raw bindings, status, errors and resource laws.
The root ordered gate must exercise the stable fixture and packed consumer.
No feature draft is imported, no active feature contract is dropped, and no
other worktree, main checkout, publication or deployment is authorized.
Status: spec-frozen. This is the compiler-only prerequisite for the Profile proving slice. It does not rewrite mono-web, exercise its browser/PostgreSQL journey, or authorize deployment. Gate 2A's client/Foldkit contracts remain in force for implemented operations (spec 0007).
Observable result
One declaration-only Profile twin compiles twice from the same source:
effx build --emit=contract writes a group contract; --emit=handlers
writes an external raw-binding factory. Both passes have identical normalized
IR and semantic hash even when their tsconfigs and output directories differ.
The generated files typecheck against an isolated Effect 4.0.0-rc.116
project, not just effx's installed 4.0.0. The original users example still
builds with the default --emit=all.
The target is the same Profile GET/PATCH declaration surface as mono-web's
packages/http-api/src/profile.ts:72-143: group profile, endpoint keys
readOwnProfile/updateOwnProfile, paths /api/profile, conditional 200/304
GET, merge-patch PATCH, security and problem metadata. Its backend owns the
raw webHandler path and committing transaction
(apps/backend/src/profile/http.ts:202-354,
docs/research/profile-baseline.md:45-374). Effx generates wiring only.
Project target and module names
ProjectConfig.target?: "effect-4.0" | "effect-4.0-rc"selects the generated application API profile. CLI--target=<value>or--target <value>is accepted forcheckandbuild. An explicit target wins. Otherwise resolve the target project's installedeffect/package.jsonfrom its tsconfig module-resolution context and decode its version; stable4.0.0selectseffect-4.0,4.0.0-rc.<n>selectseffect-4.0-rc. Do not use the CLI bundle's own Effect, catalog declarations, or an installed sibling package. Missing/unsupported packages produceEFFX2701with location and skip files. The parsed target tsconfig decidesallowImportingTsExtensions(including inherited settings).compileCollectedwithout a project receives an explicit default stable profile and conservative.jsrelative imports. Source:packages/frontend-ts/src/project.ts:60-114,docs/research/rc116-compat.md:16-43.- One import-resolution table covers every generated file. Stable
effect-4.0keepseffect/http,effect/http-api,effect/rpc,effect/cli,effect/net,effect/sql; rc uses respectivelyeffect/unstable/http,effect/unstable/httpapi,effect/unstable/rpc,effect/unstable/cli,effect/unstable/net,effect/unstable/sql. The rooteffectimport remains unchanged. An unsupported package subpath is an error, not a guessed import. For rc the spelling ishttpapi, nothttp-api(docs/research/rc116-compat.md:32-43, installed targetnode_modules/effect/package.jsonexport map). Emitters, including hand-built type-only imports, must use this table instead of each keeping an alias. - Emit local relative imports with
.jsunless the target tsconfig explicitly permits TypeScript extensions; with that option, retain.ts. Apply this to generated-to-generated imports and frontend-derivedSchemaRefandSymbolRefimports. Keep package specifiers bare. The IR and StableIds never depend on the chosen suffix (docs/research/rc116-compat.md:26-43,packages/compiler/src/generate/emit.ts:42-78). - The frontend recognizes rc's
HttpApiMiddlewaresecurity type stamp and rc'sHttpApiSchemastatus annotation at their actual installed paths.EFFX2503cannot be bypassed because the rc marker was missed; an explicit status must not vanish. Only source API differences verified in the target installation are rewritten. The CLI may use its own bundled stable Effect internally (packages/frontend-ts/src/{lower,signature}.ts,docs/research/rc116-compat.md:45).
One semantic source identity, two output directories
A project's tsconfig.effx.json declares effx.projectRoot as a path relative
to that tsconfig. ProjectConfig.projectRoot?: string can supply the same
value programmatically. For each pass, resolve that path to the common source
root, and collect the same explicit entry. Its local refs and StableIds use
that source root. Lower local symbol modules relative to the canonical
<projectRoot>/.effx/generated import base, not the selected artifact
directory. At render time, resolve that source location and rebase its import
specifier against the actual artifact directory. The common base equals the
old default for a single-package project, so its existing IR shape and v1/v2
migration laws remain valid. Do not hash the emit mode, target profile,
absolute checkout path, actual output directory, or file suffix. Both passes
must compare byte-identical canonical IR and semanticHash; differences in
source declarations still change the hash (packages/frontend-ts/src/{project,TsSourceFrontend}.ts,
packages/ir/src/{Refs,canonical}.ts, ADR 0003).
Declaration-only operations and group metadata
- Builder terminal
.declare()returns an operation declaration with no callable handler. It lowers the sameQuery/Command,Http.*,ErrorsandRequirementsannotations as.handler(...), but recordsbinding: "external". TheOperationIR node has optionalhandleronly when bound locally; analysis rejects(external, handler present)and(local, handler missing). External operations do not infer emptyEorRfrom an absent function.@Errorsand@Requirementson an external operation remain declarations in the IR, including theRequiresedge; local handlers retain the existing assertion semantics. Generated raw callbacks restrict typed failures to endpoint problems but infer their requirement channel from the backend binding. A declared external@Requirementsservice must never be an upper bound that excludes additional backend-only services. An external operation with RPC/CLI exposure but no executable binding is an error rather than a generated fake call (packages/runtime/src/builder.ts,packages/frontend-ts/src/{collect,signature}.ts,packages/ir/src/Node.ts,packages/compiler/src/extensions/core.ts). @Http.Group({ root: RootApiSymbol, group, title, description, displayName })on an exported class and the exported builder valueHttp.group(...)lower to oneHttp.Groupdeclaration.RootApiSymbolis an exported, statically resolved concrete full HttpApi value with a literalidentifier. The frontend records that identifier asHttpGroup.rootand its canonicalSymbolRefasHttpGroup.rootSymbol; neither the import path chosen for an output directory nor the emit mode enters the IR/hash. A stringrootremains valid for existing local declarations (defaulteffx), but an external raw-handler group needs a resolvable concrete root symbol instead of a guessed second root. The group contributes one Schema-definedHttpGroupnode atgroup:<root>/<group>, independently of its operations. A(root, group)has at most one compatible definition; conflicting root symbols or metadata fail analysis. The root identifier must match the operations'Http.Contract.rootstring. Emit.annotateMerge(OpenApi.annotations({ title, description, override: { "x-displayName": displayName } }))on the generatedHttpApiGroup, not on the imported root; the contract imports no backend root (mono-web/packages/http-api/src/profile.ts:129-143,packages/compiler/src/generate/http-contracts.ts).- For a named endpoint,
Http.Contract.metadata.operationIdmust be exactly<group>.<endpointKey>with a nonempty identifier-safe key. That key drivesHttpApiEndpoint.<verb>(key, ...), the raw binding record, the guard record (<group>.<key>), and every client lookup. The stable IR operation ID remains distinct. Duplicate(root,group,key), wrong prefix, empty/invalid suffix, or missing ID on an externally bound HTTP operation producesEFFX2403. Unannotated legacy local operations preserve their existing key (User.Getetc.); metadata validation does not silently rename them (packages/compiler/src/generate/{http,client}.ts, mono-webpackages/http-api/src/profile.ts:72-76,102-109). - Optional
Http.Contract.metadata.annotatoris an application-exported callable symbol, lowered to aSymbolRef(not evaluated by effx). Its input is the present{ operationId, summary, description, tags }metadata fields; its return value is an EffectContextmerged onto the generated endpoint after built-inOpenApi.annotations. Absent fields are omitted, not emitted asundefined. This permits application-owned OpenAPI transforms such as operation provenance without embedding application constants in the compiler or importing backend code. Invalid, inline, or unexported callables fail withEFFX1102(mono-webpackages/http-api/src/common.ts:193-222). Http.Problems.codesremains the single code-list source. The registry call defaults toRegistry("<endpointKey>Problem", [...codes]). The explicitly approvedHttp.Problems.identifier?: stringoverride preserves existing schema identity when needed: Profile declaresProfileReadOwnProfileProblemandProfileUpdateOwnProfileProblem. The override changes only the schema name, not endpoint key or operation ID. Without it, mono-web'sproblemUnion(identifier,codes)would emit a different OpenAPI schema identity. Invalid/empty identifiers fail analysis (docs/specs/0009-profile-proving-slice.md:40, mono-webpackages/http-api/src/{endpoint-problems,http-semantics}.ts).
Emit modes and raw bindings
ProjectConfig.emit?: "contract" | "handlers" | "all"; CLI
effx build --emit=contract|handlers|all defaults to all.
effx check may accept an emit choice for diagnostics but writes nothing.
Emit mode is a projection choice, not an IR annotation or semantic hash
input. The manifest records the selected mode and the exact written files;
when a mode changes, obsolete artifacts in the selected output directory
must not masquerade as this build's output (packages/cli/src/{main,commands,manifest}.ts,
packages/compiler/src/{Extension,pipeline}.ts).
| Mode | Exact output for an external-only Profile group | Bound behavior |
|---|---|---|
contract | profile-contract.ts only | Endpoint constants plus exported ProfileApi (HttpApiGroup); imports no backend handler, root or AppRoutes. |
handlers | profile-handlers.ts only | Imports the concrete canonical root symbol; exports ProfileApiHandlers({ raw, guards }) without backend implementation or partial root. |
all | Both Profile files plus the existing projections for locally implemented operations | Existing users HTTP/RPC/CLI/client/Foldkit behavior stays callable; no external operation gains an invented handler call. |
For equal group names under different roots in one output directory, prefix
both filenames with the root identifier; never overwrite. External-only
groups may not have executable RPC/CLI/Foldkit projections unless explicitly
implemented. A mixed local/external binding for the same (root,group) is a
diagnostic until a coherent combined group is specified. No mode emits an
empty placeholder file as a substitute for a missing implementation.
The handler factory imports the concrete full canonical root named by the
HttpGroup.rootSymbol and accepts exact raw and guards records, not a
generic Root argument. It registers via
HttpApiBuilder.group(ExternalNativeApi, "profile", ...) and chains
.handleRaw("readOwnProfile", ...) and
.handleRaw("updateOwnProfile", ...); it does not serve a second root.
Only the backend-owned handlers artifact imports that root; the generated
contract never imports it, so the backend-to-http-api dependency has no reverse
edge. Native HttpApi/HttpApiGroup mark group/endpoint parameters invariant:
a generic full-root factory cannot prove the conditional group identifier or
endpoint-map keys under rc.116 without a cast. No cast is permitted.
The generated per-operation raw types derive the decoded request from
HttpApiEndpoint.HandlerRawWithIdentifier on that concrete root (including
HttpServerRequest, params/query/headers when present, but no payload).
Each lazy authorize: () => Effect<Principal, Problem, RGuard> uses an explicit
per-operation authorization Effect type. The raw callback returns
Effect<HttpServerResponse, declared endpoint/middleware problems, RGuard | ROp>.
Each operation has its own free ROp inferred from its raw callback at the
ProfileApiHandlers({ raw, guards }) binding site. Native handleRaw joins
GET/PATCH backend services as request-level HttpRouter.Request.From<"Requires", R>
markers in the returned Layer. The fixture extracts their payloads with
HttpRouter.Request.Only<"Requires", Layer.Services<typeof ProfileBindings>>.
@Requirements remains in the IR/Requires graph but does not cap either
channel or force a backend implementation import into the generated handlers.
Each qualified guard receives the same HttpServerRequest from that raw
request and runs only when the application calls the thunk. Missing raw or
guard keys, incompatible response/error channels, and duplicate bindings fail
target TypeScript. The source backend may call its existing webHandler once
and must retain its Web Request, raw body, and transaction ownership; this
compiler fixture does not claim its GET authority check shares a SQL snapshot
or that PATCH has run inside a real transaction
(docs/specs/0009-profile-proving-slice.md:46-54, mono-web
apps/backend/src/profile/http.ts:202-354, installed rc
HttpApiEndpoint.HandlerRawWithIdentifier and HttpApiBuilder.handleRaw).
Amendment (2026-10-04) — mixed-ownership concrete groups
Effx must never emit a handler module that fails native group completeness,
even when an application imports only its raw or guard types. The frontend
inspects the concrete root's group and endpoint maps statically. The proven
endpoint inventory is compiler generation context only: no TypeScript object
or inventory enters the semantic IR or its hash. An unprovable inventory,
missing group, or declared endpoint key absent from that group is EFFX2415
(data diagnostic); generation is skipped, not replaced with an invalid factory.
Generation-only inventory diagnostics are deferred behind earlier fatal
diagnostics, because those already prevent every generated artifact. Once the
earlier defect is fixed, a remaining inventory defect must surface as
EFFX2415 and still prevent factory emission.
An authored root may import its generated group contract, as in mono-web. Generate the contract before handlers. A committed fixture contract seed is checked by regenerate-and-compare alongside the Profile, Directory and Contact seeds; a changed byte must fail that guard. The seed is never a second authored endpoint definition, and tests isolate writes in fresh temporary copies.
When every concrete group endpoint is declared in effx, the existing factory
and all generated bytes remain unchanged. When the concrete group also has
application-owned endpoints, only the raw-handler factory changes: binding
the declared raw and guards records returns a completion continuation.
For onboarding with declared readBoard/command and manual claim:
const completeOnboarding = OnboardingApiHandlers({ raw, guards });
const OnboardingLive = completeOnboarding((handlers) => handlers.handle("claim", claim));The continuation receives native HttpApiBuilder.Handlers after registering
the declared raw callbacks. Those endpoint identifiers are already handled;
the remaining identifiers are still required. Native Handlers.ValidateReturn
rejects an incomplete completion, native registration rejects duplicate keys,
and endpoint request/success/error types check the manual callback. Only after
completion does the existing concrete-root HttpApiBuilder.group build its
layer. Raw/guard records remain restricted to declared operations. There is no
partial-root cast, alternate root, or fabricated implementation.
Completion may return the fully implemented collection directly or an Effect
that constructs it. Native Handlers.Error and Handlers.Context determine
the returned layer's startup error and services, excluding native Scope.
Generated raw/guard services and manual handler services retain native
request-level markers; effectful completion's startup services/errors remain
visible rather than being erased. Stable Effect 4.0.0 and 4.0.0-rc.116 are
the acceptance targets. Tests use fresh temporary projects, including a
type-only consumer, positive claim, negative incomplete/duplicate/wrong
request/wrong error completions, and exact service/startup channel assertions.
Existing closed-group identity snapshots and mono-web's nine complete groups
must remain byte-identical; only a new mixed-group fixture may differ.
Amendment (2026-10-05, director) — inventory bootstrap and unresolved leaves
Endpoint inventory proof is a handler-factory generation precondition, never a
declaration-lowering precondition. Contract-only emission must not evaluate it
or report EFFX2415; exported-root branding, the literal root identifier and
all other declaration checks still apply. Inventory stays generation context,
outside semantic IR and its hash. Only groups for which a handler factory is
being emitted require a group and every declared endpoint key to be proven.
Generate every group contract imported by the authored root before proving handlers, including generated groups with no declarations in the current handler invocation. Thus the cold full-root contract → full-root handlers and full-root contract → healthy-only handlers sequences succeed. Healthy-only contract emission succeeds on its own, but cannot bootstrap an unrelated missing onboarding contract; cold healthy-only handlers must still reject it.
For unresolved root leaves, choose Option B: whole-root rejection. Report
one root-level EFFX2415 naming the unresolved leaf, not one diagnostic at
each group reference or cascading missing-group errors. Do not emit any factory
using that root. In installed stable Effect 4.0.0,
src/http-api/HttpApi.ts:40-42 maps groups by identifier, :70 unions incoming
group types, and :147-154 assigns each incoming group at group.identifier.
Installed 4.0.0-rc.116 has the same semantics at
src/unstable/httpapi/HttpApi.ts:38-40, :67, and :142-149. An unresolved
leaf cannot be proven to have a different identifier: it may replace the
apparently healthy group. A resolved remainder therefore does not prove that
group complete. Earlier fatal diagnostics still defer inventory diagnostics.
Known limit — completion callback annotations. Native Effect permits an
explicitly incorrect callback parameter type such as
(handlers: AlreadyComplete) => handlers without actual endpoint registrations.
Readonly HandledIdentifiers allows covariant widening: see rc.116
HttpApiBuilder.ts:285-319 and stable 4.0.0 HttpApiBuilder.ts:291-326.
The same annotation bypasses native HttpApiBuilder.group completeness. A tiny
stable type test confirms this behavior and rejection of an unannotated incomplete
callback. Unannotated mixed-group completions remain checked. This is an Effect
type limit; effx adds no cast or workaround.
Cold --emit=all remains atomic: it reports the one root inventory diagnostic
and writes nothing, including no contract or manifest. Contract-first is
required. This preserves the CLI ownership transaction: error rejection occurs
before manifest reconciliation, and obsolete generated files are removed only
against a successful replacement manifest. A failing build must not erase
previously owned handler outputs or publish a partial ownership record.
Fixture and required checks
Create packages/frontend-ts/test/fixtures/rc116/ with its own
package.json pinning effect@4.0.0-rc.116 and an rc-target tsconfig without
allowImportingTsExtensions. No @vektorprogrammet/* source or sibling effx
workspace dependency enters the fixture. The Profile twin declares GET/PATCH
/api/profile, ConditionalReadHeaders, IdempotencyIfMatchHeaders,
merge-patch media type, 200/304 and ETag response headers,
PersonSecurity, access and registry-backed problems, one Http.group, and
two .declare() operations. The test-only concrete full root imports
the generated ProfileApi group from a checked-in, pre-generated contract
artifact. This real generated seed makes the root source-resolvable on a clean
first pass, without a hand-authored duplicate group or a contract → backend
import. Contract mode regenerates it and checks byte equality before handlers
mode runs. Hand-written raw and guard records exercise the native binding
types. Two fixture-only Context.Service dependencies (one GET and one PATCH)
are absent from the shared Profile contract; no casts are needed to infer both
in the resulting handler layer. Generate both modes into distinct fixture
directories, then typecheck
the generated files against rc.116 from its own installed
node_modules/effect. Keep the original users example and Gate 2A fixture
typechecks green under stable 4.0.0.
The rc fixture exports fixtureOperationAnnotations through its support
module, declares it for both Profile operations, and applies an OpenApi
transform that adds x-test. After regenerating the checked-in contract,
the rc runtime test calls OpenApi.fromApi on the actual generated Profile
group mounted on the full root and inspects GET and PATCH at /api/profile.
It does not accept a source-text assertion as evidence of the transform.
Falsifiers:
- Two modes of the same source have identical canonical IR/semantic hash, group keys/metadata/statuses, source-root-relative references and no fake handler symbol. Their manifests list only their selected files and mode.
- The Profile contract imports no backend module, exports a group but no
root, and has the exact GET/PATCH keys, declared problem identifier
overrides, merge-patch media, 200/304 and headers. Handler output imports
only the concrete canonical root symbol (not its implementation) and
contains two
handleRawregistrations on it, lazy guards and nohandleAll/AppRoutes/body read. The raw handler factory infers independent free requirement channels rather than importing declared backend services;HttpRouter.Request.Only<"Requires", Layer.Services<typeof ProfileBindings>>contains distinct GET and PATCH services without widening tounknown. Negative rc type witnesses fail for a missing raw/guard key and an undeclared raw failure. EFFX2403catches wrong ID prefix, empty/duplicate endpoint key and a missing external key. Invalid binding/group forms, nonliteral or unexported root symbols, and hidden source symbols stop generation. Declared and local operations cannot swap behavior without a diagnostic.- Both emitted files typecheck under installed rc.116; unchanged users and
Gate 2A artifacts still typecheck under stable 4.0.0. Relative
.jsimports resolve to target TS sources, while profile-mappedeffect/*imports exist on the target. Unknown target modules fail diagnostically. - The generated Profile root's rc.116 OpenAPI GET and PATCH operations both
retain
x-testwith their own operation ID, summary, description and tags. An invalid or unexported annotator yieldsEFFX1102; a contract without this optional metadata retains its prior projection. - On the clean committed branch, regenerate the users example, then run
bun run typecheck,bun run effect:diagnostics,bun run lint,bun run fmt:check,bun run testand the rc116 fixture tsc harness. Fast-forward main only when green. Then runbun run packfrom clean main; reportdist-artifacts/manifest.jsoncommit, tarball names and SHA-256. Packaging itself is an external distribution artifact, not proof of mono-web's frozen install or browser/PostgreSQL journey.
The rc116 fixture tests compiler output and installed target types. Mono-web's
patched rc116 target, portable tarball install in its repository, unchanged
just e2e profile, real transactions, and v0.2 byte parity remain Gate 3
integration work under spec 0009.
Spec 0009 — Profile proving slice
spec-frozen 2026-10-03. This freezes the observable Gate 3 contract from spec 0004 §4–5. It does not claim that the compiler or mono-web integration already implements it. Change this contract explic…
Spec 0011 — Schools directory and management migration
operator-approved and accepted locally at the clean, unlanded mono-web effx/profile-baseline commit 188f2385da419af3b4cbf60bcf4c9ccb5768c136 (unpublished). This frozen design originally inspected mon…