diff --git a/.beans/beans-rvfe--extract-core-graphql-into-pkgbeangraph.md b/.beans/beans-rvfe--extract-core-graphql-into-pkgbeangraph.md new file mode 100644 index 00000000..6f0f7ed8 --- /dev/null +++ b/.beans/beans-rvfe--extract-core-graphql-into-pkgbeangraph.md @@ -0,0 +1,37 @@ +--- +# beans-rvfe +title: Extract core GraphQL into pkg/beangraph +status: completed +type: task +priority: normal +created_at: 2026-03-21T08:11:51Z +updated_at: 2026-03-21T08:24:36Z +--- + +Split the GraphQL layer: core bean CRUD into pkg/beangraph/ (public), UI-specific resolvers stay in internal/graph/. Move model types to pkg/beangraph/model/. CLI commands switch to using CoreResolver directly, dropping internal/graph dependency. + +## Tasks + +- [x] Create `pkg/beangraph/` package with `CoreResolver` struct +- [x] Move model types from `internal/graph/model/` to `pkg/beangraph/model/` +- [x] Update gqlgen.yml to generate models in new location +- [x] Move core bean query resolvers to `pkg/beangraph/queries.go` +- [x] Move core bean mutation resolvers to `pkg/beangraph/mutations.go` +- [x] Move bean field resolvers to `pkg/beangraph/bean_fields.go` +- [x] Move filter logic to `pkg/beangraph/filters.go` +- [x] Move ETag/validation helpers to `pkg/beangraph/resolver.go` +- [x] Update `internal/graph/` to embed `CoreResolver` and delegate +- [x] Update CLI commands to use `beangraph.CoreResolver` directly +- [x] Run codegen, verify build compiles +- [x] Run tests + +## Summary of Changes + +- Created `pkg/beangraph/` package with `CoreResolver` struct containing all core bean CRUD operations +- Moved model types from `internal/graph/model/` to `pkg/beangraph/model/` (updated gqlgen.yml) +- Extracted core resolvers: queries (Bean, Beans, ProjectName, MainBranch), mutations (CreateBean, UpdateBean, DeleteBean, SetParent, Add/RemoveBlocking, Add/RemoveBlockedBy, ArchiveBean), bean field resolvers, filter logic, and ETag/validation helpers +- `internal/graph/Resolver` now embeds `*beangraph.CoreResolver` and delegates core operations +- CLI commands (`create`, `list`, `show`, `update`, `delete`, `roadmap`) now use `beangraph.CoreResolver` directly, removing their dependency on `internal/graph` +- TUI also migrated to use `beangraph.CoreResolver` directly +- Only `graphql.go` and `serve.go` still import `internal/graph` (expected — they need the full gqlgen schema) +- All 16 test packages pass, codegen works diff --git a/gqlgen.yml b/gqlgen.yml index 8a49b707..7f806499 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -9,7 +9,7 @@ exec: package: graph model: - filename: internal/graph/model/models_gen.go + filename: pkg/beangraph/model/models_gen.go package: model resolver: diff --git a/internal/commands/create.go b/internal/commands/create.go index 607bbec4..91d613b0 100644 --- a/internal/commands/create.go +++ b/internal/commands/create.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/internal/output" "github.com/hmans/beans/internal/ui" "github.com/spf13/cobra" @@ -98,9 +98,9 @@ var createCmd = &cobra.Command{ input.Prefix = &createPrefix } - // Create via GraphQL mutation - resolver := &graph.Resolver{Core: core} - b, err := resolver.Mutation().CreateBean(context.Background(), input) + // Create via core resolver + resolver := &beangraph.CoreResolver{Core: core} + b, err := resolver.CreateBean(context.Background(), input) if err != nil { return cmdError(createJSON, output.ErrFileError, "failed to create bean: %v", err) } diff --git a/internal/commands/delete.go b/internal/commands/delete.go index 28191587..b5b27409 100644 --- a/internal/commands/delete.go +++ b/internal/commands/delete.go @@ -9,7 +9,7 @@ import ( "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/internal/output" "github.com/spf13/cobra" ) @@ -36,12 +36,12 @@ warned and those references will be removed after confirmation. Use -f to skip a Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - resolver := &graph.Resolver{Core: core} + resolver := &beangraph.CoreResolver{Core: core} // Collect all beans and their incoming links upfront (validate before deleting) var targets []beanWithLinks for _, id := range args { - b, err := resolver.Query().Bean(ctx, id) + b, err := resolver.Bean(ctx, id) if err != nil { return cmdError(deleteJSON, output.ErrNotFound, "failed to find bean: %v", err) } @@ -66,7 +66,7 @@ warned and those references will be removed after confirmation. Use -f to skip a var deleted []*bean.Bean var totalLinksRemoved int for _, target := range targets { - _, err := resolver.Mutation().DeleteBean(ctx, target.bean.ID) + _, err := resolver.DeleteBean(ctx, target.bean.ID) if err != nil { return cmdError(deleteJSON, output.ErrFileError, "failed to delete bean %s: %v", target.bean.ID, err) } diff --git a/internal/commands/graphql.go b/internal/commands/graphql.go index ada473d4..4a3c9822 100644 --- a/internal/commands/graphql.go +++ b/internal/commands/graphql.go @@ -16,6 +16,7 @@ import ( "github.com/vektah/gqlparser/v2/formatter" "github.com/vektah/gqlparser/v2/gqlerror" "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" ) var ( @@ -138,7 +139,7 @@ func readFromStdin() (string, error) { // On error, it returns an error so the CLI can handle it appropriately. func executeQuery(query string, variables map[string]any, operationName string) ([]byte, error) { es := graph.NewExecutableSchema(graph.Config{ - Resolvers: &graph.Resolver{Core: core}, + Resolvers: &graph.Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}}, }) exec := executor.New(es) @@ -191,7 +192,7 @@ func printSchema() error { // This is exported so it can be used by other commands like prompt. func GetGraphQLSchema() string { es := graph.NewExecutableSchema(graph.Config{ - Resolvers: &graph.Resolver{Core: core}, + Resolvers: &graph.Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}}, }) var buf bytes.Buffer diff --git a/internal/commands/list.go b/internal/commands/list.go index bd65c30b..7a0ff71b 100644 --- a/internal/commands/list.go +++ b/internal/commands/list.go @@ -7,9 +7,9 @@ import ( "sort" "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" - "github.com/hmans/beans/internal/graph/model" "github.com/hmans/beans/internal/output" "github.com/hmans/beans/internal/ui" "github.com/spf13/cobra" @@ -111,9 +111,9 @@ Search Syntax (--search/-S): filter.ExcludeImplicitTerminal = &excludeImplicitTerminal } - // Execute query via GraphQL resolver - resolver := &graph.Resolver{Core: core} - beans, err := resolver.Query().Beans(context.Background(), filter) + // Execute query via core resolver + resolver := &beangraph.CoreResolver{Core: core} + beans, err := resolver.Beans(context.Background(), filter) if err != nil { return fmt.Errorf("querying beans: %w", err) } @@ -141,7 +141,7 @@ Search Syntax (--search/-S): // Default: tree view // We need all beans to find ancestors for context - allBeans, err := resolver.Query().Beans(context.Background(), nil) + allBeans, err := resolver.Beans(context.Background(), nil) if err != nil { return fmt.Errorf("querying all beans for tree: %w", err) } diff --git a/internal/commands/roadmap.go b/internal/commands/roadmap.go index 78a371ab..15f180f2 100644 --- a/internal/commands/roadmap.go +++ b/internal/commands/roadmap.go @@ -13,7 +13,7 @@ import ( "text/template" "github.com/hmans/beans/pkg/bean" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/spf13/cobra" ) @@ -60,8 +60,8 @@ var roadmapCmd = &cobra.Command{ Short: "Generate a Markdown roadmap from milestones and epics", RunE: func(cmd *cobra.Command, args []string) error { // Query all beans via GraphQL resolver - resolver := &graph.Resolver{Core: core} - allBeans, err := resolver.Query().Beans(context.Background(), nil) + resolver := &beangraph.CoreResolver{Core: core} + allBeans, err := resolver.Beans(context.Background(), nil) if err != nil { return fmt.Errorf("querying beans: %w", err) } diff --git a/internal/commands/serve.go b/internal/commands/serve.go index 7ad5cc2d..ad1bb6f5 100644 --- a/internal/commands/serve.go +++ b/internal/commands/serve.go @@ -27,6 +27,7 @@ import ( "github.com/hmans/beans/internal/terminal" "github.com/hmans/beans/internal/web" "github.com/hmans/beans/internal/worktree" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/pkg/config" "github.com/hmans/beans/pkg/forge" ) @@ -331,13 +332,13 @@ func runServer(port int, origins []string) error { // Create GraphQL server with explicit transports es := graph.NewExecutableSchema(graph.Config{ Resolvers: &graph.Resolver{ - Core: core, - WorktreeMgr: wtManager, - AgentMgr: agentMgr, - TerminalMgr: termMgr, - PortAlloc: portAlloc, - Forge: forgeProvider, - ProjectRoot: projectRoot, + CoreResolver: &beangraph.CoreResolver{Core: core}, + WorktreeMgr: wtManager, + AgentMgr: agentMgr, + TerminalMgr: termMgr, + PortAlloc: portAlloc, + Forge: forgeProvider, + ProjectRoot: projectRoot, }, }) gqlHandler := handler.New(es) diff --git a/internal/commands/show.go b/internal/commands/show.go index 2da71128..51a7547d 100644 --- a/internal/commands/show.go +++ b/internal/commands/show.go @@ -8,7 +8,7 @@ import ( "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/hmans/beans/pkg/bean" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/internal/output" "github.com/hmans/beans/internal/ui" "github.com/spf13/cobra" @@ -27,12 +27,12 @@ var showCmd = &cobra.Command{ Long: `Displays the full contents of one or more beans, including front matter and body.`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - resolver := &graph.Resolver{Core: core} + resolver := &beangraph.CoreResolver{Core: core} // Collect all beans var beans []*bean.Bean for _, id := range args { - b, err := resolver.Query().Bean(context.Background(), id) + b, err := resolver.Bean(context.Background(), id) if err != nil { if showJSON { return output.Error(output.ErrNotFound, err.Error()) diff --git a/internal/commands/update.go b/internal/commands/update.go index b32b7c75..a789e1c9 100644 --- a/internal/commands/update.go +++ b/internal/commands/update.go @@ -9,8 +9,8 @@ import ( "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/internal/output" "github.com/hmans/beans/internal/ui" "github.com/spf13/cobra" @@ -46,10 +46,10 @@ var updateCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - resolver := &graph.Resolver{Core: core} + resolver := &beangraph.CoreResolver{Core: core} // Find the bean - b, err := resolver.Query().Bean(ctx, args[0]) + b, err := resolver.Bean(ctx, args[0]) if err != nil { return cmdError(updateJSON, output.ErrNotFound, "failed to find bean: %v", err) } @@ -62,7 +62,7 @@ var updateCmd = &cobra.Command{ return cmdError(updateJSON, output.ErrNotFound, "bean not found: %s", args[0]) } // Re-query to get the model.Bean - b, err = resolver.Query().Bean(ctx, unarchived.ID) + b, err = resolver.Bean(ctx, unarchived.ID) if err != nil || b == nil { return cmdError(updateJSON, output.ErrNotFound, "bean not found: %s", args[0]) } @@ -93,7 +93,7 @@ var updateCmd = &cobra.Command{ // Apply all updates atomically via single UpdateBean mutation // This includes field updates, body modifications, and relationship changes if hasFieldUpdates(input) { - b, err = resolver.Mutation().UpdateBean(ctx, b.ID, input) + b, err = resolver.UpdateBean(ctx, b.ID, input) if err != nil { return mutationError(updateJSON, err) } diff --git a/internal/graph/agent_helpers.go b/internal/graph/agent_helpers.go index 847493eb..424b396b 100644 --- a/internal/graph/agent_helpers.go +++ b/internal/graph/agent_helpers.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/hmans/beans/internal/agent" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/forge" ) diff --git a/internal/graph/generated.go b/internal/graph/generated.go index c8585cdf..e6cf9f28 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -15,8 +15,8 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" - "github.com/hmans/beans/internal/graph/model" "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph/model" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" ) @@ -1608,7 +1608,7 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) func (ec *executionContext) field_Bean_blockedBy_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanFilter) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanFilter) if err != nil { return nil, err } @@ -1619,7 +1619,7 @@ func (ec *executionContext) field_Bean_blockedBy_args(ctx context.Context, rawAr func (ec *executionContext) field_Bean_blocking_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanFilter) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanFilter) if err != nil { return nil, err } @@ -1630,7 +1630,7 @@ func (ec *executionContext) field_Bean_blocking_args(ctx context.Context, rawArg func (ec *executionContext) field_Bean_children_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanFilter) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanFilter) if err != nil { return nil, err } @@ -1705,7 +1705,7 @@ func (ec *executionContext) field_Mutation_clearAgentSession_args(ctx context.Co func (ec *executionContext) field_Mutation_createBean_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNCreateBeanInput2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐCreateBeanInput) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNCreateBeanInput2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐCreateBeanInput) if err != nil { return nil, err } @@ -1860,7 +1860,7 @@ func (ec *executionContext) field_Mutation_sendAgentMessage_args(ctx context.Con return nil, err } args["message"] = arg1 - arg2, err := graphql.ProcessArgField(ctx, rawArgs, "images", ec.unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐImageInputᚄ) + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "images", ec.unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐImageInputᚄ) if err != nil { return nil, err } @@ -1908,7 +1908,7 @@ func (ec *executionContext) field_Mutation_setAgentPendingInteraction_args(ctx c return nil, err } args["beanId"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "type", ec.unmarshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐInteractionType) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "type", ec.unmarshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐInteractionType) if err != nil { return nil, err } @@ -1999,7 +1999,7 @@ func (ec *executionContext) field_Mutation_updateBean_args(ctx context.Context, return nil, err } args["id"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateBeanInput2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐUpdateBeanInput) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateBeanInput2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐUpdateBeanInput) if err != nil { return nil, err } @@ -2102,7 +2102,7 @@ func (ec *executionContext) field_Query_bean_args(ctx context.Context, rawArgs m func (ec *executionContext) field_Query_beans_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanFilter) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanFilter) if err != nil { return nil, err } @@ -2288,7 +2288,7 @@ func (ec *executionContext) _ActiveAgentStatus_status(ctx context.Context, field return obj.Status, nil }, nil, - ec.marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSessionStatus, + ec.marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSessionStatus, true, true, ) @@ -2462,7 +2462,7 @@ func (ec *executionContext) _AgentMessage_role(ctx context.Context, field graphq return obj.Role, nil }, nil, - ec.marshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageRole, + ec.marshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageRole, true, true, ) @@ -2520,7 +2520,7 @@ func (ec *executionContext) _AgentMessage_images(ctx context.Context, field grap return obj.Images, nil }, nil, - ec.marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageImageᚄ, + ec.marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageImageᚄ, true, true, ) @@ -2700,7 +2700,7 @@ func (ec *executionContext) _AgentSession_status(ctx context.Context, field grap return obj.Status, nil }, nil, - ec.marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSessionStatus, + ec.marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSessionStatus, true, true, ) @@ -2729,7 +2729,7 @@ func (ec *executionContext) _AgentSession_messages(ctx context.Context, field gr return obj.Messages, nil }, nil, - ec.marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageᚄ, + ec.marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageᚄ, true, true, ) @@ -2913,7 +2913,7 @@ func (ec *executionContext) _AgentSession_pendingInteraction(ctx context.Context return obj.PendingInteraction, nil }, nil, - ec.marshalOPendingInteraction2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐPendingInteraction, + ec.marshalOPendingInteraction2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐPendingInteraction, true, false, ) @@ -2979,7 +2979,7 @@ func (ec *executionContext) _AgentSession_subagentActivities(ctx context.Context return obj.SubagentActivities, nil }, nil, - ec.marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐSubagentActivityᚄ, + ec.marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐSubagentActivityᚄ, true, true, ) @@ -3192,7 +3192,7 @@ func (ec *executionContext) _AskUserQuestion_options(ctx context.Context, field return obj.Options, nil }, nil, - ec.marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserOptionᚄ, + ec.marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserOptionᚄ, true, true, ) @@ -4159,7 +4159,7 @@ func (ec *executionContext) _BeanChangeEvent_type(ctx context.Context, field gra return obj.Type, nil }, nil, - ec.marshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐChangeType, + ec.marshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐChangeType, true, true, ) @@ -5380,7 +5380,7 @@ func (ec *executionContext) _Mutation_createWorktree(ctx context.Context, field return ec.resolvers.Mutation().CreateWorktree(ctx, fc.Args["name"].(string)) }, nil, - ec.marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktree, + ec.marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktree, true, true, ) @@ -6010,7 +6010,7 @@ func (ec *executionContext) _PendingInteraction_type(ctx context.Context, field return obj.Type, nil }, nil, - ec.marshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐInteractionType, + ec.marshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐInteractionType, true, true, ) @@ -6068,7 +6068,7 @@ func (ec *executionContext) _PendingInteraction_questions(ctx context.Context, f return obj.Questions, nil }, nil, - ec.marshalOAskUserQuestion2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserQuestionᚄ, + ec.marshalOAskUserQuestion2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserQuestionᚄ, true, false, ) @@ -6521,7 +6521,7 @@ func (ec *executionContext) _Query_worktrees(ctx context.Context, field graphql. return ec.resolvers.Query().Worktrees(ctx) }, nil, - ec.marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeᚄ, + ec.marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeᚄ, true, true, ) @@ -6579,7 +6579,7 @@ func (ec *executionContext) _Query_agentSession(ctx context.Context, field graph return ec.resolvers.Query().AgentSession(ctx, fc.Args["beanId"].(string)) }, nil, - ec.marshalOAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSession, + ec.marshalOAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSession, true, false, ) @@ -6648,7 +6648,7 @@ func (ec *executionContext) _Query_fileChanges(ctx context.Context, field graphq return ec.resolvers.Query().FileChanges(ctx, fc.Args["path"].(*string)) }, nil, - ec.marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐFileChangeᚄ, + ec.marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐFileChangeᚄ, true, true, ) @@ -6701,7 +6701,7 @@ func (ec *executionContext) _Query_allFileChanges(ctx context.Context, field gra return ec.resolvers.Query().AllFileChanges(ctx, fc.Args["path"].(*string)) }, nil, - ec.marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐFileChangeᚄ, + ec.marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐFileChangeᚄ, true, true, ) @@ -6836,7 +6836,7 @@ func (ec *executionContext) _Query_branchStatus(ctx context.Context, field graph return ec.resolvers.Query().BranchStatus(ctx, fc.Args["path"].(*string)) }, nil, - ec.marshalNBranchStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBranchStatus, + ec.marshalNBranchStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBranchStatus, true, true, ) @@ -6912,7 +6912,7 @@ func (ec *executionContext) _Query_agentActions(ctx context.Context, field graph return ec.resolvers.Query().AgentActions(ctx, fc.Args["beanId"].(string), fc.Args["skipForge"].(*bool)) }, nil, - ec.marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentActionᚄ, + ec.marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentActionᚄ, true, true, ) @@ -7445,7 +7445,7 @@ func (ec *executionContext) _Subscription_beanChanged(ctx context.Context, field return ec.resolvers.Subscription().BeanChanged(ctx, fc.Args["includeInitial"].(*bool)) }, nil, - ec.marshalNBeanChangeEvent2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanChangeEvent, + ec.marshalNBeanChangeEvent2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanChangeEvent, true, true, ) @@ -7495,7 +7495,7 @@ func (ec *executionContext) _Subscription_worktreesChanged(ctx context.Context, return ec.resolvers.Subscription().WorktreesChanged(ctx) }, nil, - ec.marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeᚄ, + ec.marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeᚄ, true, true, ) @@ -7553,7 +7553,7 @@ func (ec *executionContext) _Subscription_agentSessionChanged(ctx context.Contex return ec.resolvers.Subscription().AgentSessionChanged(ctx, fc.Args["beanId"].(string)) }, nil, - ec.marshalNAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSession, + ec.marshalNAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSession, true, true, ) @@ -7621,7 +7621,7 @@ func (ec *executionContext) _Subscription_activeAgentStatuses(ctx context.Contex return ec.resolvers.Subscription().ActiveAgentStatuses(ctx) }, nil, - ec.marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐActiveAgentStatusᚄ, + ec.marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐActiveAgentStatusᚄ, true, true, ) @@ -7656,7 +7656,7 @@ func (ec *executionContext) _Subscription_workspaceStatuses(ctx context.Context, return ec.resolvers.Subscription().WorkspaceStatuses(ctx) }, nil, - ec.marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorkspaceStatusᚄ, + ec.marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorkspaceStatusᚄ, true, true, ) @@ -8120,7 +8120,7 @@ func (ec *executionContext) _Worktree_setupStatus(ctx context.Context, field gra return obj.SetupStatus, nil }, nil, - ec.marshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeSetupStatus, + ec.marshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeSetupStatus, true, false, ) @@ -8178,7 +8178,7 @@ func (ec *executionContext) _Worktree_pullRequest(ctx context.Context, field gra return obj.PullRequest, nil }, nil, - ec.marshalOPullRequest2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐPullRequest, + ec.marshalOPullRequest2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐPullRequest, true, false, ) @@ -9851,7 +9851,7 @@ func (ec *executionContext) unmarshalInputBodyModification(ctx context.Context, switch k { case "replace": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("replace")) - data, err := ec.unmarshalOReplaceOperation2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐReplaceOperationᚄ(ctx, v) + data, err := ec.unmarshalOReplaceOperation2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐReplaceOperationᚄ(ctx, v) if err != nil { return it, err } @@ -10099,7 +10099,7 @@ func (ec *executionContext) unmarshalInputUpdateBeanInput(ctx context.Context, o it.Body = data case "bodyMod": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bodyMod")) - data, err := ec.unmarshalOBodyModification2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBodyModification(ctx, v) + data, err := ec.unmarshalOBodyModification2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBodyModification(ctx, v) if err != nil { return it, err } @@ -12522,7 +12522,7 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** -func (ec *executionContext) marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐActiveAgentStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.ActiveAgentStatus) graphql.Marshaler { +func (ec *executionContext) marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐActiveAgentStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.ActiveAgentStatus) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12546,7 +12546,7 @@ func (ec *executionContext) marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNActiveAgentStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐActiveAgentStatus(ctx, sel, v[i]) + ret[i] = ec.marshalNActiveAgentStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐActiveAgentStatus(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12566,7 +12566,7 @@ func (ec *executionContext) marshalNActiveAgentStatus2ᚕᚖgithubᚗcomᚋhmans return ret } -func (ec *executionContext) marshalNActiveAgentStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐActiveAgentStatus(ctx context.Context, sel ast.SelectionSet, v *model.ActiveAgentStatus) graphql.Marshaler { +func (ec *executionContext) marshalNActiveAgentStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐActiveAgentStatus(ctx context.Context, sel ast.SelectionSet, v *model.ActiveAgentStatus) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12576,7 +12576,7 @@ func (ec *executionContext) marshalNActiveAgentStatus2ᚖgithubᚗcomᚋhmansᚋ return ec._ActiveAgentStatus(ctx, sel, v) } -func (ec *executionContext) marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentActionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentAction) graphql.Marshaler { +func (ec *executionContext) marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentActionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentAction) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12600,7 +12600,7 @@ func (ec *executionContext) marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbea if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNAgentAction2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentAction(ctx, sel, v[i]) + ret[i] = ec.marshalNAgentAction2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentAction(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12620,7 +12620,7 @@ func (ec *executionContext) marshalNAgentAction2ᚕᚖgithubᚗcomᚋhmansᚋbea return ret } -func (ec *executionContext) marshalNAgentAction2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentAction(ctx context.Context, sel ast.SelectionSet, v *model.AgentAction) graphql.Marshaler { +func (ec *executionContext) marshalNAgentAction2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentAction(ctx context.Context, sel ast.SelectionSet, v *model.AgentAction) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12630,7 +12630,7 @@ func (ec *executionContext) marshalNAgentAction2ᚖgithubᚗcomᚋhmansᚋbeans return ec._AgentAction(ctx, sel, v) } -func (ec *executionContext) marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentMessage) graphql.Marshaler { +func (ec *executionContext) marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentMessage) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12654,7 +12654,7 @@ func (ec *executionContext) marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbe if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNAgentMessage2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessage(ctx, sel, v[i]) + ret[i] = ec.marshalNAgentMessage2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessage(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12674,7 +12674,7 @@ func (ec *executionContext) marshalNAgentMessage2ᚕᚖgithubᚗcomᚋhmansᚋbe return ret } -func (ec *executionContext) marshalNAgentMessage2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessage(ctx context.Context, sel ast.SelectionSet, v *model.AgentMessage) graphql.Marshaler { +func (ec *executionContext) marshalNAgentMessage2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessage(ctx context.Context, sel ast.SelectionSet, v *model.AgentMessage) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12684,7 +12684,7 @@ func (ec *executionContext) marshalNAgentMessage2ᚖgithubᚗcomᚋhmansᚋbeans return ec._AgentMessage(ctx, sel, v) } -func (ec *executionContext) marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageImageᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentMessageImage) graphql.Marshaler { +func (ec *executionContext) marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageImageᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AgentMessageImage) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12708,7 +12708,7 @@ func (ec *executionContext) marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNAgentMessageImage2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageImage(ctx, sel, v[i]) + ret[i] = ec.marshalNAgentMessageImage2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageImage(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12728,7 +12728,7 @@ func (ec *executionContext) marshalNAgentMessageImage2ᚕᚖgithubᚗcomᚋhmans return ret } -func (ec *executionContext) marshalNAgentMessageImage2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageImage(ctx context.Context, sel ast.SelectionSet, v *model.AgentMessageImage) graphql.Marshaler { +func (ec *executionContext) marshalNAgentMessageImage2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageImage(ctx context.Context, sel ast.SelectionSet, v *model.AgentMessageImage) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12738,21 +12738,21 @@ func (ec *executionContext) marshalNAgentMessageImage2ᚖgithubᚗcomᚋhmansᚋ return ec._AgentMessageImage(ctx, sel, v) } -func (ec *executionContext) unmarshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageRole(ctx context.Context, v any) (model.AgentMessageRole, error) { +func (ec *executionContext) unmarshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageRole(ctx context.Context, v any) (model.AgentMessageRole, error) { var res model.AgentMessageRole err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentMessageRole(ctx context.Context, sel ast.SelectionSet, v model.AgentMessageRole) graphql.Marshaler { +func (ec *executionContext) marshalNAgentMessageRole2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentMessageRole(ctx context.Context, sel ast.SelectionSet, v model.AgentMessageRole) graphql.Marshaler { return v } -func (ec *executionContext) marshalNAgentSession2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v model.AgentSession) graphql.Marshaler { +func (ec *executionContext) marshalNAgentSession2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v model.AgentSession) graphql.Marshaler { return ec._AgentSession(ctx, sel, &v) } -func (ec *executionContext) marshalNAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v *model.AgentSession) graphql.Marshaler { +func (ec *executionContext) marshalNAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v *model.AgentSession) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12762,17 +12762,17 @@ func (ec *executionContext) marshalNAgentSession2ᚖgithubᚗcomᚋhmansᚋbeans return ec._AgentSession(ctx, sel, v) } -func (ec *executionContext) unmarshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSessionStatus(ctx context.Context, v any) (model.AgentSessionStatus, error) { +func (ec *executionContext) unmarshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSessionStatus(ctx context.Context, v any) (model.AgentSessionStatus, error) { var res model.AgentSessionStatus err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSessionStatus(ctx context.Context, sel ast.SelectionSet, v model.AgentSessionStatus) graphql.Marshaler { +func (ec *executionContext) marshalNAgentSessionStatus2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSessionStatus(ctx context.Context, sel ast.SelectionSet, v model.AgentSessionStatus) graphql.Marshaler { return v } -func (ec *executionContext) marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserOptionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AskUserOption) graphql.Marshaler { +func (ec *executionContext) marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserOptionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AskUserOption) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12796,7 +12796,7 @@ func (ec *executionContext) marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋb if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNAskUserOption2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserOption(ctx, sel, v[i]) + ret[i] = ec.marshalNAskUserOption2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserOption(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12816,7 +12816,7 @@ func (ec *executionContext) marshalNAskUserOption2ᚕᚖgithubᚗcomᚋhmansᚋb return ret } -func (ec *executionContext) marshalNAskUserOption2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserOption(ctx context.Context, sel ast.SelectionSet, v *model.AskUserOption) graphql.Marshaler { +func (ec *executionContext) marshalNAskUserOption2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserOption(ctx context.Context, sel ast.SelectionSet, v *model.AskUserOption) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12826,7 +12826,7 @@ func (ec *executionContext) marshalNAskUserOption2ᚖgithubᚗcomᚋhmansᚋbean return ec._AskUserOption(ctx, sel, v) } -func (ec *executionContext) marshalNAskUserQuestion2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserQuestion(ctx context.Context, sel ast.SelectionSet, v *model.AskUserQuestion) graphql.Marshaler { +func (ec *executionContext) marshalNAskUserQuestion2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserQuestion(ctx context.Context, sel ast.SelectionSet, v *model.AskUserQuestion) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12894,11 +12894,11 @@ func (ec *executionContext) marshalNBean2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkg return ec._Bean(ctx, sel, v) } -func (ec *executionContext) marshalNBeanChangeEvent2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanChangeEvent(ctx context.Context, sel ast.SelectionSet, v model.BeanChangeEvent) graphql.Marshaler { +func (ec *executionContext) marshalNBeanChangeEvent2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanChangeEvent(ctx context.Context, sel ast.SelectionSet, v model.BeanChangeEvent) graphql.Marshaler { return ec._BeanChangeEvent(ctx, sel, &v) } -func (ec *executionContext) marshalNBeanChangeEvent2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanChangeEvent(ctx context.Context, sel ast.SelectionSet, v *model.BeanChangeEvent) graphql.Marshaler { +func (ec *executionContext) marshalNBeanChangeEvent2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanChangeEvent(ctx context.Context, sel ast.SelectionSet, v *model.BeanChangeEvent) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12924,11 +12924,11 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } -func (ec *executionContext) marshalNBranchStatus2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBranchStatus(ctx context.Context, sel ast.SelectionSet, v model.BranchStatus) graphql.Marshaler { +func (ec *executionContext) marshalNBranchStatus2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBranchStatus(ctx context.Context, sel ast.SelectionSet, v model.BranchStatus) graphql.Marshaler { return ec._BranchStatus(ctx, sel, &v) } -func (ec *executionContext) marshalNBranchStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBranchStatus(ctx context.Context, sel ast.SelectionSet, v *model.BranchStatus) graphql.Marshaler { +func (ec *executionContext) marshalNBranchStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBranchStatus(ctx context.Context, sel ast.SelectionSet, v *model.BranchStatus) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -12938,22 +12938,22 @@ func (ec *executionContext) marshalNBranchStatus2ᚖgithubᚗcomᚋhmansᚋbeans return ec._BranchStatus(ctx, sel, v) } -func (ec *executionContext) unmarshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐChangeType(ctx context.Context, v any) (model.ChangeType, error) { +func (ec *executionContext) unmarshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐChangeType(ctx context.Context, v any) (model.ChangeType, error) { var res model.ChangeType err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐChangeType(ctx context.Context, sel ast.SelectionSet, v model.ChangeType) graphql.Marshaler { +func (ec *executionContext) marshalNChangeType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐChangeType(ctx context.Context, sel ast.SelectionSet, v model.ChangeType) graphql.Marshaler { return v } -func (ec *executionContext) unmarshalNCreateBeanInput2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐCreateBeanInput(ctx context.Context, v any) (model.CreateBeanInput, error) { +func (ec *executionContext) unmarshalNCreateBeanInput2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐCreateBeanInput(ctx context.Context, v any) (model.CreateBeanInput, error) { res, err := ec.unmarshalInputCreateBeanInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐFileChangeᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.FileChange) graphql.Marshaler { +func (ec *executionContext) marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐFileChangeᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.FileChange) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -12977,7 +12977,7 @@ func (ec *executionContext) marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbean if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNFileChange2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐFileChange(ctx, sel, v[i]) + ret[i] = ec.marshalNFileChange2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐFileChange(ctx, sel, v[i]) } if isLen1 { f(i) @@ -12997,7 +12997,7 @@ func (ec *executionContext) marshalNFileChange2ᚕᚖgithubᚗcomᚋhmansᚋbean return ret } -func (ec *executionContext) marshalNFileChange2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐFileChange(ctx context.Context, sel ast.SelectionSet, v *model.FileChange) graphql.Marshaler { +func (ec *executionContext) marshalNFileChange2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐFileChange(ctx context.Context, sel ast.SelectionSet, v *model.FileChange) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -13023,7 +13023,7 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } -func (ec *executionContext) unmarshalNImageInput2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐImageInput(ctx context.Context, v any) (*model.ImageInput, error) { +func (ec *executionContext) unmarshalNImageInput2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐImageInput(ctx context.Context, v any) (*model.ImageInput, error) { res, err := ec.unmarshalInputImageInput(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) } @@ -13044,17 +13044,17 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } -func (ec *executionContext) unmarshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐInteractionType(ctx context.Context, v any) (model.InteractionType, error) { +func (ec *executionContext) unmarshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐInteractionType(ctx context.Context, v any) (model.InteractionType, error) { var res model.InteractionType err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐInteractionType(ctx context.Context, sel ast.SelectionSet, v model.InteractionType) graphql.Marshaler { +func (ec *executionContext) marshalNInteractionType2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐInteractionType(ctx context.Context, sel ast.SelectionSet, v model.InteractionType) graphql.Marshaler { return v } -func (ec *executionContext) unmarshalNReplaceOperation2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐReplaceOperation(ctx context.Context, v any) (*model.ReplaceOperation, error) { +func (ec *executionContext) unmarshalNReplaceOperation2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐReplaceOperation(ctx context.Context, v any) (*model.ReplaceOperation, error) { res, err := ec.unmarshalInputReplaceOperation(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) } @@ -13105,7 +13105,7 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } -func (ec *executionContext) marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐSubagentActivityᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.SubagentActivity) graphql.Marshaler { +func (ec *executionContext) marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐSubagentActivityᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.SubagentActivity) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -13129,7 +13129,7 @@ func (ec *executionContext) marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNSubagentActivity2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐSubagentActivity(ctx, sel, v[i]) + ret[i] = ec.marshalNSubagentActivity2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐSubagentActivity(ctx, sel, v[i]) } if isLen1 { f(i) @@ -13149,7 +13149,7 @@ func (ec *executionContext) marshalNSubagentActivity2ᚕᚖgithubᚗcomᚋhmans return ret } -func (ec *executionContext) marshalNSubagentActivity2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐSubagentActivity(ctx context.Context, sel ast.SelectionSet, v *model.SubagentActivity) graphql.Marshaler { +func (ec *executionContext) marshalNSubagentActivity2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐSubagentActivity(ctx context.Context, sel ast.SelectionSet, v *model.SubagentActivity) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -13181,12 +13181,12 @@ func (ec *executionContext) marshalNTime2ᚖtimeᚐTime(ctx context.Context, sel return res } -func (ec *executionContext) unmarshalNUpdateBeanInput2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐUpdateBeanInput(ctx context.Context, v any) (model.UpdateBeanInput, error) { +func (ec *executionContext) unmarshalNUpdateBeanInput2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐUpdateBeanInput(ctx context.Context, v any) (model.UpdateBeanInput, error) { res, err := ec.unmarshalInputUpdateBeanInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorkspaceStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.WorkspaceStatus) graphql.Marshaler { +func (ec *executionContext) marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorkspaceStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.WorkspaceStatus) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -13210,7 +13210,7 @@ func (ec *executionContext) marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNWorkspaceStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorkspaceStatus(ctx, sel, v[i]) + ret[i] = ec.marshalNWorkspaceStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorkspaceStatus(ctx, sel, v[i]) } if isLen1 { f(i) @@ -13230,7 +13230,7 @@ func (ec *executionContext) marshalNWorkspaceStatus2ᚕᚖgithubᚗcomᚋhmans return ret } -func (ec *executionContext) marshalNWorkspaceStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorkspaceStatus(ctx context.Context, sel ast.SelectionSet, v *model.WorkspaceStatus) graphql.Marshaler { +func (ec *executionContext) marshalNWorkspaceStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorkspaceStatus(ctx context.Context, sel ast.SelectionSet, v *model.WorkspaceStatus) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -13240,11 +13240,11 @@ func (ec *executionContext) marshalNWorkspaceStatus2ᚖgithubᚗcomᚋhmansᚋbe return ec._WorkspaceStatus(ctx, sel, v) } -func (ec *executionContext) marshalNWorktree2githubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktree(ctx context.Context, sel ast.SelectionSet, v model.Worktree) graphql.Marshaler { +func (ec *executionContext) marshalNWorktree2githubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktree(ctx context.Context, sel ast.SelectionSet, v model.Worktree) graphql.Marshaler { return ec._Worktree(ctx, sel, &v) } -func (ec *executionContext) marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Worktree) graphql.Marshaler { +func (ec *executionContext) marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Worktree) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -13268,7 +13268,7 @@ func (ec *executionContext) marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktree(ctx, sel, v[i]) + ret[i] = ec.marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktree(ctx, sel, v[i]) } if isLen1 { f(i) @@ -13288,7 +13288,7 @@ func (ec *executionContext) marshalNWorktree2ᚕᚖgithubᚗcomᚋhmansᚋbeans return ret } -func (ec *executionContext) marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktree(ctx context.Context, sel ast.SelectionSet, v *model.Worktree) graphql.Marshaler { +func (ec *executionContext) marshalNWorktree2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktree(ctx context.Context, sel ast.SelectionSet, v *model.Worktree) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -13551,14 +13551,14 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } -func (ec *executionContext) marshalOAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v *model.AgentSession) graphql.Marshaler { +func (ec *executionContext) marshalOAgentSession2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAgentSession(ctx context.Context, sel ast.SelectionSet, v *model.AgentSession) graphql.Marshaler { if v == nil { return graphql.Null } return ec._AgentSession(ctx, sel, v) } -func (ec *executionContext) marshalOAskUserQuestion2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserQuestionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AskUserQuestion) graphql.Marshaler { +func (ec *executionContext) marshalOAskUserQuestion2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserQuestionᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.AskUserQuestion) graphql.Marshaler { if v == nil { return graphql.Null } @@ -13585,7 +13585,7 @@ func (ec *executionContext) marshalOAskUserQuestion2ᚕᚖgithubᚗcomᚋhmans if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNAskUserQuestion2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐAskUserQuestion(ctx, sel, v[i]) + ret[i] = ec.marshalNAskUserQuestion2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐAskUserQuestion(ctx, sel, v[i]) } if isLen1 { f(i) @@ -13659,7 +13659,7 @@ func (ec *executionContext) marshalOBean2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkg return ec._Bean(ctx, sel, v) } -func (ec *executionContext) unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBeanFilter(ctx context.Context, v any) (*model.BeanFilter, error) { +func (ec *executionContext) unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBeanFilter(ctx context.Context, v any) (*model.BeanFilter, error) { if v == nil { return nil, nil } @@ -13667,7 +13667,7 @@ func (ec *executionContext) unmarshalOBeanFilter2ᚖgithubᚗcomᚋhmansᚋbeans return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalOBodyModification2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐBodyModification(ctx context.Context, v any) (*model.BodyModification, error) { +func (ec *executionContext) unmarshalOBodyModification2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐBodyModification(ctx context.Context, v any) (*model.BodyModification, error) { if v == nil { return nil, nil } @@ -13705,7 +13705,7 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐImageInputᚄ(ctx context.Context, v any) ([]*model.ImageInput, error) { +func (ec *executionContext) unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐImageInputᚄ(ctx context.Context, v any) ([]*model.ImageInput, error) { if v == nil { return nil, nil } @@ -13715,7 +13715,7 @@ func (ec *executionContext) unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbe res := make([]*model.ImageInput, len(vSlice)) for i := range vSlice { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNImageInput2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐImageInput(ctx, vSlice[i]) + res[i], err = ec.unmarshalNImageInput2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐImageInput(ctx, vSlice[i]) if err != nil { return nil, err } @@ -13723,21 +13723,21 @@ func (ec *executionContext) unmarshalOImageInput2ᚕᚖgithubᚗcomᚋhmansᚋbe return res, nil } -func (ec *executionContext) marshalOPendingInteraction2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐPendingInteraction(ctx context.Context, sel ast.SelectionSet, v *model.PendingInteraction) graphql.Marshaler { +func (ec *executionContext) marshalOPendingInteraction2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐPendingInteraction(ctx context.Context, sel ast.SelectionSet, v *model.PendingInteraction) graphql.Marshaler { if v == nil { return graphql.Null } return ec._PendingInteraction(ctx, sel, v) } -func (ec *executionContext) marshalOPullRequest2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐPullRequest(ctx context.Context, sel ast.SelectionSet, v *model.PullRequest) graphql.Marshaler { +func (ec *executionContext) marshalOPullRequest2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐPullRequest(ctx context.Context, sel ast.SelectionSet, v *model.PullRequest) graphql.Marshaler { if v == nil { return graphql.Null } return ec._PullRequest(ctx, sel, v) } -func (ec *executionContext) unmarshalOReplaceOperation2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐReplaceOperationᚄ(ctx context.Context, v any) ([]*model.ReplaceOperation, error) { +func (ec *executionContext) unmarshalOReplaceOperation2ᚕᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐReplaceOperationᚄ(ctx context.Context, v any) ([]*model.ReplaceOperation, error) { if v == nil { return nil, nil } @@ -13747,7 +13747,7 @@ func (ec *executionContext) unmarshalOReplaceOperation2ᚕᚖgithubᚗcomᚋhman res := make([]*model.ReplaceOperation, len(vSlice)) for i := range vSlice { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNReplaceOperation2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐReplaceOperation(ctx, vSlice[i]) + res[i], err = ec.unmarshalNReplaceOperation2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐReplaceOperation(ctx, vSlice[i]) if err != nil { return nil, err } @@ -13821,7 +13821,7 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } -func (ec *executionContext) unmarshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeSetupStatus(ctx context.Context, v any) (*model.WorktreeSetupStatus, error) { +func (ec *executionContext) unmarshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeSetupStatus(ctx context.Context, v any) (*model.WorktreeSetupStatus, error) { if v == nil { return nil, nil } @@ -13830,7 +13830,7 @@ func (ec *executionContext) unmarshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhman return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋinternalᚋgraphᚋmodelᚐWorktreeSetupStatus(ctx context.Context, sel ast.SelectionSet, v *model.WorktreeSetupStatus) graphql.Marshaler { +func (ec *executionContext) marshalOWorktreeSetupStatus2ᚖgithubᚗcomᚋhmansᚋbeansᚋpkgᚋbeangraphᚋmodelᚐWorktreeSetupStatus(ctx context.Context, sel ast.SelectionSet, v *model.WorktreeSetupStatus) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go index 0c776908..5b98c48c 100644 --- a/internal/graph/resolver.go +++ b/internal/graph/resolver.go @@ -2,16 +2,16 @@ package graph import ( "context" - "fmt" "github.com/hmans/beans/internal/agent" "github.com/hmans/beans/internal/gitutil" - "github.com/hmans/beans/internal/graph/model" "github.com/hmans/beans/internal/portalloc" "github.com/hmans/beans/internal/terminal" "github.com/hmans/beans/internal/worktree" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/forge" ) @@ -26,153 +26,16 @@ const CentralSessionID = "__central__" const RunSessionSuffix = "__run" // Resolver is the root resolver for the GraphQL schema. -// It holds a reference to beancore.Core for data access. +// It embeds CoreResolver for bean CRUD operations and adds UI-specific +// concerns (agents, worktrees, terminals, git operations). type Resolver struct { - Core *beancore.Core + *beangraph.CoreResolver WorktreeMgr *worktree.Manager AgentMgr *agent.Manager TerminalMgr *terminal.Manager PortAlloc *portalloc.Allocator - Forge forge.Provider // git forge provider (GitHub, GitLab, etc.) — nil if not detected - ProjectRoot string // absolute path to the project root (parent of .beans) -} - -// ETagMismatchError is returned when an ETag validation fails. -// This allows callers to distinguish concurrency conflicts from other errors. -type ETagMismatchError struct { - Provided string - Current string -} - -func (e *ETagMismatchError) Error() string { - return fmt.Sprintf("etag mismatch: provided %s, current is %s", e.Provided, e.Current) -} - -// ETagRequiredError is returned when require_if_match is enabled and no ETag is provided. -type ETagRequiredError struct{} - -func (e *ETagRequiredError) Error() string { - return "if-match etag is required (set require_if_match: false in config to disable)" -} - -// validateETag checks if the provided ifMatch etag matches the bean's current etag. -// Returns an error if validation fails or if require_if_match is enabled and no etag provided. -func (r *Resolver) validateETag(b *bean.Bean, ifMatch *string) error { - cfg := r.Core.Config() - requireIfMatch := cfg != nil && cfg.Beans.RequireIfMatch - - // If require_if_match is enabled and no etag provided, reject - if requireIfMatch && (ifMatch == nil || *ifMatch == "") { - return &ETagRequiredError{} - } - - // If ifMatch provided, validate it - if ifMatch != nil && *ifMatch != "" { - currentETag := b.ETag() - if currentETag != *ifMatch { - return &ETagMismatchError{Provided: *ifMatch, Current: currentETag} - } - } - - return nil -} - -// validateAndSetParent validates and sets the parent relationship. -func (r *Resolver) validateAndSetParent(b *bean.Bean, parentID string) error { - if parentID == "" { - b.Parent = "" - return nil - } - - // Normalise short ID to full ID - normalizedParent, _ := r.Core.NormalizeID(parentID) - - // Validate parent type hierarchy - if err := r.Core.ValidateParent(b, normalizedParent); err != nil { - return err - } - - // Check for cycles - if cycle := r.Core.DetectCycle(b.ID, "parent", normalizedParent); cycle != nil { - return fmt.Errorf("setting parent would create cycle: %v", cycle) - } - - b.Parent = normalizedParent - return nil -} - -// validateAndAddBlocking validates and adds blocking relationships. -func (r *Resolver) validateAndAddBlocking(b *bean.Bean, targetIDs []string) error { - for _, targetID := range targetIDs { - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - // Validate: cannot block itself - if normalizedTargetID == b.ID { - return fmt.Errorf("bean cannot block itself") - } - - // Validate: target must exist - if _, err := r.Core.Get(normalizedTargetID); err != nil { - return fmt.Errorf("blocking target bean not found: %s", targetID) - } - - // Check for cycles in both directions - if cycle := r.Core.DetectCycle(b.ID, "blocking", normalizedTargetID); cycle != nil { - return fmt.Errorf("adding blocking relationship would create cycle: %v", cycle) - } - if cycle := r.Core.DetectCycle(normalizedTargetID, "blocked_by", b.ID); cycle != nil { - return fmt.Errorf("adding blocking relationship would create cycle: %v", cycle) - } - - b.AddBlocking(normalizedTargetID) - } - return nil -} - -// removeBlockingRelationships removes blocking relationships. -func (r *Resolver) removeBlockingRelationships(b *bean.Bean, targetIDs []string) { - for _, targetID := range targetIDs { - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - b.RemoveBlocking(normalizedTargetID) - } -} - -// validateAndAddBlockedBy validates and adds blocked-by relationships. -func (r *Resolver) validateAndAddBlockedBy(b *bean.Bean, targetIDs []string) error { - for _, targetID := range targetIDs { - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - // Validate: cannot be blocked by itself - if normalizedTargetID == b.ID { - return fmt.Errorf("bean cannot be blocked by itself") - } - - // Validate: blocker must exist - if _, err := r.Core.Get(normalizedTargetID); err != nil { - return fmt.Errorf("blocker bean not found: %s", targetID) - } - - // Check for cycles in both directions - if cycle := r.Core.DetectCycle(normalizedTargetID, "blocking", b.ID); cycle != nil { - return fmt.Errorf("adding blocked-by relationship would create cycle: %v", cycle) - } - if cycle := r.Core.DetectCycle(b.ID, "blocked_by", normalizedTargetID); cycle != nil { - return fmt.Errorf("adding blocked-by relationship would create cycle: %v", cycle) - } - - b.AddBlockedBy(normalizedTargetID) - } - return nil -} - -// removeBlockedByRelationships removes blocked-by relationships. -func (r *Resolver) removeBlockedByRelationships(b *bean.Bean, targetIDs []string) { - for _, targetID := range targetIDs { - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - b.RemoveBlockedBy(normalizedTargetID) - } + Forge forge.Provider // git forge provider (GitHub, GitLab, etc.) — nil if not detected + ProjectRoot string // absolute path to the project root (parent of .beans) } // worktreeToModel converts an internal worktree to a GraphQL model. diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 8e99a268..e2146963 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -17,519 +17,106 @@ import ( "github.com/hmans/beans/internal/agent" "github.com/hmans/beans/internal/gitutil" - "github.com/hmans/beans/internal/graph/model" "github.com/hmans/beans/internal/worktree" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/config" ) // IsDirty is the resolver for the isDirty field. func (r *beanResolver) IsDirty(ctx context.Context, obj *bean.Bean) (bool, error) { - return r.Core.IsDirty(obj.ID), nil + return r.CoreResolver.BeanIsDirty(ctx, obj) } // WorktreeID is the resolver for the worktreeId field. func (r *beanResolver) WorktreeID(ctx context.Context, obj *bean.Bean) (*string, error) { - wtPath := r.Core.WorktreeForBean(obj.ID) - if wtPath == "" { - return nil, nil - } - // Extract worktree ID from the path (last path component) - id := filepath.Base(wtPath) - return &id, nil + return r.CoreResolver.BeanWorktreeID(ctx, obj) } // ParentID is the resolver for the parentId field. func (r *beanResolver) ParentID(ctx context.Context, obj *bean.Bean) (*string, error) { - if obj.Parent == "" { - return nil, nil - } - return &obj.Parent, nil + return r.CoreResolver.BeanParentID(ctx, obj) } // BlockingIds is the resolver for the blockingIds field. func (r *beanResolver) BlockingIds(ctx context.Context, obj *bean.Bean) ([]string, error) { - return obj.Blocking, nil + return r.CoreResolver.BeanBlockingIds(ctx, obj) } // BlockedByIds is the resolver for the blockedByIds field. func (r *beanResolver) BlockedByIds(ctx context.Context, obj *bean.Bean) ([]string, error) { - return obj.BlockedBy, nil + return r.CoreResolver.BeanBlockedByIds(ctx, obj) } // BlockedBy is the resolver for the blockedBy field. -// Combines both directions: the bean's own blocked_by field AND incoming -// blocking links (other beans that list this bean in their blocking field). func (r *beanResolver) BlockedBy(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { - seen := make(map[string]bool) - var result []*bean.Bean - - // 1. Resolve beans from the direct blocked_by field - for _, blockerID := range obj.BlockedBy { - if !seen[blockerID] { - seen[blockerID] = true - if blocker, err := r.Core.Get(blockerID); err == nil { - result = append(result, blocker) - } - } - } - - // 2. Resolve beans from incoming blocking links (other beans blocking this one) - incoming := r.Core.FindIncomingLinks(obj.ID) - for _, link := range incoming { - if link.LinkType == "blocking" && !seen[link.FromBean.ID] { - seen[link.FromBean.ID] = true - result = append(result, link.FromBean) - } - } - - filtered := ApplyFilter(result, filter, r.Core) - cfg := r.Core.Config() - bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) - return filtered, nil + return r.CoreResolver.BeanBlockedBy(ctx, obj, filter) } // Blocking is the resolver for the blocking field. func (r *beanResolver) Blocking(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { - var result []*bean.Bean - for _, targetID := range obj.Blocking { - // Filter out broken links - if target, err := r.Core.Get(targetID); err == nil { - result = append(result, target) - } - } - filtered := ApplyFilter(result, filter, r.Core) - cfg := r.Core.Config() - bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) - return filtered, nil + return r.CoreResolver.BeanBlocking(ctx, obj, filter) } // Parent is the resolver for the parent field. func (r *beanResolver) Parent(ctx context.Context, obj *bean.Bean) (*bean.Bean, error) { - if obj.Parent == "" { - return nil, nil - } - // Filter out broken links - parent, err := r.Core.Get(obj.Parent) - if err == beancore.ErrNotFound { - return nil, nil - } - return parent, err + return r.CoreResolver.BeanParent(ctx, obj) } // Children is the resolver for the children field. func (r *beanResolver) Children(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { - incoming := r.Core.FindIncomingLinks(obj.ID) - var result []*bean.Bean - for _, link := range incoming { - if link.LinkType == "parent" { - result = append(result, link.FromBean) - } - } - filtered := ApplyFilter(result, filter, r.Core) - cfg := r.Core.Config() - bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) - return filtered, nil + return r.CoreResolver.BeanChildren(ctx, obj, filter) } // ImplicitStatus is the resolver for the implicitStatus field. func (r *beanResolver) ImplicitStatus(ctx context.Context, obj *bean.Bean) (*string, error) { - status, _ := r.Core.ImplicitStatus(obj.ID) - if status == "" { - return nil, nil - } - return &status, nil + return r.CoreResolver.BeanImplicitStatus(ctx, obj) } // ImplicitStatusFrom is the resolver for the implicitStatusFrom field. func (r *beanResolver) ImplicitStatusFrom(ctx context.Context, obj *bean.Bean) (*string, error) { - _, fromID := r.Core.ImplicitStatus(obj.ID) - if fromID == "" { - return nil, nil - } - return &fromID, nil + return r.CoreResolver.BeanImplicitStatusFrom(ctx, obj) } // CreateBean is the resolver for the createBean field. func (r *mutationResolver) CreateBean(ctx context.Context, input model.CreateBeanInput) (*bean.Bean, error) { - b := &bean.Bean{ - Slug: bean.Slugify(input.Title), - Title: input.Title, - Type: "task", // default - Blocking: []string{}, - } - - // Optional fields with defaults documented in schema - if input.Type != nil { - b.Type = *input.Type - } - if input.Status != nil { - b.Status = *input.Status - } - if input.Priority != nil { - b.Priority = *input.Priority - } - if input.Body != nil { - b.Body = *input.Body - } - if len(input.Tags) > 0 { - b.Tags = input.Tags - } - - // Handle parent (with validation) - if input.Parent != nil && *input.Parent != "" { - // Normalise short ID to full ID - parentID, _ := r.Core.NormalizeID(*input.Parent) - if err := r.Core.ValidateParent(b, parentID); err != nil { - return nil, err - } - b.Parent = parentID - } - - // Handle blocking (with validation) - if len(input.Blocking) > 0 { - // Normalise short IDs to full IDs - normalizedBlocking := make([]string, len(input.Blocking)) - for i, id := range input.Blocking { - normalizedBlocking[i], _ = r.Core.NormalizeID(id) - // Verify target exists - if _, err := r.Core.Get(normalizedBlocking[i]); err != nil { - return nil, fmt.Errorf("target bean not found: %s", id) - } - } - b.Blocking = normalizedBlocking - } - - // Handle blocked_by (with cycle validation) - if len(input.BlockedBy) > 0 { - // Normalise short IDs to full IDs - normalizedBlockedBy := make([]string, len(input.BlockedBy)) - for i, id := range input.BlockedBy { - normalizedBlockedBy[i], _ = r.Core.NormalizeID(id) - // Verify blocker exists - if _, err := r.Core.Get(normalizedBlockedBy[i]); err != nil { - return nil, fmt.Errorf("blocker bean not found: %s", id) - } - } - // Check for cycles with blocking relationships - // (new bean being blocked_by X means X→newBean, check if newBean→X exists via blocking) - for _, blockerID := range normalizedBlockedBy { - for _, blockingID := range b.Blocking { - if blockerID == blockingID { - return nil, fmt.Errorf("would create cycle: new bean both blocks and is blocked by %s", blockerID) - } - } - } - b.BlockedBy = normalizedBlockedBy - } - - // Handle custom prefix - pre-generate ID if prefix is provided - if input.Prefix != nil && *input.Prefix != "" { - idLength := 4 // default - if cfg := r.Core.Config(); cfg != nil && cfg.Beans.IDLength > 0 { - idLength = cfg.Beans.IDLength - } - id, err := bean.NewID(*input.Prefix, idLength) - if err != nil { - return nil, fmt.Errorf("generating bean ID: %w", err) - } - b.ID = id - } - - if err := r.Core.Create(b); err != nil { - return nil, err - } - - return b, nil + return r.CoreResolver.CreateBean(ctx, input) } // UpdateBean is the resolver for the updateBean field. func (r *mutationResolver) UpdateBean(ctx context.Context, id string, input model.UpdateBeanInput) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - // Validate body and bodyMod are mutually exclusive - if input.Body != nil && input.BodyMod != nil { - return nil, fmt.Errorf("cannot specify both body and bodyMod") - } - - // Validate tags and addTags/removeTags are mutually exclusive - if input.Tags != nil && (input.AddTags != nil || input.RemoveTags != nil) { - return nil, fmt.Errorf("cannot specify both tags and addTags/removeTags") - } - - // Update fields if provided - if input.Title != nil { - b.Title = *input.Title - } - if input.Status != nil { - b.Status = *input.Status - } - if input.Type != nil { - b.Type = *input.Type - } - if input.Priority != nil { - b.Priority = *input.Priority - } - if input.Order != nil { - b.Order = *input.Order - } - if input.Body != nil { - b.Body = *input.Body - } else if input.BodyMod != nil { - // Apply body modifications - workingBody := b.Body - - // Apply replacements sequentially - if input.BodyMod.Replace != nil { - for i, replaceOp := range input.BodyMod.Replace { - newBody, err := bean.ReplaceOnce(workingBody, replaceOp.Old, replaceOp.New) - if err != nil { - return nil, fmt.Errorf("replacement %d failed: %w", i, err) - } - workingBody = newBody - } - } - - // Apply append if provided - if input.BodyMod.Append != nil && *input.BodyMod.Append != "" { - workingBody = bean.AppendWithSeparator(workingBody, *input.BodyMod.Append) - } - - b.Body = workingBody - } - // Handle tags - if input.Tags != nil { - b.Tags = input.Tags - } else if input.AddTags != nil || input.RemoveTags != nil { - // Build a set of current tags - tagSet := make(map[string]bool) - for _, tag := range b.Tags { - tagSet[tag] = true - } - - // Add new tags - if input.AddTags != nil { - for _, tag := range input.AddTags { - tagSet[tag] = true - } - } - - // Remove tags - if input.RemoveTags != nil { - for _, tag := range input.RemoveTags { - delete(tagSet, tag) - } - } - - // Convert back to slice - newTags := make([]string, 0, len(tagSet)) - for tag := range tagSet { - newTags = append(newTags, tag) - } - b.Tags = newTags - } - - // Handle parent relationship - if input.Parent != nil { - if err := r.validateAndSetParent(b, *input.Parent); err != nil { - return nil, err - } - } - - // Handle blocking relationships - if input.AddBlocking != nil { - if err := r.validateAndAddBlocking(b, input.AddBlocking); err != nil { - return nil, err - } - } - if input.RemoveBlocking != nil { - r.removeBlockingRelationships(b, input.RemoveBlocking) - } - - // Handle blocked-by relationships - if input.AddBlockedBy != nil { - if err := r.validateAndAddBlockedBy(b, input.AddBlockedBy); err != nil { - return nil, err - } - } - if input.RemoveBlockedBy != nil { - r.removeBlockedByRelationships(b, input.RemoveBlockedBy) - } - - // ETag validation now happens inside Update() under write lock. - // If the bean is linked to a worktree, Core auto-routes the write there. - if err := r.Core.Update(b, input.IfMatch); err != nil { - return nil, err - } - - return b, nil + return r.CoreResolver.UpdateBean(ctx, id, input) } // DeleteBean is the resolver for the deleteBean field. func (r *mutationResolver) DeleteBean(ctx context.Context, id string) (bool, error) { - // Verify bean exists - _, err := r.Core.Get(id) - if err != nil { - return false, err - } - - // Remove incoming links first - if _, err := r.Core.RemoveLinksTo(id); err != nil { - return false, err - } - - // Delete the bean - if err := r.Core.Delete(id); err != nil { - return false, err - } - - return true, nil + return r.CoreResolver.DeleteBean(ctx, id) } // SetParent is the resolver for the setParent field. func (r *mutationResolver) SetParent(ctx context.Context, id string, parentID *string, ifMatch *string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - newParent := "" - if parentID != nil { - // Normalise short ID to full ID - newParent, _ = r.Core.NormalizeID(*parentID) - } - - // Validate parent type hierarchy - if newParent != "" { - if err := r.Core.ValidateParent(b, newParent); err != nil { - return nil, err - } - // Check for cycles - if cycle := r.Core.DetectCycle(b.ID, "parent", newParent); cycle != nil { - return nil, fmt.Errorf("would create cycle: %v", cycle) - } - } - - b.Parent = newParent - // ETag validation now happens inside Update() under write lock - if err := r.Core.Update(b, ifMatch); err != nil { - return nil, err - } - return b, nil + return r.CoreResolver.SetParent(ctx, id, parentID, ifMatch) } // AddBlocking is the resolver for the addBlocking field. func (r *mutationResolver) AddBlocking(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - if normalizedTargetID == b.ID { - return nil, fmt.Errorf("bean cannot block itself") - } - - // Check target exists - if _, err := r.Core.Get(normalizedTargetID); err != nil { - return nil, fmt.Errorf("target bean not found: %s", targetID) - } - - // Check for cycles in both directions: - // 1. Check if targetId already has a path to id via blocking links - if cycle := r.Core.DetectCycle(b.ID, "blocking", normalizedTargetID); cycle != nil { - return nil, fmt.Errorf("would create cycle: %v", cycle) - } - // 2. Check if targetId already has a path to id via blocked_by links - if cycle := r.Core.DetectCycle(normalizedTargetID, "blocked_by", b.ID); cycle != nil { - return nil, fmt.Errorf("would create cycle: %v", cycle) - } - - b.AddBlocking(normalizedTargetID) - // ETag validation now happens inside Update() under write lock - if err := r.Core.Update(b, ifMatch); err != nil { - return nil, err - } - return b, nil + return r.CoreResolver.AddBlocking(ctx, id, targetID, ifMatch) } // RemoveBlocking is the resolver for the removeBlocking field. func (r *mutationResolver) RemoveBlocking(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - b.RemoveBlocking(normalizedTargetID) - // ETag validation now happens inside Update() under write lock - if err := r.Core.Update(b, ifMatch); err != nil { - return nil, err - } - return b, nil + return r.CoreResolver.RemoveBlocking(ctx, id, targetID, ifMatch) } // AddBlockedBy is the resolver for the addBlockedBy field. func (r *mutationResolver) AddBlockedBy(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - if normalizedTargetID == b.ID { - return nil, fmt.Errorf("bean cannot be blocked by itself") - } - - // Check target exists - if _, err := r.Core.Get(normalizedTargetID); err != nil { - return nil, fmt.Errorf("blocker bean not found: %s", targetID) - } - - // Check for cycles in both directions: - // 1. Check if targetId already has a path to id via blocking links - if cycle := r.Core.DetectCycle(normalizedTargetID, "blocking", b.ID); cycle != nil { - return nil, fmt.Errorf("would create cycle: %v", cycle) - } - // 2. Check if id already has a path to targetId via blocked_by links - if cycle := r.Core.DetectCycle(b.ID, "blocked_by", normalizedTargetID); cycle != nil { - return nil, fmt.Errorf("would create cycle: %v", cycle) - } - - b.AddBlockedBy(normalizedTargetID) - // ETag validation now happens inside Update() under write lock - if err := r.Core.Update(b, ifMatch); err != nil { - return nil, err - } - return b, nil + return r.CoreResolver.AddBlockedBy(ctx, id, targetID, ifMatch) } // RemoveBlockedBy is the resolver for the removeBlockedBy field. func (r *mutationResolver) RemoveBlockedBy(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err != nil { - return nil, err - } - - // Normalise short ID to full ID - normalizedTargetID, _ := r.Core.NormalizeID(targetID) - - b.RemoveBlockedBy(normalizedTargetID) - // ETag validation now happens inside Update() under write lock - if err := r.Core.Update(b, ifMatch); err != nil { - return nil, err - } - return b, nil + return r.CoreResolver.RemoveBlockedBy(ctx, id, targetID, ifMatch) } // WriteTerminalInput is the resolver for the writeTerminalInput field. @@ -784,10 +371,7 @@ func (r *mutationResolver) ClearAgentSession(ctx context.Context, beanID string) // ArchiveBean is the resolver for the archiveBean field. func (r *mutationResolver) ArchiveBean(ctx context.Context, id string) (bool, error) { - if err := r.Core.Archive(id); err != nil { - return false, err - } - return true, nil + return r.CoreResolver.ArchiveBean(ctx, id) } // SaveDirtyBeans is the resolver for the saveDirtyBeans field. @@ -914,35 +498,12 @@ func (r *mutationResolver) OpenInEditor(ctx context.Context, workspaceID string) // Bean is the resolver for the bean field. func (r *queryResolver) Bean(ctx context.Context, id string) (*bean.Bean, error) { - b, err := r.Core.Get(id) - if err == beancore.ErrNotFound { - return nil, nil - } - return b, err + return r.CoreResolver.Bean(ctx, id) } // Beans is the resolver for the beans field. func (r *queryResolver) Beans(ctx context.Context, filter *model.BeanFilter) ([]*bean.Bean, error) { - var beans []*bean.Bean - - // If search filter is provided, start with search results - if filter != nil && filter.Search != nil && *filter.Search != "" { - searchResults, err := r.Core.Search(*filter.Search) - if err != nil { - return nil, err - } - beans = searchResults - } else { - beans = r.Core.All() - } - - result := ApplyFilter(beans, filter, r.Core) - - // Sort using the same logic as CLI and TUI - cfg := r.Core.Config() - bean.SortByStatusPriorityAndType(result, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) - - return result, nil + return r.CoreResolver.Beans(ctx, filter) } // Worktrees is the resolver for the worktrees field. @@ -1242,19 +803,12 @@ func (r *queryResolver) AgentActions(ctx context.Context, beanID string, skipFor // ProjectName is the resolver for the projectName field. func (r *queryResolver) ProjectName(ctx context.Context) (string, error) { - cfg := r.Core.Config() - if cfg == nil { - return "", nil - } - return cfg.GetProjectName(), nil + return r.CoreResolver.ProjectName(ctx) } // MainBranch is the resolver for the mainBranch field. func (r *queryResolver) MainBranch(ctx context.Context) (string, error) { - if branch, ok := gitutil.CurrentBranch(r.ProjectRoot); ok { - return branch, nil - } - return "main", nil + return r.CoreResolver.MainBranch(ctx, r.ProjectRoot) } // AgentEnabled is the resolver for the agentEnabled field. diff --git a/internal/graph/schema.resolvers_test.go b/internal/graph/schema.resolvers_test.go index c66da690..a10f483d 100644 --- a/internal/graph/schema.resolvers_test.go +++ b/internal/graph/schema.resolvers_test.go @@ -9,7 +9,8 @@ import ( "testing" "github.com/hmans/beans/internal/agent" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" "github.com/hmans/beans/pkg/config" @@ -29,7 +30,7 @@ func setupTestResolver(t *testing.T) (*Resolver, *beancore.Core) { t.Fatalf("failed to load core: %v", err) } - return &Resolver{Core: core}, core + return &Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}}, core } func createTestBean(t *testing.T, core *beancore.Core, id, title, status string) *bean.Bean { @@ -1332,7 +1333,7 @@ func setupTestResolverWithPrefix(t *testing.T, prefix string) (*Resolver, *beanc t.Fatalf("failed to load core: %v", err) } - return &Resolver{Core: core}, core + return &Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}}, core } // setupTestResolverWithRequireIfMatch creates a test resolver with require_if_match enabled. @@ -1351,7 +1352,7 @@ func setupTestResolverWithRequireIfMatch(t *testing.T) (*Resolver, *beancore.Cor t.Fatalf("failed to load core: %v", err) } - return &Resolver{Core: core}, core + return &Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}}, core } func TestETagValidation(t *testing.T) { diff --git a/internal/tui/blockingpicker.go b/internal/tui/blockingpicker.go index 81613a41..307b98dc 100644 --- a/internal/tui/blockingpicker.go +++ b/internal/tui/blockingpicker.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/internal/ui" ) @@ -101,9 +101,9 @@ type blockingPickerModel struct { height int } -func newBlockingPickerModel(beanID, beanTitle string, currentBlocking []string, resolver *graph.Resolver, cfg *config.Config, width, height int) blockingPickerModel { +func newBlockingPickerModel(beanID, beanTitle string, currentBlocking []string, resolver *beangraph.CoreResolver, cfg *config.Config, width, height int) blockingPickerModel { // Fetch all beans - allBeans, _ := resolver.Query().Beans(context.Background(), nil) + allBeans, _ := resolver.Beans(context.Background(), nil) // Create maps for original and pending state originalBlocking := make(map[string]bool) diff --git a/internal/tui/detail.go b/internal/tui/detail.go index 000a8142..43a9e3e1 100644 --- a/internal/tui/detail.go +++ b/internal/tui/detail.go @@ -15,7 +15,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/internal/ui" ) @@ -129,7 +129,7 @@ func (d linkDelegate) Render(w io.Writer, m list.Model, index int, listItem list type detailModel struct { viewport viewport.Model bean *bean.Bean - resolver *graph.Resolver + resolver *beangraph.CoreResolver config *config.Config width int height int @@ -141,7 +141,7 @@ type detailModel struct { statusMessage string // Status message to display in footer } -func newDetailModel(b *bean.Bean, resolver *graph.Resolver, cfg *config.Config, width, height int) detailModel { +func newDetailModel(b *bean.Bean, resolver *beangraph.CoreResolver, cfg *config.Config, width, height int) detailModel { m := detailModel{ bean: b, resolver: resolver, @@ -554,25 +554,23 @@ func (m detailModel) formatLinkLabel(linkType string, incoming bool) string { func (m detailModel) resolveAllLinks() []resolvedLink { var links []resolvedLink ctx := context.Background() - beanResolver := m.resolver.Bean() - - // Resolve outgoing links via GraphQL resolvers - if blocking, _ := beanResolver.Blocking(ctx, m.bean, nil); blocking != nil { + // Resolve outgoing links via core resolver + if blocking, _ := m.resolver.BeanBlocking(ctx, m.bean, nil); blocking != nil { for _, b := range blocking { links = append(links, resolvedLink{linkType: "blocking", bean: b, incoming: false}) } } - if parent, _ := beanResolver.Parent(ctx, m.bean); parent != nil { + if parent, _ := m.resolver.BeanParent(ctx, m.bean); parent != nil { links = append(links, resolvedLink{linkType: "parent", bean: parent, incoming: false}) } - // Resolve incoming links via GraphQL resolvers - if blockedBy, _ := beanResolver.BlockedBy(ctx, m.bean, nil); blockedBy != nil { + // Resolve incoming links via core resolver + if blockedBy, _ := m.resolver.BeanBlockedBy(ctx, m.bean, nil); blockedBy != nil { for _, b := range blockedBy { links = append(links, resolvedLink{linkType: "blocking", bean: b, incoming: true}) } } - if children, _ := beanResolver.Children(ctx, m.bean, nil); children != nil { + if children, _ := m.resolver.BeanChildren(ctx, m.bean, nil); children != nil { for _, b := range children { links = append(links, resolvedLink{linkType: "parent", bean: b, incoming: true}) } diff --git a/internal/tui/list.go b/internal/tui/list.go index bef0ff1d..77c851a7 100644 --- a/internal/tui/list.go +++ b/internal/tui/list.go @@ -10,8 +10,8 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/internal/ui" ) @@ -105,7 +105,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list // listModel is the model for the bean list view type listModel struct { list list.Model - resolver *graph.Resolver + resolver *beangraph.CoreResolver config *config.Config width int height int @@ -126,7 +126,7 @@ type listModel struct { statusMessage string } -func newListModel(resolver *graph.Resolver, cfg *config.Config) listModel { +func newListModel(resolver *beangraph.CoreResolver, cfg *config.Config) listModel { selectedBeans := make(map[string]bool) delegate := itemDelegate{cfg: cfg, selectedBeans: &selectedBeans} @@ -176,13 +176,13 @@ func (m listModel) loadBeans() tea.Msg { } // Query filtered beans - filteredBeans, err := m.resolver.Query().Beans(context.Background(), filter) + filteredBeans, err := m.resolver.Beans(context.Background(), filter) if err != nil { return errMsg{err} } // Query all beans for tree context (ancestors) - allBeans, err := m.resolver.Query().Beans(context.Background(), nil) + allBeans, err := m.resolver.Beans(context.Background(), nil) if err != nil { return errMsg{err} } diff --git a/internal/tui/parentpicker.go b/internal/tui/parentpicker.go index b959a789..0ad6289a 100644 --- a/internal/tui/parentpicker.go +++ b/internal/tui/parentpicker.go @@ -13,7 +13,7 @@ import ( "github.com/hmans/beans/pkg/bean" "github.com/hmans/beans/pkg/beancore" "github.com/hmans/beans/pkg/config" - "github.com/hmans/beans/internal/graph" + "github.com/hmans/beans/pkg/beangraph" "github.com/hmans/beans/internal/ui" ) @@ -92,7 +92,7 @@ type parentPickerModel struct { height int } -func newParentPickerModel(beanIDs []string, beanTitle string, beanTypes []string, currentParent string, resolver *graph.Resolver, cfg *config.Config, width, height int) parentPickerModel { +func newParentPickerModel(beanIDs []string, beanTitle string, beanTypes []string, currentParent string, resolver *beangraph.CoreResolver, cfg *config.Config, width, height int) parentPickerModel { // Get valid parent types - for multi-select, find types valid for ALL beans var validParentTypes []string for i, beanType := range beanTypes { @@ -106,7 +106,7 @@ func newParentPickerModel(beanIDs []string, beanTitle string, beanTypes []string } // Fetch all beans and filter to eligible parents - allBeans, _ := resolver.Query().Beans(context.Background(), nil) + allBeans, _ := resolver.Beans(context.Background(), nil) // Collect all descendants of all selected beans (to prevent cycles) allDescendants := make(map[string]bool) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 8d0324b1..42aa99d9 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -14,8 +14,8 @@ import ( "github.com/hmans/beans/pkg/beancore" "github.com/hmans/beans/pkg/config" "github.com/hmans/beans/pkg/safepath" - "github.com/hmans/beans/internal/graph" - "github.com/hmans/beans/internal/graph/model" + "github.com/hmans/beans/pkg/beangraph" + "github.com/hmans/beans/pkg/beangraph/model" ) // viewState represents which view is currently active @@ -110,7 +110,7 @@ type App struct { helpOverlay helpOverlayModel history []detailModel // stack of previous detail views for back navigation core *beancore.Core - resolver *graph.Resolver + resolver *beangraph.CoreResolver config *config.Config width int height int @@ -129,7 +129,7 @@ type App struct { // New creates a new TUI application func New(core *beancore.Core, cfg *config.Config) *App { - resolver := &graph.Resolver{Core: core} + resolver := &beangraph.CoreResolver{Core: core} return &App{ state: viewList, core: core, @@ -221,7 +221,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Update preview with the newly highlighted bean _, rightWidth := calculatePaneWidths(a.width) if msg.beanID != "" { - bean, err := a.resolver.Query().Bean(context.Background(), msg.beanID) + bean, err := a.resolver.Bean(context.Background(), msg.beanID) if err == nil && bean != nil { a.preview = newPreviewModel(bean, rightWidth, a.height-2) } @@ -246,7 +246,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Beans changed on disk - refresh if a.state == viewDetail { // Try to reload the current bean via GraphQL - updatedBean, err := a.resolver.Query().Bean(context.Background(), a.detail.bean.ID) + updatedBean, err := a.resolver.Bean(context.Background(), a.detail.bean.ID) if err != nil || updatedBean == nil { // Bean was deleted - return to list a.state = viewList @@ -307,7 +307,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case statusSelectedMsg: // Update all beans' status via GraphQL mutations for _, beanID := range msg.beanIDs { - _, err := a.resolver.Mutation().UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ + _, err := a.resolver.UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ Status: &msg.status, }) if err != nil { @@ -320,7 +320,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Clear selection after batch edit clear(a.list.selectedBeans) if a.state == viewDetail && len(msg.beanIDs) == 1 { - updatedBean, _ := a.resolver.Query().Bean(context.Background(), msg.beanIDs[0]) + updatedBean, _ := a.resolver.Bean(context.Background(), msg.beanIDs[0]) if updatedBean != nil { a.detail = newDetailModel(updatedBean, a.resolver, a.config, a.width, a.height) } @@ -341,7 +341,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case typeSelectedMsg: // Update all beans' type via GraphQL mutations for _, beanID := range msg.beanIDs { - _, err := a.resolver.Mutation().UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ + _, err := a.resolver.UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ Type: &msg.beanType, }) if err != nil { @@ -354,7 +354,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Clear selection after batch edit clear(a.list.selectedBeans) if a.state == viewDetail && len(msg.beanIDs) == 1 { - updatedBean, _ := a.resolver.Query().Bean(context.Background(), msg.beanIDs[0]) + updatedBean, _ := a.resolver.Bean(context.Background(), msg.beanIDs[0]) if updatedBean != nil { a.detail = newDetailModel(updatedBean, a.resolver, a.config, a.width, a.height) } @@ -375,7 +375,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case prioritySelectedMsg: // Update all beans' priority via GraphQL mutations for _, beanID := range msg.beanIDs { - _, err := a.resolver.Mutation().UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ + _, err := a.resolver.UpdateBean(context.Background(), beanID, model.UpdateBeanInput{ Priority: &msg.priority, }) if err != nil { @@ -388,7 +388,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Clear selection after batch edit clear(a.list.selectedBeans) if a.state == viewDetail && len(msg.beanIDs) == 1 { - updatedBean, _ := a.resolver.Query().Bean(context.Background(), msg.beanIDs[0]) + updatedBean, _ := a.resolver.Bean(context.Background(), msg.beanIDs[0]) if updatedBean != nil { a.detail = newDetailModel(updatedBean, a.resolver, a.config, a.width, a.height) } @@ -419,14 +419,14 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case blockingConfirmedMsg: // Apply all blocking changes via GraphQL mutations for _, targetID := range msg.toAdd { - _, err := a.resolver.Mutation().AddBlocking(context.Background(), msg.beanID, targetID, nil) + _, err := a.resolver.AddBlocking(context.Background(), msg.beanID, targetID, nil) if err != nil { // Continue with other changes even if one fails continue } } for _, targetID := range msg.toRemove { - _, err := a.resolver.Mutation().RemoveBlocking(context.Background(), msg.beanID, targetID, nil) + _, err := a.resolver.RemoveBlocking(context.Background(), msg.beanID, targetID, nil) if err != nil { // Continue with other changes even if one fails continue @@ -435,7 +435,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Return to previous view and refresh a.state = a.previousState if a.state == viewDetail { - updatedBean, _ := a.resolver.Query().Bean(context.Background(), msg.beanID) + updatedBean, _ := a.resolver.Bean(context.Background(), msg.beanID) if updatedBean != nil { a.detail = newDetailModel(updatedBean, a.resolver, a.config, a.width, a.height) } @@ -455,7 +455,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case beanCreatedMsg: // Create the bean via GraphQL mutation with draft status draftStatus := "draft" - createdBean, err := a.resolver.Mutation().CreateBean(context.Background(), model.CreateBeanInput{ + createdBean, err := a.resolver.CreateBean(context.Background(), model.CreateBeanInput{ Title: msg.title, Status: &draftStatus, }) @@ -525,7 +525,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { parentID = &msg.parentID } for _, beanID := range msg.beanIDs { - _, err := a.resolver.Mutation().SetParent(context.Background(), beanID, parentID, nil) + _, err := a.resolver.SetParent(context.Background(), beanID, parentID, nil) if err != nil { // Continue with other beans even if one fails continue @@ -537,7 +537,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { clear(a.list.selectedBeans) if a.state == viewDetail && len(msg.beanIDs) == 1 { // Refresh the bean to show updated parent - updatedBean, _ := a.resolver.Query().Bean(context.Background(), msg.beanIDs[0]) + updatedBean, _ := a.resolver.Bean(context.Background(), msg.beanIDs[0]) if updatedBean != nil { a.detail = newDetailModel(updatedBean, a.resolver, a.config, a.width, a.height) } @@ -621,7 +621,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // collectTagsWithCounts returns all tags with their usage counts func (a *App) collectTagsWithCounts() []tagWithCount { - beans, _ := a.resolver.Query().Beans(context.Background(), nil) + beans, _ := a.resolver.Beans(context.Background(), nil) tagCounts := make(map[string]int) for _, b := range beans { for _, tag := range b.Tags { diff --git a/pkg/beangraph/bean_fields.go b/pkg/beangraph/bean_fields.go new file mode 100644 index 00000000..c94719d2 --- /dev/null +++ b/pkg/beangraph/bean_fields.go @@ -0,0 +1,137 @@ +package beangraph + +import ( + "context" + "path/filepath" + + "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph/model" + "github.com/hmans/beans/pkg/beancore" +) + +// BeanIsDirty returns whether a bean has unsaved runtime changes. +func (r *CoreResolver) BeanIsDirty(ctx context.Context, obj *bean.Bean) (bool, error) { + return r.Core.IsDirty(obj.ID), nil +} + +// BeanWorktreeID returns the worktree ID for a bean, or nil if not linked. +func (r *CoreResolver) BeanWorktreeID(ctx context.Context, obj *bean.Bean) (*string, error) { + wtPath := r.Core.WorktreeForBean(obj.ID) + if wtPath == "" { + return nil, nil + } + // Extract worktree ID from the path (last path component) + id := filepath.Base(wtPath) + return &id, nil +} + +// BeanParentID returns the parent ID as a pointer, or nil if no parent. +func (r *CoreResolver) BeanParentID(ctx context.Context, obj *bean.Bean) (*string, error) { + if obj.Parent == "" { + return nil, nil + } + return &obj.Parent, nil +} + +// BeanBlockingIds returns the blocking IDs slice. +func (r *CoreResolver) BeanBlockingIds(ctx context.Context, obj *bean.Bean) ([]string, error) { + return obj.Blocking, nil +} + +// BeanBlockedByIds returns the blocked-by IDs slice. +func (r *CoreResolver) BeanBlockedByIds(ctx context.Context, obj *bean.Bean) ([]string, error) { + return obj.BlockedBy, nil +} + +// BeanBlockedBy resolves the full list of beans blocking this one. +// Combines both directions: the bean's own blocked_by field AND incoming +// blocking links (other beans that list this bean in their blocking field). +func (r *CoreResolver) BeanBlockedBy(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { + seen := make(map[string]bool) + var result []*bean.Bean + + // 1. Resolve beans from the direct blocked_by field + for _, blockerID := range obj.BlockedBy { + if !seen[blockerID] { + seen[blockerID] = true + if blocker, err := r.Core.Get(blockerID); err == nil { + result = append(result, blocker) + } + } + } + + // 2. Resolve beans from incoming blocking links (other beans blocking this one) + incoming := r.Core.FindIncomingLinks(obj.ID) + for _, link := range incoming { + if link.LinkType == "blocking" && !seen[link.FromBean.ID] { + seen[link.FromBean.ID] = true + result = append(result, link.FromBean) + } + } + + filtered := ApplyFilter(result, filter, r.Core) + cfg := r.Core.Config() + bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) + return filtered, nil +} + +// BeanBlocking resolves the beans this bean is blocking. +func (r *CoreResolver) BeanBlocking(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { + var result []*bean.Bean + for _, targetID := range obj.Blocking { + // Filter out broken links + if target, err := r.Core.Get(targetID); err == nil { + result = append(result, target) + } + } + filtered := ApplyFilter(result, filter, r.Core) + cfg := r.Core.Config() + bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) + return filtered, nil +} + +// BeanParent resolves the parent bean. +func (r *CoreResolver) BeanParent(ctx context.Context, obj *bean.Bean) (*bean.Bean, error) { + if obj.Parent == "" { + return nil, nil + } + // Filter out broken links + parent, err := r.Core.Get(obj.Parent) + if err == beancore.ErrNotFound { + return nil, nil + } + return parent, err +} + +// BeanChildren resolves the child beans. +func (r *CoreResolver) BeanChildren(ctx context.Context, obj *bean.Bean, filter *model.BeanFilter) ([]*bean.Bean, error) { + incoming := r.Core.FindIncomingLinks(obj.ID) + var result []*bean.Bean + for _, link := range incoming { + if link.LinkType == "parent" { + result = append(result, link.FromBean) + } + } + filtered := ApplyFilter(result, filter, r.Core) + cfg := r.Core.Config() + bean.SortByStatusPriorityAndType(filtered, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) + return filtered, nil +} + +// BeanImplicitStatus returns the implicit status inherited from ancestors. +func (r *CoreResolver) BeanImplicitStatus(ctx context.Context, obj *bean.Bean) (*string, error) { + status, _ := r.Core.ImplicitStatus(obj.ID) + if status == "" { + return nil, nil + } + return &status, nil +} + +// BeanImplicitStatusFrom returns the ancestor ID from which implicit status is inherited. +func (r *CoreResolver) BeanImplicitStatusFrom(ctx context.Context, obj *bean.Bean) (*string, error) { + _, fromID := r.Core.ImplicitStatus(obj.ID) + if fromID == "" { + return nil, nil + } + return &fromID, nil +} diff --git a/internal/graph/filters.go b/pkg/beangraph/filters.go similarity index 99% rename from internal/graph/filters.go rename to pkg/beangraph/filters.go index d71fffc4..90a4eeb5 100644 --- a/internal/graph/filters.go +++ b/pkg/beangraph/filters.go @@ -1,9 +1,9 @@ -package graph +package beangraph import ( "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph/model" "github.com/hmans/beans/pkg/beancore" - "github.com/hmans/beans/internal/graph/model" ) // ApplyFilter applies BeanFilter to a slice of beans and returns filtered results. diff --git a/internal/graph/model/models_gen.go b/pkg/beangraph/model/models_gen.go similarity index 100% rename from internal/graph/model/models_gen.go rename to pkg/beangraph/model/models_gen.go diff --git a/pkg/beangraph/mutations.go b/pkg/beangraph/mutations.go new file mode 100644 index 00000000..d3d77e60 --- /dev/null +++ b/pkg/beangraph/mutations.go @@ -0,0 +1,388 @@ +package beangraph + +import ( + "context" + "fmt" + + "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph/model" + "github.com/hmans/beans/pkg/beancore" +) + +// CreateBean creates a new bean from the given input. +func (r *CoreResolver) CreateBean(ctx context.Context, input model.CreateBeanInput) (*bean.Bean, error) { + b := &bean.Bean{ + Slug: bean.Slugify(input.Title), + Title: input.Title, + Type: "task", // default + Blocking: []string{}, + } + + // Optional fields with defaults documented in schema + if input.Type != nil { + b.Type = *input.Type + } + if input.Status != nil { + b.Status = *input.Status + } + if input.Priority != nil { + b.Priority = *input.Priority + } + if input.Body != nil { + b.Body = *input.Body + } + if len(input.Tags) > 0 { + b.Tags = input.Tags + } + + // Handle parent (with validation) + if input.Parent != nil && *input.Parent != "" { + // Normalise short ID to full ID + parentID, _ := r.Core.NormalizeID(*input.Parent) + if err := r.Core.ValidateParent(b, parentID); err != nil { + return nil, err + } + b.Parent = parentID + } + + // Handle blocking (with validation) + if len(input.Blocking) > 0 { + // Normalise short IDs to full IDs + normalizedBlocking := make([]string, len(input.Blocking)) + for i, id := range input.Blocking { + normalizedBlocking[i], _ = r.Core.NormalizeID(id) + // Verify target exists + if _, err := r.Core.Get(normalizedBlocking[i]); err != nil { + return nil, fmt.Errorf("target bean not found: %s", id) + } + } + b.Blocking = normalizedBlocking + } + + // Handle blocked_by (with cycle validation) + if len(input.BlockedBy) > 0 { + // Normalise short IDs to full IDs + normalizedBlockedBy := make([]string, len(input.BlockedBy)) + for i, id := range input.BlockedBy { + normalizedBlockedBy[i], _ = r.Core.NormalizeID(id) + // Verify blocker exists + if _, err := r.Core.Get(normalizedBlockedBy[i]); err != nil { + return nil, fmt.Errorf("blocker bean not found: %s", id) + } + } + // Check for cycles with blocking relationships + // (new bean being blocked_by X means X→newBean, check if newBean→X exists via blocking) + for _, blockerID := range normalizedBlockedBy { + for _, blockingID := range b.Blocking { + if blockerID == blockingID { + return nil, fmt.Errorf("would create cycle: new bean both blocks and is blocked by %s", blockerID) + } + } + } + b.BlockedBy = normalizedBlockedBy + } + + // Handle custom prefix - pre-generate ID if prefix is provided + if input.Prefix != nil && *input.Prefix != "" { + idLength := 4 // default + if cfg := r.Core.Config(); cfg != nil && cfg.Beans.IDLength > 0 { + idLength = cfg.Beans.IDLength + } + id, err := bean.NewID(*input.Prefix, idLength) + if err != nil { + return nil, fmt.Errorf("generating bean ID: %w", err) + } + b.ID = id + } + + if err := r.Core.Create(b); err != nil { + return nil, err + } + + return b, nil +} + +// UpdateBean updates an existing bean. +func (r *CoreResolver) UpdateBean(ctx context.Context, id string, input model.UpdateBeanInput, opts ...beancore.UpdateOption) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + // Validate body and bodyMod are mutually exclusive + if input.Body != nil && input.BodyMod != nil { + return nil, fmt.Errorf("cannot specify both body and bodyMod") + } + + // Validate tags and addTags/removeTags are mutually exclusive + if input.Tags != nil && (input.AddTags != nil || input.RemoveTags != nil) { + return nil, fmt.Errorf("cannot specify both tags and addTags/removeTags") + } + + // Update fields if provided + if input.Title != nil { + b.Title = *input.Title + } + if input.Status != nil { + b.Status = *input.Status + } + if input.Type != nil { + b.Type = *input.Type + } + if input.Priority != nil { + b.Priority = *input.Priority + } + if input.Order != nil { + b.Order = *input.Order + } + if input.Body != nil { + b.Body = *input.Body + } else if input.BodyMod != nil { + // Apply body modifications + workingBody := b.Body + + // Apply replacements sequentially + if input.BodyMod.Replace != nil { + for i, replaceOp := range input.BodyMod.Replace { + newBody, err := bean.ReplaceOnce(workingBody, replaceOp.Old, replaceOp.New) + if err != nil { + return nil, fmt.Errorf("replacement %d failed: %w", i, err) + } + workingBody = newBody + } + } + + // Apply append if provided + if input.BodyMod.Append != nil && *input.BodyMod.Append != "" { + workingBody = bean.AppendWithSeparator(workingBody, *input.BodyMod.Append) + } + + b.Body = workingBody + } + // Handle tags + if input.Tags != nil { + b.Tags = input.Tags + } else if input.AddTags != nil || input.RemoveTags != nil { + // Build a set of current tags + tagSet := make(map[string]bool) + for _, tag := range b.Tags { + tagSet[tag] = true + } + + // Add new tags + if input.AddTags != nil { + for _, tag := range input.AddTags { + tagSet[tag] = true + } + } + + // Remove tags + if input.RemoveTags != nil { + for _, tag := range input.RemoveTags { + delete(tagSet, tag) + } + } + + // Convert back to slice + newTags := make([]string, 0, len(tagSet)) + for tag := range tagSet { + newTags = append(newTags, tag) + } + b.Tags = newTags + } + + // Handle parent relationship + if input.Parent != nil { + if err := r.ValidateAndSetParent(b, *input.Parent); err != nil { + return nil, err + } + } + + // Handle blocking relationships + if input.AddBlocking != nil { + if err := r.ValidateAndAddBlocking(b, input.AddBlocking); err != nil { + return nil, err + } + } + if input.RemoveBlocking != nil { + r.RemoveBlockingRelationships(b, input.RemoveBlocking) + } + + // Handle blocked-by relationships + if input.AddBlockedBy != nil { + if err := r.ValidateAndAddBlockedBy(b, input.AddBlockedBy); err != nil { + return nil, err + } + } + if input.RemoveBlockedBy != nil { + r.RemoveBlockedByRelationships(b, input.RemoveBlockedBy) + } + + // ETag validation now happens inside Update() under write lock. + // If the bean is linked to a worktree, Core auto-routes the write there. + if err := r.Core.Update(b, input.IfMatch, opts...); err != nil { + return nil, err + } + + return b, nil +} + +// DeleteBean removes a bean and its incoming links. +func (r *CoreResolver) DeleteBean(ctx context.Context, id string) (bool, error) { + // Verify bean exists + _, err := r.Core.Get(id) + if err != nil { + return false, err + } + + // Remove incoming links first + if _, err := r.Core.RemoveLinksTo(id); err != nil { + return false, err + } + + // Delete the bean + if err := r.Core.Delete(id); err != nil { + return false, err + } + + return true, nil +} + +// SetParent sets or clears the parent of a bean. +func (r *CoreResolver) SetParent(ctx context.Context, id string, parentID *string, ifMatch *string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + newParent := "" + if parentID != nil { + // Normalise short ID to full ID + newParent, _ = r.Core.NormalizeID(*parentID) + } + + // Validate parent type hierarchy + if newParent != "" { + if err := r.Core.ValidateParent(b, newParent); err != nil { + return nil, err + } + // Check for cycles + if cycle := r.Core.DetectCycle(b.ID, "parent", newParent); cycle != nil { + return nil, fmt.Errorf("would create cycle: %v", cycle) + } + } + + b.Parent = newParent + // ETag validation now happens inside Update() under write lock + if err := r.Core.Update(b, ifMatch); err != nil { + return nil, err + } + return b, nil +} + +// AddBlocking adds a blocking relationship. +func (r *CoreResolver) AddBlocking(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + // Normalise short ID to full ID + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + if normalizedTargetID == b.ID { + return nil, fmt.Errorf("bean cannot block itself") + } + + // Check target exists + if _, err := r.Core.Get(normalizedTargetID); err != nil { + return nil, fmt.Errorf("target bean not found: %s", targetID) + } + + // Check for cycles in both directions + if cycle := r.Core.DetectCycle(b.ID, "blocking", normalizedTargetID); cycle != nil { + return nil, fmt.Errorf("would create cycle: %v", cycle) + } + if cycle := r.Core.DetectCycle(normalizedTargetID, "blocked_by", b.ID); cycle != nil { + return nil, fmt.Errorf("would create cycle: %v", cycle) + } + + b.AddBlocking(normalizedTargetID) + if err := r.Core.Update(b, ifMatch); err != nil { + return nil, err + } + return b, nil +} + +// RemoveBlocking removes a blocking relationship. +func (r *CoreResolver) RemoveBlocking(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + b.RemoveBlocking(normalizedTargetID) + if err := r.Core.Update(b, ifMatch); err != nil { + return nil, err + } + return b, nil +} + +// AddBlockedBy adds a blocked-by relationship. +func (r *CoreResolver) AddBlockedBy(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + if normalizedTargetID == b.ID { + return nil, fmt.Errorf("bean cannot be blocked by itself") + } + + // Check target exists + if _, err := r.Core.Get(normalizedTargetID); err != nil { + return nil, fmt.Errorf("blocker bean not found: %s", targetID) + } + + // Check for cycles in both directions + if cycle := r.Core.DetectCycle(normalizedTargetID, "blocking", b.ID); cycle != nil { + return nil, fmt.Errorf("would create cycle: %v", cycle) + } + if cycle := r.Core.DetectCycle(b.ID, "blocked_by", normalizedTargetID); cycle != nil { + return nil, fmt.Errorf("would create cycle: %v", cycle) + } + + b.AddBlockedBy(normalizedTargetID) + if err := r.Core.Update(b, ifMatch); err != nil { + return nil, err + } + return b, nil +} + +// RemoveBlockedBy removes a blocked-by relationship. +func (r *CoreResolver) RemoveBlockedBy(ctx context.Context, id string, targetID string, ifMatch *string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err != nil { + return nil, err + } + + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + b.RemoveBlockedBy(normalizedTargetID) + if err := r.Core.Update(b, ifMatch); err != nil { + return nil, err + } + return b, nil +} + +// ArchiveBean archives a bean. +func (r *CoreResolver) ArchiveBean(ctx context.Context, id string) (bool, error) { + if err := r.Core.Archive(id); err != nil { + return false, err + } + return true, nil +} diff --git a/pkg/beangraph/queries.go b/pkg/beangraph/queries.go new file mode 100644 index 00000000..3830f6a7 --- /dev/null +++ b/pkg/beangraph/queries.go @@ -0,0 +1,60 @@ +package beangraph + +import ( + "context" + + "github.com/hmans/beans/internal/gitutil" + "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beangraph/model" + "github.com/hmans/beans/pkg/beancore" +) + +// Bean returns a single bean by ID, or nil if not found. +func (r *CoreResolver) Bean(ctx context.Context, id string) (*bean.Bean, error) { + b, err := r.Core.Get(id) + if err == beancore.ErrNotFound { + return nil, nil + } + return b, err +} + +// Beans returns a filtered, sorted list of beans. +func (r *CoreResolver) Beans(ctx context.Context, filter *model.BeanFilter) ([]*bean.Bean, error) { + var beans []*bean.Bean + + // If search filter is provided, start with search results + if filter != nil && filter.Search != nil && *filter.Search != "" { + searchResults, err := r.Core.Search(*filter.Search) + if err != nil { + return nil, err + } + beans = searchResults + } else { + beans = r.Core.All() + } + + result := ApplyFilter(beans, filter, r.Core) + + // Sort using the same logic as CLI and TUI + cfg := r.Core.Config() + bean.SortByStatusPriorityAndType(result, cfg.StatusNames(), cfg.PriorityNames(), cfg.TypeNames()) + + return result, nil +} + +// ProjectName returns the configured project name. +func (r *CoreResolver) ProjectName(ctx context.Context) (string, error) { + cfg := r.Core.Config() + if cfg == nil { + return "", nil + } + return cfg.GetProjectName(), nil +} + +// MainBranch returns the current branch of the main repository. +func (r *CoreResolver) MainBranch(ctx context.Context, projectRoot string) (string, error) { + if branch, ok := gitutil.CurrentBranch(projectRoot); ok { + return branch, nil + } + return "main", nil +} diff --git a/pkg/beangraph/resolver.go b/pkg/beangraph/resolver.go new file mode 100644 index 00000000..23458147 --- /dev/null +++ b/pkg/beangraph/resolver.go @@ -0,0 +1,156 @@ +package beangraph + +import ( + "fmt" + + "github.com/hmans/beans/pkg/bean" + "github.com/hmans/beans/pkg/beancore" +) + +// CoreResolver implements the core bean GraphQL operations (CRUD, relationships, +// filtering). It depends only on beancore.Core and has no UI-specific dependencies +// (agents, worktrees, terminals, etc.). +// +// CLI commands use CoreResolver directly. The serve command's full GraphQL resolver +// embeds CoreResolver and adds UI-specific operations on top. +type CoreResolver struct { + Core *beancore.Core +} + +// ETagMismatchError is returned when an ETag validation fails. +// This allows callers to distinguish concurrency conflicts from other errors. +type ETagMismatchError struct { + Provided string + Current string +} + +func (e *ETagMismatchError) Error() string { + return fmt.Sprintf("etag mismatch: provided %s, current is %s", e.Provided, e.Current) +} + +// ETagRequiredError is returned when require_if_match is enabled and no ETag is provided. +type ETagRequiredError struct{} + +func (e *ETagRequiredError) Error() string { + return "if-match etag is required (set require_if_match: false in config to disable)" +} + +// validateETag checks if the provided ifMatch etag matches the bean's current etag. +// Returns an error if validation fails or if require_if_match is enabled and no etag provided. +func (r *CoreResolver) validateETag(b *bean.Bean, ifMatch *string) error { + cfg := r.Core.Config() + requireIfMatch := cfg != nil && cfg.Beans.RequireIfMatch + + // If require_if_match is enabled and no etag provided, reject + if requireIfMatch && (ifMatch == nil || *ifMatch == "") { + return &ETagRequiredError{} + } + + // If ifMatch provided, validate it + if ifMatch != nil && *ifMatch != "" { + currentETag := b.ETag() + if currentETag != *ifMatch { + return &ETagMismatchError{Provided: *ifMatch, Current: currentETag} + } + } + + return nil +} + +// ValidateAndSetParent validates and sets the parent relationship. +func (r *CoreResolver) ValidateAndSetParent(b *bean.Bean, parentID string) error { + if parentID == "" { + b.Parent = "" + return nil + } + + // Normalise short ID to full ID + normalizedParent, _ := r.Core.NormalizeID(parentID) + + // Validate parent type hierarchy + if err := r.Core.ValidateParent(b, normalizedParent); err != nil { + return err + } + + // Check for cycles + if cycle := r.Core.DetectCycle(b.ID, "parent", normalizedParent); cycle != nil { + return fmt.Errorf("setting parent would create cycle: %v", cycle) + } + + b.Parent = normalizedParent + return nil +} + +// ValidateAndAddBlocking validates and adds blocking relationships. +func (r *CoreResolver) ValidateAndAddBlocking(b *bean.Bean, targetIDs []string) error { + for _, targetID := range targetIDs { + // Normalise short ID to full ID + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + // Validate: cannot block itself + if normalizedTargetID == b.ID { + return fmt.Errorf("bean cannot block itself") + } + + // Validate: target must exist + if _, err := r.Core.Get(normalizedTargetID); err != nil { + return fmt.Errorf("blocking target bean not found: %s", targetID) + } + + // Check for cycles in both directions + if cycle := r.Core.DetectCycle(b.ID, "blocking", normalizedTargetID); cycle != nil { + return fmt.Errorf("adding blocking relationship would create cycle: %v", cycle) + } + if cycle := r.Core.DetectCycle(normalizedTargetID, "blocked_by", b.ID); cycle != nil { + return fmt.Errorf("adding blocking relationship would create cycle: %v", cycle) + } + + b.AddBlocking(normalizedTargetID) + } + return nil +} + +// RemoveBlockingRelationships removes blocking relationships. +func (r *CoreResolver) RemoveBlockingRelationships(b *bean.Bean, targetIDs []string) { + for _, targetID := range targetIDs { + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + b.RemoveBlocking(normalizedTargetID) + } +} + +// ValidateAndAddBlockedBy validates and adds blocked-by relationships. +func (r *CoreResolver) ValidateAndAddBlockedBy(b *bean.Bean, targetIDs []string) error { + for _, targetID := range targetIDs { + // Normalise short ID to full ID + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + + // Validate: cannot be blocked by itself + if normalizedTargetID == b.ID { + return fmt.Errorf("bean cannot be blocked by itself") + } + + // Validate: blocker must exist + if _, err := r.Core.Get(normalizedTargetID); err != nil { + return fmt.Errorf("blocker bean not found: %s", targetID) + } + + // Check for cycles in both directions + if cycle := r.Core.DetectCycle(normalizedTargetID, "blocking", b.ID); cycle != nil { + return fmt.Errorf("adding blocked-by relationship would create cycle: %v", cycle) + } + if cycle := r.Core.DetectCycle(b.ID, "blocked_by", normalizedTargetID); cycle != nil { + return fmt.Errorf("adding blocked-by relationship would create cycle: %v", cycle) + } + + b.AddBlockedBy(normalizedTargetID) + } + return nil +} + +// RemoveBlockedByRelationships removes blocked-by relationships. +func (r *CoreResolver) RemoveBlockedByRelationships(b *bean.Bean, targetIDs []string) { + for _, targetID := range targetIDs { + normalizedTargetID, _ := r.Core.NormalizeID(targetID) + b.RemoveBlockedBy(normalizedTargetID) + } +}