effx
Specifications

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, concrete HttpApi root. 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 exported Http.group value or @Http.Group class; .in does not execute the group or write an IR edge. Bare operations without an association retain their previous behavior.
  • A group association supplies root and group to that operation's existing http.contract/@Http.Contract, when omitted. Http.Contract stays 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 same HttpGroup node and does not serialize defaults.
  • The inherited fields are merged per field, not by replacing the metadata, problems, or access object wholesale. An explicitly present operation field wins over the same group field, including an empty middleware array. Default problems.registry fills an existing Http.Problems annotation; it does not create a problem declaration without operation-specific codes. Default access fields fill an existing Http.Access annotation; it does not invent capabilities, requirements, scope, concealment timing, or authorize a missing operation. Default metadata annotator fills Http.Contract.metadata, preserving explicit metadata fields. No application function is invoked during collection or compilation.
  • When metadata.operationId is absent and the operation's declared name has 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's Profile.ReadOwnProfile names do not qualify for automatic profile.readOwnProfile IDs, so that twin retains those explicit IDs.
  • Http.Contract.success defaults to the operation's declared success. Http.Contract.query: true on a GET Query means the operation's declared input schema; an explicit query schema keeps its existing meaning. For POST/PATCH Command, omitted payload defaults to input only 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, and AccessContract extension 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) and EFFX2405 (explicit root/group incompatible 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 .in group fails with EFFX2404.
  • 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

  1. Rewrite the effx rc.116 Profile twin in packages/frontend-ts/test/fixtures/rc116/src/profile.effx.ts to the dense form while preserving its output seed, exported API keys, response/problem/security behavior, and target TypeScript 6 + Effect 4.0.0-rc.116 typecheck. 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.
  2. 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 invalid query: true, including diagnostic codes and no emitted files.
  3. Report physical wc -l before/after declaration files (including imports/support constants consistently); separately report the mono-web Profile proposal's before/after physical lines in docs/research/legibility-dense-form.md after merge. Show that a real Profile declaration can omit its duplicated ID without changing semantic IR; do not edit mono-web.
  4. 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) for groupApiName, 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 ID external-native-api must produce exactly its previous bytes. Do not globally change identifier() 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/users and effx/users already coexist in packages/frontend-ts/test/fixtures/users). Test social-events against social_events within one root, and keep a negative collision control for separate roots.
  • Falsifier: the generated social-events contract and handler exports are SocialEventsApi and SocialEventsApiHandlers; the existing Profile and Directory emitted artifacts stay byte-identical; overlapping normalized group names produce EFFX2406 and 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/runtime exports Capability.one(cap), Capability.any(first, ...rest), Capability.all(first, ...rest), Capability.none, Concealment.reveal, and Concealment.notFound(firstStage, ...rest). Retain the existing Capability.make model-authority constructor. The new functions return the exact AccessCapabilities/AccessConcealment tagged 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-baseline the new dist-artifacts/manifest.json source commit and hashes for revendor. No deployment or mono-web write is authorized here.

On this page