Skip to content

feat: defer additional fixes#1464

Closed
devsergiy wants to merge 19 commits into
feat/eng-7770-add-defer-supportfrom
feat/eng-7770-add-defer-support-additional
Closed

feat: defer additional fixes#1464
devsergiy wants to merge 19 commits into
feat/eng-7770-add-defer-supportfrom
feat/eng-7770-add-defer-support-additional

Conversation

@devsergiy

Copy link
Copy Markdown
Member

@coderabbitai summary

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@devsergiy devsergiy requested a review from a team as a code owner April 8, 2026 11:56
@devsergiy devsergiy requested review from SkArchon and StarpTech and removed request for a team April 8, 2026 11:56

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f47504fe-b753-41db-b2a0-a6b70af9ce0d

📥 Commits

Reviewing files that changed from the base of the PR and between 1cd9e90 and 0f7afd2.

📒 Files selected for processing (9)
  • v2/pkg/astnormalization/defer_ensure_typename.go
  • v2/pkg/engine/plan/node_selection_visitor.go
  • v2/pkg/engine/resolve/data_buffer.go
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
💤 Files with no reviewable changes (1)
  • v2/pkg/engine/resolve/resolve.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
  • v2/pkg/engine/resolve/loader.go

📝 Walkthrough

Walkthrough

Implements parallel and nested GraphQL @defer execution in the Go engine. A new deferPopulateParentIds normalization visitor derives parentDeferId post-merging. A deferInfoCollector planning visitor builds DeferDescriptor maps. A buildDeferTree postprocessor constructs a DeferTreeNode execution tree. The Loader is decoupled from Resolvable via a new DataBuffer; errors are accumulated loader-locally and flushed once. Resolvable now emits pending/incremental/completed wire format with subPath. resolve.go drives parallel siblings via errgroup and sequential children structurally.

Changes

@defer Parallel Execution — End-to-End Implementation

Layer / File(s) Summary
AST defer ID helpers and parentDeferId normalization visitor
v2/pkg/ast/ast_field.go, v2/pkg/astnormalization/defer_populate_parent_ids.go, v2/pkg/astnormalization/defer_populate_parent_ids_test.go, v2/pkg/astnormalization/defer_expand_into_internal.go, v2/pkg/astnormalization/astnormalization.go, v2/pkg/astnormalization/astnormalization_test.go, v2/pkg/asttransform/fixtures/*.golden, execution/engine/engine_config_test.go, v2/pkg/astnormalization/defer_ensure_typename.go, v2/pkg/engine/plan/node_selection_visitor.go
FieldInternalDeferIDWithDirectiveRef and FieldDeferInfo return integer ids and label; MergeFieldsDefer compares integer ids; new deferPopulateParentIds visitor appends parentDeferId after field merging; defer_expand_into_internal resets state per document and skips fragment definitions; walkerStage gains skipCondition; golden fixtures remove label description string; backward-iteration helpers use slices.Backward.
Planning: DeferDescriptor collection
v2/pkg/engine/plan/defer_info_collector.go, v2/pkg/engine/plan/planner.go, v2/pkg/engine/plan/visitor.go
New deferInfoCollector walks selection sets to record DeferDescriptor{id, parentID, label, path} including list-anchor path truncation; wired through Planner.prepareOperationWalker; Visitor carries deferDescriptors and attaches them to GraphQLDeferResponse.
Resolve data structures: DeferDescriptor, DeferTreeNode, DataBuffer, constants
v2/pkg/engine/resolve/response.go, v2/pkg/engine/resolve/defer_tree.go, v2/pkg/engine/resolve/data_buffer.go, v2/pkg/engine/resolve/data_buffer_test.go, v2/pkg/engine/resolve/const.go
GraphQLDeferResponse gains DeferDescriptors map[int]DeferDescriptor and DeferTree *DeferTreeNode; new DeferDescriptor struct; defer_tree.go defines DeferTreeNodeKind enum and DeferSingle/DeferSequence/DeferParallel constructors; DataBuffer wraps *astjson.Value with conditional mutex; adds pending, completed, id, label, subPath byte-slice constants.
Postprocess: buildDeferTree
v2/pkg/engine/postprocess/build_defer_tree.go, v2/pkg/engine/postprocess/build_defer_tree_test.go, v2/pkg/engine/postprocess/postprocess.go
buildDeferTree.Process groups DeferFetchGroups by ParentID, sorts siblings by DeferID, and recursively builds DeferSingle/DeferSequence/DeferParallel nodes; wired into Processor after extractDeferFetches; clears response.Defers; adds DisableBuildDeferTree option; four tree-shape unit tests cover single root, two siblings, parent-child, and mixed branches.
Loader: DataBuffer ownership, per-fetch subgraph error isolation
v2/pkg/engine/resolve/loader.go, v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
Removes *Resolvable from Loader; adds dataBuffer for shared JSON tree; loader-local errors *astjson.Value and subgraphErrors map; all merge/path-selection sites lock via dataBuffer; recordSubgraphError and appendSubgraphErrorsToContext replace direct Context writes; OnFinished receives per-fetch subgraphError; unit tests verify per-fetch isolation, accumulation, and no-op nil behavior; introduces prepareSingleFetch, prepareEntityFetch, prepareBatchEntityFetch preparation stages.
Resolvable: pending/incremental/completed envelope rendering
v2/pkg/engine/resolve/resolvable.go
Replaces deferID int with currentDefer *DeferDescriptor and deferDescriptors map[int]DeferDescriptor; Resolve() emits sorted pending array before hasNext; ResolveDefer() emits incremental+completed on success or completed+errors on non-recoverable failure; adds printPendingEntries, printPathArray, printDeferSubPathIfAny, printDeferIdAndErrors; collectDeferFields uses isDeferAncestor instead of numeric comparison; walkObject branches on currentDefer != nil.
resolve.go: NewLoader, parallel defer tree traversal, subscription update refactor
v2/pkg/engine/resolve/resolve.go, v2/pkg/engine/resolve/resolve_defer_parallel_test.go
Exports NewLoader constructor; rewrites ResolveGraphQLResponse/ArenaResolveGraphQLResponse with explicit inject-after-load pattern; ResolveGraphQLDeferResponse drives tree walk via resolveDeferTree using errgroup for parallel siblings and structural sequencing for children; DataBuffer lock armed before parallel work; per-group Loader instances share only the parent DataBuffer; subscription update paths rewritten; five parallel-defer unit tests cover flush ordering, sequence ordering, failure independence, error isolation, and subgraph error aggregation.
Loader, resolvable, datasource, and integration test updates
v2/pkg/engine/resolve/loader_test.go, v2/pkg/engine/resolve/resolvable_test.go, v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go, execution/engine/execution_engine_defer_test.go, execution/engine/execution_engine_helpers_test.go, execution/engine/execution_engine_test.go
Loader tests adopt DataBuffer+NewLoader pattern without passing resolvable; datasource defer tests assert DeferDescriptors and DeferTree; execution engine defer tests rewrite expected streaming frames to pending/incremental/completed with entity id and subPath, add mock latency for deterministic ordering, and support expectedResponses multi-response assertions; test helper adds mutex-guarded used map and optional time.Sleep.
Design specs and implementation plan documents
docs/superpowers/plans/*, docs/superpowers/specs/*
Nine implementation-plan documents and two design spec documents covering the full development sequence: defer ID string-to-int, label support, label refactor, pending/completed wire format, subPath emission, populate-parentIds normalization, parallel execution design, parallel error isolation design, and per-fetch subgraph error isolation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Resolver as resolve.go
  participant DB as DataBuffer
  participant L as Loader
  participant R as Resolvable
  participant Writer as DeferWriter

  Client->>Resolver: ResolveGraphQLDeferResponse(response)
  Resolver->>R: NewResolvable + Init
  Resolver->>DB: new DataBuffer(enableLock=false)
  Resolver->>L: NewLoader(options, DB)
  L->>L: LoadGraphQLResponseData (initial fetch tree)
  Resolver->>R: inject data/errors from loader
  R->>Writer: Resolve → {data:{...}, pending:[{id,path}...], hasNext:true}
  Resolver->>DB: enableLock = true
  Resolver->>Resolver: resolveDeferTree(DeferTreeNode)
  par Parallel siblings via errgroup
    Resolver->>L: groupLoader1.LoadGraphQLResponseData
    L->>DB: Lock → mergeResult → Set
    Resolver->>R: ResolveDefer(currentDefer1, hasNext)
    R->>Writer: {incremental:[{id, data}], completed:[{id}], hasNext:true/false}
    L->>DB: Unlock
  and
    Resolver->>L: groupLoader2.LoadGraphQLResponseData
    L->>DB: Lock → mergeResult → Set
    Resolver->>R: ResolveDefer(currentDefer2, hasNext)
    R->>Writer: {incremental:[{id, data}], completed:[{id}], hasNext:false}
    L->>DB: Unlock
  end
  Resolver->>Writer: Complete()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description consists primarily of a template with unchecked checklist items and no actual implementation details about the defer fixes being introduced. Replace the template with a meaningful description explaining the key defer-related changes, their purpose, and impact on the system.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies this as a feature PR addressing defer support with additional fixes, matching the substantial implementation changes across AST, planning, and resolution layers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eng-7770-add-defer-support-additional

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/defer-design.md`:
- Line 334: The markdown has heading level jumps: change the two headings
currently using "######" (the "Normal envelope (`deferItemDataNull=false`)"
heading and the similar heading in the incremental rendering subsection) to one
level higher "#####" so they sit under the existing "####" section and resolve
MD001; update both occurrences accordingly to maintain consistent heading
hierarchy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6482f06a-05fd-44f1-bef5-5c7bde984015

📥 Commits

Reviewing files that changed from the base of the PR and between 081cac6 and 980b315.

📒 Files selected for processing (1)
  • docs/defer-design.md

Comment thread docs/defer-design.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/defer-design.md`:
- Around line 349-357: Update the "Null envelope (`deferItemDataNull=true`)"
description to match the wire-format: state that printDeferEnvelopeNullData
emits the null item ({"data": null, "path": [...], "errors": [...]}) inside the
standard {"incremental": [ ... ]} envelope rather than omitting the outer
wrapper; change the phrase "the outer `{\"incremental\": [` wrapper are never
written" to "the item is emitted inside the standard `{\"incremental\": [ ...
]}` chunk envelope and the `{\"data\": {` opener is skipped for that item," and
apply the same wording correction to the other occurrence referenced (lines
416-420).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: efeed565-a6d7-4b62-aead-60c60b77a0f2

📥 Commits

Reviewing files that changed from the base of the PR and between 980b315 and 3232978.

📒 Files selected for processing (1)
  • docs/defer-design.md

Comment thread docs/defer-design.md
Comment on lines +349 to +357
##### Null envelope (`deferItemDataNull=true`)

`printDeferEnvelopeNullData` writes the entire item as
```json
{"data": null, "path": [...], "errors": [...]}
```

in one shot — the normal `{"data": {` opener and the outer `{"incremental": [` wrapper are never written. The walker returns immediately without descending further.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null-envelope description contradicts the documented wire format.

The null-envelope section says the outer {"incremental":[ ... ]} wrapper is never written, but the wire-format section documents null responses with that wrapper. Please align the rendering description to avoid implementer confusion.

📝 Proposed doc fix
-`printDeferEnvelopeNullData` writes the entire item as
+`printDeferEnvelopeNullData` writes the null-data item as
 ```json
 {"data": null, "path": [...], "errors": [...]}

-in one shot — the normal {"data": { opener and the outer {"incremental": [ wrapper are never written. The walker returns immediately without descending further.
+in one shot — the normal {"data": { opener is skipped for that item. The item is then emitted inside the standard {"incremental": [ ... ]} chunk envelope. The walker returns immediately without descending further.

</details>


Also applies to: 416-420

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/defer-design.md around lines 349 - 357, Update the "Null envelope
(deferItemDataNull=true)" description to match the wire-format: state that
printDeferEnvelopeNullData emits the null item ({"data": null, "path": [...],
"errors": [...]}) inside the standard {"incremental": [ ... ]} envelope rather
than omitting the outer wrapper; change the phrase "the outer {\"incremental\": [ wrapper are never written" to "the item is emitted inside the standard
{\"incremental\": [ ... ]} chunk envelope and the {\"data\": { opener is
skipped for that item," and apply the same wording correction to the other
occurrence referenced (lines 416-420).


</details>

<!-- fingerprinting:phantom:triton:hawk:21fc2da2-0188-40e2-a9bb-a36a5dd0647a -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread docs/defer-design.md
Comment thread docs/defer-design.md

Resolves which datasource(s) handle each field. Also detects fields that require additional data to be fetched — `@key` fields for entity resolution and `@requires` fields for computed fields — and injects them directly into the operation AST in the correct defer scope.

**`@requires` fields** are stamped with the same `@__defer_internal` as the field that needs them. They must be present in the same deferred fetch so the field resolver has the data it depends on.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if multiple fields have overlapping requires?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, for such schemas

type User @key(fields: "id") {
id: ID!
name: String!
account: Account! @requires(fields: "billing { plan } settings { region }")
billing: Billing! @external
settings: Settings! @external
}
type Account {
type: String!
limit: Int!
}
type Billing {
plan: String! @external
}
type Settings {
region: String! @external
}
`
// Subgraph 2: owns User.billing, User.notifications.
// notifications @requires(fields: "name settings { language }") — depends on sub1 (name) and sub3 (settings).
secondSubgraphSDL := `
type User @key(fields: "id") {
id: ID!
name: String! @external
notifications: [String!]! @requires(fields: "name settings { language }")
billing: Billing!
settings: Settings! @external
}
type Billing {
plan: String!
currency: String!
}
type Settings {
language: String! @external
}
`
// Subgraph 3: owns User.settings.
thirdSubgraphSDL := `
type User @key(fields: "id") {
id: ID!
settings: Settings!
}
type Settings {
region: String!
language: String!
}

Such a query with 2 fields requires selecting a different nested field in settings, but this fields are in the same defer scope

query DeferAllFields {
user {
... @defer {
name
billing { plan }
settings { region }
account { type }
notifications
}
}
}`,

Current logic always ads an alias to the required fields in defer scope, but reuses already existing alias

So operation with added required fields will look like this

query DeferAllFields {
    user {
        name @__defer_internal(id: "1")
        billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        account @__defer_internal(id: "1") {
            type @__defer_internal(id: "1")
        }
        notifications @__defer_internal(id: "1")
        ___typename: __typename
        __internal_billing: billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }
        __internal_name: name @__defer_internal(id: "1")
        __typename
        id
    }
}

In case of such query, when the fields with requirements in different defer scopes

query DeferDerivedFieldsOnly {
user {
name
billing { plan }
settings { region language }
... @defer { account { type } }
... @defer { notifications }
}
}`,

The query will look like this

query DeferDerivedFieldsOnly {
    user {
        name
        billing {
            plan
        }
        settings {
            region
            language
        }
        account @__defer_internal(id: "1") {
            type @__defer_internal(id: "1")
        }
        notifications @__defer_internal(id: "2")
        __internal_billing: billing @__defer_internal(id: "1") {
            plan @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        __internal_name: name @__defer_internal(id: "2")
        __internal_2_settings: settings @__defer_internal(id: "2") {
            language @__defer_internal(id: "2")
        }
        __typename
        id
    }
}

We always alias requirements because in situation like this

query {
  user {
    settings {
      a
    }
    ... @defer {
      fieldRequiresSettingsB
      fieldRequiresSettingsC
    }
  }
}

We can't reuse settings, because it is not in the defer scope, and it will mean that we will fetch required fields with primary response, not via deffer group

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, after writing this comment I realized that there could be small improvement

        settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }

In this case the existing field falls into the same defer scope, so we could actually reuse it

But in the case of such an operation

        settings @__defer_internal(id: "1") {
            anyField @__defer_internal(id: "1")
            region @__defer_internal(id: "2")
        }
        __internal_settings: settings @__defer_internal(id: "1") {
            region @__defer_internal(id: "1")
            language @__defer_internal(id: "1")
        }

when needed region field is in the other defer scope, we will need to add an alias to this nested field, which currently is not implemented, as an alias is added only at the root of the requires selection set

Using aliases is a tradeoff to be able to properly distribute dependencies between fields, to assign them to a proper planner

In the last example region will be returned to the client only with defer group 2
Potentially, I could mark a field that should be returned later, but fetched earlier via the other fetch

Comment thread docs/defer-design.md
Comment thread docs/defer-design.md
Comment thread docs/defer-design.md
Comment thread docs/defer-design.md
Comment thread docs/defer-design.md
@devsergiy devsergiy marked this pull request as draft April 9, 2026 16:58
@devsergiy devsergiy changed the base branch from feat/eng-7770-add-defer-support to master May 4, 2026 15:45
@devsergiy devsergiy changed the base branch from master to feat/eng-7770-add-defer-support May 4, 2026 15:48
Comment thread v2/pkg/engine/resolve/resolve.go Outdated
Comment thread v2/pkg/engine/resolve/response.go
Comment thread v2/pkg/engine/resolve/response.go
Comment thread v2/pkg/engine/resolve/defer_tree.go
Comment thread v2/pkg/engine/resolve/resolve.go
Comment thread v2/pkg/ast/ast_field.go
Comment thread v2/pkg/engine/resolve/resolve.go
Comment thread v2/pkg/astnormalization/defer_expand_into_internal.go
Comment thread v2/pkg/engine/resolve/loader.go Outdated

@ysmolski ysmolski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1st pass is done

@ysmolski ysmolski marked this pull request as ready for review June 22, 2026 14:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md (1)

607-626: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t derive hasNext from fetch completion order.

remaining is decremented before the render lock, so “last” is defined by fetch completion, not emit order. Under concurrent siblings, a slower fetch can still flush after a faster one and wind up with hasNext:true on the final payload. Tie the terminal flag to the render/flush critical section or tree walk instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md` around
lines 607 - 626, The current design decrements `remaining` before the render
lock, causing `hasNext` to be determined by fetch completion order rather than
emit order, which can result in slower fetches incorrectly marking
`hasNext:true` on the final payload. To fix this, move the logic that determines
the terminal flag (`hasNext`) so it is evaluated within the render/flush
critical section or during a tree walk operation instead of being derived from
when `remaining` is decremented. This ensures the final payload correctly
reflects whether additional chunks will actually be emitted, not just whether
all fetches have completed.
🧹 Nitpick comments (1)
execution/engine/execution_engine_test.go (1)

151-159: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Make streaming expectation modes mutually exclusive.

At Line 153 and Line 157, both assertion paths can run if both fields are set, which makes failures ambiguous. Add a guard so tests choose exactly one mode.

Proposed diff
 		if opts.streamingResponse {
 			streamingResponse := streamingBuf.String()
+			require.False(t, testCase.expectedResponse != "" && len(testCase.expectedResponses) > 0,
+				"set either expectedResponse or expectedResponses, not both")
 			if testCase.expectedResponse != "" {
 				assert.Equal(t, testCase.expectedResponse, streamingResponse)
 			}
 
 			if len(testCase.expectedResponses) > 0 {
 				assert.Contains(t, testCase.expectedResponses, streamingResponse)
 			}
 		} else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/engine/execution_engine_test.go` around lines 151 - 159, The
streaming response validation currently allows both testCase.expectedResponse
and testCase.expectedResponses assertions to run simultaneously, creating
ambiguous test failures when both fields are populated. Change the second
condition checking len(testCase.expectedResponses) > 0 from an if statement to
an else if statement, so that only one assertion path executes - either the
assert.Equal for expectedResponse or the assert.Contains for expectedResponses,
but never both.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-04-17-defer-label-support.md`:
- Around line 5-8: This plan document (2026-04-17-defer-label-support.md) is now
superseded by later plans in the PR (central-map / pending-completed / subPath
docs) and keeping both active creates conflicting implementation contracts. Add
a clear supersession notice at the top of the document indicating that this plan
has been superseded by the newer approaches, so implementers don't follow
incompatible directions.

In `@docs/superpowers/plans/2026-04-29-defer-subpath.md`:
- Around line 120-140: The deferPath() function builds the descriptor.path using
schema field names, but it must use the response alias when one exists in the
operation (e.g., alias: user { ... `@defer` ... }). When iterating through
c.Walker.Path and collecting FieldName items, or when determining field names
from c.Walker.Ancestors NodeKindField entries, check whether each field has an
alias defined in the operation using c.operation and related helpers. If an
alias exists, use that alias name in the path instead of the actual field name
from the schema, since the response data structure will be keyed by the alias,
not the schema field name. This ensures both pending[].path and subPath work
correctly for aliased queries.

In `@docs/superpowers/plans/2026-05-26-defer-parallel-errors.md`:
- Around line 569-585: The clone-based context isolation in the defer-group
error handling prevents subgraph errors from being aggregated in
Context.SubgraphErrors(), which downstream callers depend on. Remove the
per-goroutine clone isolation by eliminating the `childCtx :=
ctx.clone(ctx.ctx)` line and pass the shared context directly to
r.resolveDeferTree instead. Then ensure all mutations to ctx.subgraphErrors
within the defer group goroutine are protected by db.Lock() (consistent with how
it's done in other parts of the code) so that errors are properly aggregated
into the request context without race conditions.

In `@execution/engine/execution_engine_defer_test.go`:
- Around line 699-700: The expectedResponse string in this test contains
malformed JSON with an extra closing brace before the `,"pending"` element. The
outer object is being closed prematurely with 4 closing braces after
"black@sabbat" when only 3 are needed. Remove one closing brace from the
sequence of closing braces that appears before `,"pending"` in the
expectedResponse string to properly structure the JSON so that the outer object
remains open to accept the pending, completed, and hasNext fields.

In `@execution/engine/execution_engine_test.go`:
- Around line 147-149: The comparison at line 148 using compactJSONForAssert
performs string-based JSON comparison, which fails when expectedJSONResponse and
actualResponse contain semantically identical JSON objects with keys in
different orders. Replace the string comparison assert.Equal call with a JSON
semantic comparison approach that parses both JSON strings into objects and
compares them structurally (either unmarshaling into maps or using a dedicated
JSON comparison library), rather than relying on compactJSONForAssert which does
not guarantee consistent key ordering.

---

Outside diff comments:
In `@docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md`:
- Around line 607-626: The current design decrements `remaining` before the
render lock, causing `hasNext` to be determined by fetch completion order rather
than emit order, which can result in slower fetches incorrectly marking
`hasNext:true` on the final payload. To fix this, move the logic that determines
the terminal flag (`hasNext`) so it is evaluated within the render/flush
critical section or during a tree walk operation instead of being derived from
when `remaining` is decremented. This ensures the final payload correctly
reflects whether additional chunks will actually be emitted, not just whether
all fetches have completed.

---

Nitpick comments:
In `@execution/engine/execution_engine_test.go`:
- Around line 151-159: The streaming response validation currently allows both
testCase.expectedResponse and testCase.expectedResponses assertions to run
simultaneously, creating ambiguous test failures when both fields are populated.
Change the second condition checking len(testCase.expectedResponses) > 0 from an
if statement to an else if statement, so that only one assertion path executes -
either the assert.Equal for expectedResponse or the assert.Contains for
expectedResponses, but never both.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 67884d71-388f-459b-911b-e8b76f7115e6

📥 Commits

Reviewing files that changed from the base of the PR and between 3232978 and 1cd9e90.

📒 Files selected for processing (48)
  • docs/superpowers/plans/2026-04-17-defer-id-string-to-int.md
  • docs/superpowers/plans/2026-04-17-defer-label-support.md
  • docs/superpowers/plans/2026-04-28-defer-label-refactor.md
  • docs/superpowers/plans/2026-04-28-defer-pending-completed.md
  • docs/superpowers/plans/2026-04-29-defer-subpath.md
  • docs/superpowers/plans/2026-05-18-defer-parallel-execution.md
  • docs/superpowers/plans/2026-05-20-defer-populate-parent-ids.md
  • docs/superpowers/plans/2026-05-26-defer-parallel-errors.md
  • docs/superpowers/plans/2026-06-09-per-fetch-subgraph-errors.md
  • docs/superpowers/specs/2026-05-15-defer-parallel-execution-design.md
  • docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md
  • execution/engine/engine_config_test.go
  • execution/engine/execution_engine_defer_test.go
  • execution/engine/execution_engine_helpers_test.go
  • execution/engine/execution_engine_test.go
  • v2/pkg/ast/ast_field.go
  • v2/pkg/astnormalization/astnormalization.go
  • v2/pkg/astnormalization/astnormalization_test.go
  • v2/pkg/astnormalization/defer_expand_into_internal.go
  • v2/pkg/astnormalization/defer_populate_parent_ids.go
  • v2/pkg/astnormalization/defer_populate_parent_ids_test.go
  • v2/pkg/asttransform/fixtures/complete.golden
  • v2/pkg/asttransform/fixtures/custom_query_name.golden
  • v2/pkg/asttransform/fixtures/mutation_only.golden
  • v2/pkg/asttransform/fixtures/schema_missing.golden
  • v2/pkg/asttransform/fixtures/simple.golden
  • v2/pkg/asttransform/fixtures/subscription_only.golden
  • v2/pkg/asttransform/fixtures/subscription_renamed.golden
  • v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go
  • v2/pkg/engine/plan/defer_info_collector.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/plan/visitor.go
  • v2/pkg/engine/postprocess/build_defer_tree.go
  • v2/pkg/engine/postprocess/build_defer_tree_test.go
  • v2/pkg/engine/postprocess/postprocess.go
  • v2/pkg/engine/resolve/const.go
  • v2/pkg/engine/resolve/data_buffer.go
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/engine/resolve/defer_tree.go
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/resolve/resolvable_test.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
  • v2/pkg/engine/resolve/response.go
💤 Files with no reviewable changes (33)
  • v2/pkg/engine/resolve/const.go
  • v2/pkg/engine/resolve/defer_tree.go
  • v2/pkg/engine/resolve/data_buffer.go
  • v2/pkg/astnormalization/defer_populate_parent_ids_test.go
  • v2/pkg/asttransform/fixtures/simple.golden
  • v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/asttransform/fixtures/subscription_only.golden
  • v2/pkg/asttransform/fixtures/subscription_renamed.golden
  • v2/pkg/engine/postprocess/build_defer_tree.go
  • v2/pkg/astnormalization/astnormalization_test.go
  • v2/pkg/asttransform/fixtures/mutation_only.golden
  • v2/pkg/astnormalization/defer_populate_parent_ids.go
  • v2/pkg/asttransform/fixtures/complete.golden
  • v2/pkg/asttransform/fixtures/schema_missing.golden
  • v2/pkg/asttransform/fixtures/custom_query_name.golden
  • v2/pkg/engine/postprocess/build_defer_tree_test.go
  • v2/pkg/engine/resolve/response.go
  • v2/pkg/engine/plan/defer_info_collector.go
  • v2/pkg/astnormalization/astnormalization.go
  • v2/pkg/astnormalization/defer_expand_into_internal.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/plan/visitor.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/postprocess/postprocess.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
  • v2/pkg/engine/resolve/resolvable_test.go
  • v2/pkg/ast/ast_field.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/resolve/loader.go
✅ Files skipped from review due to trivial changes (3)
  • execution/engine/engine_config_test.go
  • docs/superpowers/plans/2026-04-17-defer-id-string-to-int.md
  • docs/superpowers/plans/2026-06-09-per-fetch-subgraph-errors.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md (1)

607-626: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t derive hasNext from fetch completion order.

remaining is decremented before the render lock, so “last” is defined by fetch completion, not emit order. Under concurrent siblings, a slower fetch can still flush after a faster one and wind up with hasNext:true on the final payload. Tie the terminal flag to the render/flush critical section or tree walk instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md` around
lines 607 - 626, The current design decrements `remaining` before the render
lock, causing `hasNext` to be determined by fetch completion order rather than
emit order, which can result in slower fetches incorrectly marking
`hasNext:true` on the final payload. To fix this, move the logic that determines
the terminal flag (`hasNext`) so it is evaluated within the render/flush
critical section or during a tree walk operation instead of being derived from
when `remaining` is decremented. This ensures the final payload correctly
reflects whether additional chunks will actually be emitted, not just whether
all fetches have completed.
🧹 Nitpick comments (1)
execution/engine/execution_engine_test.go (1)

151-159: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Make streaming expectation modes mutually exclusive.

At Line 153 and Line 157, both assertion paths can run if both fields are set, which makes failures ambiguous. Add a guard so tests choose exactly one mode.

Proposed diff
 		if opts.streamingResponse {
 			streamingResponse := streamingBuf.String()
+			require.False(t, testCase.expectedResponse != "" && len(testCase.expectedResponses) > 0,
+				"set either expectedResponse or expectedResponses, not both")
 			if testCase.expectedResponse != "" {
 				assert.Equal(t, testCase.expectedResponse, streamingResponse)
 			}
 
 			if len(testCase.expectedResponses) > 0 {
 				assert.Contains(t, testCase.expectedResponses, streamingResponse)
 			}
 		} else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/engine/execution_engine_test.go` around lines 151 - 159, The
streaming response validation currently allows both testCase.expectedResponse
and testCase.expectedResponses assertions to run simultaneously, creating
ambiguous test failures when both fields are populated. Change the second
condition checking len(testCase.expectedResponses) > 0 from an if statement to
an else if statement, so that only one assertion path executes - either the
assert.Equal for expectedResponse or the assert.Contains for expectedResponses,
but never both.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-04-17-defer-label-support.md`:
- Around line 5-8: This plan document (2026-04-17-defer-label-support.md) is now
superseded by later plans in the PR (central-map / pending-completed / subPath
docs) and keeping both active creates conflicting implementation contracts. Add
a clear supersession notice at the top of the document indicating that this plan
has been superseded by the newer approaches, so implementers don't follow
incompatible directions.

In `@docs/superpowers/plans/2026-04-29-defer-subpath.md`:
- Around line 120-140: The deferPath() function builds the descriptor.path using
schema field names, but it must use the response alias when one exists in the
operation (e.g., alias: user { ... `@defer` ... }). When iterating through
c.Walker.Path and collecting FieldName items, or when determining field names
from c.Walker.Ancestors NodeKindField entries, check whether each field has an
alias defined in the operation using c.operation and related helpers. If an
alias exists, use that alias name in the path instead of the actual field name
from the schema, since the response data structure will be keyed by the alias,
not the schema field name. This ensures both pending[].path and subPath work
correctly for aliased queries.

In `@docs/superpowers/plans/2026-05-26-defer-parallel-errors.md`:
- Around line 569-585: The clone-based context isolation in the defer-group
error handling prevents subgraph errors from being aggregated in
Context.SubgraphErrors(), which downstream callers depend on. Remove the
per-goroutine clone isolation by eliminating the `childCtx :=
ctx.clone(ctx.ctx)` line and pass the shared context directly to
r.resolveDeferTree instead. Then ensure all mutations to ctx.subgraphErrors
within the defer group goroutine are protected by db.Lock() (consistent with how
it's done in other parts of the code) so that errors are properly aggregated
into the request context without race conditions.

In `@execution/engine/execution_engine_defer_test.go`:
- Around line 699-700: The expectedResponse string in this test contains
malformed JSON with an extra closing brace before the `,"pending"` element. The
outer object is being closed prematurely with 4 closing braces after
"black@sabbat" when only 3 are needed. Remove one closing brace from the
sequence of closing braces that appears before `,"pending"` in the
expectedResponse string to properly structure the JSON so that the outer object
remains open to accept the pending, completed, and hasNext fields.

In `@execution/engine/execution_engine_test.go`:
- Around line 147-149: The comparison at line 148 using compactJSONForAssert
performs string-based JSON comparison, which fails when expectedJSONResponse and
actualResponse contain semantically identical JSON objects with keys in
different orders. Replace the string comparison assert.Equal call with a JSON
semantic comparison approach that parses both JSON strings into objects and
compares them structurally (either unmarshaling into maps or using a dedicated
JSON comparison library), rather than relying on compactJSONForAssert which does
not guarantee consistent key ordering.

---

Outside diff comments:
In `@docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md`:
- Around line 607-626: The current design decrements `remaining` before the
render lock, causing `hasNext` to be determined by fetch completion order rather
than emit order, which can result in slower fetches incorrectly marking
`hasNext:true` on the final payload. To fix this, move the logic that determines
the terminal flag (`hasNext`) so it is evaluated within the render/flush
critical section or during a tree walk operation instead of being derived from
when `remaining` is decremented. This ensures the final payload correctly
reflects whether additional chunks will actually be emitted, not just whether
all fetches have completed.

---

Nitpick comments:
In `@execution/engine/execution_engine_test.go`:
- Around line 151-159: The streaming response validation currently allows both
testCase.expectedResponse and testCase.expectedResponses assertions to run
simultaneously, creating ambiguous test failures when both fields are populated.
Change the second condition checking len(testCase.expectedResponses) > 0 from an
if statement to an else if statement, so that only one assertion path executes -
either the assert.Equal for expectedResponse or the assert.Contains for
expectedResponses, but never both.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 67884d71-388f-459b-911b-e8b76f7115e6

📥 Commits

Reviewing files that changed from the base of the PR and between 3232978 and 1cd9e90.

📒 Files selected for processing (48)
  • docs/superpowers/plans/2026-04-17-defer-id-string-to-int.md
  • docs/superpowers/plans/2026-04-17-defer-label-support.md
  • docs/superpowers/plans/2026-04-28-defer-label-refactor.md
  • docs/superpowers/plans/2026-04-28-defer-pending-completed.md
  • docs/superpowers/plans/2026-04-29-defer-subpath.md
  • docs/superpowers/plans/2026-05-18-defer-parallel-execution.md
  • docs/superpowers/plans/2026-05-20-defer-populate-parent-ids.md
  • docs/superpowers/plans/2026-05-26-defer-parallel-errors.md
  • docs/superpowers/plans/2026-06-09-per-fetch-subgraph-errors.md
  • docs/superpowers/specs/2026-05-15-defer-parallel-execution-design.md
  • docs/superpowers/specs/2026-05-26-defer-parallel-errors-design.md
  • execution/engine/engine_config_test.go
  • execution/engine/execution_engine_defer_test.go
  • execution/engine/execution_engine_helpers_test.go
  • execution/engine/execution_engine_test.go
  • v2/pkg/ast/ast_field.go
  • v2/pkg/astnormalization/astnormalization.go
  • v2/pkg/astnormalization/astnormalization_test.go
  • v2/pkg/astnormalization/defer_expand_into_internal.go
  • v2/pkg/astnormalization/defer_populate_parent_ids.go
  • v2/pkg/astnormalization/defer_populate_parent_ids_test.go
  • v2/pkg/asttransform/fixtures/complete.golden
  • v2/pkg/asttransform/fixtures/custom_query_name.golden
  • v2/pkg/asttransform/fixtures/mutation_only.golden
  • v2/pkg/asttransform/fixtures/schema_missing.golden
  • v2/pkg/asttransform/fixtures/simple.golden
  • v2/pkg/asttransform/fixtures/subscription_only.golden
  • v2/pkg/asttransform/fixtures/subscription_renamed.golden
  • v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go
  • v2/pkg/engine/plan/defer_info_collector.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/plan/visitor.go
  • v2/pkg/engine/postprocess/build_defer_tree.go
  • v2/pkg/engine/postprocess/build_defer_tree_test.go
  • v2/pkg/engine/postprocess/postprocess.go
  • v2/pkg/engine/resolve/const.go
  • v2/pkg/engine/resolve/data_buffer.go
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/engine/resolve/defer_tree.go
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/resolve/resolvable_test.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
  • v2/pkg/engine/resolve/response.go
💤 Files with no reviewable changes (33)
  • v2/pkg/engine/resolve/const.go
  • v2/pkg/engine/resolve/defer_tree.go
  • v2/pkg/engine/resolve/data_buffer.go
  • v2/pkg/astnormalization/defer_populate_parent_ids_test.go
  • v2/pkg/asttransform/fixtures/simple.golden
  • v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
  • v2/pkg/engine/resolve/data_buffer_test.go
  • v2/pkg/asttransform/fixtures/subscription_only.golden
  • v2/pkg/asttransform/fixtures/subscription_renamed.golden
  • v2/pkg/engine/postprocess/build_defer_tree.go
  • v2/pkg/astnormalization/astnormalization_test.go
  • v2/pkg/asttransform/fixtures/mutation_only.golden
  • v2/pkg/astnormalization/defer_populate_parent_ids.go
  • v2/pkg/asttransform/fixtures/complete.golden
  • v2/pkg/asttransform/fixtures/schema_missing.golden
  • v2/pkg/asttransform/fixtures/custom_query_name.golden
  • v2/pkg/engine/postprocess/build_defer_tree_test.go
  • v2/pkg/engine/resolve/response.go
  • v2/pkg/engine/plan/defer_info_collector.go
  • v2/pkg/astnormalization/astnormalization.go
  • v2/pkg/astnormalization/defer_expand_into_internal.go
  • v2/pkg/engine/resolve/per_fetch_subgraph_errors_test.go
  • v2/pkg/engine/plan/visitor.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/postprocess/postprocess.go
  • v2/pkg/engine/resolve/resolve_defer_parallel_test.go
  • v2/pkg/engine/resolve/resolvable_test.go
  • v2/pkg/ast/ast_field.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/resolve/loader.go
✅ Files skipped from review due to trivial changes (3)
  • execution/engine/engine_config_test.go
  • docs/superpowers/plans/2026-04-17-defer-id-string-to-int.md
  • docs/superpowers/plans/2026-06-09-per-fetch-subgraph-errors.md
🛑 Comments failed to post (5)
docs/superpowers/plans/2026-04-17-defer-label-support.md (1)

5-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mark this plan superseded.

This “purely additive” label-threading plan now conflicts with the later central-map / pending-completed / subPath docs in this PR. Keeping both active will send implementers down two incompatible contracts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-04-17-defer-label-support.md` around lines 5 - 8,
This plan document (2026-04-17-defer-label-support.md) is now superseded by
later plans in the PR (central-map / pending-completed / subPath docs) and
keeping both active creates conflicting implementation contracts. Add a clear
supersession notice at the top of the document indicating that this plan has
been superseded by the newer approaches, so implementers don't follow
incompatible directions.
docs/superpowers/plans/2026-04-29-defer-subpath.md (1)

120-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the response alias when computing descriptor.path.

The algorithm here derives the path from schema field names, so an aliased mount like alias: user { ... @defer ... } will be anchored at user instead of alias. That breaks both pending[].path and subPath for aliased queries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-04-29-defer-subpath.md` around lines 120 - 140,
The deferPath() function builds the descriptor.path using schema field names,
but it must use the response alias when one exists in the operation (e.g.,
alias: user { ... `@defer` ... }). When iterating through c.Walker.Path and
collecting FieldName items, or when determining field names from
c.Walker.Ancestors NodeKindField entries, check whether each field has an alias
defined in the operation using c.operation and related helpers. If an alias
exists, use that alias name in the path instead of the actual field name from
the schema, since the response data structure will be keyed by the alias, not
the schema field name. This ensures both pending[].path and subPath work
correctly for aliased queries.
docs/superpowers/plans/2026-05-26-defer-parallel-errors.md (1)

569-585: 🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n '\bSubgraphErrors\(' v2 execution docs

Repository: wundergraph/graphql-go-tools

Length of output: 4395


🏁 Script executed:

# Check the newer planning document to see if it addresses the defer-group subgraph errors issue
sed -n '690,900p' docs/superpowers/plans/2026-06-09-per-fetch-subgraph-errors.md | head -100

Repository: wundergraph/graphql-go-tools

Length of output: 3722


🏁 Script executed:

# Check the current implementation in resolve.go around the area mentioned in test comments
sed -n '660,680p' v2/pkg/engine/resolve/resolve.go

Repository: wundergraph/graphql-go-tools

Length of output: 929


🏁 Script executed:

# Check what the test expects around line 262-264 in resolve_defer_parallel_test.go
sed -n '260,320p' v2/pkg/engine/resolve/resolve_defer_parallel_test.go

Repository: wundergraph/graphql-go-tools

Length of output: 2089


🏁 Script executed:

# Check the actual current implementation in resolve.go around the defer parallel handling
sed -n '650,690p' v2/pkg/engine/resolve/resolve.go

Repository: wundergraph/graphql-go-tools

Length of output: 1578


🏁 Script executed:

# Check if there's a flushSubgraphErrors method that might have been added as part of the fix
rg -n 'flushSubgraphErrors' v2/pkg/engine/resolve/

Repository: wundergraph/graphql-go-tools

Length of output: 54


The clone-based isolation prevents defer-group subgraph errors from reaching Context.SubgraphErrors(), but downstream callers do depend on it.

Verification confirms that the router and other consumers rely on Context.SubgraphErrors() to aggregate all subgraph errors including those from defer groups (see loader hooks tests, test expectations in resolve_defer_parallel_test.go, and router usage). The limitation is not tenable; a merge-back path under lock after the group completes is the required fix. The per-goroutine clone should be removed and the context shared directly, with all mutations to ctx.subgraphErrors protected by db.Lock() as done elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-26-defer-parallel-errors.md` around lines 569
- 585, The clone-based context isolation in the defer-group error handling
prevents subgraph errors from being aggregated in Context.SubgraphErrors(),
which downstream callers depend on. Remove the per-goroutine clone isolation by
eliminating the `childCtx := ctx.clone(ctx.ctx)` line and pass the shared
context directly to r.resolveDeferTree instead. Then ensure all mutations to
ctx.subgraphErrors within the defer group goroutine are protected by db.Lock()
(consistent with how it's done in other parts of the code) so that errors are
properly aggregated into the request context without race conditions.
execution/engine/execution_engine_defer_test.go (1)

699-700: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Malformed JSON: extra closing brace before ,"pending".

The expected response has 4 closing braces (}}}}) after "black@sabbat" but should only have 3 (}}}). The current JSON closes the outer object before ,"pending", making it invalid.

🐛 Proposed fix
-				expectedResponse: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}}},"pending":[{"id":"1","path":["user"]}],"hasNext":true}
+				expectedResponse: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}},"pending":[{"id":"1","path":["user"]}],"hasNext":true}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/engine/execution_engine_defer_test.go` around lines 699 - 700, The
expectedResponse string in this test contains malformed JSON with an extra
closing brace before the `,"pending"` element. The outer object is being closed
prematurely with 4 closing braces after "black@sabbat" when only 3 are needed.
Remove one closing brace from the sequence of closing braces that appears before
`,"pending"` in the expectedResponse string to properly structure the JSON so
that the outer object remains open to accept the pending, completed, and hasNext
fields.
execution/engine/execution_engine_test.go (1)

147-149: ⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate compactJSONForAssert definition and usages"
rg -n -C3 'func compactJSONForAssert|compactJSONForAssert\(' execution/engine

echo
echo "Show helper implementation file candidates"
fd -a 'execution_engine_helpers_test.go|execution_engine_test.go' execution/engine

Repository: wundergraph/graphql-go-tools

Length of output: 1230


🏁 Script executed:

cat -n execution/engine/json_assert_test.go

Repository: wundergraph/graphql-go-tools

Length of output: 565


Confirm: compactJSONForAssert does not normalize key order; use semantic JSON comparison instead.

The function unmarshal then re-marshals JSON (lines 14–18 of json_assert_test.go), but Go's json.Marshal does not canonicalize key ordering. If expectedJSONResponse and actualResponse contain semantically identical objects with different key orders (e.g., {"a":1,"b":2} vs {"b":2,"a":1}), the string comparison at line 148 will incorrectly fail. Replace with a JSON semantic comparison library or normalize key order before comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/engine/execution_engine_test.go` around lines 147 - 149, The
comparison at line 148 using compactJSONForAssert performs string-based JSON
comparison, which fails when expectedJSONResponse and actualResponse contain
semantically identical JSON objects with keys in different orders. Replace the
string comparison assert.Equal call with a JSON semantic comparison approach
that parses both JSON strings into objects and compares them structurally
(either unmarshaling into maps or using a dedicated JSON comparison library),
rather than relying on compactJSONForAssert which does not guarantee consistent
key ordering.

Comment thread v2/pkg/engine/resolve/resolve_defer_parallel_test.go
@devsergiy devsergiy closed this Jul 2, 2026
@devsergiy devsergiy reopened this Jul 2, 2026
@devsergiy devsergiy closed this Jul 2, 2026
@devsergiy devsergiy deleted the feat/eng-7770-add-defer-support-additional branch July 2, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants