Why a classifier failed to produce a model variant is a typed value, not a string. Every builder-step lift returns a sealed Resolved result; the rejection arm carries a Rejection instance whose variant tells the validator and downstream consumers (LSP fix-its, watch-mode formatters) what kind of failure happened and what data accompanies it. Switches across success and failure modes are exhaustive at compile time; relaxing a producer surfaces as a missing-arm error in every consumer, not a runtime surprise.
This page is the chapter narrative for that contract. Reference detail (the per-leaf record components, the per-resolver Resolved arms) lives on the source: javadoc on Rejection and on each *DirectiveResolver.Resolved carries the per-permit data shape.
Sealed Resolved across the resolver siblings
The directive resolvers share the same shape. Each entry point returns a sealed Resolved whose arms split success from failure; the caller’s switch is exhaustive across both. The arm names vary by domain (the @lookupKey resolver has only Ok / Rejected; the @service resolver fans Ok into typed sub-arms because the downstream emitters need different model types per success kind), but the contract is uniform: rejection is a typed sibling of success, not a return-string-or-null.
Where a rejection comes from is at the boundary of one builder step; how it surfaces to the user is at the boundary of the validator. In between, the typed value rides on the result without losing structure: a Rejection.AuthorError.UnknownName arm carries its attempt, its candidates, and a typed AttemptKind tag identifying the lookup space, all the way from the resolver that built it to the LSP fix-it that reads them off the wire.
The validator does not parse prose. It switches on the Rejection variant, formats the human-readable line with message(), classifies it as AUTHOR_ERROR or INVALID_SCHEMA or DEFERRED for the diagnostic surface, and emits. The same typed value drives every consumer: the validator’s log, the LSP fix-it, an editor’s hover-card, a CI annotation. None of them re-parse text.
Rejection taxonomy
The Rejection sealed hierarchy splits along who can fix this: the schema author, the runtime author who hasn’t shipped support yet, or the schema as a whole.
Rejection
├─ AuthorError ← the schema author can correct this
│ ├─ UnknownName ← name that didn't resolve against a closed set
│ ├─ Structural ← rule violation carrying prose only
│ ├─ AccessorMismatch ← record-backed parent's class doesn't expose the accessor
│ ├─ RecordBindingMultiProducer ← multiple producers reach one SDL type with disagreeing classes
│ ├─ TypeConflict ← cross-site contextArgument type-agreement disagreement
│ ├─ MultiProducerDomainTypeDisagreement ← producers reach one SDL Object type with disagreeing env.getSource() Java domain types
│ └─ SortEnumMissingOrder ← an @orderBy sort enum value declares neither @order nor @index
├─ InvalidSchema ← the schema can't accept this combination at all
│ ├─ DirectiveConflict ← two directives co-occur in a rejected combination
│ ├─ CaseFoldCollision ← two type names are equal under case-folding
│ └─ Structural ← rule violation carrying prose only
└─ Deferred ← classifies cleanly but the generator hasn't emitted support yet
AuthorError.UnknownName is the data-rich arm: it carries the attempt the author wrote, the candidates the catalog had at this site (column names, table names, FK names, service-method names, …), and an AttemptKind tag for downstream tooling. The kind is a typed tag rather than an arm split because every space carries the same (attempt, candidates) shape; an LSP fix-it that wants to offer "rename to one of these" reads attempt and candidates directly off the rejection without parsing the validator’s prose.
AuthorError.Structural and InvalidSchema.Structural are the prose-majority arms. They carry one String reason and exist because most rule violations are not name-against-closed-set lookups; they’re "this combination cannot work, period" or "this rule was broken." Two arms instead of one because the diagnostic surface treats AUTHOR_ERROR and INVALID_SCHEMA differently: the first prompts the author to edit; the second prompts the author to drop or replace a directive entirely.
AuthorError.AccessorMismatch is the third AuthorError arm because the record-backed-parent accessor-resolution surface (resolving an SDL output field’s accessor against the parent’s reflection-derived backing class) produces uniform diagnostics with a uniform fix shape (@field(name: "…")); a single arm with the hint baked into message() lets the resolver hand the validator the same typed value every site produces.
AuthorError.RecordBindingMultiProducer is the fourth AuthorError arm, surfacing R96’s producer-agreement check: when two or more producers (root producers or parent-accessor chains) reach the same SDL type with disagreeing reflected backing classes, the validator halts with a typed payload naming the SDL type and every disagreeing ProducerBinding site. The arm sits under AuthorError because the fix is author-correctable (align the producers on a single backing class via the same rename / retype / split toolbox the rest of AuthorError supports); the typed List<ProducerBinding> payload follows the sub-data pattern that UnknownName established.
AuthorError.TypeConflict is the fifth AuthorError arm, surfacing R190’s cross-site contextArgument type-agreement check: when two or more directive sites (@service, @tableMethod, @condition) reference the same contextArgument name with mutually-incompatible Java types, the schema-driven Graphitron.newExecutionInput(…) factory cannot produce a single typed parameter slot. The arm carries the contextArgument name plus the typed List<ConflictSite> (each site’s MethodRef coordinate and the TypeName that site declared); message() renders one indented line per site for the validator’s prose surface, while LSP fix-its read the typed sites field directly. Like RecordBindingMultiProducer, the arm sits under AuthorError because the fix is author-correctable (align every site on a single Java type).
AuthorError.MultiProducerDomainTypeDisagreement is the sixth AuthorError arm, surfacing R204’s uniform-domain-return-type check: when two or more OutputField producers reach the same SDL Object return type with disagreeing DomainReturnType sealed arms (Record(table) vs TableRecord(class) vs Plain(class)), the producers put structurally different Java values at env.getSource() for the SDL type’s child datafetchers. The generator commits to one source-Java-type per child-field coord at emit time and does not branch on runtime source type, so a runtime disagreement would feed a datafetcher generated against the other producer’s record shape. The arm carries the SDL type name plus a typed List<Participant> (each producer’s (parentTypeName, fieldName, DomainReturnType)); message() renders one indented line per participant. The today-exercised case is the carrier-payload conflict (DML @mutation returning Record(table) vs @service-on-Mutation returning a typed TableRecord for the same payload SDL Object); the constraint is general across any current or future producer permit.
AuthorError.SortEnumMissingOrder is the seventh AuthorError arm, surfacing R453’s per-value ordering-completeness check: when a sort enum bound to an @orderBy argument carries one or more values declaring neither @order nor @index, OrderByResolver would silently skip each such value and produce no NamedOrder, so a request selecting only unannotated values generates an empty ORDER BY, and on a paginated connection keyset pagination then slices a nondeterministic set (rows duplicate or vanish across pages). The rejection fires at the parse boundary in OrderByResolver.resolveOrderByArgSpec, making the empty-ORDER-BY state unrepresentable in the model. The arm carries the sort enum’s type name plus the typed List<String> of unannotated value names, accumulated across the whole enum in declaration order rather than fail-fast so the author sees every missing value at once; message() renders one indented line per value. Like RecordBindingMultiProducer, the arm sits under AuthorError because the fix is author-correctable (annotate each value with @order), and the typed value list rides so LSP fix-its read the missing set without parsing prose. R181 (validate-order-directive-args) covers the sibling gap of an empty @order or @order+@index coexistence; the arm is named so R181 can fold its rejections into a shared order-directive family later.
ServiceMethodCallError is R238’s sub-seal of AuthorError, scoped to the @service-binding failures of the ServiceMethodCallWalker / ServiceCatalog.reflectServiceMethod path that projects @service directive sites onto the ServiceMethodCall carrier on root sync service permits (QueryServiceTableField, QueryServiceRecordField, MutationServiceTableField, MutationServiceRecordField). Each typed arm carries the structural data its diagnostic message needs and a stable lspCode() under the graphitron.service-method-call. namespace; downstream tooling switches on the arm rather than parsing prose. R238 shipped only the two arms its translator-walker produced; R256 (service-walker-substrate-absorption) re-landed the service-binding failures that ServiceCatalog previously produced as AuthorError.Structural prose as typed arms, and partitioned the reflection-intrinsic failures (class-load, return-type, parameter-names, overload) into the sibling ReflectionError sub-seal (below) so a @tableMethod / @externalField failure of the same shape is not forced through a @service-named arm. Subsequent walker slices (condition, tableMethod, externalField) each add their own sibling sub-seal alongside this one, keeping the dimensional pivot one-row-per-walker rather than piling typed arms under a single flat Structural.
ServiceMethodCallError.MultipleDslContextSlots fires when a single round (constructor or method) carries more than one DSLContext parameter slot; carries className and a Round enum identifying which round violated the invariant (R256 makes the constructor round reachable, since the holder may now bind a multi-parameter constructor). ServiceMethodCallError.ParameterUnbindable fires when a Java parameter slot does not match any GraphQL argument, declared context key, or DSLContext slot; carries paramName, the available argument names, and a Levenshtein-ranked suggestion. ServiceMethodCallError.InstanceHolderUnconstructible (R256) fires when an instance @service method’s enclosing class cannot be used as a holder, either because it is abstract / an interface or because it exposes no public constructor whose parameters are each a DSLContext or a declared context argument; carries the class/method coordinate, the class’s simple name (for the fix hint), and a HolderProblem discriminant. ServiceMethodCallError.ArgumentParameterMismatch (R256) fires when a Java parameter matches no GraphQL argument or context key; carries the parameter and method names, the available argument names and context keys, and a pre-rendered rename / argMapping / dot-path suggestion (subsumes the prose the legacy Structural arm produced at ServiceCatalog). ServiceMethodCallError.DtoSourcesUnsupported (R256) fires when a @service SOURCES parameter is a List<DTO> / Set<DTO> whose element is not backed by a jOOQ TableRecord; carries the parameter and method names plus the @sourceRow hint. ServiceMethodCallError.UnrecognizedSourcesType (R256) fires when a parameter looks like a SOURCES batch shape but its element type is none the classifier recognises; carries the parameter and method names plus the unrecognised Java type name.
ReflectionError is R256’s sub-seal of AuthorError for the reflection-intrinsic failures shared across ServiceCatalog’s three reflect helpers (`reflectServiceMethod, reflectTableMethod, reflectExternalField). A class that cannot be loaded, a method whose return type does not match the field’s declared type, a class compiled without -parameters, or an overloaded method name are properties of the reflected Java method regardless of which directive references it, so these arms live under the graphitron.reflect. namespace rather than being forced through ServiceMethodCallError. Like its siblings it carries one stable lspCode() per arm. ReflectionError.ClassNotLoaded fires when the referenced class cannot be loaded through the codegen classloader; carries the binary class name. ReflectionError.ReturnTypeMismatch fires when the reflected return type does not equal the type the field’s declared return requires; carries the class/method coordinate, the expected vs. actual type in their message-surfaced simple form, and a ReturnContext discriminant selecting the @service vs. @tableMethod prose. ReflectionError.ParameterNamesMissing fires when the class was compiled without -parameters so a parameter that needs its name to bind has none; carries the class/method coordinate. ReflectionError.AmbiguousMethod fires when more than one declared method shares the referenced name (the reflect helpers previously took the first JVM-declaration-order match silently); carries the class/method coordinate and every same-name declaration’s parameter arity.
UpdateRowsError is R246’s sub-seal of AuthorError, scoped to the UpdateRowsWalker that projects an @mutation(typeName: UPDATE) field’s @table input onto the UpdateRows carrier on MutationUpdateTableField. Each typed arm carries the structural data its diagnostic message needs and a stable lspCode() under the graphitron.update-rows. namespace; downstream tooling switches on the arm rather than parsing prose. Like R238’s ServiceMethodCallError, it is a sibling sub-seal of AuthorError rather than a set of arms under the flat Structural, keeping the dimensional pivot one-row-per-walker. The arms subsume the per-input-field and PK-coverage prose the legacy MutationInputResolver produced for the UPDATE path.
UpdateRowsError.NoUniqueKeyCoverage fires when no primary key and no unique key has its column set covered by the input’s columns; carries the table name, the input-covered columns, and every candidate key the walker considered (a table with no keys at all is the degenerate empty-candidate case). UpdateRowsError.NoSetFields fires when every input field contributes to the matched key, leaving an empty SET; carries the table name and the matched key. UpdateRowsError.MixedCarrierKeyMembership fires when a single cross-table FK composite-reference field’s lifted columns straddle the matched key (some are key members, some are not); carries the field name and the in-key / outside-key column split. A cross-table FK reference partitions by key membership because its lifted column can legitimately be the row’s own identity, so a straddle is unexpressible. A self-FK @nodeId @reference no longer reaches this arm (R354): its lifted columns are a pointer to a sibling row, never identity, so they route wholly to the SET partition (a shared key column then appears in both the WHERE and SET, reconciled by an emit-side cross-partition value-agreement check). UpdateRowsError.UnsupportedInputFieldShape fires for nesting fields, unbound fields without an override condition, or any non-admitted carrier; carries the field name, the classifier shape, and a reason. UpdateRowsError.OverrideConditionNotSupported fires when an input field carries @condition(override: true) (R215 admits the shape at classify time, but its emit-side never landed, so the filter would silently never run); carries the field name and the directive’s source location. UpdateRowsError.PlainColumnCollision fires when two or more plain @field writers (no @nodeId decode among them) resolve to one SET column, which the single-row Map.put would silently last-write-wins and the bulk VALUES-join would crash on a duplicate derived column; carries the two field names and the column (R322, the UPDATE mirror of the INSERT-path and @service field-vs-field reject). An overlap involving a decode is admitted and reconciled by R322’s runtime value-agreement check rather than rejected here.
DeleteRowsError is R266’s sub-seal of AuthorError, scoped to the DeleteRowsWalker that projects an @mutation(typeName: DELETE) field’s @table input onto the DeleteRows carrier on MutationDeleteTableField, MutationDeletePayloadField, and MutationBulkDeletePayloadField. Each typed arm carries the structural data its diagnostic message needs and a stable lspCode() under the graphitron.delete-rows. namespace; downstream tooling switches on the arm rather than parsing prose. Like R246’s UpdateRowsError, it is a sibling sub-seal of AuthorError rather than a set of arms under the flat Structural, keeping the dimensional pivot one-row-per-walker. The arm set is UpdateRowsError’s minus the two arms DELETE’s shape makes meaningless: there is no `NoSetFields (DELETE has no SET clause to be empty) and no MixedCarrierKeyMembership (DELETE has no SET boundary for a composite carrier to straddle, since every admitted column is a WHERE filter). Carving DELETE off MutationInputResolver.resolveInput retired the last live @value consumer and the directive itself (absorbing R188).
DeleteRowsError.NoUniqueKeyCoverage fires when no primary key and no unique key has its column set covered by the input’s columns and the mutation did not opt into multiRow: true; carries the table name, the input-covered columns, and every candidate key the walker considered (a table with no keys at all is the degenerate empty-candidate case, which the message points at multiRow: true). It subsumes R188’s table-has-no-pk rejection. DeleteRowsError.UnsupportedInputFieldShape fires for nesting fields, unbound fields without an override condition, or any non-admitted carrier; carries the field name, the classifier shape, and a reason. DeleteRowsError.OverrideConditionNotSupported fires when an input field carries @condition(override: true) (R215 admits the shape at classify time, but its emit-side never landed, so the filter would silently never run); carries the field name and the directive’s source location.
MutationTableArgError is R457’s sub-seal of AuthorError, scoped to the @mutation(table:) argument that names a @mutation(typeName: DELETE) field’s write target on the consuming field (the field-relative alternative to the deprecated @table on the input type). Like its sibling walker sub-seals it carries a stable lspCode() under the graphitron.mutation-table-arg. namespace rather than collapsing into the flat Structural. MutationTableArgError.UnsupportedVerb fires when table: is supplied on a verb that does not read it: it is wired for DELETE only (INSERT/UPDATE/UPSERT derive their write target by other means), and silently ignoring an author-written directive argument is the green-build-wrong-intent failure the axioms forbid, so the arm rejects loudly, carrying the offending verb and the set of verbs that do accept table:.
ErrorChannelWalkerError is R244’s sub-seal of AuthorError, scoped to the error-channel domain: the ErrorChannelWalker that resolves an outcome type’s errors-field channel onto ErrorChannel.Mapped, plus the OutcomeType classification that produces the walker’s input. Each typed arm carries the structural data its diagnostic message needs and a stable lspCode() under the graphitron.error-channel. namespace; downstream tooling switches on the arm rather than parsing prose. Like R238’s ServiceMethodCallError, it is a sibling sub-seal of AuthorError rather than a set of arms under the flat Structural, keeping the dimensional pivot one-row-per-walker. Three arms are raised by the OutcomeType classification and the rest by walk(); they share one family because they share one SDL surface (the outcome type and its errors field) and one LSP namespace.
ErrorChannelWalkerError.MultipleErrorsFields fires when a type carries more than one errors field; the binary Outcome witness has one error slot, so a type with two errors fields has no well-defined fork. Carries the outcome type name and the offending errors-field names. ErrorChannelWalkerError.NonNullableSuccessProjectionField fires when a success-projection (data) field is non-null; on the error arm that field resolves null and would raise NonNullableFieldWasNullError, bubbling the null up and dropping the sibling errors field, so success-projection fields must be nullable. Carries the outcome type name and the field name. ErrorChannelWalkerError.NonNullableErrorsField (R275) is the mirror: it fires when the errors field itself is non-null ([X!]!); on the success arm there are no errors and the field resolves null, which would raise NonNullableFieldWasNullError and drop the sibling data field, so errors fields must be nullable. Carries the outcome type name and the field name. ErrorChannelWalkerError.ChannelRuleViolation fires on a channel-level handler-rule violation (rule 7: no two VALIDATION handlers in one channel; rule 8: no duplicate match-criteria across the flattened handler list); carries the outcome type name, the errors-field name, the rule number, and a detail string, with lspCode() specialising per rule. ErrorChannelWalkerError.HandlerSourceAccessorMissing fires when an @error type’s handler source class exposes no PropertyDataFetcher-visible accessor for one of the @error type’s declared SDL fields (path and message exempt); carries the outcome type name, the @error type name, the handler class name, the missing field name, and the available accessors.
WireCoercionError is R261’s sub-seal of AuthorError, scoped to the wire-coercion failures a scalar/enum SDL leaf hits when its consumer-declared Java type does not match what graphql-java delivers on the wire. Before R261 every arg-classification site fell through to CallSiteExtraction.Direct and emitted a raw (DeclaredType) wireValue cast that compiled cleanly and ClassCastException`d (or, for enums, `IllegalArgumentException`d) on the first request; graphql-java delivers `ID and enum values as String, Int as Integer, Float as Double, input-objects as Map, so a declared cast target of a jOOQ record, a numeric PK type, a domain class, or a width-mismatched numeric is a guaranteed runtime crash invisible at build time. The classifier now confirms the coercion output is assignable to the declared type before emitting Direct — the Direct fall-through becomes the narrow arm the predicate confirms is wire-pass-through — and a mismatch surfaces here instead. Each typed arm carries the structural data its diagnostic message needs and a stable lspCode() under the graphitron.wire-coercion. namespace; downstream tooling switches on the arm rather than parsing prose. The judgment lives at the classifier (a new WireCoercionResolver predicate consuming ScalarTypeResolver.coercionOutputType), not on ScalarTypeResolver, which stays a pure name↔type mapping.
WireCoercionError.Assignability fires when the graphql-java coercion output for a scalar SDL leaf does not equal the declared Java type (sites A-D of the audit: @service input-bean scalar fields, @service scalar args, and the non-service @condition / @externalField sites in a later slice); carries the SDL leaf type as written, the fully-qualified coercion-output class graphql-java delivers, the fully-qualified declared Java type the cast targets, and a site string. WireCoercionError.EnumConstantDivergence fires when the declared type is the enum and assignment succeeds but an SDL enum value name has no matching Java constant, so Enum.valueOf((String) …) would throw (site E); a constant-name-set membership check on a distinct axis from Assignability, populated from the single EnumMappingResolver parity home shared with the column/arg enum path. Carries the Java enum class name, the divergent SDL value names, the full Java constant set, and a site string.
ServiceCarrierShapeError is R308’s sub-seal of AuthorError, scoped to the @service list-payload carrier shape verdict (BuildContext.ServiceCarrierShape) computed at the @service payload seat over the triple (carrier field wrapper, @service producer return shape, payload data-field wrapper). Like its sibling walker sub-seals it carries a stable lspCode() under the graphitron.service-carrier-shape. namespace rather than collapsing into the flat Structural. The verdict only ever rejects a list-returning carrier (@service …: [Payload]); a single carrier is always coherent and keeps its existing classification, so a coherent shape’s model and emit are byte-for-byte unchanged. Each arm carries the disagreeing arrival axes as typed SourceKey.Cardinality values plus the offending field/payload coordinate, not a reason string composed at the detection site. ServiceCarrierShapeError.ProducerArrivalMismatch fires when a list carrier’s @service producer returns a single value (arrival one) instead of a collection (arrival many): graphql-java iterates the producer’s return into the [Payload] list, so a single value yields a non-iterable source and list coercion fails at runtime. This one arm subsumes what the uncoordinated reads used to surface three ways — the a1 silent admit (bare-record producer into a single @table data field) and the two misleading record-handoff rejections (class-backed carrier, and carrier with a @table data field) — because all three are one fact: carrier arrival disagrees with producer arrival. ServiceCarrierShapeError.DataFieldArrivalConflict fires when a list carrier’s data field is itself a list produced by a flat collection: the producer’s flat list is consumed element-by-element to build the [Payload] carrier, so a single value reaches each payload and cannot also populate a list-valued data field, a per-request ClassCastException the acceptance axiom forbids. Both admitting element kinds crash this way: a @table-element data field (List<Record> producer) on the per-element key-extraction cast to Iterable, and a class-backed record-composite (RecordElement) data field (List<Composite> producer) on the source-passthrough cast of one composite to List<Composite>; filling the shape would need a List<List<…>> producer the model has no shape for. It is a distinct axis pairing from ProducerArrivalMismatch (data-field arrival vs. a flat producer list that cannot re-nest per carrier element), so it earns its own arm; only an ID-element data field re-nests per element and stays coherent.
InvalidSchema.DirectiveConflict carries the bare directive names (no leading @) plus the prose. The names ride as typed data so an LSP fix-it can offer "remove this directive" without scraping the prose for which one to remove.
InvalidSchema.CaseFoldCollision is the case-fold-uniqueness arm: two or more type-name stems collapse to the same identifier on case-insensitive filesystems (APFS, NTFS), which would clobber the emitted Java files. The variant carries the full case-equivalent group as a typed List<String> plus a CaseFoldCollision.Origin enum (SDL, SYNTH_CONNECTION, SYNTH_EDGE, SYNTH_PAGE_INFO) identifying which classifier arm each demoted member came from; message() switches on origin to specialise the actionable fix hint (synthesised arms point at @asConnection(connectionName: …); SDL arms suggest a rename). Carrying the group as structured data lets an LSP fix-it offer "rename one of these" with the candidate list ready, without scraping prose.
Deferred classifies cleanly but the generator hasn’t shipped emit support for the variant yet. The arm carries a summary plus a planSlug (the roadmap file basename, no extension) plus a StubKey. The stub key is itself a sealed sub-type: a VariantClass arm names the stubbed variant class (or carries null for inline-defer sites whose rejection names a feature shape); an EmitBlock arm names a typed enum value for "this shape can’t emit yet" sites inside the emitters. The validator projects every Deferred through the same renderer; the typed key lets the LSP offer "open the roadmap item" instead of parsing the path back out.
The classifier-shaped emitter-assumption principle and the validator-mirrors-classifier rule both ride this contract: the validator switches on the same dispatch sets the generator does, so an unsupported classification surfaces as a typed Deferred at validate time, not as an UnsupportedOperationException at runtime. Validate is a typed-rejection projection of classify.
BuildContext.candidateHint: Levenshtein-ranked suggestions
When a name doesn’t resolve, the rejection carries the closed set of candidates the catalog had at that site; the user-visible hint sorts them by edit distance from the attempt, top five.
String hint = candidateHint(attempt, candidates);
// "; did you mean: candidate1, candidate2, candidate3"
The contract has consolidated onto two construction sites: BuildContext.candidateHint(attempt, candidates) for callers building rejection messages directly, and Rejection.unknownName(…) (and its kind-specific factories unknownColumn, unknownTable, unknownForeignKey, …) for callers producing the rejection through the typed sealed-result path. Both compute the same hint; the typed-result path additionally rides the attempt and candidates as structured data on the rejection so downstream tooling can offer a fix-it without parsing prose.
When adding a new existence check to the validator or builder, follow the same pattern: pass the relevant candidate list from JooqCatalog (or whatever closed set the lookup ranges over) to candidateHint, or produce the rejection through Rejection.unknownName(…) so the candidate list rides on the typed result.
The diagnostic surface that consumers see, what each rejection class renders as, what severity it gets, what the build’s log line looks like, is documented at the diagnostics glossary; that page is the user-facing entry point for "I saw this message, what does it mean."
Drift protection
The chapter prose above enumerates Rejection’s permits: `AuthorError.UnknownName, AuthorError.Structural, AuthorError.AccessorMismatch, AuthorError.RecordBindingMultiProducer, AuthorError.TypeConflict, AuthorError.MultiProducerDomainTypeDisagreement, AuthorError.SortEnumMissingOrder, ServiceMethodCallError.MultipleDslContextSlots, ServiceMethodCallError.ParameterUnbindable, ServiceMethodCallError.InstanceHolderUnconstructible, ServiceMethodCallError.ArgumentParameterMismatch, ServiceMethodCallError.DtoSourcesUnsupported, ServiceMethodCallError.UnrecognizedSourcesType, ReflectionError.ClassNotLoaded, ReflectionError.ReturnTypeMismatch, ReflectionError.ParameterNamesMissing, ReflectionError.AmbiguousMethod, UpdateRowsError.NoUniqueKeyCoverage, UpdateRowsError.NoSetFields, UpdateRowsError.MixedCarrierKeyMembership, UpdateRowsError.UnsupportedInputFieldShape, UpdateRowsError.OverrideConditionNotSupported, UpdateRowsError.PlainColumnCollision, DeleteRowsError.NoUniqueKeyCoverage, DeleteRowsError.UnsupportedInputFieldShape, DeleteRowsError.OverrideConditionNotSupported, MutationTableArgError.UnsupportedVerb, ErrorChannelWalkerError.MultipleErrorsFields, ErrorChannelWalkerError.NonNullableSuccessProjectionField, ErrorChannelWalkerError.NonNullableErrorsField, ErrorChannelWalkerError.ChannelRuleViolation, ErrorChannelWalkerError.HandlerSourceAccessorMissing, WireCoercionError.Assignability, WireCoercionError.EnumConstantDivergence, ServiceCarrierShapeError.ProducerArrivalMismatch, ServiceCarrierShapeError.DataFieldArrivalConflict, InvalidSchema.DirectiveConflict, InvalidSchema.CaseFoldCollision, InvalidSchema.Structural, and Deferred. A new permit on the sealed hierarchy must land with a corresponding mention in this page; otherwise the prose silently goes stale. SealedHierarchyDocCoverageTest walks Rejection.permits() transitively and asserts each permit name appears in typed-rejection.adoc: bidirectional, tied to a closed set the compiler already exhaustivity-checks. A new permit added without a paragraph here fails the test; a permit removed without removing its mention fails too.
The sealed-Resolved pattern across the sibling resolvers is described above shape-only; per-resolver arm enumerations (e.g. LookupKeyDirectiveResolver.Resolved.{Ok, Rejected}, ServiceDirectiveResolver.Resolved.{Success.{TableBound | Result | Scalar} | ErrorsLifted | Rejected}) live as javadoc on each *DirectiveResolver.Resolved declaration. There is no single Resolved parent class to walk, and the chapter does not pin per-resolver permits.
Sealed hierarchy diagram
classDiagram
class Rejection {
<<sealed interface>>
+String message()
+Rejection prefixedWith(String)
}
class AuthorError {
<<sealed interface>>
}
class InvalidSchema {
<<sealed interface>>
}
class UnknownName {
+String summary
+AttemptKind attemptKind
+String attempt
+List~String~ candidates
}
class Structural_AE["AuthorError.Structural"] {
+String reason
}
class AccessorMismatch {
+String reason
}
class RecordBindingMultiProducer {
+String sdlTypeName
+List~ProducerBinding~ bindings
}
class DirectiveConflict {
+List~String~ directives
+String reason
}
class CaseFoldCollision {
+List~String~ group
+Origin origin
}
class Structural_IS["InvalidSchema.Structural"] {
+String reason
}
class Deferred {
+String summary
+String planSlug
+StubKey stubKey
}
Rejection <|-- AuthorError
Rejection <|-- InvalidSchema
Rejection <|-- Deferred
AuthorError <|-- UnknownName
AuthorError <|-- Structural_AE
AuthorError <|-- AccessorMismatch
AuthorError <|-- RecordBindingMultiProducer
InvalidSchema <|-- DirectiveConflict
InvalidSchema <|-- CaseFoldCollision
InvalidSchema <|-- Structural_IS
class LookupKeyResolved["LookupKeyDirectiveResolver.Resolved"] {
<<sealed interface>>
}
class LookupKeyOk["Resolved.Ok"] {
+ReturnTypeRef.TableBoundReturnType returnType
}
class LookupKeyRejected["Resolved.Rejected"] {
+Rejection rejection
}
LookupKeyResolved <|-- LookupKeyOk
LookupKeyResolved <|-- LookupKeyRejected
LookupKeyRejected --> Rejection : carries
The overlay shows LookupKeyDirectiveResolver.Resolved as one worked example of how a resolver’s typed-result wraps a Rejection. The other twelve resolvers follow the same shape; check each *DirectiveResolver.Resolved for its specific arm set.
See also:
-
Builder-step results are sealed: the principle this page is the canonical source for.
-
Rejections: validator mirrors classifier invariants: the validator-side rule that consumes typed rejections at the build’s diagnostic surface.
-
Diagnostics glossary: the user-facing reference for what each rejection-derived diagnostic message means.