ID |
|
|---|---|
Status |
Spec |
Bucket |
bug |
Priority |
3 |
Theme |
dev-loop |
Created |
2026-07-10 |
Updated |
2026-07-13 |
Model the nested fetcher own outgoing per-field precise edges in CompileDependencyGraphBuilder
Specced 2026-07-13, from the Backlog stub R459 filed as its scope-boundary follow-up.
Problem
R459 registered the <Type>Fetchers node for a fetcher-owning plain-object nesting type and got the
schema-shape-to-fetcher wiring edge (FilmMetaType → FilmMetaFetchers) for free. It deliberately did
not model the nested fetcher’s own outgoing per-field edges: a nested SplitTableField’s DataLoader
methods reference the target type’s projection class; a nested composite / `@nodeId read references
NodeIdEncoder. CompileDependencyGraphBuilder.addFieldEdges never sees these fields because they are
absent from schema.fields() (and schema.fieldsOf(nestedType) is empty for a coordinate-less nesting
type). The TypeSpecReferenceWalk completeness oracle will flag any such edge as a superset gap once a
harness fixture exercises it.
This is not a routing one-liner. Folding a nested SplitTableField through addFieldEdges’
`TableTargetField arm would call addConditionsEdge(fetcher, "FilmMeta"), whose hasSqlGeneratingField
reads the empty schema.fieldsOf("FilmMeta") and produces the wrong answer, the same fieldsOf
blindness over coordinate-less nested types that caused the R459 bug. Conditions attribution (and, more
broadly, per-field edge sourcing) over nested types needs its own design that does not depend on the
nested type’s SDL coordinates being populated.
Context, verified 2026-07-13
-
The emitted nested universe is closed.
GraphitronSchemaValidator.NESTED_WIREABLE_LEAVESrejects every leaf outside {ColumnBackedField,TableField,LookupTableField,NestingField,SplitTableField,SplitLookupTableField} at nested depth at validate time, so the builder’s nested per-field sourcing only ever sees those seven live. -
Nested fetcher outgoing references, traced through the emitters.
SplitTableField/SplitLookupTableFieldrows methods (SplitRowsMethodEmitter) projecttypes.<ReturnType>via$fields(...); aGeneratedConditionFilteris called asClassName.bestGuess(filter.className())=conditions.<ReturnType>Conditions(FkTargetConditionEmitter.emitTerm); a@nodeId-decoding filter argument lifts a decode helper onto the fetcher class (CompositeDecodeHelperRegistry.collectIntoinTypeFetcherGenerator), referencingNodeIdEncoder. Encoded reads (ColumnBackedFieldwithNodeIdEncodeKeyscompaction, any arity) referenceNodeIdEncoderviaFetcherEmitter.bindRaw, uniformly at any depth. InlineTableField/LookupTableFieldand innerNestingFieldreads reference no generated unit from the nested fetcher (source pickups); their projections and inline-filter edges land on the outer type class, already modeled by the R455 projection walk’sNestingFieldrecursion, and R459’s In Review verification pinned this empirically (disabling the R459 walk left exactly the one wiring gap).FetcherRegistrationsEmitter.nestedBodyreferences only the nested fetchers class; frozen scaffolds andGraphitronContextare blanket-covered because R459 registers the node beforeaddBlanketAndWiringEdgesruns. -
Conditions attribution truth. Only root fetchers reference a parent-named conditions class (the
QueryConditions/MutationConditionsenv-shim layer,QueryConditionsGenerator). A child fetcher referencesGeneratedConditionFilter.className()=conditions.<ReturnType>Conditionsdirectly (pre-resolved byFieldBuilderat classify time). The childTableTargetFieldarm’saddConditionsEdge(fetcher, parentTypeName)therefore models a reference that is never emitted, and misses the real one for any top-level split-with-filter whose parent and return type names differ, plus the fetcher’s decode-helperNodeIdEncoderreference. Corpus-blind today: the harness’s only filtered reference field (Language.films) is inline, where the R455 projection walk attributes correctly. -
The root shim’s own composition is unmodeled.
QueryConditions.<field>Conditioncalls the entity-scoped<ReturnType>Conditionsmethods and lifts decode helpers onto the shim class, soconditions(root) → gcf.className()andconditions(root) → NodeIdEncoderedges exist in the emit and are absent from the graph. Same corpus blindness (no filtered root field in the harness). -
MapCompileDependencyGraph.Accumulator.addEdgeauto-registers both endpoints as nodes, so filter-sourced conditions edges need no separate node registration.
Design (settled with principles-architect, 2026-07-13)
-
One filter seam. Generalize
addInlineFilterEdges(source, filters)(logic unchanged: perGeneratedConditionFilteraddsource → gcf.className(); iffiltersDecodeNodeIdaddsource → NodeIdEncoder) sosourcemay be a type class, a fetcher, or a conditions shim. It stays the single enforcer of the filter-to-edges projection; no second copy is written. -
One per-leaf routine, two callers. Extract the
ChildFielddispatch ofaddFieldEdgesinto a routine parameterized by the sourcing fetcher node, called from both the top-levelschema.fields()loop and the nested walk. The nested walk stays pure iteration over the same reachabilityaddNestedFetcherNodesalready walks (extend that walk or mirror it); no second per-leaf switch exists, so per-leaf edge logic cannot diverge between depths. -
Child conditions re-attribution. In the child
TableTargetFieldarm, replaceaddConditionsEdge(fetcher, parentTypeName)withaddInlineFilterEdges(fetcher, f.filters()). This removes the child-sidehasSqlGeneratingField/fieldsOfdependence entirely, which is exactly what makes coordinate-less nested types attributable: the pre-resolved model fact (the filter carries its class name) replaces the SDL-coordinate lookup. Removing the parent-named edge is superset-safe: it corresponds to no emitted reference (it is either spurious or coincides with the filter-sourced edge when parent and return names coincide).hasSqlGeneratingFieldremains only for root conditions node and edge gating, where the parent-named shim really exists. -
The
addTypeClassEdgestays uniform over all eightTableTargetFieldleaves at both depths. For the inlineTableField/LookupTableFieldleaves the fetcher-sidetypeClassedge is a tolerated over-approximation (the real fetcher-side reference set of an inline read is empty; the projection lives on the outer type class). Uniformity dissolves the outer/nested asymmetry a forked switch would otherwise need its own pin for; tightening the over-approximation is a pruning refinement, out of scope, and unpinnable by the oracle (which enforces only the superset direction). -
Root shim composition edges. Root arms keep the parent-named
addConditionsEdgeand additionally calladdInlineFilterEdges(units.conditions(parentTypeName), f.filters())for exactly the armsQueryConditionsGeneratorcollects (QueryTableField/QueryTableInterfaceFieldtoday; the implementer mirrors the generator’s collection set, and the mutation-shim analogue where present). -
The derived view stays deferred. R459’s named collapse target (nested fetcher-owning types and fields as a derived view on
GraphitronSchema) is not needed for correctness here: edges come off the field records during the existing walk, and conditions attribution no longer needs a non-blindfieldsOf. The collapse target stays named for a future item (now four re-walks of theNestingFieldtree: emitter, registrations emitter, projection walk, builder).
Failing pins (harness, extend both SCHEMA and SCHEMA_EDITED identically)
-
Nested split, no filterable args:
FilmMeta.languages: [Language!] @reference(path: [{key: "film_language_id_fkey"}]) @splitQuerypinsFilmMetaFetchers → types.Languagein the completeness oracle. -
Top-level split re-attribution:
Language.filmsSplit(filter: FilmFilter): [Film!] @reference(path: [{key: "film_language_id_fkey"}]) @splitQuerypinsLanguageFetchers → conditions.FilmConditionsandLanguageFetchers → util.NodeIdEncoder(FilmFilter.idsdecodes@nodeId). -
Root shim composition:
Query.filmsByFilter(filter: FilmFilter): [Film!](plain table read) pinsconditions.QueryConditions → conditions.FilmConditionsand→ util.NodeIdEncoder.
Pin 1 deliberately carries no filterable args: a GeneratedConditionFilter on a nested field trips
the separate emit bug filed as R472 (see Scope). Exact SDL shapes are the implementer’s discretion if
classification differs from the sketch; the pinned edges are the contract. Each pin is verified
non-vacuous R459-style: with the new edge code disabled, the oracle reports exactly the expected gaps
and nothing else (record the verification in the In Review note).
Tests
-
Unit tier (
CompileDependencyGraphBuilderTest, hand-built records): nestedSplitTableFieldyieldsfetcher → typeClassplus, with aGeneratedConditionFilter,fetcher → gcf.className()andfetcher → NodeIdEncoderwhen its filters decode@nodeId; a nested encodedColumnBackedFieldyieldsfetcher → NodeIdEncoder; a top-level child split with aGeneratedConditionFilterassertsfetcher → gcf.className()present and the parent-named conditions edge absent (pins the re-attribution); root shim edges as designed. -
CompileDependencyGraphPipelineTest.rootAndChildFetchersReferenceTargetProjectionsAndConditionsexpectsFilmFetchers → conditions.FilmConditionsfor the filterless inlineFilm.language; that edge is an artifact of the removed over-approximation and the expectation updates to the new truth. -
IncrementalCompileHarnessTestclauses (a) and (b) stay green over the extended corpus.
Scope
-
In: the builder changes above, tests, and the harness fixture extensions. The child conditions re-attribution and the root shim edges are in scope because they are the same seam and the same design fork the Problem section demands ("conditions attribution … needs its own design"); they share the generalized
addInlineFilterEdgesand the harness pins. -
Out, filed as R472 (
nested-generated-condition-filters-never-emitted):TypeConditionsGenerator’s walk (schema.types()` ×fieldsOf) cannot see nested fields, so aGeneratedConditionFilteron a nested split or inline field emits a call to a<ReturnType>Conditionsmethod that is never generated (consumer javac failure) ; a validator-mirror gap. This item’s nested filter-sourced edge code is still written (unit-tier covered) and becomes live once R472 lands; the harness fixture avoids tripping the bug until then. -
Out: tightening the inline-leaf fetcher-to-typeClass over-approximation (named in Design).
-
Observation recorded, no item filed:
ChildField.ServiceTableField.filters()is never read by any emitter (an inert model fact; the leaf cannot appear at nested depth). Surfaced for a future triage.
Notes
Filed by R459 as its scope-boundary follow-up. R459’s related collapse target (the derived
nested-type view on GraphitronSchema) is deliberately not built here; see Design.
Scoping advisory (added 2026-07-26, no design change). Fix the defect, do not generalise the
machinery. This item is the third on the same root cause (R455’s oracle blind spots, R459’s missing
node, this item’s missing outgoing edges), and the shared cause is structural: the graph is derived
from coordinates while the emit contains methods no coordinate exposes, so such a method is invisible
to CompileDependencyGraphBuilder by construction. That whole class dissolves if the dependency graph
becomes a projection over a core-produced command relation (nodes grouped by unit, edges projected to
unit granularity), because a command exists per method emitted rather than per coordinate found. See
gap 7 ("the recompile graph is a second derivation of the same relation") in
roadmap/audits/2026-07-26-fcis-command-layer-distance.md, which carries the full argument and its
caveats, and slice 7 of R549.
Consequence for whoever implements this: a targeted fix for the shapes named above is the right size, and per-field edge sourcing over nested types wanting "its own design" (per the Problem section) should stay minimal rather than growing a general framework that the command projection would delete. The dev loop is wrong for this shape today, so the fix should not wait on that program, which is unproven and has an explicit abandon condition.