ID

R66

Status

Backlog

Bucket

architecture

Priority

6

Theme

diagnostics

Widen string-carrier intermediates onto Rejection (R58 follow-up)

R58 lifted the direct candidate-hint producers onto typed Rejection.AuthorError.UnknownName factories. Five intermediate carriers still flatten the typed shape into prose before it reaches a Rejection consumer, blocking five candidate-hint producers from reaching the typed surface their factories (unknownForeignKey, unknownTypeName, unknownEnumConstant, unknownNodeIdKeyColumn, unknownColumn) already exist for. R58 Phase D shipped the factories; this plan adds the carrier widenings so the typed values reach consumers.

R58 is Done (see the changelog), so its factories are settled surface; the original reversion hedge is struck. Anchor note (2026-07-13): the carrier audit below is re-verified as still accurate at the symbol level, but BuildContext.java has roughly tripled since filing, so the bare line numbers in items 1-5 have drifted; re-derive them from the named symbols at pickup. Current definition anchors: record ParsedPath(..., String errorMessage, ...) at BuildContext.java:1037 with the sibling record ChainSegment(..., String errorMessage) at :1403 (a sixth string carrier of the same shape, added after filing; fold it into Phase A), record Unresolved(..., String reason) at InputFieldResolution.java:20, record UnboundArg(...) at ArgumentRef.java:218, and record Mismatch(Rejection rejection) at EnumMappingResolver.java:76.

Carrier audit

  1. BuildContext.ParsedPath.errorMessage: String ; populated by parsePathElement’s `List<String> errors accumulator (BuildContext.java:388); surfaces at every parsePath().hasError() consumer in FieldBuilder, BuildContext, NodeIdLeafResolver, TypeBuilder. Widening to a single rejection: Rejection unlocks the producer at BuildContext.java:583-584 (FK SQL name miss → unknownForeignKey).

  2. InputFieldResolution.Unresolved.reason: String ; built by BuildContext.classifyInputField and consumed by BuildContext (typename inference, nested field aggregation) and InputFieldResolver. Widening to rejection: Rejection unlocks BuildContext.java:875-878 (typename in @nodeIdunknownTypeName) and BuildContext.java:1015-1016 (column-in-path-leg → unknownColumn).

  3. ArgumentRef.ScalarArg.UnboundArg.reason: String ; single producer at FieldBuilder.java:849-852 (column on filter table). Called out as out of scope in R58 Phase D. Widening to rejection: Rejection unlocks the unknownColumn migration.

  4. EnumMappingResolver.EnumValidation.Mismatch(Rejection rejection) ; the carrier already holds a Rejection, but the producer at EnumMappingResolver.java:159-162 joins per-constant misses into a single prose blob via String.join("; ", mismatches) and wraps in Rejection.structural(...). Surfacing each miss as unknownEnumConstant with its own candidate set requires widening the component to List<Rejection> rejections. The "GraphQL type is not an enum" branch at :145-146 is single-rejection; under the widened shape it becomes List.of(Rejection.structural(...)).

  5. TypeBuilder aggregations ; same multi-miss pattern as (4):

    • TypeBuilder.java:397-409 (keyColumnErrors: List<String>) joins to one Rejection.structural(...). Widen the accumulator to List<Rejection> and migrate per miss to unknownNodeIdKeyColumn.

    • TypeBuilder.java:686-708 (failures: List<InputFieldResolution.Unresolved>) joins Unresolved.reason strings. After Phase A2 each Unresolved carries a Rejection; this site fans those out per miss onto unknownColumn.

Phases

  • Phase A ; single-Rejection carrier widenings (items 1-3). Independent; each independently shippable.

  • A1 (item 1): widen ParsedPath.errorMessage: Stringrejection: Rejection. Update parsePathElement’s accumulator from `List<String> to List<Rejection> and join into a single Rejection at parsePath exit (one rejection per path-parse failure preserves the current per-call-site error semantic). Migrate the FK-name producer at :583-584 onto unknownForeignKey. Other producers in parsePathElement construct Rejection.structural(reason) to preserve current prose at the byte boundary.

  • A2 (item 2): widen InputFieldResolution.Unresolved.reason: Stringrejection: Rejection. Migrate :875-878 onto unknownTypeName and :1015-1016 onto unknownColumn. Other Unresolved construction sites wrap in Rejection.structural(reason). Required before B2’s second site (TypeBuilder.java:686-708) so Unresolved already carries the typed shape its consumer can fan out. Co-design note (2026-07-15): two other Backlog items grow this same Unresolved record and its fold path, and neither cross-referenced this one before now. R209 (field-registry-typed-rejection-trace) fork (a) is verbatim this A2 change (widen Unresolved to carry a Rejection); if A2 ships first, R209 collapses to a consumer update. R213 (input-field-rejection-attribution) adds a SourceLocation to the same record and reshapes foldRejections to carry List<LocatedRejection>. A located typed rejection is the natural union of A2 and R213, so whichever lands first should carry the combined (SourceLocation, Rejection) shape rather than force the second to revert it. Sequence the three knowingly.

  • A3 (item 3): widen ArgumentRef.ScalarArg.UnboundArg.reason: Stringrejection: Rejection. Migrate :849-852 onto unknownColumn.

  • Phase B ; multi-miss carrier widenings (items 4-5). Decided shape: List<Rejection> at the aggregating carrier; consumer fans out to N UnclassifiedField/UnclassifiedType (and therefore N ValidationError`s). Rationale: keeps R58’s single-rejection-per-Unclassified*` / ValidationError invariant intact; each typed miss surfaces as its own validator entry, which is more useful for IDE/LSP consumers than a joined prose blob. Considered alternatives: a Rejection.Multi(List<Rejection>) seal arm (rejected: complicates the seal and forces every Rejection consumer to handle nesting); widening ValidationError itself (rejected: R58 Phase I just locked the single-rejection shape).

  • B1 (item 4): widen EnumValidation.Mismatch(Rejection)Mismatch(List<Rejection>). Producer at :159-162 emits one unknownEnumConstant per missing constant. Branch at :145-146 becomes a singleton list. Consumer in FieldBuilder (column-bound enum filter classification) fans out to one UnclassifiedField per typed rejection.

  • B2 (item 5): same widening on TypeBuilder.keyColumnErrors and the failures-list rendering at :686-708. Producers emit one UnclassifiedType per typed miss instead of one type-level Rejection.structural(joined). Author-side UX shift: a multi-miss @node type now produces N validator entries (one per unresolved key column) instead of one entry with semicolon-joined prose. Documented in the changelog as an intentional trade for typed candidate hints.

Tests

  • Pipeline (extending R58TypedRejectionPipelineTest):

  • One case per Phase A unlock point (unknownForeignKey via path-parse, unknownTypeName via @nodeId, unknownColumn via path leg, unknownColumn via filter-table column).

  • Phase B fan-out: a 3-miss enum mapping produces 3 ValidationError`s, each carrying `Rejection.AuthorError.UnknownName with attemptKind = ENUM_CONSTANT and its own candidate list.

  • Phase B fan-out: a 2-miss @node(keyColumns:) produces 2 ValidationError`s, each carrying `unknownNodeIdKeyColumn.

  • Model (extending RejectionRenderingTest): EnumValidation.Mismatch.message() joins the list via ; so the byte-stable log surface validator consumers depend on is preserved when a caller chooses to render the multi-miss carrier as a single line (e.g. log emission).

Out of scope

  • Threading nested rejection chains as a typed Rejection.NestedReject arm. Same deferral as R58 ; not on any in-flight consumer’s path.

  • LSP fix-its consuming the typed UnknownName.candidates lists this plan unlocks. R18 wires the consumer; this plan supplies the typed shape.

  • Lifting ArgumentRef.UnclassifiedArg.reason onto Rejection. Separate axis (argument classification, not field classification); same shape-pattern but different producer set.

  • BuildWarning.message. Single producer site; premature.

  • Renaming RejectionKind. Same deferral as R58.

Fact-base note (2026-08-06)

Split the phases by survivorship before spending effort: several carriers here (Unresolved, UnboundArg) sit on the surface the strangler frame retires, where the typed payload becomes columns on a violation row, so widening them is work against a dissolving surface. Widen only the carriers whose consumers survive the migration. Context and the whole-board picture: roadmap/audits/2026-08-06-fact-base-impact-sweep.md.