Spec 0013 — Group defaults and dense HTTP declarations
Goal and evidence
An effx declaration should state the operation-specific contract once, rather than repeat the same group, middleware, annotation factory, registry, and access policy on every operation. The accepted mono-web Profile and Schools source repeats those values in packages/http-api/src/profile.effx.ts and directory.effx.ts at mono-web commit 188f2385 (unpublished); effx currently exposes Http.group, Operation.query/command, and @Http.Group but leaves HTTP contracts independent (packages/runtime/src/{Annotation,builder,decorators}.ts). Shorter source must not change its meaning.
Source contract
Http.group({ root, group, title?, description?, displayName?, defaults?: { middleware?, metadata?: { annotator? }, problems?: { registry? }, access?: { annotator?, exposure?, acceptedCredentials?, principalKinds?, concealment? } } })accepts the existing exported, concreteHttpApiroot. Class-level@Http.Group(options)accepts the same defaults and associates its static HTTP operations with that group.Operation.query/command(...).in(Group)associates a builder operation with one exportedHttp.groupvalue or@Http.Groupclass;.indoes not execute the group or write an IR edge. Bare operations without an association retain their previous behavior.- A group association supplies
rootandgroupto that operation's existinghttp.contract/@Http.Contract, when omitted.Http.Contractstays an explicit annotation for each HTTP operation; method/path, response headers/status, request headers, problems codes, capabilities, requirements, scope resolver, decision time, and identity stay operation-specific unless explicitly shared above. A group declaration still produces the sameHttpGroupnode and does not serializedefaults. - The inherited fields are merged per field, not by replacing the
metadata,problems, oraccessobject wholesale. An explicitly present operation field wins over the same group field, including an empty middleware array. Defaultproblems.registryfills an existingHttp.Problemsannotation; it does not create a problem declaration without operation-specificcodes. Default access fields fill an existingHttp.Accessannotation; it does not invent capabilities, requirements, scope, concealment timing, or authorize a missing operation. Default metadata annotator fillsHttp.Contract.metadata, preserving explicit metadata fields. No application function is invoked during collection or compilation. - When
metadata.operationIdis absent and the operation's declarednamehas exactly the associated<group>.<key>shape (non-empty safe key), its operation ID is that name. An explicit operation ID remains supported and wins; otherwise retain the existing generator behavior. In particular, the existing rc.116 Profile twin'sProfile.ReadOwnProfilenames do not qualify for automaticprofile.readOwnProfileIDs, so that twin retains those explicit IDs. Http.Contract.successdefaults to the operation's declaredsuccess.Http.Contract.query: trueon a GET Query means the operation's declaredinputschema; an explicit query schema keeps its existing meaning. For POST/PATCH Command, omittedpayloaddefaults toinputonly when it is not already assigned to request params, query, or headers; an explicit payload always wins. Query POST, DELETE, PUT, and ambiguous request mappings keep their explicit schema fields; a query marker on an incompatible verb/kind is a diagnostic, never a guessed route shape.- Builder and decorator forms must lower to the same expanded annotation arguments for the same declaration. After lowering, the existing
HttpContract,ProblemContract, andAccessContractextension data and all existing generators remain unchanged; association/defaults are source syntax, not a new persistent IR node or a runtime enforcement path.
Diagnostics and safety
- New error codes
EFFX2404(unresolved, non-exported, non-group, repeated or multiple group association) andEFFX2405(explicitroot/groupincompatible with associated group; incompatible query marker or ambiguous inference request). A conflicting association or invalid explicit override is never silently reconciled. Other explicit fields may override a default even when their values differ; existing contract/access analyses still validate the result. - A group reference must resolve statically to a single exported group declaration in the same project; only source symbols are read. No runtime import or evaluation, no implicit cross-group defaults, and no TS compiler object in canonical IR. An operation with both a class-level group and another
.ingroup fails withEFFX2404. - Existing verbose declarations and fixtures remain valid; their IR and generated output do not change. A truly dense/verbose pair with the same semantic group metadata has identical
canonical(IR),semanticHash(IR), and generated file contents (modulo output filename/project-relative source refs). Compilation must not silently skip invalid declarations.
Acceptance / falsifiers
- Rewrite the effx rc.116 Profile twin in
packages/frontend-ts/test/fixtures/rc116/src/profile.effx.tsto the dense form while preserving its output seed, exported API keys, response/problem/security behavior, and target TypeScript 6 + Effect4.0.0-rc.116typecheck. Keep a fully spelled-out twin in the isolated fixture project and assert canonical IR, semantic hash and generated files are identical for both forms; compare builder and class-decorator association. - Add a Directory-like twin with distinct GET query/no-query and POST/PATCH request cases, group defaults, operation-specific access/problem codes, and a
<group>.<key>operation name for derived operation ID; assert the same three equalities against verbose declarations. Include an explicit override for every inherited field and tests for incompatible root/group, duplicate association, and invalidquery: true, including diagnostic codes and no emitted files. - Report physical
wc -lbefore/after declaration files (including imports/support constants consistently); separately report the mono-web Profile proposal's before/after physical lines indocs/research/legibility-dense-form.mdafter merge. Show that a real Profile declaration can omit its duplicated ID without changing semantic IR; do not edit mono-web. - Run focused compiler/frontend tests, rc.116 generated TypeScript typecheck,
bun run check; pack the merged effx commit. Freeze this spec before source edits, commit on an isolated worktree branch, then fast-forward merge once green and remove the clean worktree/branch. Do not publish or transfer production ownership.
Operator addendum — punctuation-aware group export names (2026-10-03)
The same effx distribution is a compiler prerequisite for mono-web SocialEvents. packages/compiler/src/generate/emit.ts:133-138 currently strips punctuation without capitalizing the next word; generate/http-contracts.ts:37-60 therefore exports SocialeventsApi/SocialeventsApiHandlers for group: "social-events", not the native SocialEventsApi/SocialEventsApiHandlers. The approved SocialEvents migration identifies this as a compiler defect, not a call-site alias.
- Derive PascalCase group export-name parts at
-,_, and.boundaries (e.g.social-events→SocialEvents) forgroupApiName, its handler export, and other exported HTTP symbols that include the group part. Keep the existing root-name derivation untouched: the already accepted Profile/Directory root IDexternal-native-apimust produce exactly its previous bytes. Do not globally changeidentifier()because it also names operations, RPCs, and import aliases (generate/{client,http,rpc}.ts). Preserve raw group IDs in IR, route identity and generated filenames (social-events-contract.ts). - Within one HTTP root, two distinct raw group IDs that normalize to the same generated export name must fail analysis with
EFFX2406, naming both original root/group identities; never silently choose one. Reusing the same raw group ID under separate roots is legal and must retain its existing output (admin/usersandeffx/usersalready coexist inpackages/frontend-ts/test/fixtures/users). Testsocial-eventsagainstsocial_eventswithin one root, and keep a negative collision control for separate roots. - Falsifier: the generated
social-eventscontract and handler exports areSocialEventsApiandSocialEventsApiHandlers; the existing Profile and Directory emitted artifacts stay byte-identical; overlapping normalized group names produceEFFX2406and no files. Include this in the same integrated gates and commit-bound pack requested above.
Operator addendum — source constructors for tagged access values (2026-10-03)
Access declarations currently write tagged capabilities and concealment literals. The mono-web Profile source needs anti-slop-effect/no-manual-tagged-construction suppressions at those sites (packages/http-api/src/profile.effx.ts at 188f2385 (unpublished)); the accepted dense proposal repeats them in docs/research/legibility-dense-form.md. Move construction into the effx source language, not into generated HTTP behavior.
@effx/runtimeexportsCapability.one(cap),Capability.any(first, ...rest),Capability.all(first, ...rest),Capability.none,Concealment.reveal, andConcealment.notFound(firstStage, ...rest). Retain the existingCapability.makemodel-authority constructor. The new functions return the exactAccessCapabilities/AccessConcealmenttagged values; the zero-argument variants are values, and variadic arrays that require values are nonempty by type.- The TypeScript source frontend statically recognizes constructor calls and values only after resolving their root symbol to
@effx/runtime, including renamed imports. It lowers them to the same JSON annotation arguments as equivalent tagged literals. It does not execute application code or change the schema-defined IR. Reject missing or non-static string arguments as source diagnostics. Keep accepting already-written literal objects without adding another emission format. - Falsifier: constructor and literal builder/decorator declarations produce identical canonical IR, semantic hash, and generated bytes; Profile/Directory rc.116 twin output and target typecheck stay unchanged. Tests cover one/any/all/none and reveal/notFound plus an unrelated application symbol with the same member name. Replace the rc.116 dense declaration spellings with constructors, retain the verbose literal twin, and remove suppression comments from the proposed Profile diff only. Report its new physical line count without editing mono-web.
- Re-run focused and full gates on a clean isolated worktree; fast-forward effx main, pack that commit, remove owned clean worktrees, and send
profile-baselinethe newdist-artifacts/manifest.jsonsource commit and hashes for revendor. No deployment or mono-web write is authorized here.
Spec 0012 — SocialEvents migration, Gate 4
approved design; implementation and acceptance pending. This does not authorize branch integration, deployment, credentials, or production cutover. Mono-web source inspected at clean effx/profile-bas…
Spec 0014 — Content publication migration, Gate 4
approved Content design; three prior repairs and generated cutover pending. This source survey used clean mono-web da1f63f240fb382df60b2768c59f0da5461c70b4 (unpublished) and vendored effx cbea7c7440a…