ID

R431

Status

Backlog

Bucket

structural

Priority

4

Theme

classification-model

Created

2026-07-04

Updated

2026-07-08

Decompose SourceKey onto the model’s facts

SourceKey is (target, columns, path, wrap, cardinality, reader) and bundles three separable concerns, only one of which is a source key (R222 "What SourceKey decomposes into"; R333 sharpens the destinations). This item is the eager, mechanical decomposition, sequenced ahead of the reentry emit re-platforming (R314) so the emit slices land on decomposed facts instead of extending the conflated record.

Destinations, already settled in R333 (2026-07-04 design session):

  • target / path leave by deletion. They are denormalized copies of returnType.table() / joinPath already carried first-class on the leaves; SourceKey.path() has zero generator readers.

  • wrap / reader / backing dissolve into the read-side facts. The source object’s shape is a type-level fact (jOOQ record vs Java object); the key lift is N reads through the same field-level locator family the ordinary read side uses; `@sourceRow’s lifter is provenance on the member-read arm (authored where the catalog cannot infer the mapping), not a third mechanism. Nothing key-specific survives in `Reader’s seven arms, which conflate shape, provenance, and envelope.

  • The envelope names its destination too. SourceEnvelope (DIRECT vs OUTCOME_SUCCESS, carried on Reader.ResultRowWalk) is neither shape nor provenance: it is a read-site fact about whether env.getSource() is the row directly or an Outcome.Success wrapper. It coalesces onto the type-level source-object / error-channel fact (Reader.ProducedRecordRead’s javadoc already hoists it as `sourceIsOutcome; R333’s errorGuard.channel is the natural home), never onto the key residue. Leaving it unrouted would push an implementer to keep a Reader remnant alive for it, reintroducing the conflation this item removes.

  • The residue that stays: columns (the key tuple) and the source-field arity, which is a wrapper position of the wrapper algebra, not a free Cardinality enum.

Transition technique: additive-then-cutover (R222’s technique, stated here because four items pin on this type). Introduce the decomposed facts alongside SourceKey, dual-source, migrate consumers arm by arm behind the compiler, then delete the record; the execution-tier acceptance holds at every intermediate commit, not just the endpoint. Not a single atomic edit ; SourceKey is too widely pinned for big-bang in a trunk-based, concurrently-edited repo.

Incoming from R438 (materialize-joinpath-facts, the join-path twin; R438 lands first, this item reads its two-axis Hop): retiring JoinStep.LiftedHop is this item’s work, since its lifted slots are the Lift source-side provenance and moving it out of the JoinStep seal requires the SourceKey.path re-typing done here. Three payoffs ride along: the four defensive LiftedHop-unreachable arms in the @reference-path emitters become type-level impossibilities; the transitional HasSlots capability dies (R438’s On.ColumnPairs becomes its only implementor); and the denormalized Hop.originTable component becomes deletable in favor of a path-position derivation once the path carrier owns its start.

Two cleanups from R438’s self-review land naturally while this item is in the seal (both pre-existing patterns R438 mechanically widened, not regressions): (1) the "is this hop FK-derived" narrowing (x instanceof JoinStep.Hop h && h.on() instanceof On.ColumnPairs, or the blind (On.ColumnPairs) hop.on() cast) is spelled inline at roughly forty sites across nineteen files; a model-level isFkHop(JoinStep) / pairsOf(JoinStep) pair would serve the predicate and blind-cast sites (the exhaustive sealed-switch sites are proper dispatch and stay). (2) The four inline/split emitters each carry the same seven-line switch (hop.on()) bridging-join emit (onKey vs .on(condition(…​))); JoinPathEmitter already hosts the shared join-path emit helpers and is the single home for it. Both get cheaper to place once LiftedHop is out of the seal, and the sealed exhaustiveness means neither can drift silently in the meantime.

Why eager rather than pulled by the emit slices: the surface is being extended in its conflated form right now. R425 (parent projection omits a @splitQuery/@service child’s key columns) and R426 (TableRecord-sourced @service keys are partial records) both pin on sourceKey().columns() / SourceKey.Wrap.TableRecord, and R71 / R234 wait on the same decomposition. The 2026-07-03 staleness audit names this the next load-bearing structural pivot; landing it first gives the bug cluster and R314 a stable surface. When it lands, re-check R71, R234, R314, R425, R426 (staleness-audit observation 3).