From 2b0be6c5ed5278b640176a2404e0a56f3cd3acb1 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 21:33:58 -0700 Subject: [PATCH 01/13] feat: add schema-shaped operation facades --- AGENTS.md | 20 ++++ cmd/local.go | 18 ++-- internal/graph/runtime.go | 85 ++++++++++++--- internal/graph/schema.go | 14 +++ internal/seed/seed_test.go | 14 +-- internal/seed/store.go | 4 +- pkg/owl/api.go | 193 ++++++++++++++++++++++++++++++++++- pkg/owl/api_internal_test.go | 12 +-- pkg/owl/api_test.go | 111 ++++++++++++++++---- pkg/owl/seed/seed_test.go | 6 +- 10 files changed, 417 insertions(+), 60 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d87c286..1fc4a5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,26 @@ Avoid: old v1 GraphQL-backed runtime as a second owner of state semantics ``` +## Graph Facade Layering + +Keep Owl operation facades layered deliberately: + +- Friendly API: typed Owl input to typed Owl output. Normal Go callers should + use methods such as `Store.Snapshot`, `Store.Source`, `Store.Check`, + `Store.Resolve`, and `Store.ApplyPromptAnswers`. +- Operation builders: typed Owl input to canonical GraphQL document plus + schema-shaped variables. Builders such as `BuildSnapshotOperation` should be + pure and useful for tests, bindings, and debug tooling. +- Graph escape hatch: caller-provided GraphQL document plus variables to raw + graph result. Expose as an advanced/debug path such as `ExecuteGraphQL`, not + as the normal CLI, Extension, or Runme API. + +Edges own I/O. CLI, Extension, Runme gRPC, and resolver/provider code should +read files, process env, protobuf streams, prompts, and external systems before +calling Owl. Graph execution receives already-materialized bytes/strings and +typed variables; it should not open project files, read process env, prompt +users, call secret managers, or speak gRPC directly. + ## Store Cutover Decisions - Implement the cutover as one cohesive PR with focused commits, not separate diff --git a/cmd/local.go b/cmd/local.go index 799b000..fd4d0b7 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -82,12 +82,15 @@ func (c *LocalStoreClient) Snapshot(ctx context.Context, req SnapshotRequest) (* return nil, err } - items, err := store.Snapshot(owl.SnapshotPolicy{Reveal: req.Reveal && req.Insecure}) + snapshot, err := store.Snapshot(ctx, owl.SnapshotInput{ + Policy: owl.SnapshotPolicy{Reveal: req.Reveal && req.Insecure}, + Filter: owl.SnapshotFilter{All: req.All, Limit: req.Limit}, + }) if err != nil { return nil, err } - return &SnapshotResult{Envs: snapshotEnvsFromItems(items)}, nil + return &SnapshotResult{Envs: snapshotEnvsFromItems(snapshot.Envs)}, nil } func (c *LocalStoreClient) Source(ctx context.Context, req SourceRequest) (*SourceResult, error) { @@ -96,12 +99,14 @@ func (c *LocalStoreClient) Source(ctx context.Context, req SourceRequest) (*Sour return nil, err } - envs, err := store.Dotenv(owl.DotenvPolicy{Insecure: req.Insecure}) + source, err := store.Source(ctx, owl.SourceInput{ + Policy: owl.DotenvPolicy{Insecure: req.Insecure}, + }) if err != nil { return nil, err } - return &SourceResult{Envs: envs}, nil + return &SourceResult{Envs: source.Envs}, nil } func (c *LocalStoreClient) Check(ctx context.Context, req CheckRequest) (*CheckResult, error) { @@ -110,15 +115,14 @@ func (c *LocalStoreClient) Check(ctx context.Context, req CheckRequest) (*CheckR return nil, err } - check := store.Check() - items, err := store.Snapshot(owl.SnapshotPolicy{}) + check, err := store.Check(ctx, owl.CheckInput{}) if err != nil { return nil, err } return &CheckResult{ OK: check.OK, Diagnostics: append(diagnosticStrings(c.lastSourceDiagnostics, req.Details), diagnosticStrings(check.Diagnostics, req.Details)...), - Checked: len(items), + Checked: check.Checked, }, nil } diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index e59344d..b63507d 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -40,6 +40,16 @@ type CheckResult = store.CheckResult type StateEnvelope = store.StateEnvelope +type Operation struct { + Name string + Document string + Variables map[string]interface{} +} + +type ExecuteResult struct { + Data json.RawMessage +} + var traceGraphQLQuery func(query string, vars map[string]interface{}) func NewRuntime(types registry.TypeProvider) (*Runtime, error) { @@ -56,10 +66,8 @@ func NewRuntime(types registry.TypeProvider) (*Runtime, error) { } func (r *Runtime) Snapshot(ctx context.Context, input LoadInput, policy SnapshotPolicy) ([]SnapshotItem, error) { - result, err := r.do(ctx, snapshotQuery, map[string]interface{}{ - "input": marshalInput(input), - "reveal": policy.Reveal, - }) + op := SnapshotOperation(input, policy) + result, err := r.do(ctx, op.Document, op.Variables) if err != nil { return nil, err } @@ -70,11 +78,20 @@ func (r *Runtime) Snapshot(ctx context.Context, input LoadInput, policy Snapshot return decodeSnapshot(raw), nil } +func SnapshotOperation(input LoadInput, policy SnapshotPolicy) Operation { + return Operation{ + Name: "OwlSnapshot", + Document: snapshotQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + "reveal": policy.Reveal, + }, + } +} + func (r *Runtime) Dotenv(ctx context.Context, input LoadInput, policy DotenvPolicy) ([]string, error) { - result, err := r.do(ctx, dotenvQuery, map[string]interface{}{ - "input": marshalInput(input), - "insecure": policy.Insecure, - }) + op := DotenvOperation(input, policy) + result, err := r.do(ctx, op.Document, op.Variables) if err != nil { return nil, err } @@ -89,6 +106,17 @@ func (r *Runtime) Dotenv(ctx context.Context, input LoadInput, policy DotenvPoli return envs, nil } +func DotenvOperation(input LoadInput, policy DotenvPolicy) Operation { + return Operation{ + Name: "OwlDotenv", + Document: dotenvQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + "insecure": policy.Insecure, + }, + } +} + func (r *Runtime) Get(ctx context.Context, input LoadInput, key string, policy GetPolicy) (GetResult, bool, error) { result, err := r.do(ctx, getQuery, map[string]interface{}{ "input": marshalInput(input), @@ -165,9 +193,8 @@ func (r *Runtime) StateEnvelopeAfter(ctx context.Context, input LoadInput, patch } func (r *Runtime) Check(ctx context.Context, input LoadInput) (CheckResult, error) { - result, err := r.do(ctx, checkQuery, map[string]interface{}{ - "input": marshalInput(input), - }) + op := CheckOperation(input) + result, err := r.do(ctx, op.Document, op.Variables) if err != nil { return CheckResult{}, err } @@ -178,6 +205,28 @@ func (r *Runtime) Check(ctx context.Context, input LoadInput) (CheckResult, erro return decodeCheck(raw), nil } +func CheckOperation(input LoadInput) Operation { + return Operation{ + Name: "OwlCheck", + Document: checkQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + }, + } +} + +func (r *Runtime) Execute(ctx context.Context, query string, vars map[string]interface{}) (ExecuteResult, error) { + result, err := r.do(ctx, query, vars) + if err != nil { + return ExecuteResult{}, err + } + raw, err := json.Marshal(result.Data) + if err != nil { + return ExecuteResult{}, err + } + return ExecuteResult{Data: raw}, nil +} + func (r *Runtime) SchemaJSON(ctx context.Context) (string, error) { result, err := r.do(ctx, introspectionQuery, nil) if err != nil { @@ -558,7 +607,9 @@ query OwlSnapshot($input: LoadInput!, $reveal: Boolean = false) { fieldInstance fieldName source + sourceRef { name kind } origin + originRef { name kind } explicit confidence visibility @@ -595,8 +646,8 @@ func decodeSnapshot(raw interface{}) []SnapshotItem { Instance: stringValue(item["fieldInstance"]), Field: stringValue(item["fieldName"]), }, - Source: model.Source{Name: stringValue(item["source"])}, - Origin: model.Source{Name: stringValue(item["origin"])}, + Source: decodeSnapshotSource(item["sourceRef"], item["source"]), + Origin: decodeSnapshotSource(item["originRef"], item["origin"]), Explicit: boolValue(item["explicit"]), Confidence: model.BindingConfidence(stringValue(item["confidence"])), Visibility: model.Visibility(stringValue(item["visibility"])), @@ -609,6 +660,14 @@ func decodeSnapshot(raw interface{}) []SnapshotItem { return items } +func decodeSnapshotSource(raw, fallback interface{}) model.Source { + source := decodeSource(raw) + if source.Name == "" && source.Kind == "" { + source.Name = stringValue(fallback) + } + return source +} + func decodeCheck(raw interface{}) CheckResult { row, ok := raw.(map[string]interface{}) if !ok { diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 4635358..0e46069 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -466,6 +466,13 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { return item.Source.Name, nil }, }, + "sourceRef": &graphql.Field{ + Type: sourceType, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + item := p.Source.(store.SnapshotItem) + return item.Source, nil + }, + }, "origin": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { @@ -473,6 +480,13 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { return item.Origin.Name, nil }, }, + "originRef": &graphql.Field{ + Type: sourceType, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + item := p.Source.(store.SnapshotItem) + return item.Origin, nil + }, + }, "explicit": &graphql.Field{Type: graphql.Boolean}, "confidence": &graphql.Field{ Type: graphql.String, diff --git a/internal/seed/seed_test.go b/internal/seed/seed_test.go index 445bb08..70ff66d 100644 --- a/internal/seed/seed_test.go +++ b/internal/seed/seed_test.go @@ -40,7 +40,7 @@ func TestNewStoreSeedsObservedSourceWithCallerProvenance(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) env := snapshotItemByName(items)["KERNEL_ONLY"] assert.Equal(t, "[hidden]", env.Value) @@ -78,7 +78,7 @@ func TestNewStoreAttributesMatchingObservedEnvToDirenv(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) env := snapshotItemByName(items)["DIRENV_WINS"] assert.Equal(t, "from-direnv", env.Value) @@ -111,7 +111,7 @@ func TestNewStoreDefaultsDirenvToWarn(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) env := snapshotItemByName(items)["DIRENV_DEFAULT"] assert.Equal(t, "from-direnv", env.Value) @@ -215,7 +215,7 @@ func TestNewRawValueStoreSkipsMissingEnvFiles(t *testing.T) { }, false) require.NoError(t, err) - items, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + items, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) env := snapshotItemByName(items)["PRESENT"] assert.Equal(t, "from-dotenv", env.Value) @@ -236,7 +236,7 @@ func TestNewRawValueStoreUsesDefaultEnvFilesInOrder(t *testing.T) { store, err := NewRawValueStore(Options{WorkDir: dir}, false) require.NoError(t, err) - items, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + items, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) env := snapshotItemByName(items)["DEFAULT_ORDER"] assert.Equal(t, "from-env-dev", env.Value) @@ -307,7 +307,7 @@ func TestStoreBuildersUseConfiguredTypeProvider(t *testing.T) { require.NoError(t, err) _, err = result.Store.Resolve(context.Background(), owl.ResolveInput{Process: result.Catalog.ProcessResolverInput()}) require.NoError(t, err) - _ = result.Store.Check() + _ = result.Store.CheckState() assert.Positive(t, seededProvider.validations) rawProvider := &seedTrackingTypeProvider{BuiltInRegistry: registry.NewBuiltInRegistry()} @@ -317,7 +317,7 @@ func TestStoreBuildersUseConfiguredTypeProvider(t *testing.T) { TypeProvider: rawProvider, }, false) require.NoError(t, err) - _ = rawStore.Check() + _ = rawStore.CheckState() assert.Positive(t, rawProvider.validations) } diff --git a/internal/seed/store.go b/internal/seed/store.go index 37f30e4..951a6c7 100644 --- a/internal/seed/store.go +++ b/internal/seed/store.go @@ -99,7 +99,7 @@ func seedInheritedValues(store *owl.Store, catalog Catalog) error { } func explicitSnapshotKeys(store *owl.Store) (map[string]struct{}, error) { - items, err := store.Snapshot(owl.SnapshotPolicy{}) + items, err := store.SnapshotItems(owl.SnapshotPolicy{}) if err != nil { return nil, err } @@ -141,7 +141,7 @@ func loadInheritedVariables(store *owl.Store, vars []owl.DotenvVariable, explici } func projectionKeys(store *owl.Store) map[string]struct{} { - items, err := store.Snapshot(owl.SnapshotPolicy{}) + items, err := store.SnapshotItems(owl.SnapshotPolicy{}) if err != nil { return nil } diff --git a/pkg/owl/api.go b/pkg/owl/api.go index 2ca975f..19795c0 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -2,6 +2,7 @@ package owl import ( "context" + "encoding/json" "fmt" "io" "strings" @@ -22,6 +23,7 @@ type ( TypePolicy = store.TypePolicy GetPolicy = store.GetPolicy SnapshotItem = store.SnapshotItem + SnapshotEnv = store.SnapshotItem TypeResult = store.TypeResult TypeProposal = store.TypeProposal GetResult = store.GetResult @@ -44,6 +46,7 @@ type ( DotenvVariable = store.DotenvVariable EnvContract = store.EnvContract EnvBinding = store.EnvBinding + LoadInput = store.LoadInput StateEnvelope = store.StateEnvelope StateProvenance = store.StateProvenance Visibility = model.Visibility @@ -139,6 +142,56 @@ type ResolveInput struct { Chain ChainConfig } +type SnapshotInput struct { + Load LoadInput + Policy SnapshotPolicy + Filter SnapshotFilter +} + +type SnapshotFilter struct { + All bool + Limit int +} + +type SnapshotOutput struct { + Envs []SnapshotEnv + Diagnostics []Diagnostic +} + +type SourceInput struct { + Load LoadInput + Policy DotenvPolicy +} + +type SourceOutput struct { + Envs []string +} + +type CheckInput struct { + Load LoadInput +} + +type CheckOutput struct { + OK bool + Diagnostics []Diagnostic + Checked int +} + +type GraphOperation struct { + Name string + Document string + Variables map[string]interface{} +} + +type GraphQLRequest struct { + Document string + Variables map[string]interface{} +} + +type GraphQLResult struct { + Data json.RawMessage +} + func ContextWithExecutionInfo(ctx context.Context, info ExecutionInfo) context.Context { if ctx == nil { ctx = context.Background() @@ -275,10 +328,74 @@ func withClock(clock model.Clock) StoreOption { } } -func (s *Store) Snapshot(policy SnapshotPolicy) ([]SnapshotItem, error) { +func (s *Store) Snapshot(ctx context.Context, input SnapshotInput) (SnapshotOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return SnapshotOutput{}, err + } + items, err := s.runtime.Snapshot(ctx, load, input.Policy) + if err != nil { + return SnapshotOutput{}, err + } + return SnapshotOutput{Envs: snapshotEnvsForInput(items, input)}, nil +} + +func (s *Store) BuildSnapshotOperation(ctx context.Context, input SnapshotInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.SnapshotOperation(load, input.Policy)), nil +} + +func (s *Store) SnapshotItems(policy SnapshotPolicy) ([]SnapshotItem, error) { return store.NewState(s.state, s.types).Snapshot(policy) } +func snapshotEnvsForInput(items []SnapshotItem, input SnapshotInput) []SnapshotEnv { + envs := make([]SnapshotEnv, 0, len(items)) + limit := input.Filter.Limit + for i, item := range items { + if limit > 0 && !input.Filter.All && i >= limit { + break + } + envs = append(envs, item) + } + return envs +} + +func (s *Store) Source(ctx context.Context, input SourceInput) (SourceOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return SourceOutput{}, err + } + envs, err := s.runtime.Dotenv(ctx, load, input.Policy) + if err != nil { + return SourceOutput{}, err + } + return SourceOutput{Envs: envs}, nil +} + +func (s *Store) BuildSourceOperation(ctx context.Context, input SourceInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.DotenvOperation(load, input.Policy)), nil +} + func (s *Store) Dotenv(policy DotenvPolicy) ([]string, error) { return store.NewState(s.state, s.types).Dotenv(policy) } @@ -302,10 +419,55 @@ func (s *Store) SensitiveKeys() ([]string, error) { return store.NewState(s.state, s.types).SensitiveKeys() } -func (s *Store) Check() CheckResult { +func (s *Store) Check(ctx context.Context, input CheckInput) (CheckOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return CheckOutput{}, err + } + check, err := s.runtime.Check(ctx, load) + if err != nil { + return CheckOutput{}, err + } + snapshot, err := s.runtime.Snapshot(ctx, load, SnapshotPolicy{}) + if err != nil { + return CheckOutput{}, err + } + return CheckOutput{ + OK: check.OK, + Diagnostics: check.Diagnostics, + Checked: len(snapshot), + }, nil +} + +func (s *Store) BuildCheckOperation(ctx context.Context, input CheckInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.CheckOperation(load)), nil +} + +func (s *Store) CheckState() store.CheckResult { return store.NewState(s.state, s.types).Check() } +func (s *Store) ExecuteGraphQL(ctx context.Context, req GraphQLRequest) (GraphQLResult, error) { + if ctx == nil { + ctx = context.Background() + } + result, err := s.runtime.Execute(ctx, req.Document, req.Variables) + if err != nil { + return GraphQLResult{}, err + } + return GraphQLResult{Data: result.Data}, nil +} + func (s *Store) ResolverAttempts() []ResolverAttempt { return append([]ResolverAttempt{}, s.state.ResolverAttempts...) } @@ -448,6 +610,33 @@ func (s *Store) StateEnvelope(ctx context.Context) (StateEnvelope, error) { return store.NewState(s.state, s.types).StateEnvelope(), nil } +func (s *Store) loadInputForOperation(ctx context.Context, input LoadInput) (LoadInput, error) { + if !isZeroLoadInput(input) { + return input, nil + } + envelope, err := s.StateEnvelope(ctx) + if err != nil { + return LoadInput{}, err + } + return LoadInput{Envelope: &envelope}, nil +} + +func isZeroLoadInput(input LoadInput) bool { + return input.DotenvSource == (Source{}) && + len(input.Dotenv) == 0 && + len(input.Contracts) == 0 && + input.Envelope == nil && + input.Timestamp.IsZero() +} + +func graphOperation(op graph.Operation) GraphOperation { + return GraphOperation{ + Name: op.Name, + Document: op.Document, + Variables: op.Variables, + } +} + func (s *Store) GraphQLSchema() (string, error) { return s.runtime.SchemaJSON(context.Background()) } diff --git a/pkg/owl/api_internal_test.go b/pkg/owl/api_internal_test.go index 7b61a66..c8dc827 100644 --- a/pkg/owl/api_internal_test.go +++ b/pkg/owl/api_internal_test.go @@ -25,21 +25,21 @@ func TestPublicAPIUpdateTimestampsOnlyChangedItems(t *testing.T) { ) require.NoError(t, err) - initial, err := store.Snapshot(SnapshotPolicy{Reveal: true}) + initial, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) initialByName := snapshotItemsByName(initial) assert.Equal(t, timestamps.At(0), initialByName["API_URL"].UpdatedAt) assert.Equal(t, timestamps.At(0), initialByName["TOKEN"].UpdatedAt) require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://two.example.com"}, nil)) - afterAPIURLUpdate, err := store.Snapshot(SnapshotPolicy{Reveal: true}) + afterAPIURLUpdate, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) afterAPIURLUpdateByName := snapshotItemsByName(afterAPIURLUpdate) assert.Equal(t, timestamps.At(1), afterAPIURLUpdateByName["API_URL"].UpdatedAt) assert.Equal(t, timestamps.At(0), afterAPIURLUpdateByName["TOKEN"].UpdatedAt) require.NoError(t, store.Update(context.Background(), []string{"TOKEN=two"}, nil)) - afterTokenUpdate, err := store.Snapshot(SnapshotPolicy{Reveal: true}) + afterTokenUpdate, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) afterTokenUpdateByName := snapshotItemsByName(afterTokenUpdate) assert.Equal(t, timestamps.At(1), afterTokenUpdateByName["API_URL"].UpdatedAt) @@ -59,7 +59,7 @@ func TestPublicAPIUpdateClearsResolvedRequiredDiagnostics(t *testing.T) { ) require.NoError(t, err) - initial, err := store.Snapshot(SnapshotPolicy{Reveal: true}) + initial, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) initialByName := snapshotItemsByName(initial) require.Contains(t, initialByName, "TOKEN") @@ -68,7 +68,7 @@ func TestPublicAPIUpdateClearsResolvedRequiredDiagnostics(t *testing.T) { ctx := ContextWithExecutionInfo(context.Background(), ExecutionInfo{ExecContext: "direnv"}) require.NoError(t, store.Update(ctx, []string{"TOKEN=secret"}, nil)) - updated, err := store.Snapshot(SnapshotPolicy{Reveal: true}) + updated, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) updatedByName := snapshotItemsByName(updated) assert.Equal(t, "secret", updatedByName["TOKEN"].Value) @@ -110,7 +110,7 @@ func TestPublicAPIStateEnvelopePreservesOperationTimestamps(t *testing.T) { assert.Contains(t, operationTimestamps(roundTrippedEnvelope.Provenance.Operations), timestamps.At(1)) require.NoError(t, roundTripped.Update(context.Background(), []string{"API_URL=https://three.example.com"}, nil)) - snapshot, err := roundTripped.Snapshot(SnapshotPolicy{Reveal: true}) + snapshot, err := roundTripped.SnapshotItems(SnapshotPolicy{Reveal: true}) require.NoError(t, err) assert.Equal(t, timestamps.At(2), snapshotItemsByName(snapshot)["API_URL"].UpdatedAt) } diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index a20b70c..ccc19af 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -22,7 +22,7 @@ func TestV2PublicAPI(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) assert.Equal(t, "[masked]", snapshotByName(snapshot)["API_KEY"].Value) @@ -62,6 +62,77 @@ func TestV2PublicAPI(t *testing.T) { assert.False(t, ok) } +func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { + t.Parallel() + + store, err := owl.NewStore( + owl.WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\nAPI_KEY=secret\n")), + owl.WithEnvSpec(".env.spec", strings.NewReader("API_URL=\"API URL\" # Plain!\nAPI_KEY=\"API key\" # Secret!\n")), + ) + require.NoError(t, err) + + snapshot, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Policy: owl.SnapshotPolicy{Reveal: true}, + Filter: owl.SnapshotFilter{Limit: 1}, + }) + require.NoError(t, err) + require.Len(t, snapshot.Envs, 1) + assert.Equal(t, "API_URL", snapshot.Envs[0].Name) + assert.Equal(t, owl.Source{Name: ".env", Kind: "dotenv"}, snapshot.Envs[0].Source) + + source, err := store.Source(context.Background(), owl.SourceInput{ + Policy: owl.DotenvPolicy{Insecure: true}, + }) + require.NoError(t, err) + assert.Equal(t, []string{ + "API_KEY=secret", + "API_URL=https://api.example.com", + }, source.Envs) + + check, err := store.Check(context.Background(), owl.CheckInput{}) + require.NoError(t, err) + assert.True(t, check.OK) + assert.Equal(t, 2, check.Checked) + + for _, op := range []owl.GraphOperation{ + mustBuildSnapshotOperation(t, store), + mustBuildSourceOperation(t, store), + mustBuildCheckOperation(t, store), + } { + require.Contains(t, op.Variables, "input") + input, ok := op.Variables["input"].(map[string]interface{}) + require.True(t, ok) + assert.Contains(t, input, "envelope") + } +} + +func mustBuildSnapshotOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildSnapshotOperation(context.Background(), owl.SnapshotInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlSnapshot", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + +func mustBuildSourceOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildSourceOperation(context.Background(), owl.SourceInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlDotenv", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + +func mustBuildCheckOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildCheckOperation(context.Background(), owl.CheckInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlCheck", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + func TestPublicAPISnapshotOrderSurvivesStateEnvelopeRoundTrip(t *testing.T) { t.Parallel() @@ -77,7 +148,7 @@ func TestPublicAPISnapshotOrderSurvivesStateEnvelopeRoundTrip(t *testing.T) { roundTripped, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - snapshot, err := roundTripped.Snapshot(owl.SnapshotPolicy{Reveal: true}) + snapshot, err := roundTripped.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) assert.Equal(t, []string{"ZETA", "BETA", "APPLE", "OMEGA"}, snapshotNames(snapshot)) } @@ -91,7 +162,7 @@ func TestPublicAPIVisibilityAndExposure(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) @@ -115,7 +186,7 @@ func TestPublicAPIVisibilityAndExposure(t *testing.T) { assert.Empty(t, byName["MISSING_TOKEN"].Source) assert.Equal(t, ".env.spec", byName["MISSING_TOKEN"].Origin.Name) - revealed, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) revealedByName := snapshotByName(revealed) assert.Equal(t, "secret", revealedByName["API_KEY"].Value) @@ -133,7 +204,7 @@ func TestPublicAPIUndeclaredOpaqueKeysStayHidden(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) @@ -146,7 +217,7 @@ func TestPublicAPIUndeclaredOpaqueKeysStayHidden(t *testing.T) { assert.Equal(t, "[process]", byName[name].Source.Name) } - revealed, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) revealedByName := snapshotByName(revealed) assert.Equal(t, "sk-example", revealedByName["OPENAI_API_KEY"].Value) @@ -163,7 +234,7 @@ func TestPublicAPIObservedEmptyValuesArePresent(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) @@ -181,7 +252,7 @@ func TestPublicAPIObservedEmptyValuesArePresent(t *testing.T) { assert.Equal(t, "[process]", byName["EMPTY_OPAQUE"].Source.Name) assert.Equal(t, ".env.spec", byName["EMPTY_OPAQUE"].Origin.Name) - revealed, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) revealedByName := snapshotByName(revealed) assert.Equal(t, "", revealedByName["RUNME_TEST_TOKEN"].Value) @@ -189,7 +260,7 @@ func TestPublicAPIObservedEmptyValuesArePresent(t *testing.T) { assert.Equal(t, "", revealedByName["EMPTY_OPAQUE"].Value) assert.Equal(t, owl.VisibilityLiteral, revealedByName["EMPTY_OPAQUE"].Visibility) - check := store.Check() + check := store.CheckState() assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-secret") assert.NotContains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") @@ -257,7 +328,7 @@ func TestPublicAPIDotenvSecureAndInsecure(t *testing.T) { "DATABASE_URL=postgres://example", }, insecure) - check := store.Check() + check := store.CheckState() assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") } @@ -278,9 +349,9 @@ func TestPublicAPIStateEnvelopeRoundTrip(t *testing.T) { roundTripped, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) - roundTrippedSnapshot, err := roundTripped.Snapshot(owl.SnapshotPolicy{}) + roundTrippedSnapshot, err := roundTripped.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) assert.Equal(t, snapshotByName(snapshot)["API_KEY"].Visibility, snapshotByName(roundTrippedSnapshot)["API_KEY"].Visibility) assert.Equal(t, snapshotByName(snapshot)["DATABASE_URL"].Exposure, snapshotByName(roundTrippedSnapshot)["DATABASE_URL"].Exposure) @@ -424,7 +495,7 @@ func TestPublicAPIExecutionInfoSetsUpdateSource(t *testing.T) { "TOKEN=secret", }, nil)) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) byName := snapshotByName(snapshot) @@ -457,7 +528,7 @@ func TestPublicAPIWithEnvContractMapsBindings(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) item := snapshotByName(snapshot)["DATABASE_URL"] assert.Equal(t, "postgres://example", item.Value) @@ -491,7 +562,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) @@ -522,7 +593,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { "", }, "\n"), dotenvSpec) - check := store.Check() + check := store.CheckState() assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") } @@ -544,7 +615,7 @@ func TestPublicAPIWithConfigValidatesRedisPort(t *testing.T) { ) require.NoError(t, err) - check := store.Check() + check := store.CheckState() assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-port") @@ -571,7 +642,7 @@ func TestPublicAPIWithConfigValidatesRedisHostRequiredByRedis(t *testing.T) { ) require.NoError(t, err) - check := store.Check() + check := store.CheckState() assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-host") } @@ -593,7 +664,7 @@ func TestPublicAPIWithConfigIncludesRedisHostDiagnosticsInSnapshot(t *testing.T) ) require.NoError(t, err) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) assert.Contains(t, diagnosticCodes(byName["QUEUES_REDIS_HOST"].Diagnostics), "type.invalid-host") @@ -630,7 +701,7 @@ func TestPublicAPIResolveReturnsPromptActionsAndAppliesAnswers(t *testing.T) { require.Len(t, applied.Attempts, 1) assert.Equal(t, owl.ResolverResolved, applied.Attempts[0].Outcome) - snapshot, err := store.Snapshot(owl.SnapshotPolicy{}) + snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) require.NoError(t, err) byName := snapshotByName(snapshot) assert.Equal(t, "[masked]", byName["API_KEY"].Value) diff --git a/pkg/owl/seed/seed_test.go b/pkg/owl/seed/seed_test.go index 1b62d21..800ea06 100644 --- a/pkg/owl/seed/seed_test.go +++ b/pkg/owl/seed/seed_test.go @@ -28,7 +28,7 @@ func TestNewStoreReturnsSeededStore(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) require.Len(t, items, 1) assert.Equal(t, "API_KEY", items[0].Name) @@ -52,7 +52,7 @@ func TestNewStoreSkipsMissingEnvFiles(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) require.Len(t, items, 1) assert.Equal(t, "PRESENT", items[0].Name) @@ -79,7 +79,7 @@ func TestNewStoreUsesDefaultEnvFilesInOrder(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.Snapshot(owl.SnapshotPolicy{Reveal: true}) + items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) require.NoError(t, err) require.Len(t, items, 1) assert.Equal(t, "DEFAULT_ORDER", items[0].Name) From 57f668aa14482fcb65668fb06d484be3bc2b796e Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 21:48:21 -0700 Subject: [PATCH 02/13] fix: normalize snapshot source fields --- internal/graph/runtime.go | 18 ++++-------------- internal/graph/schema.go | 14 -------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index b63507d..562bfd9 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -606,10 +606,8 @@ query OwlSnapshot($input: LoadInput!, $reveal: Boolean = false) { fieldTypeID fieldInstance fieldName - source - sourceRef { name kind } - origin - originRef { name kind } + source { name kind } + origin { name kind } explicit confidence visibility @@ -646,8 +644,8 @@ func decodeSnapshot(raw interface{}) []SnapshotItem { Instance: stringValue(item["fieldInstance"]), Field: stringValue(item["fieldName"]), }, - Source: decodeSnapshotSource(item["sourceRef"], item["source"]), - Origin: decodeSnapshotSource(item["originRef"], item["origin"]), + Source: decodeSource(item["source"]), + Origin: decodeSource(item["origin"]), Explicit: boolValue(item["explicit"]), Confidence: model.BindingConfidence(stringValue(item["confidence"])), Visibility: model.Visibility(stringValue(item["visibility"])), @@ -660,14 +658,6 @@ func decodeSnapshot(raw interface{}) []SnapshotItem { return items } -func decodeSnapshotSource(raw, fallback interface{}) model.Source { - source := decodeSource(raw) - if source.Name == "" && source.Kind == "" { - source.Name = stringValue(fallback) - } - return source -} - func decodeCheck(raw interface{}) CheckResult { row, ok := raw.(map[string]interface{}) if !ok { diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 0e46069..00df60e 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -460,13 +460,6 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, }, "source": &graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) - return item.Source.Name, nil - }, - }, - "sourceRef": &graphql.Field{ Type: sourceType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { item := p.Source.(store.SnapshotItem) @@ -474,13 +467,6 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, }, "origin": &graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) - return item.Origin.Name, nil - }, - }, - "originRef": &graphql.Field{ Type: sourceType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { item := p.Source.(store.SnapshotItem) From dd0d7221ec24a30d802f28bebbb67b1e03b19446 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:20:24 -0700 Subject: [PATCH 03/13] feat: derive graph inputs from descriptors --- internal/graph/input_descriptor.go | 23 ++ internal/graph/runtime_test.go | 65 ++++++ internal/graph/schema.go | 343 ++++++++++++----------------- 3 files changed, 231 insertions(+), 200 deletions(-) create mode 100644 internal/graph/input_descriptor.go diff --git a/internal/graph/input_descriptor.go b/internal/graph/input_descriptor.go new file mode 100644 index 0000000..6b0cf49 --- /dev/null +++ b/internal/graph/input_descriptor.go @@ -0,0 +1,23 @@ +package graph + +import "github.com/graphql-go/graphql" + +type graphInputField struct { + name string + typ graphql.Input +} + +func graphInput(name string, typ graphql.Input) graphInputField { + return graphInputField{name: name, typ: typ} +} + +func graphInputObject(name string, fields ...graphInputField) *graphql.InputObject { + fieldMap := make(graphql.InputObjectConfigFieldMap, len(fields)) + for _, field := range fields { + fieldMap[field.name] = &graphql.InputObjectFieldConfig{Type: field.typ} + } + return graphql.NewInputObject(graphql.InputObjectConfig{ + Name: name, + Fields: fieldMap, + }) +} diff --git a/internal/graph/runtime_test.go b/internal/graph/runtime_test.go index 96bcea7..206d7b5 100644 --- a/internal/graph/runtime_test.go +++ b/internal/graph/runtime_test.go @@ -2,6 +2,8 @@ package graph import ( "context" + "encoding/json" + "sort" "testing" "time" @@ -165,6 +167,69 @@ func TestRuntimeSchemaUsesVisibilityAndExposureNames(t *testing.T) { } } +func TestRuntimeSchemaInputFieldsMatchBoundaryDescriptors(t *testing.T) { + t.Parallel() + + runtime, err := NewRuntime(nil) + require.NoError(t, err) + + schemaJSON, err := runtime.SchemaJSON(context.Background()) + require.NoError(t, err) + + fieldsByInput := introspectionInputFields(t, schemaJSON) + for inputName, want := range map[string][]string{ + "SourceInput": {"kind", "name"}, + "FieldRefInput": {"field", "instance", "typeID"}, + "DotenvVariableInput": {"key", "source", "value"}, + "DotenvInput": {"source", "timestamp", "variables"}, + "EnvBindingInput": {"description", "exposure", "field", "key", "order", "projection", "required", "sensitivity", "source"}, + "EnvContractInput": {"bindings", "projection", "source"}, + "DiagnosticInput": {"code", "details", "field", "key", "message", "owner", "severity"}, + "ResolverAttemptInput": {"diagnostics", "field", "finishedAt", "id", "message", "outcome", "projectionKey", "resolverID", "source", "startedAt"}, + "ResolverProposalInput": {"attemptID", "field", "needID", "projectionKey", "resolverID", "value"}, + "StateEnvelopeInput": {"modelVersion", "provenance", "state"}, + "LoadInput": {"contracts", "dotenv", "envelope", "timestamp"}, + "OperationMetadataInput": {"actor", "id", "kind", "projection", "source", "timestamp"}, + "StateProvenanceInput": {"operations", "sources"}, + "EffectiveStateInput": {"bindings", "diagnostics", "resolverAttempts", "unresolvedFrontier", "values"}, + "UnresolvedFrontierInput": {"needs"}, + } { + got, ok := fieldsByInput[inputName] + require.True(t, ok, "missing input %s", inputName) + assert.Equal(t, want, got, inputName) + } +} + +func introspectionInputFields(t *testing.T, schemaJSON string) map[string][]string { + t.Helper() + + var payload struct { + Schema struct { + Types []struct { + Name string `json:"name"` + InputFields []struct { + Name string `json:"name"` + } `json:"inputFields"` + } `json:"types"` + } `json:"__schema"` + } + require.NoError(t, json.Unmarshal([]byte(schemaJSON), &payload)) + + fieldsByInput := make(map[string][]string) + for _, typ := range payload.Schema.Types { + if len(typ.InputFields) == 0 { + continue + } + fields := make([]string, 0, len(typ.InputFields)) + for _, field := range typ.InputFields { + fields = append(fields, field.Name) + } + sort.Strings(fields) + fieldsByInput[typ.Name] = fields + } + return fieldsByInput +} + func TestPlanStateEnvelopeQueryStacksOperationRecords(t *testing.T) { t.Parallel() diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 00df60e..6f4ba05 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -14,206 +14,149 @@ import ( ) func (r *Runtime) newSchema() (graphql.Schema, error) { - sourceInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "SourceInput", - Fields: graphql.InputObjectConfigFieldMap{ - "name": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "kind": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - }, - }) - fieldRefInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "FieldRefInput", - Fields: graphql.InputObjectConfigFieldMap{ - "typeID": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "instance": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - }, - }) - dotenvVariableInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "DotenvVariableInput", - Fields: graphql.InputObjectConfigFieldMap{ - "key": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "value": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - }, - }) - dotenvInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "DotenvInput", - Fields: graphql.InputObjectConfigFieldMap{ - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "variables": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(dotenvVariableInput))}, - "timestamp": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - envBindingInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "EnvBindingInput", - Fields: graphql.InputObjectConfigFieldMap{ - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(fieldRefInput)}, - "key": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "projection": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "required": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - "description": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "order": &graphql.InputObjectFieldConfig{Type: graphql.Int}, - "sensitivity": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "exposure": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - envContractInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "EnvContractInput", - Fields: graphql.InputObjectConfigFieldMap{ - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "projection": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "bindings": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(envBindingInput))}, - }, - }) - diagnosticInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "DiagnosticInput", - Fields: graphql.InputObjectConfigFieldMap{ - "severity": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "code": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "message": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "details": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.String)}, - "key": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "field": &graphql.InputObjectFieldConfig{Type: fieldRefInput}, - "owner": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - resolverAttemptInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "ResolverAttemptInput", - Fields: graphql.InputObjectConfigFieldMap{ - "id": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "resolverID": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "field": &graphql.InputObjectFieldConfig{Type: fieldRefInput}, - "projectionKey": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "outcome": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "message": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "startedAt": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "finishedAt": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "diagnostics": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(diagnosticInput))}, - }, - }) - proposedValueInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "ProposedValueInput", - Fields: graphql.InputObjectConfigFieldMap{ - "value": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "sensitivity": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "exposure": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - resolverProposalInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "ResolverProposalInput", - Fields: graphql.InputObjectConfigFieldMap{ - "needID": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "attemptID": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "resolverID": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(fieldRefInput)}, - "projectionKey": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "value": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(proposedValueInput)}, - }, - }) - unresolvedNeedInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "UnresolvedNeedInput", - Fields: graphql.InputObjectConfigFieldMap{ - "id": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(fieldRefInput)}, - "projectionKey": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "required": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - "blocking": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - "reason": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "description": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "sensitivity": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "exposure": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "origin": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "resolverAttemptIDs": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.String)}, - }, - }) - unresolvedFrontierInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "UnresolvedFrontierInput", - Fields: graphql.InputObjectConfigFieldMap{ - "needs": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(unresolvedNeedInput))}, - }, - }) - stateValueInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "StateValueInput", - Fields: graphql.InputObjectConfigFieldMap{ - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(fieldRefInput)}, - "original": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "resolved": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "visibility": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "sensitivity": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "exposure": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "origin": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "createdAt": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "updatedAt": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - stateBindingInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "StateBindingInput", - Fields: graphql.InputObjectConfigFieldMap{ - "id": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "field": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(fieldRefInput)}, - "projection": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "key": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "description": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "origin": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "confidence": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "explicit": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - "order": &graphql.InputObjectFieldConfig{Type: graphql.Int}, - "preserveKey": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - "required": &graphql.InputObjectFieldConfig{Type: graphql.Boolean}, - }, - }) - effectiveStateInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "EffectiveStateInput", - Fields: graphql.InputObjectConfigFieldMap{ - "values": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(stateValueInput))}, - "bindings": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(stateBindingInput))}, - "resolverAttempts": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(resolverAttemptInput))}, - "unresolvedFrontier": &graphql.InputObjectFieldConfig{Type: unresolvedFrontierInput}, - "diagnostics": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(diagnosticInput))}, - }, - }) - operationMetadataInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "OperationMetadataInput", - Fields: graphql.InputObjectConfigFieldMap{ - "id": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "kind": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "timestamp": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "actor": &graphql.InputObjectFieldConfig{Type: graphql.String}, - "source": &graphql.InputObjectFieldConfig{Type: sourceInput}, - "projection": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) - stateProvenanceInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "StateProvenanceInput", - Fields: graphql.InputObjectConfigFieldMap{ - "sources": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(sourceInput))}, - "operations": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(operationMetadataInput))}, - }, - }) - stateEnvelopeInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "StateEnvelopeInput", - Fields: graphql.InputObjectConfigFieldMap{ - "modelVersion": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(graphql.String)}, - "state": &graphql.InputObjectFieldConfig{Type: graphql.NewNonNull(effectiveStateInput)}, - "provenance": &graphql.InputObjectFieldConfig{Type: stateProvenanceInput}, - }, - }) - loadInput := graphql.NewInputObject(graphql.InputObjectConfig{ - Name: "LoadInput", - Fields: graphql.InputObjectConfigFieldMap{ - "dotenv": &graphql.InputObjectFieldConfig{Type: dotenvInput}, - "contracts": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.NewNonNull(envContractInput))}, - "envelope": &graphql.InputObjectFieldConfig{Type: stateEnvelopeInput}, - "timestamp": &graphql.InputObjectFieldConfig{Type: graphql.String}, - }, - }) + sourceInput := graphInputObject("SourceInput", + graphInput("name", graphql.NewNonNull(graphql.String)), + graphInput("kind", graphql.NewNonNull(graphql.String)), + ) + fieldRefInput := graphInputObject("FieldRefInput", + graphInput("typeID", graphql.NewNonNull(graphql.String)), + graphInput("instance", graphql.String), + graphInput("field", graphql.NewNonNull(graphql.String)), + ) + dotenvVariableInput := graphInputObject("DotenvVariableInput", + graphInput("key", graphql.NewNonNull(graphql.String)), + graphInput("value", graphql.String), + graphInput("source", sourceInput), + ) + dotenvInput := graphInputObject("DotenvInput", + graphInput("source", sourceInput), + graphInput("variables", graphql.NewList(graphql.NewNonNull(dotenvVariableInput))), + graphInput("timestamp", graphql.String), + ) + envBindingInput := graphInputObject("EnvBindingInput", + graphInput("field", graphql.NewNonNull(fieldRefInput)), + graphInput("key", graphql.String), + graphInput("projection", graphql.String), + graphInput("required", graphql.Boolean), + graphInput("description", graphql.String), + graphInput("source", sourceInput), + graphInput("order", graphql.Int), + graphInput("sensitivity", graphql.String), + graphInput("exposure", graphql.String), + ) + envContractInput := graphInputObject("EnvContractInput", + graphInput("source", sourceInput), + graphInput("projection", graphql.String), + graphInput("bindings", graphql.NewList(graphql.NewNonNull(envBindingInput))), + ) + diagnosticInput := graphInputObject("DiagnosticInput", + graphInput("severity", graphql.String), + graphInput("code", graphql.String), + graphInput("message", graphql.String), + graphInput("details", graphql.NewList(graphql.String)), + graphInput("key", graphql.String), + graphInput("field", fieldRefInput), + graphInput("owner", graphql.String), + ) + resolverAttemptInput := graphInputObject("ResolverAttemptInput", + graphInput("id", graphql.String), + graphInput("resolverID", graphql.NewNonNull(graphql.String)), + graphInput("field", fieldRefInput), + graphInput("projectionKey", graphql.String), + graphInput("outcome", graphql.NewNonNull(graphql.String)), + graphInput("message", graphql.String), + graphInput("source", sourceInput), + graphInput("startedAt", graphql.String), + graphInput("finishedAt", graphql.String), + graphInput("diagnostics", graphql.NewList(graphql.NewNonNull(diagnosticInput))), + ) + proposedValueInput := graphInputObject("ProposedValueInput", + graphInput("value", graphql.NewNonNull(graphql.String)), + graphInput("source", sourceInput), + graphInput("sensitivity", graphql.String), + graphInput("exposure", graphql.String), + ) + resolverProposalInput := graphInputObject("ResolverProposalInput", + graphInput("needID", graphql.String), + graphInput("attemptID", graphql.String), + graphInput("resolverID", graphql.NewNonNull(graphql.String)), + graphInput("field", graphql.NewNonNull(fieldRefInput)), + graphInput("projectionKey", graphql.NewNonNull(graphql.String)), + graphInput("value", graphql.NewNonNull(proposedValueInput)), + ) + unresolvedNeedInput := graphInputObject("UnresolvedNeedInput", + graphInput("id", graphql.String), + graphInput("field", graphql.NewNonNull(fieldRefInput)), + graphInput("projectionKey", graphql.String), + graphInput("required", graphql.Boolean), + graphInput("blocking", graphql.Boolean), + graphInput("reason", graphql.String), + graphInput("description", graphql.String), + graphInput("sensitivity", graphql.String), + graphInput("exposure", graphql.String), + graphInput("source", sourceInput), + graphInput("origin", sourceInput), + graphInput("resolverAttemptIDs", graphql.NewList(graphql.String)), + ) + unresolvedFrontierInput := graphInputObject("UnresolvedFrontierInput", + graphInput("needs", graphql.NewList(graphql.NewNonNull(unresolvedNeedInput))), + ) + stateValueInput := graphInputObject("StateValueInput", + graphInput("field", graphql.NewNonNull(fieldRefInput)), + graphInput("original", graphql.String), + graphInput("resolved", graphql.String), + graphInput("visibility", graphql.String), + graphInput("sensitivity", graphql.String), + graphInput("exposure", graphql.String), + graphInput("origin", sourceInput), + graphInput("source", sourceInput), + graphInput("createdAt", graphql.String), + graphInput("updatedAt", graphql.String), + ) + stateBindingInput := graphInputObject("StateBindingInput", + graphInput("id", graphql.String), + graphInput("field", graphql.NewNonNull(fieldRefInput)), + graphInput("projection", graphql.String), + graphInput("key", graphql.String), + graphInput("description", graphql.String), + graphInput("source", sourceInput), + graphInput("origin", sourceInput), + graphInput("confidence", graphql.String), + graphInput("explicit", graphql.Boolean), + graphInput("order", graphql.Int), + graphInput("preserveKey", graphql.Boolean), + graphInput("required", graphql.Boolean), + ) + effectiveStateInput := graphInputObject("EffectiveStateInput", + graphInput("values", graphql.NewList(graphql.NewNonNull(stateValueInput))), + graphInput("bindings", graphql.NewList(graphql.NewNonNull(stateBindingInput))), + graphInput("resolverAttempts", graphql.NewList(graphql.NewNonNull(resolverAttemptInput))), + graphInput("unresolvedFrontier", unresolvedFrontierInput), + graphInput("diagnostics", graphql.NewList(graphql.NewNonNull(diagnosticInput))), + ) + operationMetadataInput := graphInputObject("OperationMetadataInput", + graphInput("id", graphql.String), + graphInput("kind", graphql.String), + graphInput("timestamp", graphql.String), + graphInput("actor", graphql.String), + graphInput("source", sourceInput), + graphInput("projection", graphql.String), + ) + stateProvenanceInput := graphInputObject("StateProvenanceInput", + graphInput("sources", graphql.NewList(graphql.NewNonNull(sourceInput))), + graphInput("operations", graphql.NewList(graphql.NewNonNull(operationMetadataInput))), + ) + stateEnvelopeInput := graphInputObject("StateEnvelopeInput", + graphInput("modelVersion", graphql.NewNonNull(graphql.String)), + graphInput("state", graphql.NewNonNull(effectiveStateInput)), + graphInput("provenance", stateProvenanceInput), + ) + loadInput := graphInputObject("LoadInput", + graphInput("dotenv", dotenvInput), + graphInput("contracts", graphql.NewList(graphql.NewNonNull(envContractInput))), + graphInput("envelope", stateEnvelopeInput), + graphInput("timestamp", graphql.String), + ) diagnosticType := graphql.NewObject(graphql.ObjectConfig{ Name: "Diagnostic", From 74dd364888c85b037494b919a8f77939fbd4ecbe Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:25:20 -0700 Subject: [PATCH 04/13] feat: route read facades through graph --- cmd/local.go | 9 +-- internal/graph/runtime.go | 75 +++++++++++++++++++--- internal/graph/schema.go | 63 ++++++++++++++++++ internal/store/store.go | 2 + pkg/owl/api.go | 132 +++++++++++++++++++++++++++++++++----- pkg/owl/api_test.go | 66 +++++++++++++------ 6 files changed, 299 insertions(+), 48 deletions(-) diff --git a/cmd/local.go b/cmd/local.go index fd4d0b7..444e256 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -267,7 +267,7 @@ func processEnvForOptions(options LocalStoreOptions) []string { return filtered } -func (c *LocalStoreClient) ProjectSpec(_ context.Context, req ProjectSpecRequest) (*ProjectSpecResult, error) { +func (c *LocalStoreClient) ProjectSpec(ctx context.Context, req ProjectSpecRequest) (*ProjectSpecResult, error) { configPath, err := resolveConfigPath(req.ConfigPath, true) if err != nil { return nil, err @@ -284,20 +284,21 @@ func (c *LocalStoreClient) ProjectSpec(_ context.Context, req ProjectSpecRequest if err != nil { return nil, err } - rendered, err := store.DotenvSpec() + rendered, err := store.DotenvSpec(ctx, owl.DotenvSpecInput{}) if err != nil { return nil, err } + outputText := rendered.Rendered output := req.Output if req.Write { output = ".env.spec" } if output != "" && output != "-" { - if err := writeGeneratedDotenvSpec(output, rendered); err != nil { + if err := writeGeneratedDotenvSpec(output, outputText); err != nil { return nil, err } } - return &ProjectSpecResult{Rendered: rendered}, nil + return &ProjectSpecResult{Rendered: outputText}, nil } func resolveConfigPath(explicit string, required bool) (string, error) { diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index 562bfd9..d6b18e3 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -118,11 +118,8 @@ func DotenvOperation(input LoadInput, policy DotenvPolicy) Operation { } func (r *Runtime) Get(ctx context.Context, input LoadInput, key string, policy GetPolicy) (GetResult, bool, error) { - result, err := r.do(ctx, getQuery, map[string]interface{}{ - "input": marshalInput(input), - "key": key, - "reveal": policy.Reveal, - }) + op := GetOperation(input, key, policy) + result, err := r.do(ctx, op.Document, op.Variables) if err != nil { return GetResult{}, false, err } @@ -136,10 +133,21 @@ func (r *Runtime) Get(ctx context.Context, input LoadInput, key string, policy G return decodeGet(raw), true, nil } +func GetOperation(input LoadInput, key string, policy GetPolicy) Operation { + return Operation{ + Name: "OwlGet", + Document: getQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + "key": key, + "reveal": policy.Reveal, + }, + } +} + func (r *Runtime) SensitiveKeys(ctx context.Context, input LoadInput) ([]string, error) { - result, err := r.do(ctx, sensitiveKeysQuery, map[string]interface{}{ - "input": marshalInput(input), - }) + op := SensitiveKeysOperation(input) + result, err := r.do(ctx, op.Document, op.Variables) if err != nil { return nil, err } @@ -150,6 +158,39 @@ func (r *Runtime) SensitiveKeys(ctx context.Context, input LoadInput) ([]string, return decodeStringList(raw), nil } +func SensitiveKeysOperation(input LoadInput) Operation { + return Operation{ + Name: "OwlSensitiveKeys", + Document: sensitiveKeysQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + }, + } +} + +func (r *Runtime) DotenvSpec(ctx context.Context, input LoadInput) (string, error) { + op := DotenvSpecOperation(input) + result, err := r.do(ctx, op.Document, op.Variables) + if err != nil { + return "", err + } + raw, err := extractPath(result.Data, "Environment", "load", "normalize", "validate", "render", "dotenvSpec") + if err != nil { + return "", err + } + return stringValue(raw), nil +} + +func DotenvSpecOperation(input LoadInput) Operation { + return Operation{ + Name: "OwlDotenvSpec", + Document: dotenvSpecQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + }, + } +} + func (r *Runtime) StateEnvelope(ctx context.Context, input LoadInput) (StateEnvelope, error) { return r.StateEnvelopeForOperations(ctx, []store.OperationRecord{ {Kind: store.OperationRecordLoad, Load: input}, @@ -666,6 +707,7 @@ func decodeCheck(raw interface{}) CheckResult { return CheckResult{ OK: boolValue(row["ok"]), Diagnostics: decodeDiagnostics(row["diagnostics"]), + Checked: intValue(row["checked"]), } } @@ -904,7 +946,22 @@ query OwlCheck($input: LoadInput!) { normalize { validate { render { - check { ok diagnostics { severity code message details key field owner } } + check { ok checked diagnostics { severity code message details key field owner } } + } + } + } + } + } +}` + +const dotenvSpecQuery = ` +query OwlDotenvSpec($input: LoadInput!) { + Environment { + load(input: $input) { + normalize { + validate { + render { + dotenvSpec } } } diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 6f4ba05..dfbf28c 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -9,6 +9,7 @@ import ( "github.com/graphql-go/graphql" "github.com/runmedev/owl/internal/model" + "github.com/runmedev/owl/internal/requirements" "github.com/runmedev/owl/internal/resolver" "github.com/runmedev/owl/internal/store" ) @@ -359,6 +360,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Fields: graphql.Fields{ "ok": &graphql.Field{Type: graphql.Boolean}, "diagnostics": &graphql.Field{Type: graphql.NewList(diagnosticType)}, + "checked": &graphql.Field{Type: graphql.Int}, }, }) snapshotItemType := graphql.NewObject(graphql.ObjectConfig{ @@ -487,6 +489,13 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { return check, nil }, }, + "dotenvSpec": &graphql.Field{ + Type: graphql.String, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + gctx := p.Source.(Context) + return requirements.RenderDotenvSpec(contractsFromState(gctx.State), gctx.Types) + }, + }, "get": &graphql.Field{ Type: getResultType, Args: graphql.FieldConfigArgument{ @@ -664,6 +673,46 @@ func contextFromParams(p graphql.ResolveParams) context.Context { return p.Context } +func contractsFromState(state model.EffectiveState) []store.EnvContract { + type contractKey struct { + source model.Source + projection model.ProjectionID + } + positions := make(map[contractKey]int) + var contracts []store.EnvContract + for _, binding := range state.Bindings { + source := binding.Origin + key := contractKey{source: source, projection: binding.ProjectionID} + position, ok := positions[key] + if !ok { + position = len(contracts) + positions[key] = position + contracts = append(contracts, store.EnvContract{ + Source: source, + Projection: binding.ProjectionID, + }) + } + contracts[position].Bindings = append(contracts[position].Bindings, store.EnvBinding{ + FieldRef: binding.FieldRef, + Key: string(binding.Key), + Projection: binding.ProjectionID, + Required: binding.Required, + Description: binding.Description, + Source: source, + Order: binding.Order, + Sensitivity: state.Values[binding.FieldRef].Sensitivity, + Exposure: state.Values[binding.FieldRef].Exposure, + }) + } + sort.SliceStable(contracts, func(i, j int) bool { + if contracts[i].Source.Name != contracts[j].Source.Name { + return contracts[i].Source.Name < contracts[j].Source.Name + } + return contracts[i].Projection < contracts[j].Projection + }) + return contracts +} + func decodeLoadInput(raw map[string]interface{}) store.LoadInput { var input store.LoadInput if envelopeRaw, ok := raw["envelope"].(map[string]interface{}); ok { @@ -994,6 +1043,20 @@ func uintValue(raw interface{}) uint { return 0 } +func intValue(raw interface{}) int { + switch value := raw.(type) { + case int: + return value + case int32: + return int(value) + case int64: + return int(value) + case float64: + return int(value) + } + return 0 +} + func timeString(value time.Time) string { if value.IsZero() { return "" diff --git a/internal/store/store.go b/internal/store/store.go index 88c7e31..d3c8ca4 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -100,6 +100,7 @@ type SnapshotItem struct { type CheckResult struct { OK bool Diagnostics []model.Diagnostic + Checked int } type GetPolicy struct { @@ -857,6 +858,7 @@ func (s *Store) Check() CheckResult { result := CheckResult{ OK: true, Diagnostics: append([]model.Diagnostic{}, s.state.Diagnostics...), + Checked: len(s.state.Bindings), } for _, diagnostic := range result.Diagnostics { if diagnostic.Severity == model.DiagnosticError { diff --git a/pkg/owl/api.go b/pkg/owl/api.go index 19795c0..16247cf 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -167,6 +167,30 @@ type SourceOutput struct { Envs []string } +type GetInput struct { + Load LoadInput + Key string + Policy GetPolicy +} + +type GetOutput = GetResult + +type SensitiveKeysInput struct { + Load LoadInput +} + +type SensitiveKeysOutput struct { + Keys []string +} + +type DotenvSpecInput struct { + Load LoadInput +} + +type DotenvSpecOutput struct { + Rendered string +} + type CheckInput struct { Load LoadInput } @@ -355,7 +379,11 @@ func (s *Store) BuildSnapshotOperation(ctx context.Context, input SnapshotInput) } func (s *Store) SnapshotItems(policy SnapshotPolicy) ([]SnapshotItem, error) { - return store.NewState(s.state, s.types).Snapshot(policy) + output, err := s.Snapshot(context.Background(), SnapshotInput{Policy: policy, Filter: SnapshotFilter{All: true}}) + if err != nil { + return nil, err + } + return output.Envs, nil } func snapshotEnvsForInput(items []SnapshotItem, input SnapshotInput) []SnapshotEnv { @@ -397,48 +425,107 @@ func (s *Store) BuildSourceOperation(ctx context.Context, input SourceInput) (Gr } func (s *Store) Dotenv(policy DotenvPolicy) ([]string, error) { - return store.NewState(s.state, s.types).Dotenv(policy) + output, err := s.Source(context.Background(), SourceInput{Policy: policy}) + if err != nil { + return nil, err + } + return output.Envs, nil } -func (s *Store) DotenvSpec() (string, error) { - if len(s.operations) == 0 || s.operations[0].Kind != store.OperationRecordLoad { - return requirements.RenderDotenvSpec(nil, s.types) +func (s *Store) DotenvSpec(ctx context.Context, input DotenvSpecInput) (DotenvSpecOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return DotenvSpecOutput{}, err } - return requirements.RenderDotenvSpec(s.operations[0].Load.Contracts, s.types) + rendered, err := s.runtime.DotenvSpec(ctx, load) + if err != nil { + return DotenvSpecOutput{}, err + } + return DotenvSpecOutput{Rendered: rendered}, nil +} + +func (s *Store) BuildDotenvSpecOperation(ctx context.Context, input DotenvSpecInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.DotenvSpecOperation(load)), nil } func (s *Store) Type(policy TypePolicy) (TypeResult, error) { return store.NewState(s.state, s.types).Type(policy) } -func (s *Store) Get(key string, policy GetPolicy) (GetResult, bool, error) { - return store.NewState(s.state, s.types).Get(key, policy) +func (s *Store) Get(ctx context.Context, input GetInput) (GetOutput, bool, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GetOutput{}, false, err + } + return s.runtime.Get(ctx, load, input.Key, input.Policy) +} + +func (s *Store) BuildGetOperation(ctx context.Context, input GetInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.GetOperation(load, input.Key, input.Policy)), nil } -func (s *Store) SensitiveKeys() ([]string, error) { - return store.NewState(s.state, s.types).SensitiveKeys() +func (s *Store) SensitiveKeys(ctx context.Context, input SensitiveKeysInput) (SensitiveKeysOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return SensitiveKeysOutput{}, err + } + keys, err := s.runtime.SensitiveKeys(ctx, load) + if err != nil { + return SensitiveKeysOutput{}, err + } + return SensitiveKeysOutput{Keys: keys}, nil } -func (s *Store) Check(ctx context.Context, input CheckInput) (CheckOutput, error) { +func (s *Store) BuildSensitiveKeysOperation(ctx context.Context, input SensitiveKeysInput) (GraphOperation, error) { if ctx == nil { ctx = context.Background() } load, err := s.loadInputForOperation(ctx, input.Load) if err != nil { - return CheckOutput{}, err + return GraphOperation{}, err } - check, err := s.runtime.Check(ctx, load) + return graphOperation(graph.SensitiveKeysOperation(load)), nil +} + +func (s *Store) Check(ctx context.Context, input CheckInput) (CheckOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) if err != nil { return CheckOutput{}, err } - snapshot, err := s.runtime.Snapshot(ctx, load, SnapshotPolicy{}) + check, err := s.runtime.Check(ctx, load) if err != nil { return CheckOutput{}, err } return CheckOutput{ OK: check.OK, Diagnostics: check.Diagnostics, - Checked: len(snapshot), + Checked: check.Checked, }, nil } @@ -454,7 +541,15 @@ func (s *Store) BuildCheckOperation(ctx context.Context, input CheckInput) (Grap } func (s *Store) CheckState() store.CheckResult { - return store.NewState(s.state, s.types).Check() + envelope, err := s.StateEnvelope(context.Background()) + if err != nil { + return store.CheckResult{} + } + check, err := s.runtime.Check(context.Background(), LoadInput{Envelope: &envelope}) + if err != nil { + return store.CheckResult{} + } + return check } func (s *Store) ExecuteGraphQL(ctx context.Context, req GraphQLRequest) (GraphQLResult, error) { @@ -607,7 +702,10 @@ func (s *Store) Delete(ctx context.Context, keys ...string) error { } func (s *Store) StateEnvelope(ctx context.Context) (StateEnvelope, error) { - return store.NewState(s.state, s.types).StateEnvelope(), nil + if ctx == nil { + ctx = context.Background() + } + return s.runtime.StateEnvelopeForOperations(ctx, s.operations) } func (s *Store) loadInputForOperation(ctx context.Context, input LoadInput) (LoadInput, error) { diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index ccc19af..067300e 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -34,14 +34,14 @@ func TestV2PublicAPI(t *testing.T) { "REDIS_PASSWORD=hunter2", }, envs) - got, ok, err := store.Get("API_KEY", owl.GetPolicy{}) + got, ok, err := store.Get(context.Background(), owl.GetInput{Key: "API_KEY"}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "[masked]", got.Value) - keys, err := store.SensitiveKeys() + keys, err := store.SensitiveKeys(context.Background(), owl.SensitiveKeysInput{}) require.NoError(t, err) - assert.Equal(t, []string{"API_KEY"}, keys) + assert.Equal(t, []string{"API_KEY"}, keys.Keys) envelope, err := store.StateEnvelope(context.Background()) require.NoError(t, err) @@ -51,13 +51,13 @@ func TestV2PublicAPI(t *testing.T) { require.NoError(t, err) require.NoError(t, next.LoadDotenvLines("[override]", "API_URL=https://next.example.com")) - got, ok, err = next.Get("API_URL", owl.GetPolicy{Reveal: true}) + got, ok, err = next.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://next.example.com", got.Value) require.NoError(t, next.Delete(context.Background(), "API_KEY")) - _, ok, err = next.Get("API_KEY", owl.GetPolicy{Reveal: true}) + _, ok, err = next.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) } @@ -97,6 +97,9 @@ func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { for _, op := range []owl.GraphOperation{ mustBuildSnapshotOperation(t, store), mustBuildSourceOperation(t, store), + mustBuildGetOperation(t, store), + mustBuildSensitiveKeysOperation(t, store), + mustBuildDotenvSpecOperation(t, store), mustBuildCheckOperation(t, store), } { require.Contains(t, op.Variables, "input") @@ -124,6 +127,33 @@ func mustBuildSourceOperation(t *testing.T, store *owl.Store) owl.GraphOperation return op } +func mustBuildGetOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildGetOperation(context.Background(), owl.GetInput{Key: "API_KEY"}) + require.NoError(t, err) + assert.Equal(t, "OwlGet", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + +func mustBuildSensitiveKeysOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildSensitiveKeysOperation(context.Background(), owl.SensitiveKeysInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlSensitiveKeys", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + +func mustBuildDotenvSpecOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildDotenvSpecOperation(context.Background(), owl.DotenvSpecInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlDotenvSpec", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + func mustBuildCheckOperation(t *testing.T, store *owl.Store) owl.GraphOperation { t.Helper() op, err := store.BuildCheckOperation(context.Background(), owl.CheckInput{}) @@ -275,27 +305,27 @@ func TestPublicAPIGetRevealPolicy(t *testing.T) { ) require.NoError(t, err) - got, ok, err := store.Get("API_KEY", owl.GetPolicy{}) + got, ok, err := store.Get(context.Background(), owl.GetInput{Key: "API_KEY"}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "[masked]", got.Value) assert.Equal(t, owl.VisibilityMasked, got.Visibility) assert.Equal(t, owl.ExposureClear, got.Exposure) - got, ok, err = store.Get("API_KEY", owl.GetPolicy{Reveal: true}) + got, ok, err = store.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "secret", got.Value) assert.Equal(t, owl.VisibilityLiteral, got.Visibility) - got, ok, err = store.Get("DATABASE_URL", owl.GetPolicy{}) + got, ok, err = store.Get(context.Background(), owl.GetInput{Key: "DATABASE_URL"}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "[hidden]", got.Value) assert.Equal(t, owl.VisibilityHidden, got.Visibility) assert.Equal(t, owl.ExposureOpaque, got.Exposure) - got, ok, err = store.Get("DATABASE_URL", owl.GetPolicy{Reveal: true}) + got, ok, err = store.Get(context.Background(), owl.GetInput{Key: "DATABASE_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "postgres://example", got.Value) @@ -357,13 +387,13 @@ func TestPublicAPIStateEnvelopeRoundTrip(t *testing.T) { assert.Equal(t, snapshotByName(snapshot)["DATABASE_URL"].Exposure, snapshotByName(roundTrippedSnapshot)["DATABASE_URL"].Exposure) require.NoError(t, roundTripped.LoadDotenvLines("[override]", "API_URL=https://next.example.com")) - got, ok, err := roundTripped.Get("API_URL", owl.GetPolicy{Reveal: true}) + got, ok, err := roundTripped.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://next.example.com", got.Value) require.NoError(t, roundTripped.Delete(context.Background(), "API_KEY")) - _, ok, err = roundTripped.Get("API_KEY", owl.GetPolicy{Reveal: true}) + _, ok, err = roundTripped.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) } @@ -410,7 +440,7 @@ func TestPublicAPIStateEnvelopeRoundTripPreservesResolverAttempts(t *testing.T) assert.Equal(t, attempt.FinishedAt, gotAttempt.FinishedAt) assert.Empty(t, gotAttempt.Diagnostics) - got, ok, err := roundTripped.Get("API_URL", owl.GetPolicy{Reveal: true}) + got, ok, err := roundTripped.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://api.example.com", got.Value) @@ -456,13 +486,13 @@ func TestPublicAPIUpdatesMaterializeFromOperationLog(t *testing.T) { require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://one.example.com"}, nil)) require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://two.example.com"}, nil)) - got, ok, err := store.Get("API_URL", owl.GetPolicy{Reveal: true}) + got, ok, err := store.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://two.example.com", got.Value) require.NoError(t, store.Delete(context.Background(), "API_KEY")) - _, ok, err = store.Get("API_KEY", owl.GetPolicy{Reveal: true}) + _, ok, err = store.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) @@ -470,7 +500,7 @@ func TestPublicAPIUpdatesMaterializeFromOperationLog(t *testing.T) { require.NoError(t, err) roundTripped, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - got, ok, err = roundTripped.Get("API_URL", owl.GetPolicy{Reveal: true}) + got, ok, err = roundTripped.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://two.example.com", got.Value) @@ -582,7 +612,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { assert.Equal(t, owl.VisibilityUnresolved, byName["REDIS_AUTH_TOKEN"].Visibility) assert.Equal(t, "[config]", byName["REDIS_AUTH_TOKEN"].Origin.Name) - dotenvSpec, err := store.DotenvSpec() + dotenvSpec, err := store.DotenvSpec(context.Background(), owl.DotenvSpecInput{}) require.NoError(t, err) assert.Equal(t, strings.Join([]string{ "# Generated by Owl from Owl config. Do not edit by hand.", @@ -591,7 +621,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { `QUEUES_REDIS_PORT="Redis server port" # Plain!`, `REDIS_AUTH_TOKEN="Redis password" # Secret!`, "", - }, "\n"), dotenvSpec) + }, "\n"), dotenvSpec.Rendered) check := store.CheckState() assert.False(t, check.OK) @@ -619,7 +649,7 @@ func TestPublicAPIWithConfigValidatesRedisPort(t *testing.T) { assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-port") - port, ok, err := store.Get("QUEUES_REDIS_PORT", owl.GetPolicy{Reveal: true}) + port, ok, err := store.Get(context.Background(), owl.GetInput{Key: "QUEUES_REDIS_PORT", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, `universe/redis("queues").port`, port.Field.String()) From da1256bb106504a096004481ba87ad4fea933c6b Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:27:14 -0700 Subject: [PATCH 05/13] feat: route type suggestions through graph --- cmd/local.go | 4 +- internal/graph/runtime.go | 81 +++++++++++++++++++++++++++++++++++++++ internal/graph/schema.go | 63 ++++++++++++++++++++++++++++++ pkg/owl/api.go | 29 +++++++++++++- pkg/owl/api_test.go | 10 +++++ 5 files changed, 183 insertions(+), 4 deletions(-) diff --git a/cmd/local.go b/cmd/local.go index 444e256..0702ba6 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -126,7 +126,7 @@ func (c *LocalStoreClient) Check(ctx context.Context, req CheckRequest) (*CheckR }, nil } -func (c *LocalStoreClient) Type(_ context.Context, req TypeRequest) (*TypeResult, error) { +func (c *LocalStoreClient) Type(ctx context.Context, req TypeRequest) (*TypeResult, error) { if req.SpecPath == "" { req.SpecPath = ".env.spec" } @@ -137,7 +137,7 @@ func (c *LocalStoreClient) Type(_ context.Context, req TypeRequest) (*TypeResult return nil, err } - result, err := store.Type(owl.TypePolicy{All: req.All}) + result, err := store.Type(ctx, owl.TypeInput{Policy: owl.TypePolicy{All: req.All}}) if err != nil { return nil, err } diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index d6b18e3..36d40a8 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -32,10 +32,14 @@ type DotenvPolicy = store.DotenvPolicy type GetPolicy = store.GetPolicy +type TypePolicy = store.TypePolicy + type SnapshotItem = store.SnapshotItem type GetResult = store.GetResult +type TypeResult = store.TypeResult + type CheckResult = store.CheckResult type StateEnvelope = store.StateEnvelope @@ -191,6 +195,30 @@ func DotenvSpecOperation(input LoadInput) Operation { } } +func (r *Runtime) Type(ctx context.Context, input LoadInput, policy TypePolicy) (TypeResult, error) { + op := TypeOperation(input, policy) + result, err := r.do(ctx, op.Document, op.Variables) + if err != nil { + return TypeResult{}, err + } + raw, err := extractPath(result.Data, "Environment", "load", "normalize", "validate", "assist", "typeSuggestions") + if err != nil { + return TypeResult{}, err + } + return decodeType(raw), nil +} + +func TypeOperation(input LoadInput, policy TypePolicy) Operation { + return Operation{ + Name: "OwlTypeSuggestions", + Document: typeQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + "all": policy.All, + }, + } +} + func (r *Runtime) StateEnvelope(ctx context.Context, input LoadInput) (StateEnvelope, error) { return r.StateEnvelopeForOperations(ctx, []store.OperationRecord{ {Kind: store.OperationRecordLoad, Load: input}, @@ -727,6 +755,34 @@ func decodeGet(raw interface{}) GetResult { } } +func decodeType(raw interface{}) TypeResult { + row, ok := raw.(map[string]interface{}) + if !ok { + return TypeResult{} + } + proposalsRaw, ok := row["proposals"].([]interface{}) + if !ok { + return TypeResult{} + } + proposals := make([]store.TypeProposal, 0, len(proposalsRaw)) + for _, item := range proposalsRaw { + proposalRaw, ok := item.(map[string]interface{}) + if !ok { + continue + } + proposals = append(proposals, store.TypeProposal{ + Key: stringValue(proposalRaw["key"]), + CurrentType: model.TypeID(stringValue(proposalRaw["currentType"])), + SuggestedType: model.TypeID(stringValue(proposalRaw["suggestedType"])), + Confidence: model.BindingConfidence(stringValue(proposalRaw["confidence"])), + Reason: stringValue(proposalRaw["reason"]), + Description: stringValue(proposalRaw["description"]), + Required: boolValue(proposalRaw["required"]), + }) + } + return TypeResult{Proposals: proposals} +} + func decodeEnvelope(raw interface{}) (StateEnvelope, error) { row, ok := raw.(map[string]interface{}) if !ok { @@ -969,6 +1025,31 @@ query OwlDotenvSpec($input: LoadInput!) { } }` +const typeQuery = ` +query OwlTypeSuggestions($input: LoadInput!, $all: Boolean = false) { + Environment { + load(input: $input) { + normalize { + validate { + assist { + typeSuggestions(all: $all) { + proposals { + key + currentType + suggestedType + confidence + reason + description + required + } + } + } + } + } + } + } +}` + const introspectionQuery = ` query OwlSchema { __schema { diff --git a/internal/graph/schema.go b/internal/graph/schema.go index dfbf28c..115db79 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -343,6 +343,24 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, }, }) + typeProposalType := graphql.NewObject(graphql.ObjectConfig{ + Name: "TypeProposal", + Fields: graphql.Fields{ + "key": &graphql.Field{Type: graphql.String}, + "currentType": &graphql.Field{Type: graphql.String}, + "suggestedType": &graphql.Field{Type: graphql.String}, + "confidence": &graphql.Field{Type: graphql.String}, + "reason": &graphql.Field{Type: graphql.String}, + "description": &graphql.Field{Type: graphql.String}, + "required": &graphql.Field{Type: graphql.Boolean}, + }, + }) + typeResultType := graphql.NewObject(graphql.ObjectConfig{ + Name: "TypeResult", + Fields: graphql.Fields{ + "proposals": &graphql.Field{Type: graphql.NewList(typeProposalType)}, + }, + }) getResultType := graphql.NewObject(graphql.ObjectConfig{ Name: "GetResult", Fields: graphql.Fields{ @@ -454,6 +472,29 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { var environmentType *graphql.Object + assistType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Assist", + Fields: graphql.FieldsThunk(func() graphql.Fields { + return graphql.Fields{ + "typeSuggestions": &graphql.Field{ + Type: typeResultType, + Args: graphql.FieldConfigArgument{ + "all": &graphql.ArgumentConfig{Type: graphql.Boolean, DefaultValue: false}, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + gctx := p.Source.(Context) + all, _ := p.Args["all"].(bool) + result, err := store.NewState(gctx.State, gctx.Types).Type(store.TypePolicy{All: all}) + if err != nil { + return nil, err + } + return typeResultView(result), nil + }, + }, + } + }), + }) + renderType := graphql.NewObject(graphql.ObjectConfig{ Name: "Render", Fields: graphql.FieldsThunk(func() graphql.Fields { @@ -647,6 +688,12 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { return p.Source, nil }, }, + "assist": &graphql.Field{ + Type: assistType, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return p.Source, nil + }, + }, } }), }) @@ -1236,6 +1283,22 @@ func getResultView(result store.GetResult) map[string]interface{} { } } +func typeResultView(result store.TypeResult) map[string]interface{} { + proposals := make([]map[string]interface{}, 0, len(result.Proposals)) + for _, proposal := range result.Proposals { + proposals = append(proposals, map[string]interface{}{ + "key": proposal.Key, + "currentType": string(proposal.CurrentType), + "suggestedType": string(proposal.SuggestedType), + "confidence": string(proposal.Confidence), + "reason": proposal.Reason, + "description": proposal.Description, + "required": proposal.Required, + }) + } + return map[string]interface{}{"proposals": proposals} +} + func sourceView(source model.Source) map[string]interface{} { return map[string]interface{}{ "name": source.Name, diff --git a/pkg/owl/api.go b/pkg/owl/api.go index 16247cf..a4d0730 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -191,6 +191,13 @@ type DotenvSpecOutput struct { Rendered string } +type TypeInput struct { + Load LoadInput + Policy TypePolicy +} + +type TypeOutput = TypeResult + type CheckInput struct { Load LoadInput } @@ -458,8 +465,26 @@ func (s *Store) BuildDotenvSpecOperation(ctx context.Context, input DotenvSpecIn return graphOperation(graph.DotenvSpecOperation(load)), nil } -func (s *Store) Type(policy TypePolicy) (TypeResult, error) { - return store.NewState(s.state, s.types).Type(policy) +func (s *Store) Type(ctx context.Context, input TypeInput) (TypeOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return TypeOutput{}, err + } + return s.runtime.Type(ctx, load, input.Policy) +} + +func (s *Store) BuildTypeOperation(ctx context.Context, input TypeInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.TypeOperation(load, input.Policy)), nil } func (s *Store) Get(ctx context.Context, input GetInput) (GetOutput, bool, error) { diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index 067300e..6489ba2 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -100,6 +100,7 @@ func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { mustBuildGetOperation(t, store), mustBuildSensitiveKeysOperation(t, store), mustBuildDotenvSpecOperation(t, store), + mustBuildTypeOperation(t, store), mustBuildCheckOperation(t, store), } { require.Contains(t, op.Variables, "input") @@ -154,6 +155,15 @@ func mustBuildDotenvSpecOperation(t *testing.T, store *owl.Store) owl.GraphOpera return op } +func mustBuildTypeOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildTypeOperation(context.Background(), owl.TypeInput{Policy: owl.TypePolicy{All: true}}) + require.NoError(t, err) + assert.Equal(t, "OwlTypeSuggestions", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + func mustBuildCheckOperation(t *testing.T, store *owl.Store) owl.GraphOperation { t.Helper() op, err := store.BuildCheckOperation(context.Background(), owl.CheckInput{}) From e2a8ab4af585628ba4d11019e3fd1dd28ae1f9d4 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:28:18 -0700 Subject: [PATCH 06/13] feat: route project spec through graph --- cmd/local.go | 15 +++++++++++---- internal/graph/runtime.go | 38 ++++++++++++++++++++++++++++++++++++++ pkg/owl/api.go | 34 ++++++++++++++++++++++++++++++++++ pkg/owl/api_test.go | 10 ++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/cmd/local.go b/cmd/local.go index 0702ba6..8c53bc8 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -276,15 +276,22 @@ func (c *LocalStoreClient) ProjectSpec(ctx context.Context, req ProjectSpecReque if err != nil { return nil, err } - storeOptions := []owl.StoreOption{owl.WithConfigSource(configPath, input)} - if c.options.TypeProvider != nil { - storeOptions = append(storeOptions, owl.WithTypeProvider(c.options.TypeProvider)) + types := c.options.TypeProvider + if types == nil { + types = registry.NewBuiltInRegistry() } + contracts, err := requirements.ContractsFromConfig(input, model.Source{Name: configPath, Kind: "owl-config"}, types) + if err != nil { + return nil, err + } + storeOptions := []owl.StoreOption{owl.WithTypeProvider(types)} store, err := owl.NewStore(storeOptions...) if err != nil { return nil, err } - rendered, err := store.DotenvSpec(ctx, owl.DotenvSpecInput{}) + rendered, err := store.ProjectSpec(ctx, owl.ProjectSpecInput{ + Load: owl.LoadInput{Contracts: contracts}, + }) if err != nil { return nil, err } diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index 36d40a8..d3175b6 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -195,6 +195,29 @@ func DotenvSpecOperation(input LoadInput) Operation { } } +func (r *Runtime) ProjectSpec(ctx context.Context, input LoadInput) (string, error) { + op := ProjectSpecOperation(input) + result, err := r.do(ctx, op.Document, op.Variables) + if err != nil { + return "", err + } + raw, err := extractPath(result.Data, "Environment", "load", "normalize", "validate", "render", "dotenvSpec") + if err != nil { + return "", err + } + return stringValue(raw), nil +} + +func ProjectSpecOperation(input LoadInput) Operation { + return Operation{ + Name: "OwlProjectSpec", + Document: projectSpecQuery, + Variables: map[string]interface{}{ + "input": marshalInput(input), + }, + } +} + func (r *Runtime) Type(ctx context.Context, input LoadInput, policy TypePolicy) (TypeResult, error) { op := TypeOperation(input, policy) result, err := r.do(ctx, op.Document, op.Variables) @@ -1025,6 +1048,21 @@ query OwlDotenvSpec($input: LoadInput!) { } }` +const projectSpecQuery = ` +query OwlProjectSpec($input: LoadInput!) { + Environment { + load(input: $input) { + normalize { + validate { + render { + dotenvSpec + } + } + } + } + } +}` + const typeQuery = ` query OwlTypeSuggestions($input: LoadInput!, $all: Boolean = false) { Environment { diff --git a/pkg/owl/api.go b/pkg/owl/api.go index a4d0730..630e226 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -198,6 +198,14 @@ type TypeInput struct { type TypeOutput = TypeResult +type ProjectSpecInput struct { + Load LoadInput +} + +type ProjectSpecOutput struct { + Rendered string +} + type CheckInput struct { Load LoadInput } @@ -465,6 +473,32 @@ func (s *Store) BuildDotenvSpecOperation(ctx context.Context, input DotenvSpecIn return graphOperation(graph.DotenvSpecOperation(load)), nil } +func (s *Store) ProjectSpec(ctx context.Context, input ProjectSpecInput) (ProjectSpecOutput, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return ProjectSpecOutput{}, err + } + rendered, err := s.runtime.ProjectSpec(ctx, load) + if err != nil { + return ProjectSpecOutput{}, err + } + return ProjectSpecOutput{Rendered: rendered}, nil +} + +func (s *Store) BuildProjectSpecOperation(ctx context.Context, input ProjectSpecInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + load, err := s.loadInputForOperation(ctx, input.Load) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(graph.ProjectSpecOperation(load)), nil +} + func (s *Store) Type(ctx context.Context, input TypeInput) (TypeOutput, error) { if ctx == nil { ctx = context.Background() diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index 6489ba2..ec06834 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -100,6 +100,7 @@ func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { mustBuildGetOperation(t, store), mustBuildSensitiveKeysOperation(t, store), mustBuildDotenvSpecOperation(t, store), + mustBuildProjectSpecOperation(t, store), mustBuildTypeOperation(t, store), mustBuildCheckOperation(t, store), } { @@ -155,6 +156,15 @@ func mustBuildDotenvSpecOperation(t *testing.T, store *owl.Store) owl.GraphOpera return op } +func mustBuildProjectSpecOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildProjectSpecOperation(context.Background(), owl.ProjectSpecInput{}) + require.NoError(t, err) + assert.Equal(t, "OwlProjectSpec", op.Name) + assert.Contains(t, op.Document, "$input: LoadInput!") + return op +} + func mustBuildTypeOperation(t *testing.T, store *owl.Store) owl.GraphOperation { t.Helper() op, err := store.BuildTypeOperation(context.Background(), owl.TypeInput{Policy: owl.TypePolicy{All: true}}) From d7e1f0168cfc4a66907ca23db684df481816e964 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:29:06 -0700 Subject: [PATCH 07/13] feat: hydrate resolver state through graph --- pkg/owl/api.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pkg/owl/api.go b/pkg/owl/api.go index 630e226..d4bbeb0 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -623,11 +623,19 @@ func (s *Store) ExecuteGraphQL(ctx context.Context, req GraphQLRequest) (GraphQL } func (s *Store) ResolverAttempts() []ResolverAttempt { - return append([]ResolverAttempt{}, s.state.ResolverAttempts...) + state, err := s.resolverState(context.Background()) + if err != nil { + return nil + } + return append([]ResolverAttempt{}, state.ResolverAttempts...) } func (s *Store) UnresolvedFrontier() UnresolvedFrontier { - return UnresolvedFrontier{Needs: append([]UnresolvedNeed{}, s.state.UnresolvedFrontier.Needs...)} + state, err := s.resolverState(context.Background()) + if err != nil { + return UnresolvedFrontier{} + } + return UnresolvedFrontier{Needs: append([]UnresolvedNeed{}, state.UnresolvedFrontier.Needs...)} } func (s *Store) Resolve(ctx context.Context, input ResolveInput) (ResolveResult, error) { @@ -643,8 +651,12 @@ func (s *Store) Resolve(ctx context.Context, input ResolveInput) (ResolveResult, NewAttemptID: publicAttemptIDGenerator(len(s.operations)), Clock: s.clock, } + state, err := s.resolverState(ctx) + if err != nil { + return ResolveResult{}, err + } result, err := runner.Resolve(ctx, resolver.RunRequest{ - State: s.state, + State: state, Policy: input.Policy, Chain: input.Chain, }) @@ -661,8 +673,12 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, answers []PromptAnswer) if ctx == nil { ctx = context.Background() } + state, err := s.resolverState(ctx) + if err != nil { + return ResolveResult{}, err + } timestamp := s.clock() - needs := needsByID(s.state.UnresolvedFrontier.Needs) + needs := needsByID(state.UnresolvedFrontier.Needs) newAttemptID := publicAttemptIDGenerator(len(s.operations)) var result ResolveResult for _, answer := range answers { @@ -722,6 +738,14 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, answers []PromptAnswer) return result, nil } +func (s *Store) resolverState(ctx context.Context) (model.EffectiveState, error) { + envelope, err := s.StateEnvelope(ctx) + if err != nil { + return model.EffectiveState{}, err + } + return envelope.State, nil +} + func (s *Store) LoadDotenv(source Source, vars []DotenvVariable) error { return s.applyDotenv(source, vars, nil) } From 042e21a9cd04182e873cd1935729e0ddea40fcc2 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 22:30:34 -0700 Subject: [PATCH 08/13] feat: expose graph mutation operations --- internal/graph/runtime.go | 8 +++++++ pkg/owl/api.go | 49 +++++++++++++++++++++++++++++++++++++++ pkg/owl/api_test.go | 22 ++++++++++++++---- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index d3175b6..d2cdff0 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -264,6 +264,14 @@ func (r *Runtime) StateEnvelopeForOperations(ctx context.Context, records []stor return decodeEnvelope(raw) } +func StateEnvelopeOperation(records []store.OperationRecord) (Operation, error) { + plan, err := planStateEnvelopeQuery(records) + if err != nil { + return Operation{}, err + } + return Operation{Name: "OwlStateEnvelope", Document: plan.Query, Variables: plan.Vars}, nil +} + func (r *Runtime) StateEnvelopeAfter(ctx context.Context, input LoadInput, patch store.LoadInput, deleted []string) (StateEnvelope, error) { records := []store.OperationRecord{{Kind: store.OperationRecordLoad, Load: input}} if len(patch.Dotenv) > 0 { diff --git a/pkg/owl/api.go b/pkg/owl/api.go index d4bbeb0..8513ca5 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -206,6 +206,12 @@ type ProjectSpecOutput struct { Rendered string } +type UpdateInput struct { + Source Source + Dotenv []DotenvVariable + Delete []string +} + type CheckInput struct { Load LoadInput } @@ -784,6 +790,41 @@ func (s *Store) Delete(ctx context.Context, keys ...string) error { return s.applyDotenvWithContext(ctx, sourceFromContext(ctx, Source{}), nil, keys) } +func (s *Store) BuildUpdateOperation(ctx context.Context, input UpdateInput) (GraphOperation, error) { + if ctx == nil { + ctx = context.Background() + } + records := append([]store.OperationRecord{}, s.operations...) + timestamp := s.clock() + source := input.Source + if source == (Source{}) { + source = sourceFromContext(ctx, Source{Name: "[update]", Kind: "dotenv"}) + } + if len(input.Dotenv) > 0 { + records = append(records, store.OperationRecord{ + Kind: store.OperationRecordUpdate, + Timestamp: timestamp, + Update: store.UpdateOperation{ + Source: source, + Dotenv: append([]DotenvVariable{}, input.Dotenv...), + Timestamp: timestamp, + }, + }) + } + if len(input.Delete) > 0 { + records = append(records, store.OperationRecord{ + Kind: store.OperationRecordDelete, + Timestamp: timestamp, + Delete: store.DeleteOperation{ + Keys: append([]string{}, input.Delete...), + Source: source, + Timestamp: timestamp, + }, + }) + } + return stateEnvelopeOperation(records) +} + func (s *Store) StateEnvelope(ctx context.Context) (StateEnvelope, error) { if ctx == nil { ctx = context.Background() @@ -818,6 +859,14 @@ func graphOperation(op graph.Operation) GraphOperation { } } +func stateEnvelopeOperation(records []store.OperationRecord) (GraphOperation, error) { + op, err := graph.StateEnvelopeOperation(records) + if err != nil { + return GraphOperation{}, err + } + return graphOperation(op), nil +} + func (s *Store) GraphQLSchema() (string, error) { return s.runtime.SchemaJSON(context.Background()) } diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index ec06834..a98454c 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -103,11 +103,12 @@ func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { mustBuildProjectSpecOperation(t, store), mustBuildTypeOperation(t, store), mustBuildCheckOperation(t, store), + mustBuildUpdateOperation(t, store), } { - require.Contains(t, op.Variables, "input") - input, ok := op.Variables["input"].(map[string]interface{}) - require.True(t, ok) - assert.Contains(t, input, "envelope") + if input, ok := op.Variables["input"].(map[string]interface{}); ok { + assert.Contains(t, input, "envelope") + } + assert.NotEmpty(t, op.Variables) } } @@ -183,6 +184,19 @@ func mustBuildCheckOperation(t *testing.T, store *owl.Store) owl.GraphOperation return op } +func mustBuildUpdateOperation(t *testing.T, store *owl.Store) owl.GraphOperation { + t.Helper() + op, err := store.BuildUpdateOperation(context.Background(), owl.UpdateInput{ + Dotenv: []owl.DotenvVariable{{Key: "API_URL", Value: "https://next.example.com"}}, + Delete: []string{"API_KEY"}, + }) + require.NoError(t, err) + assert.Equal(t, "OwlStateEnvelope", op.Name) + assert.Contains(t, op.Document, "update") + assert.Contains(t, op.Document, "delete") + return op +} + func TestPublicAPISnapshotOrderSurvivesStateEnvelopeRoundTrip(t *testing.T) { t.Parallel() From 3fdbc6d7777c66c4c17140c0d7d99629cd8e6191 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Mon, 10 Aug 2026 23:01:06 -0700 Subject: [PATCH 09/13] fix: normalize snapshot field output --- internal/graph/runtime.go | 29 +++++++++++------------------ internal/graph/schema.go | 25 ++----------------------- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index d2cdff0..58aaeba 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -702,10 +702,7 @@ query OwlSnapshot($input: LoadInput!, $reveal: Boolean = false) { value originalValue type - field - fieldTypeID - fieldInstance - fieldName + field { typeID instance field } source { name kind } origin { name kind } explicit @@ -739,20 +736,16 @@ func decodeSnapshot(raw interface{}) []SnapshotItem { Value: stringValue(item["value"]), OriginalValue: stringValue(item["originalValue"]), Type: model.TypeID(stringValue(item["type"])), - Field: model.FieldRef{ - TypeID: model.TypeID(stringValue(item["fieldTypeID"])), - Instance: stringValue(item["fieldInstance"]), - Field: stringValue(item["fieldName"]), - }, - Source: decodeSource(item["source"]), - Origin: decodeSource(item["origin"]), - Explicit: boolValue(item["explicit"]), - Confidence: model.BindingConfidence(stringValue(item["confidence"])), - Visibility: model.Visibility(stringValue(item["visibility"])), - Exposure: model.Exposure(stringValue(item["exposure"])), - Description: stringValue(item["description"]), - UpdatedAt: timeValue(item["updatedAt"]), - Diagnostics: decodeDiagnostics(item["diagnostics"]), + Field: decodeFieldRef(item["field"]), + Source: decodeSource(item["source"]), + Origin: decodeSource(item["origin"]), + Explicit: boolValue(item["explicit"]), + Confidence: model.BindingConfidence(stringValue(item["confidence"])), + Visibility: model.Visibility(stringValue(item["visibility"])), + Exposure: model.Exposure(stringValue(item["exposure"])), + Description: stringValue(item["description"]), + UpdatedAt: timeValue(item["updatedAt"]), + Diagnostics: decodeDiagnostics(item["diagnostics"]), }) } return items diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 115db79..5f0e708 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -395,31 +395,10 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, }, "field": &graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) - return item.Field.String(), nil - }, - }, - "fieldTypeID": &graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) - return string(item.Field.TypeID), nil - }, - }, - "fieldInstance": &graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) - return item.Field.Instance, nil - }, - }, - "fieldName": &graphql.Field{ - Type: graphql.String, + Type: fieldRefType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { item := p.Source.(store.SnapshotItem) - return item.Field.Field, nil + return fieldRefView(item.Field), nil }, }, "source": &graphql.Field{ From e6480bce6cae4e1dfb2be40c325a749f669a217a Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Tue, 11 Aug 2026 08:57:16 -0700 Subject: [PATCH 10/13] chore: rename internal store state machine Signed-off-by: Sebastian (Tiedtke) Huckleberry --- internal/graph/plan.go | 16 +-- internal/graph/runtime.go | 56 +++++----- internal/graph/runtime_test.go | 112 ++++++++++---------- internal/graph/schema.go | 100 +++++++++--------- internal/requirements/config.go | 30 +++--- internal/requirements/config_test.go | 6 +- internal/requirements/dotenv_spec.go | 8 +- internal/resolver/resolver_test.go | 2 +- internal/{store => state}/diagnostics.go | 2 +- internal/state/doc.go | 2 + internal/{store => state}/frontier.go | 2 +- internal/{store => state}/integrity.go | 2 +- internal/{store => state}/store.go | 126 +++++++++++------------ internal/{store => state}/store_test.go | 32 +++--- internal/store/doc.go | 2 - pkg/owl/api.go | 118 ++++++++++----------- 16 files changed, 308 insertions(+), 308 deletions(-) rename internal/{store => state}/diagnostics.go (99%) create mode 100644 internal/state/doc.go rename internal/{store => state}/frontier.go (99%) rename internal/{store => state}/integrity.go (99%) rename internal/{store => state}/store.go (90%) rename internal/{store => state}/store_test.go (96%) delete mode 100644 internal/store/doc.go diff --git a/internal/graph/plan.go b/internal/graph/plan.go index bb3d03d..7c014fe 100644 --- a/internal/graph/plan.go +++ b/internal/graph/plan.go @@ -7,7 +7,7 @@ import ( "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/printer" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) type plannedQuery struct { @@ -16,7 +16,7 @@ type plannedQuery struct { Path []string } -func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, error) { +func planStateEnvelopeQuery(records []state.OperationRecord) (plannedQuery, error) { if len(records) == 0 { return plannedQuery{}, errors.New("operation plan is empty") } @@ -30,7 +30,7 @@ func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, erro for index, record := range records { next := ast.NewSelectionSet(&ast.SelectionSet{}) switch record.Kind { - case store.OperationRecordLoad: + case state.OperationRecordLoad: name := fmt.Sprintf("load_%d", index) varDefs = append(varDefs, variableDefinition(name, nonNull(namedType("LoadInput")))) input := record.Load @@ -40,10 +40,10 @@ func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, erro argument("input", variable(name)), }, next)) path = append(path, "load") - case store.OperationRecordUpdate: + case state.OperationRecordUpdate: name := fmt.Sprintf("update_%d", index) varDefs = append(varDefs, variableDefinition(name, namedType("DotenvInput"))) - vars[name] = marshalDotenvInput(store.LoadInput{ + vars[name] = marshalDotenvInput(state.LoadInput{ DotenvSource: record.Update.Source, Dotenv: record.Update.Dotenv, Timestamp: record.Timestamp, @@ -52,7 +52,7 @@ func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, erro argument("dotenv", variable(name)), }, next)) path = append(path, "update") - case store.OperationRecordDelete: + case state.OperationRecordDelete: name := fmt.Sprintf("delete_%d", index) varDefs = append(varDefs, variableDefinition(name, list(nonNull(namedType("String"))))) vars[name] = append([]string{}, record.Delete.Keys...) @@ -60,7 +60,7 @@ func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, erro argument("keys", variable(name)), }, next)) path = append(path, "delete") - case store.OperationRecordResolverAttempt: + case state.OperationRecordResolverAttempt: name := fmt.Sprintf("resolverAttempt_%d", index) varDefs = append(varDefs, variableDefinition(name, nonNull(namedType("ResolverAttemptInput")))) vars[name] = marshalResolverAttempt(record.ResolverAttempt) @@ -68,7 +68,7 @@ func planStateEnvelopeQuery(records []store.OperationRecord) (plannedQuery, erro argument("attempt", variable(name)), }, next)) path = append(path, "recordResolverAttempt") - case store.OperationRecordApplyResolverProposal: + case state.OperationRecordApplyResolverProposal: name := fmt.Sprintf("resolverProposal_%d", index) varDefs = append(varDefs, variableDefinition(name, nonNull(namedType("ResolverProposalInput")))) timestampName := fmt.Sprintf("resolverProposalTimestamp_%d", index) diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index 58aaeba..c879615 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -11,7 +11,7 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/registry" "github.com/runmedev/owl/internal/resolver" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) type Runtime struct { @@ -24,25 +24,25 @@ type Context struct { Types registry.TypeProvider } -type LoadInput = store.LoadInput +type LoadInput = state.LoadInput -type SnapshotPolicy = store.SnapshotPolicy +type SnapshotPolicy = state.SnapshotPolicy -type DotenvPolicy = store.DotenvPolicy +type DotenvPolicy = state.DotenvPolicy -type GetPolicy = store.GetPolicy +type GetPolicy = state.GetPolicy -type TypePolicy = store.TypePolicy +type TypePolicy = state.TypePolicy -type SnapshotItem = store.SnapshotItem +type SnapshotItem = state.SnapshotItem -type GetResult = store.GetResult +type GetResult = state.GetResult -type TypeResult = store.TypeResult +type TypeResult = state.TypeResult -type CheckResult = store.CheckResult +type CheckResult = state.CheckResult -type StateEnvelope = store.StateEnvelope +type StateEnvelope = state.StateEnvelope type Operation struct { Name string @@ -243,12 +243,12 @@ func TypeOperation(input LoadInput, policy TypePolicy) Operation { } func (r *Runtime) StateEnvelope(ctx context.Context, input LoadInput) (StateEnvelope, error) { - return r.StateEnvelopeForOperations(ctx, []store.OperationRecord{ - {Kind: store.OperationRecordLoad, Load: input}, + return r.StateEnvelopeForOperations(ctx, []state.OperationRecord{ + {Kind: state.OperationRecordLoad, Load: input}, }) } -func (r *Runtime) StateEnvelopeForOperations(ctx context.Context, records []store.OperationRecord) (StateEnvelope, error) { +func (r *Runtime) StateEnvelopeForOperations(ctx context.Context, records []state.OperationRecord) (StateEnvelope, error) { plan, err := planStateEnvelopeQuery(records) if err != nil { return StateEnvelope{}, err @@ -264,7 +264,7 @@ func (r *Runtime) StateEnvelopeForOperations(ctx context.Context, records []stor return decodeEnvelope(raw) } -func StateEnvelopeOperation(records []store.OperationRecord) (Operation, error) { +func StateEnvelopeOperation(records []state.OperationRecord) (Operation, error) { plan, err := planStateEnvelopeQuery(records) if err != nil { return Operation{}, err @@ -272,21 +272,21 @@ func StateEnvelopeOperation(records []store.OperationRecord) (Operation, error) return Operation{Name: "OwlStateEnvelope", Document: plan.Query, Variables: plan.Vars}, nil } -func (r *Runtime) StateEnvelopeAfter(ctx context.Context, input LoadInput, patch store.LoadInput, deleted []string) (StateEnvelope, error) { - records := []store.OperationRecord{{Kind: store.OperationRecordLoad, Load: input}} +func (r *Runtime) StateEnvelopeAfter(ctx context.Context, input LoadInput, patch state.LoadInput, deleted []string) (StateEnvelope, error) { + records := []state.OperationRecord{{Kind: state.OperationRecordLoad, Load: input}} if len(patch.Dotenv) > 0 { - records = append(records, store.OperationRecord{ - Kind: store.OperationRecordUpdate, - Update: store.UpdateOperation{ + records = append(records, state.OperationRecord{ + Kind: state.OperationRecordUpdate, + Update: state.UpdateOperation{ Source: patch.DotenvSource, Dotenv: patch.Dotenv, }, }) } if len(deleted) > 0 { - records = append(records, store.OperationRecord{ - Kind: store.OperationRecordDelete, - Delete: store.DeleteOperation{Keys: append([]string{}, deleted...)}, + records = append(records, state.OperationRecord{ + Kind: state.OperationRecordDelete, + Delete: state.DeleteOperation{Keys: append([]string{}, deleted...)}, }) } return r.StateEnvelopeForOperations(ctx, records) @@ -455,7 +455,7 @@ func marshalEnvelope(envelope StateEnvelope) map[string]interface{} { } } -func marshalStateProvenance(provenance store.StateProvenance) map[string]interface{} { +func marshalStateProvenance(provenance state.StateProvenance) map[string]interface{} { sources := make([]map[string]interface{}, 0, len(provenance.Sources)) for _, source := range provenance.Sources { if marshaled := marshalSource(source); marshaled != nil { @@ -788,13 +788,13 @@ func decodeType(raw interface{}) TypeResult { if !ok { return TypeResult{} } - proposals := make([]store.TypeProposal, 0, len(proposalsRaw)) + proposals := make([]state.TypeProposal, 0, len(proposalsRaw)) for _, item := range proposalsRaw { proposalRaw, ok := item.(map[string]interface{}) if !ok { continue } - proposals = append(proposals, store.TypeProposal{ + proposals = append(proposals, state.TypeProposal{ Key: stringValue(proposalRaw["key"]), CurrentType: model.TypeID(stringValue(proposalRaw["currentType"])), SuggestedType: model.TypeID(stringValue(proposalRaw["suggestedType"])), @@ -916,8 +916,8 @@ func decodeUnresolvedFrontier(raw interface{}) model.UnresolvedFrontier { return frontier } -func decodeStateProvenance(raw interface{}) store.StateProvenance { - var provenance store.StateProvenance +func decodeStateProvenance(raw interface{}) state.StateProvenance { + var provenance state.StateProvenance row, ok := raw.(map[string]interface{}) if !ok { return provenance diff --git a/internal/graph/runtime_test.go b/internal/graph/runtime_test.go index 206d7b5..3212302 100644 --- a/internal/graph/runtime_test.go +++ b/internal/graph/runtime_test.go @@ -12,7 +12,7 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/resolver" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) func TestRuntimeDrivesLoadNormalizeValidateSnapshot(t *testing.T) { @@ -21,18 +21,18 @@ func TestRuntimeDrivesLoadNormalizeValidateSnapshot(t *testing.T) { runtime, err := NewRuntime(nil) require.NoError(t, err) - items, err := runtime.Snapshot(context.Background(), store.LoadInput{ + items, err := runtime.Snapshot(context.Background(), state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{ + Dotenv: []state.DotenvVariable{ {Key: "API_URL", Value: "https://api.example.com"}, {Key: "API_KEY", Value: "secret"}, {Key: "REDIS_HOST", Value: "localhost"}, }, - Contracts: []store.EnvContract{ + Contracts: []state.EnvContract{ { Source: model.Source{Name: ".env.spec", Kind: "dotenv-spec"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{ + Bindings: []state.EnvBinding{ { Key: "API_URL", FieldRef: model.FieldRef{TypeID: model.TypeCorePlain, Instance: "default", Field: "api.url"}, @@ -68,17 +68,17 @@ func TestRuntimeRendersDotenvThroughGraphQL(t *testing.T) { runtime, err := NewRuntime(nil) require.NoError(t, err) - envs, err := runtime.Dotenv(context.Background(), store.LoadInput{ + envs, err := runtime.Dotenv(context.Background(), state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{ + Dotenv: []state.DotenvVariable{ {Key: "API_KEY", Value: "secret"}, {Key: "API_URL", Value: "https://api.example.com"}, }, - Contracts: []store.EnvContract{ + Contracts: []state.EnvContract{ { Source: model.Source{Name: "package.json", Kind: "package-json"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{ + Bindings: []state.EnvBinding{ { Key: "API_KEY", FieldRef: model.FieldRef{TypeID: model.TypeCoreSecret, Instance: "default", Field: "api.key"}, @@ -107,13 +107,13 @@ func TestRuntimeCheckReportsRequiredDiagnostics(t *testing.T) { runtime, err := NewRuntime(nil) require.NoError(t, err) - check, err := runtime.Check(context.Background(), store.LoadInput{ + check, err := runtime.Check(context.Background(), state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Contracts: []store.EnvContract{ + Contracts: []state.EnvContract{ { Source: model.Source{Name: ".env.spec", Kind: "dotenv-spec"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{ + Bindings: []state.EnvBinding{ { Key: "API_KEY", FieldRef: model.FieldRef{TypeID: model.TypeCoreSecret, Instance: "default", Field: "api.key"}, @@ -233,27 +233,27 @@ func introspectionInputFields(t *testing.T, schemaJSON string) map[string][]stri func TestPlanStateEnvelopeQueryStacksOperationRecords(t *testing.T) { t.Parallel() - plan, err := planStateEnvelopeQuery([]store.OperationRecord{ + plan, err := planStateEnvelopeQuery([]state.OperationRecord{ { - Kind: store.OperationRecordLoad, - Load: store.LoadInput{ + Kind: state.OperationRecordLoad, + Load: state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{{Key: "API_URL", Value: "https://api.example.com"}}, + Dotenv: []state.DotenvVariable{{Key: "API_URL", Value: "https://api.example.com"}}, }, }, { - Kind: store.OperationRecordUpdate, - Update: store.UpdateOperation{ + Kind: state.OperationRecordUpdate, + Update: state.UpdateOperation{ Source: model.Source{Name: "[update]", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{{Key: "API_URL", Value: "https://next.example.com"}}, + Dotenv: []state.DotenvVariable{{Key: "API_URL", Value: "https://next.example.com"}}, }, }, { - Kind: store.OperationRecordDelete, - Delete: store.DeleteOperation{Keys: []string{"API_KEY"}}, + Kind: state.OperationRecordDelete, + Delete: state.DeleteOperation{Keys: []string{"API_KEY"}}, }, { - Kind: store.OperationRecordResolverAttempt, + Kind: state.OperationRecordResolverAttempt, ResolverAttempt: model.ResolverAttempt{ ID: "attempt-000001", ResolverID: "core/dotenv", @@ -263,7 +263,7 @@ func TestPlanStateEnvelopeQueryStacksOperationRecords(t *testing.T) { }, }, { - Kind: store.OperationRecordApplyResolverProposal, + Kind: state.OperationRecordApplyResolverProposal, ResolverProposal: resolver.Proposal{ AttemptID: "attempt-000002", ResolverID: "core/dotenv", @@ -301,20 +301,20 @@ func TestRuntimeMaterializesStateEnvelopeFromOperationRecords(t *testing.T) { runtime, err := NewRuntime(nil) require.NoError(t, err) - envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []store.OperationRecord{ + envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []state.OperationRecord{ { - Kind: store.OperationRecordLoad, - Load: store.LoadInput{ + Kind: state.OperationRecordLoad, + Load: state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{ + Dotenv: []state.DotenvVariable{ {Key: "API_URL", Value: "https://api.example.com"}, {Key: "API_KEY", Value: "secret"}, }, - Contracts: []store.EnvContract{ + Contracts: []state.EnvContract{ { Source: model.Source{Name: ".env.spec", Kind: "dotenv-spec"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{ + Bindings: []state.EnvBinding{ { Key: "API_KEY", FieldRef: model.FieldRef{TypeID: model.TypeCoreSecret, Instance: "default", Field: "api.key"}, @@ -327,26 +327,26 @@ func TestRuntimeMaterializesStateEnvelopeFromOperationRecords(t *testing.T) { }, }, { - Kind: store.OperationRecordUpdate, - Update: store.UpdateOperation{ + Kind: state.OperationRecordUpdate, + Update: state.UpdateOperation{ Source: model.Source{Name: "[update]", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{{Key: "API_URL", Value: "https://next.example.com"}}, + Dotenv: []state.DotenvVariable{{Key: "API_URL", Value: "https://next.example.com"}}, }, }, { - Kind: store.OperationRecordDelete, - Delete: store.DeleteOperation{Keys: []string{"API_KEY"}}, + Kind: state.OperationRecordDelete, + Delete: state.DeleteOperation{Keys: []string{"API_KEY"}}, }, }) require.NoError(t, err) - s := store.NewState(envelope.State, nil) - got, ok, err := s.Get("API_URL", store.GetPolicy{Reveal: true}) + s := state.MachineFromState(envelope.State, nil) + got, ok, err := s.Get("API_URL", state.GetPolicy{Reveal: true}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://next.example.com", got.Value) assert.False(t, envelope.State.Values[got.Field].UpdatedAt.IsZero()) - _, ok, err = s.Get("API_KEY", store.GetPolicy{Reveal: true}) + _, ok, err = s.Get("API_KEY", state.GetPolicy{Reveal: true}) require.NoError(t, err) assert.False(t, ok) } @@ -380,16 +380,16 @@ func TestRuntimeMaterializesResolverAttemptsFromOperationRecords(t *testing.T) { }, } - envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []store.OperationRecord{ + envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []state.OperationRecord{ { - Kind: store.OperationRecordLoad, - Load: store.LoadInput{ + Kind: state.OperationRecordLoad, + Load: state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{{Key: "API_URL", Value: "https://api.example.com"}}, + Dotenv: []state.DotenvVariable{{Key: "API_URL", Value: "https://api.example.com"}}, }, }, { - Kind: store.OperationRecordResolverAttempt, + Kind: state.OperationRecordResolverAttempt, ResolverAttempt: attempt, }, }) @@ -399,8 +399,8 @@ func TestRuntimeMaterializesResolverAttemptsFromOperationRecords(t *testing.T) { assert.Equal(t, attempt, envelope.State.ResolverAttempts[0]) assert.NotContains(t, envelope.State.ResolverAttempts[0].Message, "secret") - s := store.NewState(envelope.State, nil) - got, ok, err := s.Get("API_URL", store.GetPolicy{Reveal: true}) + s := state.MachineFromState(envelope.State, nil) + got, ok, err := s.Get("API_URL", state.GetPolicy{Reveal: true}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://api.example.com", got.Value) @@ -414,15 +414,15 @@ func TestRuntimeMaterializesResolverProposalsFromOperationRecords(t *testing.T) ref := model.FieldRef{TypeID: model.TypeCoreSecret, Instance: "default", Field: "api.key"} timestamp := time.Date(2026, 8, 3, 20, 30, 0, 0, time.UTC) - envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []store.OperationRecord{ + envelope, err := runtime.StateEnvelopeForOperations(context.Background(), []state.OperationRecord{ { - Kind: store.OperationRecordLoad, - Load: store.LoadInput{ + Kind: state.OperationRecordLoad, + Load: state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Contracts: []store.EnvContract{{ + Contracts: []state.EnvContract{{ Source: model.Source{Name: ".env.example", Kind: "dotenv-spec"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{{ + Bindings: []state.EnvBinding{{ Key: "API_KEY", FieldRef: ref, Projection: model.ProjectionDotenv, @@ -432,7 +432,7 @@ func TestRuntimeMaterializesResolverProposalsFromOperationRecords(t *testing.T) }, }, { - Kind: store.OperationRecordApplyResolverProposal, + Kind: state.OperationRecordApplyResolverProposal, Timestamp: timestamp, ResolverProposal: resolver.Proposal{ AttemptID: "attempt-000001", @@ -450,8 +450,8 @@ func TestRuntimeMaterializesResolverProposalsFromOperationRecords(t *testing.T) }) require.NoError(t, err) - s := store.NewState(envelope.State, nil) - got, ok, err := s.Get("API_KEY", store.GetPolicy{Reveal: true}) + s := state.MachineFromState(envelope.State, nil) + got, ok, err := s.Get("API_KEY", state.GetPolicy{Reveal: true}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "secret", got.Value) @@ -468,14 +468,14 @@ func TestRuntimeMaterializesUnresolvedFrontier(t *testing.T) { runtime, err := NewRuntime(nil) require.NoError(t, err) - envelope, err := runtime.StateEnvelope(context.Background(), store.LoadInput{ + envelope, err := runtime.StateEnvelope(context.Background(), state.LoadInput{ DotenvSource: model.Source{Name: ".env", Kind: "dotenv"}, - Dotenv: []store.DotenvVariable{{Key: "PRESENT_SECRET", Value: "secret"}}, - Contracts: []store.EnvContract{ + Dotenv: []state.DotenvVariable{{Key: "PRESENT_SECRET", Value: "secret"}}, + Contracts: []state.EnvContract{ { Source: model.Source{Name: "owl.toml", Kind: "owl-config"}, Projection: model.ProjectionDotenv, - Bindings: []store.EnvBinding{ + Bindings: []state.EnvBinding{ { Key: "MISSING_SECRET", FieldRef: model.FieldRef{TypeID: model.TypeCoreSecret, Instance: "default", Field: "missing.secret"}, diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 5f0e708..5c07bf2 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -11,7 +11,7 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/requirements" "github.com/runmedev/owl/internal/resolver" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) func (r *Runtime) newSchema() (graphql.Schema, error) { @@ -338,7 +338,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Type: stateEnvelopeType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - return stateEnvelopeView(store.NewState(gctx.State, gctx.Types).StateEnvelope()), nil + return stateEnvelopeView(state.MachineFromState(gctx.State, gctx.Types).StateEnvelope()), nil }, }, }, @@ -390,28 +390,28 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "type": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return string(item.Type), nil }, }, "field": &graphql.Field{ Type: fieldRefType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return fieldRefView(item.Field), nil }, }, "source": &graphql.Field{ Type: sourceType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return item.Source, nil }, }, "origin": &graphql.Field{ Type: sourceType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return item.Origin, nil }, }, @@ -419,21 +419,21 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "confidence": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return string(item.Confidence), nil }, }, "visibility": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return string(item.Visibility), nil }, }, "exposure": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return string(item.Exposure), nil }, }, @@ -442,7 +442,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "updatedAt": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - item := p.Source.(store.SnapshotItem) + item := p.Source.(state.SnapshotItem) return timeString(item.UpdatedAt), nil }, }, @@ -463,7 +463,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) all, _ := p.Args["all"].(bool) - result, err := store.NewState(gctx.State, gctx.Types).Type(store.TypePolicy{All: all}) + result, err := state.MachineFromState(gctx.State, gctx.Types).Type(state.TypePolicy{All: all}) if err != nil { return nil, err } @@ -486,7 +486,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) reveal, _ := p.Args["reveal"].(bool) - return store.NewState(gctx.State, gctx.Types).Snapshot(store.SnapshotPolicy{Reveal: reveal}) + return state.MachineFromState(gctx.State, gctx.Types).Snapshot(state.SnapshotPolicy{Reveal: reveal}) }, }, "dotenv": &graphql.Field{ @@ -497,14 +497,14 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) insecure, _ := p.Args["insecure"].(bool) - return store.NewState(gctx.State, gctx.Types).Dotenv(store.DotenvPolicy{Insecure: insecure}) + return state.MachineFromState(gctx.State, gctx.Types).Dotenv(state.DotenvPolicy{Insecure: insecure}) }, }, "check": &graphql.Field{ Type: checkType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - check := store.NewState(gctx.State, gctx.Types).Check() + check := state.MachineFromState(gctx.State, gctx.Types).Check() check.Diagnostics = sortedDiagnostics(check.Diagnostics) return check, nil }, @@ -526,7 +526,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { gctx := p.Source.(Context) key := p.Args["key"].(string) reveal, _ := p.Args["reveal"].(bool) - result, ok, err := store.NewState(gctx.State, gctx.Types).Get(key, store.GetPolicy{Reveal: reveal}) + result, ok, err := state.MachineFromState(gctx.State, gctx.Types).Get(key, state.GetPolicy{Reveal: reveal}) if err != nil || !ok { return nil, err } @@ -537,7 +537,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Type: graphql.NewList(graphql.String), Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - return store.NewState(gctx.State, gctx.Types).SensitiveKeys() + return state.MachineFromState(gctx.State, gctx.Types).SensitiveKeys() }, }, } @@ -556,8 +556,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { input := decodeLoadInput(p.Args["input"].(map[string]interface{})) gctx := p.Source.(Context) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.LoadOperation{Input: input, Timestamp: input.Timestamp}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.LoadOperation{Input: input, Timestamp: input.Timestamp}) if err != nil { return nil, err } @@ -572,8 +572,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) input := decodeDotenvInput(p.Args["dotenv"]) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.UpdateOperation{Source: input.DotenvSource, Dotenv: input.Dotenv, Timestamp: input.Timestamp}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.UpdateOperation{Source: input.DotenvSource, Dotenv: input.Dotenv, Timestamp: input.Timestamp}) if err != nil { return nil, err } @@ -587,8 +587,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.DeleteOperation{Keys: decodeStringList(p.Args["keys"])}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.DeleteOperation{Keys: decodeStringList(p.Args["keys"])}) if err != nil { return nil, err } @@ -603,8 +603,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) attempt := decodeResolverAttemptInput(p.Args["attempt"]) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.RecordResolverAttemptOperation{Attempt: attempt}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.RecordResolverAttemptOperation{Attempt: attempt}) if err != nil { return nil, err } @@ -620,8 +620,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) proposal := decodeResolverProposalInput(p.Args["proposal"]) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.ApplyResolverProposalOperation{ + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.ApplyResolverProposalOperation{ Proposal: proposal, Timestamp: timeValue(p.Args["timestamp"]), }) @@ -635,8 +635,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Type: environmentType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.NormalizeOperation{}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.NormalizeOperation{}) if err != nil { return nil, err } @@ -647,8 +647,8 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { Type: environmentType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { gctx := p.Source.(Context) - s := store.NewState(gctx.State, gctx.Types) - state, err := s.Apply(contextFromParams(p), store.IntegrityOperation{Types: gctx.Types}) + s := state.MachineFromState(gctx.State, gctx.Types) + state, err := s.Apply(contextFromParams(p), state.IntegrityOperation{Types: gctx.Types}) if err != nil { return nil, err } @@ -699,26 +699,26 @@ func contextFromParams(p graphql.ResolveParams) context.Context { return p.Context } -func contractsFromState(state model.EffectiveState) []store.EnvContract { +func contractsFromState(effective model.EffectiveState) []state.EnvContract { type contractKey struct { source model.Source projection model.ProjectionID } positions := make(map[contractKey]int) - var contracts []store.EnvContract - for _, binding := range state.Bindings { + var contracts []state.EnvContract + for _, binding := range effective.Bindings { source := binding.Origin key := contractKey{source: source, projection: binding.ProjectionID} position, ok := positions[key] if !ok { position = len(contracts) positions[key] = position - contracts = append(contracts, store.EnvContract{ + contracts = append(contracts, state.EnvContract{ Source: source, Projection: binding.ProjectionID, }) } - contracts[position].Bindings = append(contracts[position].Bindings, store.EnvBinding{ + contracts[position].Bindings = append(contracts[position].Bindings, state.EnvBinding{ FieldRef: binding.FieldRef, Key: string(binding.Key), Projection: binding.ProjectionID, @@ -726,8 +726,8 @@ func contractsFromState(state model.EffectiveState) []store.EnvContract { Description: binding.Description, Source: source, Order: binding.Order, - Sensitivity: state.Values[binding.FieldRef].Sensitivity, - Exposure: state.Values[binding.FieldRef].Exposure, + Sensitivity: effective.Values[binding.FieldRef].Sensitivity, + Exposure: effective.Values[binding.FieldRef].Exposure, }) } sort.SliceStable(contracts, func(i, j int) bool { @@ -739,10 +739,10 @@ func contractsFromState(state model.EffectiveState) []store.EnvContract { return contracts } -func decodeLoadInput(raw map[string]interface{}) store.LoadInput { - var input store.LoadInput +func decodeLoadInput(raw map[string]interface{}) state.LoadInput { + var input state.LoadInput if envelopeRaw, ok := raw["envelope"].(map[string]interface{}); ok { - envelope := store.StateEnvelope{ + envelope := state.StateEnvelope{ ModelVersion: stringValue(envelopeRaw["modelVersion"]), State: decodeEffectiveStateInput(envelopeRaw["state"]), Provenance: decodeStateProvenanceInput(envelopeRaw["provenance"]), @@ -758,7 +758,7 @@ func decodeLoadInput(raw map[string]interface{}) store.LoadInput { if !ok { continue } - contract := store.EnvContract{ + contract := state.EnvContract{ Source: decodeSource(contractRaw["source"]), Projection: model.ProjectionID(stringValue(contractRaw["projection"])), } @@ -767,7 +767,7 @@ func decodeLoadInput(raw map[string]interface{}) store.LoadInput { if !ok { continue } - contract.Bindings = append(contract.Bindings, store.EnvBinding{ + contract.Bindings = append(contract.Bindings, state.EnvBinding{ FieldRef: decodeFieldRef(bindingRaw["field"]), Key: stringValue(bindingRaw["key"]), Projection: model.ProjectionID(stringValue(bindingRaw["projection"])), @@ -787,8 +787,8 @@ func decodeLoadInput(raw map[string]interface{}) store.LoadInput { return input } -func decodeDotenvInput(raw interface{}) store.LoadInput { - var input store.LoadInput +func decodeDotenvInput(raw interface{}) state.LoadInput { + var input state.LoadInput dotenvRaw, ok := raw.(map[string]interface{}) if !ok { return input @@ -800,7 +800,7 @@ func decodeDotenvInput(raw interface{}) store.LoadInput { if !ok { continue } - input.Dotenv = append(input.Dotenv, store.DotenvVariable{ + input.Dotenv = append(input.Dotenv, state.DotenvVariable{ Key: stringValue(variable["key"]), Value: stringValue(variable["value"]), Source: decodeSource(variable["source"]), @@ -952,8 +952,8 @@ func decodeDiagnosticsInput(raw interface{}) []model.Diagnostic { return diagnostics } -func decodeStateProvenanceInput(raw interface{}) store.StateProvenance { - var provenance store.StateProvenance +func decodeStateProvenanceInput(raw interface{}) state.StateProvenance { + var provenance state.StateProvenance row, ok := raw.(map[string]interface{}) if !ok { return provenance @@ -1102,7 +1102,7 @@ func timeValue(raw interface{}) time.Time { return parsed } -func stateEnvelopeView(envelope store.StateEnvelope) map[string]interface{} { +func stateEnvelopeView(envelope state.StateEnvelope) map[string]interface{} { return map[string]interface{}{ "modelVersion": envelope.ModelVersion, "state": effectiveStateView(envelope.State), @@ -1250,7 +1250,7 @@ func unresolvedFrontierView(frontier model.UnresolvedFrontier) map[string]interf return map[string]interface{}{"needs": needs} } -func getResultView(result store.GetResult) map[string]interface{} { +func getResultView(result state.GetResult) map[string]interface{} { return map[string]interface{}{ "key": result.Key, "field": fieldRefView(result.Field), @@ -1262,7 +1262,7 @@ func getResultView(result store.GetResult) map[string]interface{} { } } -func typeResultView(result store.TypeResult) map[string]interface{} { +func typeResultView(result state.TypeResult) map[string]interface{} { proposals := make([]map[string]interface{}, 0, len(result.Proposals)) for _, proposal := range result.Proposals { proposals = append(proposals, map[string]interface{}{ diff --git a/internal/requirements/config.go b/internal/requirements/config.go index bfd2cce..bc090e2 100644 --- a/internal/requirements/config.go +++ b/internal/requirements/config.go @@ -8,7 +8,7 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/registry" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) type ConfigCompiler struct { @@ -38,11 +38,11 @@ func NewConfigCompiler(types registry.TypeProvider, source model.Source) *Config } } -func ContractsFromConfig(input model.ConfigInput, source model.Source, types registry.TypeProvider) ([]store.EnvContract, error) { +func ContractsFromConfig(input model.ConfigInput, source model.Source, types registry.TypeProvider) ([]state.EnvContract, error) { return NewConfigCompiler(types, source).Compile(input) } -func (c *ConfigCompiler) Compile(input model.ConfigInput) ([]store.EnvContract, error) { +func (c *ConfigCompiler) Compile(input model.ConfigInput) ([]state.EnvContract, error) { if len(input.Needs) == 0 { return nil, nil } @@ -53,7 +53,7 @@ func (c *ConfigCompiler) Compile(input model.ConfigInput) ([]store.EnvContract, } c.instanceCounts = instanceCounts - contracts := make([]store.EnvContract, 0, len(input.Needs)) + contracts := make([]state.EnvContract, 0, len(input.Needs)) for i, need := range input.Needs { contract, err := c.compileNeed(fmt.Sprintf("needs[%d]", i), need) if err != nil { @@ -96,52 +96,52 @@ func (c *ConfigCompiler) countInstances(input model.ConfigInput) (map[model.Type return counts, nil } -func (c *ConfigCompiler) compileNeed(path string, need model.NeedInput) (store.EnvContract, error) { +func (c *ConfigCompiler) compileNeed(path string, need model.NeedInput) (state.EnvContract, error) { typeRef := strings.TrimSpace(string(need.Type)) if typeRef == "" { - return store.EnvContract{}, fmt.Errorf("%s.type: type is required", path) + return state.EnvContract{}, fmt.Errorf("%s.type: type is required", path) } instance := strings.TrimSpace(need.Instance) if instance == "" { - return store.EnvContract{}, fmt.Errorf("%s.instance: instance is required", path) + return state.EnvContract{}, fmt.Errorf("%s.instance: instance is required", path) } typeDef, ok, err := c.types.ResolveTypeRef(typeRef) if err != nil { - return store.EnvContract{}, fmt.Errorf("%s.type: %w", path, err) + return state.EnvContract{}, fmt.Errorf("%s.type: %w", path, err) } if !ok { - return store.EnvContract{}, fmt.Errorf("%s.type: unknown type %q", path, typeRef) + return state.EnvContract{}, fmt.Errorf("%s.type: unknown type %q", path, typeRef) } fieldKeys, err := c.dotenvFieldKeys(path, need, typeDef) if err != nil { - return store.EnvContract{}, err + return state.EnvContract{}, err } fields := sortedFieldNames(fieldKeys) - contract := store.EnvContract{ + contract := state.EnvContract{ Source: c.source, Projection: model.ProjectionDotenv, } for _, fieldName := range fields { binding, err := c.compileBinding(path, typeDef, instance, fieldName, fieldKeys[fieldName]) if err != nil { - return store.EnvContract{}, err + return state.EnvContract{}, err } contract.Bindings = append(contract.Bindings, binding) } return contract, nil } -func (c *ConfigCompiler) compileBinding(path string, typeDef model.TypeDef, instance string, fieldName string, key string) (store.EnvBinding, error) { +func (c *ConfigCompiler) compileBinding(path string, typeDef model.TypeDef, instance string, fieldName string, key string) (state.EnvBinding, error) { fieldDef := typeDef.Fields[fieldName] ref := model.FieldRef{TypeID: typeDef.ID, Instance: instance, Field: fieldName} if previous, ok := c.seenKeys[key]; ok && previous != ref { - return store.EnvBinding{}, fmt.Errorf("%s.dotenv.%s: duplicate dotenv key %q already bound to %s", path, fieldName, key, previous.String()) + return state.EnvBinding{}, fmt.Errorf("%s.dotenv.%s: duplicate dotenv key %q already bound to %s", path, fieldName, key, previous.String()) } c.seenKeys[key] = ref c.order++ - return store.EnvBinding{ + return state.EnvBinding{ FieldRef: ref, Key: key, Projection: model.ProjectionDotenv, diff --git a/internal/requirements/config_test.go b/internal/requirements/config_test.go index 5ef11de..0e3ab4e 100644 --- a/internal/requirements/config_test.go +++ b/internal/requirements/config_test.go @@ -9,7 +9,7 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/registry" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) func TestContractsFromConfigInfersRequiredDotenvBindings(t *testing.T) { @@ -304,8 +304,8 @@ func TestRenderDotenvSpec(t *testing.T) { }, "\n"), rendered) } -func bindingsByKey(bindings []store.EnvBinding) map[string]store.EnvBinding { - result := make(map[string]store.EnvBinding, len(bindings)) +func bindingsByKey(bindings []state.EnvBinding) map[string]state.EnvBinding { + result := make(map[string]state.EnvBinding, len(bindings)) for _, binding := range bindings { result[binding.Key] = binding } diff --git a/internal/requirements/dotenv_spec.go b/internal/requirements/dotenv_spec.go index aaec862..cadd055 100644 --- a/internal/requirements/dotenv_spec.go +++ b/internal/requirements/dotenv_spec.go @@ -7,14 +7,14 @@ import ( "github.com/runmedev/owl/internal/model" "github.com/runmedev/owl/internal/registry" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) const GeneratedDotenvSpecHeaderPrefix = "# Generated by Owl from " const GeneratedDotenvSpecHeader = GeneratedDotenvSpecHeaderPrefix + "Owl config. Do not edit by hand." -func RenderDotenvSpec(contracts []store.EnvContract, types registry.TypeProvider) (string, error) { +func RenderDotenvSpec(contracts []state.EnvContract, types registry.TypeProvider) (string, error) { if len(contracts) == 0 { return GeneratedDotenvSpecHeader + "\n", nil } @@ -22,7 +22,7 @@ func RenderDotenvSpec(contracts []store.EnvContract, types registry.TypeProvider types = registry.NewBuiltInRegistry() } - var bindings []store.EnvBinding + var bindings []state.EnvBinding for _, contract := range contracts { bindings = append(bindings, contract.Bindings...) } @@ -71,7 +71,7 @@ func RenderDotenvSpec(contracts []store.EnvContract, types registry.TypeProvider return b.String(), nil } -func generatedDotenvSpecHeader(contracts []store.EnvContract) string { +func generatedDotenvSpecHeader(contracts []state.EnvContract) string { source := "Owl config" if len(contracts) > 0 { name := strings.TrimSpace(contracts[0].Source.Name) diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index 0e0f0be..e7fc731 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -156,7 +156,7 @@ func TestResolverPackageDoesNotImportStoreOrGraph(t *testing.T) { require.NoError(t, err) for _, imported := range file.Imports { unquoted := strings.Trim(imported.Path.Value, `"`) - assert.NotEqual(t, "github.com/runmedev/owl/internal/store", unquoted) + assert.NotEqual(t, "github.com/runmedev/owl/internal/state", unquoted) assert.NotEqual(t, "github.com/runmedev/owl/internal/graph", unquoted) } } diff --git a/internal/store/diagnostics.go b/internal/state/diagnostics.go similarity index 99% rename from internal/store/diagnostics.go rename to internal/state/diagnostics.go index b58f2a1..adafba2 100644 --- a/internal/store/diagnostics.go +++ b/internal/state/diagnostics.go @@ -1,4 +1,4 @@ -package store +package state import ( "fmt" diff --git a/internal/state/doc.go b/internal/state/doc.go new file mode 100644 index 0000000..ce43ddb --- /dev/null +++ b/internal/state/doc.go @@ -0,0 +1,2 @@ +// Package state owns Owl's EffectiveState machine, projections, and envelopes. +package state diff --git a/internal/store/frontier.go b/internal/state/frontier.go similarity index 99% rename from internal/store/frontier.go rename to internal/state/frontier.go index e96f81b..2941ec7 100644 --- a/internal/store/frontier.go +++ b/internal/state/frontier.go @@ -1,4 +1,4 @@ -package store +package state import ( "sort" diff --git a/internal/store/integrity.go b/internal/state/integrity.go similarity index 99% rename from internal/store/integrity.go rename to internal/state/integrity.go index 5a6c3cb..a5959d3 100644 --- a/internal/store/integrity.go +++ b/internal/state/integrity.go @@ -1,4 +1,4 @@ -package store +package state import ( "fmt" diff --git a/internal/store/store.go b/internal/state/store.go similarity index 90% rename from internal/store/store.go rename to internal/state/store.go index d3c8ca4..07681a7 100644 --- a/internal/store/store.go +++ b/internal/state/store.go @@ -1,4 +1,4 @@ -package store +package state import ( "context" @@ -16,7 +16,7 @@ import ( "github.com/runmedev/owl/internal/resolver/builtin" ) -type Store struct { +type Machine struct { types registry.TypeProvider state model.EffectiveState operations []OperationRecord @@ -31,7 +31,7 @@ type RecordedOperation interface { Record() OperationRecord } -type StoreOption func(*config) error +type MachineOption func(*config) error type config struct { envs []sourceInput @@ -327,7 +327,7 @@ func withoutDiagnosticOwner(diagnostics []model.Diagnostic, owner model.Diagnost return filtered } -func NewStore(opts ...StoreOption) (*Store, error) { +func NewMachine(opts ...MachineOption) (*Machine, error) { cfg := config{} for _, opt := range opts { if err := opt(&cfg); err != nil { @@ -344,22 +344,22 @@ func NewStore(opts ...StoreOption) (*Store, error) { return nil, err } - store := &Store{types: cfg.types, state: model.NewEffectiveState()} - if _, err := store.Apply(context.Background(), LoadOperation{Input: load}); err != nil { + m := &Machine{types: cfg.types, state: model.NewEffectiveState()} + if _, err := m.Apply(context.Background(), LoadOperation{Input: load}); err != nil { return nil, err } - if _, err := store.Apply(context.Background(), NormalizeOperation{}); err != nil { + if _, err := m.Apply(context.Background(), NormalizeOperation{}); err != nil { return nil, err } - state, err := store.Apply(context.Background(), IntegrityOperation{Types: cfg.types}) + next, err := m.Apply(context.Background(), IntegrityOperation{Types: cfg.types}) if err != nil { return nil, err } - store.state = state - return store, nil + m.state = next + return m, nil } -func WithDotenv(source string, r io.Reader) StoreOption { +func WithDotenv(source string, r io.Reader) MachineOption { return func(cfg *config) error { input, err := readSource(source, r) if err != nil { @@ -370,7 +370,7 @@ func WithDotenv(source string, r io.Reader) StoreOption { } } -func WithEnvSpec(source string, r io.Reader) StoreOption { +func WithEnvSpec(source string, r io.Reader) MachineOption { return func(cfg *config) error { input, err := readSource(source, r) if err != nil { @@ -381,29 +381,29 @@ func WithEnvSpec(source string, r io.Reader) StoreOption { } } -func WithTypeProvider(types registry.TypeProvider) StoreOption { +func WithTypeProvider(types registry.TypeProvider) MachineOption { return func(cfg *config) error { cfg.types = types return nil } } -func (s *Store) Apply(ctx context.Context, op Operation) (model.EffectiveState, error) { +func (m *Machine) Apply(ctx context.Context, op Operation) (model.EffectiveState, error) { if recorded, ok := op.(RecordedOperation); ok { record, timestamped := timestampRecordedOperation(recorded.Record()) - s.operations = append(s.operations, record) + m.operations = append(m.operations, record) op = timestamped } - state, err := op.Apply(ctx, s.state) + state, err := op.Apply(ctx, m.state) if err != nil { return model.EffectiveState{}, err } - s.state = state + m.state = state return state, nil } -func (s *Store) ResolveSources(ctx context.Context, input ResolveSourcesInput) (resolver.RunResult, error) { - if _, err := s.Apply(ctx, IntegrityOperation{Types: s.types}); err != nil { +func (m *Machine) ResolveSources(ctx context.Context, input ResolveSourcesInput) (resolver.RunResult, error) { + if _, err := m.Apply(ctx, IntegrityOperation{Types: m.types}); err != nil { return resolver.RunResult{}, err } clock := input.Clock @@ -420,7 +420,7 @@ func (s *Store) ResolveSources(ctx context.Context, input ResolveSourcesInput) ( Clock: clock, } result, err := runner.Resolve(ctx, resolver.RunRequest{ - State: s.state, + State: m.state, Policy: input.Policy, Chain: input.Chain, }) @@ -428,28 +428,28 @@ func (s *Store) ResolveSources(ctx context.Context, input ResolveSourcesInput) ( return result, err } for _, attempt := range result.Attempts { - if _, err := s.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { + if _, err := m.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { return result, err } } for _, proposal := range result.Proposals { - if _, err := s.Apply(ctx, ApplyResolverProposalOperation{Proposal: proposal, Timestamp: input.Timestamp}); err != nil { + if _, err := m.Apply(ctx, ApplyResolverProposalOperation{Proposal: proposal, Timestamp: input.Timestamp}); err != nil { return result, err } } - if _, err := s.Apply(ctx, IntegrityOperation{Types: s.types}); err != nil { + if _, err := m.Apply(ctx, IntegrityOperation{Types: m.types}); err != nil { return result, err } return result, nil } -func (s *Store) ApplyPromptAnswers(ctx context.Context, input ApplyPromptAnswersInput) (resolver.RunResult, error) { +func (m *Machine) ApplyPromptAnswers(ctx context.Context, input ApplyPromptAnswersInput) (resolver.RunResult, error) { newAttemptID := input.NewAttemptID if newAttemptID == nil { - newAttemptID = promptAttemptIDGenerator(len(s.operations)) + newAttemptID = promptAttemptIDGenerator(len(m.operations)) } timestamp := operationTimestamp(input.Timestamp) - needs := needsByID(s.state.UnresolvedFrontier.Needs) + needs := needsByID(m.state.UnresolvedFrontier.Needs) var result resolver.RunResult for _, answer := range input.Answers { need, ok := needs[answer.NeedID] @@ -464,7 +464,7 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, input ApplyPromptAnswers if !ok { attempt.Outcome = model.ResolverAttemptInvalidResult attempt.Message = "interactive answer references an unknown unresolved need" - if _, err := s.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { + if _, err := m.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { return result, err } result.Attempts = append(result.Attempts, attempt) @@ -485,16 +485,16 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, input ApplyPromptAnswers Exposure: need.Exposure, }, } - if _, err := s.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { + if _, err := m.Apply(ctx, RecordResolverAttemptOperation{Attempt: attempt}); err != nil { return result, err } - if _, err := s.Apply(ctx, ApplyResolverProposalOperation{Proposal: proposal, Timestamp: timestamp}); err != nil { + if _, err := m.Apply(ctx, ApplyResolverProposalOperation{Proposal: proposal, Timestamp: timestamp}); err != nil { return result, err } result.Attempts = append(result.Attempts, attempt) result.Proposals = append(result.Proposals, proposal) } - if _, err := s.Apply(ctx, IntegrityOperation{Types: s.types}); err != nil { + if _, err := m.Apply(ctx, IntegrityOperation{Types: m.types}); err != nil { return result, err } return result, nil @@ -736,10 +736,10 @@ func (op IntegrityOperation) Apply(_ context.Context, state model.EffectiveState return state, nil } -func (s *Store) Snapshot(policy SnapshotPolicy) ([]SnapshotItem, error) { - items := make([]SnapshotItem, 0, len(s.state.Bindings)) - for _, binding := range s.state.Bindings { - value := s.state.Values[binding.FieldRef] +func (m *Machine) Snapshot(policy SnapshotPolicy) ([]SnapshotItem, error) { + items := make([]SnapshotItem, 0, len(m.state.Bindings)) + for _, binding := range m.state.Bindings { + value := m.state.Values[binding.FieldRef] rendered := renderSnapshotValue(value, policy) original := value.Original if rendered.visibility != model.VisibilityLiteral { @@ -760,7 +760,7 @@ func (s *Store) Snapshot(policy SnapshotPolicy) ([]SnapshotItem, error) { Exposure: value.Exposure, Description: binding.Description, UpdatedAt: value.UpdatedAt, - Diagnostics: diagnosticsFor(s.state.Diagnostics, binding), + Diagnostics: diagnosticsFor(m.state.Diagnostics, binding), }) } sort.SliceStable(items, func(i, j int) bool { @@ -780,12 +780,12 @@ func (s *Store) Snapshot(policy SnapshotPolicy) ([]SnapshotItem, error) { return items, nil } -func (s *Store) Source(policy SourcePolicy) ([]string, error) { - return s.Dotenv(DotenvPolicy(policy)) +func (m *Machine) Source(policy SourcePolicy) ([]string, error) { + return m.Dotenv(DotenvPolicy(policy)) } -func (s *Store) Dotenv(policy DotenvPolicy) ([]string, error) { - rendered := dotenv.RenderDotenvProjection(s.state, model.RenderPolicy{Insecure: policy.Insecure}) +func (m *Machine) Dotenv(policy DotenvPolicy) ([]string, error) { + rendered := dotenv.RenderDotenvProjection(m.state, model.RenderPolicy{Insecure: policy.Insecure}) envs := make([]string, 0, len(rendered.Variables)) for _, variable := range rendered.Variables { envs = append(envs, variable.Key+"="+variable.Value) @@ -794,13 +794,13 @@ func (s *Store) Dotenv(policy DotenvPolicy) ([]string, error) { return envs, nil } -func (s *Store) Type(policy TypePolicy) (TypeResult, error) { - proposals := make([]TypeProposal, 0, len(s.state.Bindings)) - for _, binding := range s.state.Bindings { +func (m *Machine) Type(policy TypePolicy) (TypeResult, error) { + proposals := make([]TypeProposal, 0, len(m.state.Bindings)) + for _, binding := range m.state.Bindings { if binding.Explicit { continue } - value := s.state.Values[binding.FieldRef] + value := m.state.Values[binding.FieldRef] suggested, reason, ok := suggestPrimitiveType(string(binding.Key), value) if !policy.All && !ok { continue @@ -824,12 +824,12 @@ func (s *Store) Type(policy TypePolicy) (TypeResult, error) { return TypeResult{Proposals: proposals}, nil } -func (s *Store) Get(key string, policy GetPolicy) (GetResult, bool, error) { - ref, binding, found := findBinding(s.state.Bindings, key) +func (m *Machine) Get(key string, policy GetPolicy) (GetResult, bool, error) { + ref, binding, found := findBinding(m.state.Bindings, key) if !found { return GetResult{}, false, nil } - value := s.state.Values[ref] + value := m.state.Values[ref] rendered := renderSnapshotValue(value, SnapshotPolicy(policy)) return GetResult{ Key: key, @@ -838,14 +838,14 @@ func (s *Store) Get(key string, policy GetPolicy) (GetResult, bool, error) { Visibility: rendered.visibility, Exposure: value.Exposure, Source: value.Source, - Diagnostics: diagnosticsFor(s.state.Diagnostics, binding), + Diagnostics: diagnosticsFor(m.state.Diagnostics, binding), }, true, nil } -func (s *Store) SensitiveKeys() ([]string, error) { +func (m *Machine) SensitiveKeys() ([]string, error) { var keys []string - for _, binding := range s.state.Bindings { - value := s.state.Values[binding.FieldRef] + for _, binding := range m.state.Bindings { + value := m.state.Values[binding.FieldRef] if value.Sensitivity == model.SensitivitySensitive { keys = append(keys, string(binding.Key)) } @@ -854,11 +854,11 @@ func (s *Store) SensitiveKeys() ([]string, error) { return keys, nil } -func (s *Store) Check() CheckResult { +func (m *Machine) Check() CheckResult { result := CheckResult{ OK: true, - Diagnostics: append([]model.Diagnostic{}, s.state.Diagnostics...), - Checked: len(s.state.Bindings), + Diagnostics: append([]model.Diagnostic{}, m.state.Diagnostics...), + Checked: len(m.state.Bindings), } for _, diagnostic := range result.Diagnostics { if diagnostic.Severity == model.DiagnosticError { @@ -869,30 +869,30 @@ func (s *Store) Check() CheckResult { return result } -func (s *Store) State() model.EffectiveState { - return s.state +func (m *Machine) State() model.EffectiveState { + return m.state } -func (s *Store) OperationRecords() []OperationRecord { - return append([]OperationRecord{}, s.operations...) +func (m *Machine) OperationRecords() []OperationRecord { + return append([]OperationRecord{}, m.operations...) } -func (s *Store) StateEnvelope() StateEnvelope { +func (m *Machine) StateEnvelope() StateEnvelope { return StateEnvelope{ ModelVersion: "owl.store.v2", - State: s.state, + State: m.state, Provenance: StateProvenance{ - Sources: sourcesFromState(s.state), - Operations: append([]model.OperationMetadata{}, s.state.Operations...), + Sources: sourcesFromState(m.state), + Operations: append([]model.OperationMetadata{}, m.state.Operations...), }, } } -func NewState(state model.EffectiveState, types registry.TypeProvider) *Store { +func MachineFromState(state model.EffectiveState, types registry.TypeProvider) *Machine { if types == nil { types = registry.NewBuiltInRegistry() } - return &Store{types: types, state: state} + return &Machine{types: types, state: state} } func readSource(name string, r io.Reader) (sourceInput, error) { diff --git a/internal/store/store_test.go b/internal/state/store_test.go similarity index 96% rename from internal/store/store_test.go rename to internal/state/store_test.go index 8ccf0c9..38c2da4 100644 --- a/internal/store/store_test.go +++ b/internal/state/store_test.go @@ -1,4 +1,4 @@ -package store +package state import ( "context" @@ -18,7 +18,7 @@ import ( func TestStoreSnapshotSourceAndCheck(t *testing.T) { t.Parallel() - s, err := NewStore( + s, err := NewMachine( WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\nAPI_KEY=secret\nDATABASE_URL=postgres://example\n")), WithEnvSpec(".env.example", strings.NewReader("API_URL=\"API URL\" # Plain\nAPI_KEY=\"API key\" # Secret!\nDATABASE_URL=\"Database URL\" # Opaque\nMISSING_TOKEN=\"Missing token\" # Secret!\n")), ) @@ -55,7 +55,7 @@ func TestStoreSnapshotSourceAndCheck(t *testing.T) { func TestStoreSnapshotOrdersExplicitBindingsBeforeInferredBindings(t *testing.T) { t.Parallel() - s, err := NewStore( + s, err := NewMachine( WithDotenv(".env", strings.NewReader("OMEGA=value\nAPPLE=value\nZETA=value\nBETA=value\n")), WithEnvSpec(".env.example", strings.NewReader("ZETA=\"Zeta\" # Plain\nBETA=\"Beta\" # Plain\n")), ) @@ -74,7 +74,7 @@ func TestStoreSnapshotOrdersExplicitBindingsBeforeInferredBindings(t *testing.T) func TestStoreTypeProposesMissingPrimitiveTypes(t *testing.T) { t.Parallel() - s, err := NewStore( + s, err := NewMachine( WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\nAPI_KEY=secret\nSERVICE_HOST=localhost\nSERVICE_PORT=8080\nTARGET_PLATFORM=darwin/arm64\n")), WithEnvSpec(".env.spec", strings.NewReader("API_URL=\"API URL\" # Plain\n")), ) @@ -100,7 +100,7 @@ func TestStoreTypeProposesMissingPrimitiveTypes(t *testing.T) { func TestStoreTypeSkipsDefaultPlainProposalsByDefault(t *testing.T) { t.Parallel() - s, err := NewStore( + s, err := NewMachine( WithDotenv(".env", strings.NewReader("API_KEY=secret\nTARGET_PLATFORM=darwin/arm64\n")), ) require.NoError(t, err) @@ -206,7 +206,7 @@ func TestPrimitiveValueDiagnostics(t *testing.T) { func TestStoreWithDotenv(t *testing.T) { t.Parallel() - s, err := NewStore(WithDotenv("[process]", strings.NewReader("REDIS_HOST=localhost\nREDIS_PORT=6379\n"))) + s, err := NewMachine(WithDotenv("[process]", strings.NewReader("REDIS_HOST=localhost\nREDIS_PORT=6379\n"))) require.NoError(t, err) snapshot, err := s.Snapshot(SnapshotPolicy{Reveal: true}) @@ -221,7 +221,7 @@ func TestStoreWithDotenv(t *testing.T) { func TestStorePreservesDotenvValueSource(t *testing.T) { t.Parallel() - s, err := NewStore( + s, err := NewMachine( WithDotenv("[process]", strings.NewReader("PROCESS_ONLY=from-process\nDUPLICATE_KEY=from-process\n")), WithDotenv(".env", strings.NewReader("FILE_ONLY=from-file\nDUPLICATE_KEY=from-file\n")), ) @@ -330,7 +330,7 @@ func TestWithoutDiagnosticOwnerDoesNotReuseInputSlice(t *testing.T) { func TestStoreRecordsFactOperationsOnly(t *testing.T) { t.Parallel() - s, err := NewStore(WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\n"))) + s, err := NewMachine(WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\n"))) require.NoError(t, err) records := s.OperationRecords() @@ -357,7 +357,7 @@ func TestStoreRecordsFactOperationsOnly(t *testing.T) { func TestStoreRecordsResolverAttemptWithoutMutatingValues(t *testing.T) { t.Parallel() - s, err := NewStore(WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\n"))) + s, err := NewMachine(WithDotenv(".env", strings.NewReader("API_URL=https://api.example.com\n"))) require.NoError(t, err) before := s.State() startedAt := time.Date(2026, 8, 3, 16, 0, 0, 0, time.UTC) @@ -392,7 +392,7 @@ func TestStoreRecordsResolverAttemptWithoutMutatingValues(t *testing.T) { func TestStoreAppliesResolverProposalThroughStateOperation(t *testing.T) { t.Parallel() - s, err := NewStore(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) + s, err := NewMachine(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) require.NoError(t, err) before := s.State() require.Len(t, before.UnresolvedFrontier.Needs, 1) @@ -437,7 +437,7 @@ func TestStoreAppliesResolverProposalThroughStateOperation(t *testing.T) { assert.Equal(t, model.ResolverAttemptID("attempt-000001"), records[1].ResolverProposal.AttemptID) } -func TestStoreProposalApplicationLeavesInvalidValuesForIntegrity(t *testing.T) { +func TestMachineProposalApplicationLeavesInvalidValuesForIntegrity(t *testing.T) { t.Parallel() ref := model.FieldRef{TypeID: model.TypeUniverseRedis, Instance: "queues", Field: "port"} @@ -456,7 +456,7 @@ func TestStoreProposalApplicationLeavesInvalidValuesForIntegrity(t *testing.T) { Exposure: model.ExposureClear, } state.UnresolvedFrontier = BuildUnresolvedFrontier(state) - s := NewState(state, registry.NewBuiltInRegistry()) + s := MachineFromState(state, registry.NewBuiltInRegistry()) after, err := s.Apply(context.Background(), ApplyResolverProposalOperation{ Proposal: resolver.Proposal{ @@ -488,7 +488,7 @@ func TestStoreResolveSourcesAppliesDotenvResolverProposals(t *testing.T) { t.Parallel() timestamp := time.Date(2026, 8, 3, 23, 45, 0, 0, time.UTC) - s, err := NewStore(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) + s, err := NewMachine(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) require.NoError(t, err) require.Len(t, s.State().UnresolvedFrontier.Needs, 1) @@ -524,7 +524,7 @@ func TestStoreResolveSourcesAppliesDotenvResolverProposals(t *testing.T) { func TestStoreResolveSourcesUsesDotenvBeforeProcess(t *testing.T) { t.Parallel() - s, err := NewStore(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) + s, err := NewMachine(WithEnvSpec(".env.example", strings.NewReader("API_KEY=\"API key\" # Secret!\n"))) require.NoError(t, err) result, err := s.ResolveSources(context.Background(), ResolveSourcesInput{ @@ -554,7 +554,7 @@ func TestStoreResolveSourcesUsesDotenvBeforeProcess(t *testing.T) { assert.Equal(t, model.Source{Name: ".env", Kind: "dotenv"}, value.Source) } -func TestStoreResolveSourcesKeepsInvalidResolvedValueProvenance(t *testing.T) { +func TestMachineResolveSourcesKeepsInvalidResolvedValueProvenance(t *testing.T) { t.Parallel() ref := model.FieldRef{TypeID: model.TypeUniverseRedis, Instance: "queues", Field: "port"} @@ -566,7 +566,7 @@ func TestStoreResolveSourcesKeepsInvalidResolvedValueProvenance(t *testing.T) { Explicit: true, Required: true, }} - s := NewState(state, registry.NewBuiltInRegistry()) + s := MachineFromState(state, registry.NewBuiltInRegistry()) result, err := s.ResolveSources(context.Background(), ResolveSourcesInput{ Dotenv: []DotenvVariable{{ diff --git a/internal/store/doc.go b/internal/store/doc.go deleted file mode 100644 index 5d17fab..0000000 --- a/internal/store/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package store owns Owl's v2 store lifecycle. -package store diff --git a/pkg/owl/api.go b/pkg/owl/api.go index 8513ca5..bedb171 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -14,20 +14,20 @@ import ( "github.com/runmedev/owl/internal/requirements" "github.com/runmedev/owl/internal/resolver" "github.com/runmedev/owl/internal/resolver/builtin" - "github.com/runmedev/owl/internal/store" + "github.com/runmedev/owl/internal/state" ) type ( - SnapshotPolicy = store.SnapshotPolicy - DotenvPolicy = store.DotenvPolicy - TypePolicy = store.TypePolicy - GetPolicy = store.GetPolicy - SnapshotItem = store.SnapshotItem - SnapshotEnv = store.SnapshotItem - TypeResult = store.TypeResult - TypeProposal = store.TypeProposal - GetResult = store.GetResult - CheckResult = store.CheckResult + SnapshotPolicy = state.SnapshotPolicy + DotenvPolicy = state.DotenvPolicy + TypePolicy = state.TypePolicy + GetPolicy = state.GetPolicy + SnapshotItem = state.SnapshotItem + SnapshotEnv = state.SnapshotItem + TypeResult = state.TypeResult + TypeProposal = state.TypeProposal + GetResult = state.GetResult + CheckResult = state.CheckResult ResolvePolicy = resolver.Policy ChainConfig = resolver.ChainConfig ResolverConfig = resolver.ResolverConfig @@ -43,12 +43,12 @@ type ( DotenvProjection = model.DotenvProjectionInput DotenvFieldBinding = model.DotenvFieldBindingInput Source = model.Source - DotenvVariable = store.DotenvVariable - EnvContract = store.EnvContract - EnvBinding = store.EnvBinding - LoadInput = store.LoadInput - StateEnvelope = store.StateEnvelope - StateProvenance = store.StateProvenance + DotenvVariable = state.DotenvVariable + EnvContract = state.EnvContract + EnvBinding = state.EnvBinding + LoadInput = state.LoadInput + StateEnvelope = state.StateEnvelope + StateProvenance = state.StateProvenance Visibility = model.Visibility Exposure = model.Exposure Diagnostic = model.Diagnostic @@ -105,18 +105,18 @@ type Store struct { runtime *graph.Runtime types registry.TypeProvider state model.EffectiveState - operations []store.OperationRecord + operations []state.OperationRecord clock model.Clock } type StoreOption func(*config) error type config struct { - envs []store.SourceBytes - specs []store.SourceBytes + envs []state.SourceBytes + specs []state.SourceBytes configs []configInputSource - contracts []store.EnvContract - envelope *store.StateEnvelope + contracts []state.EnvContract + envelope *state.StateEnvelope types registry.TypeProvider clock model.Clock } @@ -259,7 +259,7 @@ func NewStore(opts ...StoreOption) (*Store, error) { return nil, err } } - load, err := store.LoadInputFromSourceBytes(cfg.envs, cfg.specs) + load, err := state.LoadInputFromSourceBytes(cfg.envs, cfg.specs) if err != nil { return nil, err } @@ -285,8 +285,8 @@ func NewStore(opts ...StoreOption) (*Store, error) { runtime: runtime, types: cfg.types, clock: clock, - operations: []store.OperationRecord{ - {Kind: store.OperationRecordLoad, Timestamp: loadTimestamp, Load: load}, + operations: []state.OperationRecord{ + {Kind: state.OperationRecordLoad, Timestamp: loadTimestamp, Load: load}, }, } if err := s.materialize(context.Background()); err != nil { @@ -301,7 +301,7 @@ func WithDotenv(source string, r io.Reader) StoreOption { if err != nil { return err } - cfg.envs = append(cfg.envs, store.SourceBytes{Name: source, Raw: raw}) + cfg.envs = append(cfg.envs, state.SourceBytes{Name: source, Raw: raw}) return nil } } @@ -312,7 +312,7 @@ func WithEnvSpec(source string, r io.Reader) StoreOption { if err != nil { return err } - cfg.specs = append(cfg.specs, store.SourceBytes{Name: source, Raw: raw}) + cfg.specs = append(cfg.specs, state.SourceBytes{Name: source, Raw: raw}) return nil } } @@ -605,14 +605,14 @@ func (s *Store) BuildCheckOperation(ctx context.Context, input CheckInput) (Grap return graphOperation(graph.CheckOperation(load)), nil } -func (s *Store) CheckState() store.CheckResult { +func (s *Store) CheckState() state.CheckResult { envelope, err := s.StateEnvelope(context.Background()) if err != nil { - return store.CheckResult{} + return state.CheckResult{} } check, err := s.runtime.Check(context.Background(), LoadInput{Envelope: &envelope}) if err != nil { - return store.CheckResult{} + return state.CheckResult{} } return check } @@ -679,12 +679,12 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, answers []PromptAnswer) if ctx == nil { ctx = context.Background() } - state, err := s.resolverState(ctx) + effective, err := s.resolverState(ctx) if err != nil { return ResolveResult{}, err } timestamp := s.clock() - needs := needsByID(state.UnresolvedFrontier.Needs) + needs := needsByID(effective.UnresolvedFrontier.Needs) newAttemptID := publicAttemptIDGenerator(len(s.operations)) var result ResolveResult for _, answer := range answers { @@ -701,8 +701,8 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, answers []PromptAnswer) attempt.Outcome = ResolverInvalidResult attempt.Message = "interactive answer references an unknown unresolved need" result.Attempts = append(result.Attempts, attempt) - s.operations = append(s.operations, store.OperationRecord{ - Kind: store.OperationRecordResolverAttempt, + s.operations = append(s.operations, state.OperationRecord{ + Kind: state.OperationRecordResolverAttempt, Timestamp: timestamp, ResolverAttempt: attempt, }) @@ -726,13 +726,13 @@ func (s *Store) ApplyPromptAnswers(ctx context.Context, answers []PromptAnswer) result.Attempts = append(result.Attempts, attempt) result.Proposals = append(result.Proposals, proposal) s.operations = append(s.operations, - store.OperationRecord{ - Kind: store.OperationRecordResolverAttempt, + state.OperationRecord{ + Kind: state.OperationRecordResolverAttempt, Timestamp: timestamp, ResolverAttempt: attempt, }, - store.OperationRecord{ - Kind: store.OperationRecordApplyResolverProposal, + state.OperationRecord{ + Kind: state.OperationRecordApplyResolverProposal, Timestamp: timestamp, ResolverProposal: proposal, }, @@ -761,7 +761,7 @@ func (s *Store) LoadDotenvLines(source string, envs ...string) error { if raw != "" { raw += "\n" } - input, err := store.LoadInputFromSourceBytes([]store.SourceBytes{{Name: source, Raw: []byte(raw)}}, nil) + input, err := state.LoadInputFromSourceBytes([]state.SourceBytes{{Name: source, Raw: []byte(raw)}}, nil) if err != nil { return err } @@ -776,7 +776,7 @@ func (s *Store) Update(ctx context.Context, newOrUpdated, deleted []string) erro if raw != "" { raw += "\n" } - input, err := store.LoadInputFromSourceBytes([]store.SourceBytes{{Name: "[update]", Raw: []byte(raw)}}, nil) + input, err := state.LoadInputFromSourceBytes([]state.SourceBytes{{Name: "[update]", Raw: []byte(raw)}}, nil) if err != nil { return err } @@ -794,17 +794,17 @@ func (s *Store) BuildUpdateOperation(ctx context.Context, input UpdateInput) (Gr if ctx == nil { ctx = context.Background() } - records := append([]store.OperationRecord{}, s.operations...) + records := append([]state.OperationRecord{}, s.operations...) timestamp := s.clock() source := input.Source if source == (Source{}) { source = sourceFromContext(ctx, Source{Name: "[update]", Kind: "dotenv"}) } if len(input.Dotenv) > 0 { - records = append(records, store.OperationRecord{ - Kind: store.OperationRecordUpdate, + records = append(records, state.OperationRecord{ + Kind: state.OperationRecordUpdate, Timestamp: timestamp, - Update: store.UpdateOperation{ + Update: state.UpdateOperation{ Source: source, Dotenv: append([]DotenvVariable{}, input.Dotenv...), Timestamp: timestamp, @@ -812,10 +812,10 @@ func (s *Store) BuildUpdateOperation(ctx context.Context, input UpdateInput) (Gr }) } if len(input.Delete) > 0 { - records = append(records, store.OperationRecord{ - Kind: store.OperationRecordDelete, + records = append(records, state.OperationRecord{ + Kind: state.OperationRecordDelete, Timestamp: timestamp, - Delete: store.DeleteOperation{ + Delete: state.DeleteOperation{ Keys: append([]string{}, input.Delete...), Source: source, Timestamp: timestamp, @@ -859,7 +859,7 @@ func graphOperation(op graph.Operation) GraphOperation { } } -func stateEnvelopeOperation(records []store.OperationRecord) (GraphOperation, error) { +func stateEnvelopeOperation(records []state.OperationRecord) (GraphOperation, error) { op, err := graph.StateEnvelopeOperation(records) if err != nil { return GraphOperation{}, err @@ -880,7 +880,7 @@ func GraphQLSchema() (string, error) { } func Diagnostics(err error) []Diagnostic { - return store.Diagnostics(err) + return state.Diagnostics(err) } func (s *Store) applyDotenv(source Source, vars []DotenvVariable, deleted []string) error { @@ -896,10 +896,10 @@ func (s *Store) applyDotenvWithContext(ctx context.Context, source Source, vars } if len(vars) > 0 { timestamp := s.clock() - s.operations = append(s.operations, store.OperationRecord{ - Kind: store.OperationRecordUpdate, + s.operations = append(s.operations, state.OperationRecord{ + Kind: state.OperationRecordUpdate, Timestamp: timestamp, - Update: store.UpdateOperation{ + Update: state.UpdateOperation{ Source: source, Dotenv: vars, Timestamp: timestamp, @@ -908,10 +908,10 @@ func (s *Store) applyDotenvWithContext(ctx context.Context, source Source, vars } if len(deleted) > 0 { timestamp := s.clock() - s.operations = append(s.operations, store.OperationRecord{ - Kind: store.OperationRecordDelete, + s.operations = append(s.operations, state.OperationRecord{ + Kind: state.OperationRecordDelete, Timestamp: timestamp, - Delete: store.DeleteOperation{ + Delete: state.DeleteOperation{ Keys: append([]string{}, deleted...), Source: source, Timestamp: timestamp, @@ -955,15 +955,15 @@ func sourceFromContext(ctx context.Context, fallback Source) Source { func (s *Store) recordResolverResult(ctx context.Context, result ResolveResult) error { timestamp := s.clock() for _, attempt := range result.Attempts { - s.operations = append(s.operations, store.OperationRecord{ - Kind: store.OperationRecordResolverAttempt, + s.operations = append(s.operations, state.OperationRecord{ + Kind: state.OperationRecordResolverAttempt, Timestamp: firstTime(attempt.FinishedAt, timestamp), ResolverAttempt: attempt, }) } for _, proposal := range result.Proposals { - s.operations = append(s.operations, store.OperationRecord{ - Kind: store.OperationRecordApplyResolverProposal, + s.operations = append(s.operations, state.OperationRecord{ + Kind: state.OperationRecordApplyResolverProposal, Timestamp: timestamp, ResolverProposal: proposal, }) From f16e20ff48e9baf8cf7cf163af949164485d399b Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Tue, 11 Aug 2026 09:22:58 -0700 Subject: [PATCH 11/13] refactor: remove legacy store facade helpers Signed-off-by: Sebastian (Tiedtke) Huckleberry --- AGENTS.md | 11 ++- Makefile | 6 +- cmd/cue_catalog.go | 8 +- cmd/cue_catalog_test.go | 13 ++- cmd/layering_test.go | 66 +++++++++++++ cmd/local.go | 25 ++--- cmd/project_test.go | 6 +- cmd/root.go | 4 +- internal/seed/seed_test.go | 36 ++++--- internal/seed/store.go | 22 +++-- pkg/owl/api.go | 119 +++++++++------------- pkg/owl/api_internal_test.go | 53 ++++++---- pkg/owl/api_test.go | 121 +++++++++++++---------- pkg/owl/seed/seed.go | 14 +++ pkg/owl/seed/seed_test.go | 19 ++-- {internal/version => pkg/owl}/version.go | 2 +- 16 files changed, 322 insertions(+), 203 deletions(-) create mode 100644 cmd/layering_test.go rename {internal/version => pkg/owl}/version.go (92%) diff --git a/AGENTS.md b/AGENTS.md index 1fc4a5e..ff3c32b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ The core ownership boundaries are: as the generic known non-sensitive env string type in the store cutover. - `internal/projection/dotenv`: dotenv projection, legacy `.env.example` comment parsing, materialization, and dotenv rendering. -- `internal/store`: target home for the v2 store lifecycle. +- `internal/state`: EffectiveState machine, projections, and envelopes. - `pkg/owl`: small public API. Export what callers need; do not re-export every internal model detail by default. - `cmd`: command wiring and rendering only. @@ -113,6 +113,15 @@ calling Owl. Graph execution receives already-materialized bytes/strings and typed variables; it should not open project files, read process env, prompt users, call secret managers, or speak gRPC directly. +`cmd/` is a public consumer. CLI code may use `pkg/...` APIs and command-local +wiring only; it must not import `github.com/runmedev/owl/internal/...` +packages. Move needed behavior behind `pkg/owl` or `pkg/owl/seed` instead. + +Normal command/public paths must not call legacy helper shapes such as +`SnapshotItems`, `Dotenv(policy)`, `CheckState`, `LoadDotenv`, +`LoadDotenvLines`, or legacy `Update`/`Delete` helpers. Use typed graph-backed +facades such as `Snapshot`, `Source`, `Check`, and `ApplyUpdate`. + ## Store Cutover Decisions - Implement the cutover as one cohesive PR with focused commits, not separate diff --git a/Makefile b/Makefile index c50464b..2021f73 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ GIT_SHA := $(shell git rev-parse HEAD) DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") VERSION := $(shell git describe --tags --match 'v[0-9]*' --always --dirty) LDFLAGS := -s -w \ - -X 'github.com/runmedev/owl/internal/version.BuildDate=$(DATE)' \ - -X 'github.com/runmedev/owl/internal/version.BuildVersion=$(subst v,,$(VERSION))' \ - -X 'github.com/runmedev/owl/internal/version.Commit=$(GIT_SHA)' + -X 'github.com/runmedev/owl/pkg/owl.BuildDate=$(DATE)' \ + -X 'github.com/runmedev/owl/pkg/owl.BuildVersion=$(subst v,,$(VERSION))' \ + -X 'github.com/runmedev/owl/pkg/owl.Commit=$(GIT_SHA)' .PHONY: build build: BUILD_OUTPUT ?= owl diff --git a/cmd/cue_catalog.go b/cmd/cue_catalog.go index 71b4760..bf3b39a 100644 --- a/cmd/cue_catalog.go +++ b/cmd/cue_catalog.go @@ -4,22 +4,22 @@ import ( "fmt" "os" - "github.com/runmedev/owl/internal/registry" + "github.com/runmedev/owl/pkg/owl" ) const cueRootEnv = "OWL_CUE_ROOT" var lookupEnv = os.LookupEnv -func commandTypeProvider() (registry.TypeProvider, error) { +func commandTypeProvider() (owl.TypeProvider, error) { root, configured := lookupEnv(cueRootEnv) if !configured { - return registry.NewBuiltInRegistry(), nil + return owl.NewBuiltInTypeProvider(), nil } if root == "" { return nil, fmt.Errorf("%s is set but empty", cueRootEnv) } - types, err := registry.NewBuiltInRegistryFromDirectory(root) + types, err := owl.NewTypeProviderFromDirectory(root) if err != nil { return nil, fmt.Errorf("load CUE catalog from %s=%q: %w", cueRootEnv, root, err) } diff --git a/cmd/cue_catalog_test.go b/cmd/cue_catalog_test.go index 7691360..88ce42b 100644 --- a/cmd/cue_catalog_test.go +++ b/cmd/cue_catalog_test.go @@ -11,9 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/runmedev/owl/internal/model" - "github.com/runmedev/owl/internal/registry" - "github.com/runmedev/owl/internal/seed" + "github.com/runmedev/owl/pkg/owl" + "github.com/runmedev/owl/pkg/owl/seed" ) func TestCommandCUECatalogPrecedence(t *testing.T) { @@ -91,7 +90,7 @@ func TestCUERootControlVariableIsNotObserved(t *testing.T) { func TestProjectSpecReceivesSelectedTypeProvider(t *testing.T) { t.Parallel() - provider := &trackingTypeProvider{BuiltInRegistry: registry.NewBuiltInRegistry()} + provider := &trackingTypeProvider{TypeProvider: owl.NewBuiltInTypeProvider()} client := NewLocalStoreClient(LocalStoreOptions{TypeProvider: provider}) result, err := client.ProjectSpec(context.Background(), ProjectSpecRequest{ ConfigPath: filepath.Join(commandRepoRoot(t), "examples/redis/owl.toml"), @@ -102,13 +101,13 @@ func TestProjectSpecReceivesSelectedTypeProvider(t *testing.T) { } type trackingTypeProvider struct { - registry.BuiltInRegistry + owl.TypeProvider resolveTypeRefs int } -func (p *trackingTypeProvider) ResolveTypeRef(ref string) (model.TypeDef, bool, error) { +func (p *trackingTypeProvider) ResolveTypeRef(ref string) (owl.TypeDef, bool, error) { p.resolveTypeRefs++ - return p.BuiltInRegistry.ResolveTypeRef(ref) + return p.TypeProvider.ResolveTypeRef(ref) } func withLookupEnv(t *testing.T, fn func(string) (string, bool)) { diff --git a/cmd/layering_test.go b/cmd/layering_test.go new file mode 100644 index 0000000..972eeda --- /dev/null +++ b/cmd/layering_test.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCommandLayeringUsesPublicPackages(t *testing.T) { + t.Parallel() + + internalImport := `"github.com/runmedev/owl/` + `internal/` + for _, file := range goFiles(t, ".") { + raw, err := os.ReadFile(file) + require.NoError(t, err) + require.NotContains(t, string(raw), internalImport, file) + } +} + +func TestCommandAndSeedAvoidLegacyStoreHelpers(t *testing.T) { + t.Parallel() + + forbidden := []string{ + ".SnapshotItems(", + ".CheckState(", + ".LoadDotenv(", + ".LoadDotenvLines(", + "store.Update(", + "store.Delete(", + "roundTripped.Update(", + "roundTripped.Delete(", + } + for _, root := range []string{".", "../internal/seed"} { + for _, file := range goFiles(t, root) { + if strings.HasSuffix(file, "_test.go") { + continue + } + raw, err := os.ReadFile(file) + require.NoError(t, err) + for _, pattern := range forbidden { + require.NotContains(t, string(raw), pattern, file) + } + } + } +} + +func goFiles(t *testing.T, root string) []string { + t.Helper() + var files []string + require.NoError(t, filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if strings.HasSuffix(path, ".go") { + files = append(files, path) + } + return nil + })) + return files +} diff --git a/cmd/local.go b/cmd/local.go index 8c53bc8..ccfed4f 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -8,11 +8,8 @@ import ( "github.com/spf13/cobra" - "github.com/runmedev/owl/internal/model" - "github.com/runmedev/owl/internal/registry" - "github.com/runmedev/owl/internal/requirements" - "github.com/runmedev/owl/internal/seed" "github.com/runmedev/owl/pkg/owl" + "github.com/runmedev/owl/pkg/owl/seed" ) type LocalStoreOptions struct { @@ -23,7 +20,7 @@ type LocalStoreOptions struct { Direnv seed.DirenvPolicy DirenvDir string DirenvRunner seed.DirenvExportRunner - TypeProvider registry.TypeProvider + TypeProvider owl.TypeProvider } type LocalStoreClient struct { @@ -272,25 +269,17 @@ func (c *LocalStoreClient) ProjectSpec(ctx context.Context, req ProjectSpecReque if err != nil { return nil, err } - input, err := requirements.ReadConfigFile(configPath) - if err != nil { - return nil, err - } types := c.options.TypeProvider if types == nil { - types = registry.NewBuiltInRegistry() - } - contracts, err := requirements.ContractsFromConfig(input, model.Source{Name: configPath, Kind: "owl-config"}, types) - if err != nil { - return nil, err + types = owl.NewBuiltInTypeProvider() } - storeOptions := []owl.StoreOption{owl.WithTypeProvider(types)} + storeOptions := []owl.StoreOption{owl.WithTypeProvider(types), owl.WithConfigFile(configPath)} store, err := owl.NewStore(storeOptions...) if err != nil { return nil, err } rendered, err := store.ProjectSpec(ctx, owl.ProjectSpecInput{ - Load: owl.LoadInput{Contracts: contracts}, + Load: owl.LoadInput{}, }) if err != nil { return nil, err @@ -348,7 +337,7 @@ func writeGeneratedDotenvSpec(path string, rendered string) error { } func isGeneratedDotenvSpec(raw []byte) bool { - return strings.HasPrefix(string(raw), requirements.GeneratedDotenvSpecHeaderPrefix) + return strings.HasPrefix(string(raw), owl.GeneratedDotenvSpecHeaderPrefix) } func renderDotenvSpecTypeProposals(proposals []owl.TypeProposal) string { @@ -541,7 +530,7 @@ func resolveResultFromOwl(result owl.ResolveResult) *ResolveResult { ProjectionKey: string(action.Prompt.ProjectionKey), Label: action.Prompt.Label, Description: action.Prompt.Description, - Sensitive: action.Prompt.Sensitivity == model.SensitivitySensitive, + Sensitive: action.Prompt.Sensitivity == owl.SensitivitySensitive, Required: action.Prompt.Required, AllowEmpty: action.Prompt.AllowEmpty, } diff --git a/cmd/project_test.go b/cmd/project_test.go index 5eb491b..235f720 100644 --- a/cmd/project_test.go +++ b/cmd/project_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/runmedev/owl/internal/requirements" + "github.com/runmedev/owl/pkg/owl" ) func TestProjectSpecRendersGeneratedSpec(t *testing.T) { @@ -146,9 +146,9 @@ needs: instance: default `), 0o600)) - jsonConfig, err := requirements.ReadConfigFile(jsonPath) + jsonConfig, err := owl.ReadConfigFile(jsonPath) require.NoError(t, err) - yamlConfig, err := requirements.ReadConfigFile(yamlPath) + yamlConfig, err := owl.ReadConfigFile(yamlPath) require.NoError(t, err) require.Len(t, jsonConfig.Needs, 1) diff --git a/cmd/root.go b/cmd/root.go index 9e72fc9..2faab65 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" - "github.com/runmedev/owl/internal/version" + "github.com/runmedev/owl/pkg/owl" ) var errSilentExit = stderrors.New("silent exit") @@ -18,7 +18,7 @@ func NewRootCommand() *cobra.Command { cmd := cobra.Command{ Use: "owl", Short: "Typed environment variable store", - Version: version.BaseVersionInfo(), + Version: owl.BaseVersionInfo(), SilenceErrors: true, SilenceUsage: true, } diff --git a/internal/seed/seed_test.go b/internal/seed/seed_test.go index 70ff66d..9f030c8 100644 --- a/internal/seed/seed_test.go +++ b/internal/seed/seed_test.go @@ -16,6 +16,23 @@ import ( "github.com/runmedev/owl/pkg/owl" ) +func snapshotItems(t *testing.T, store *owl.Store, policy owl.SnapshotPolicy) []owl.SnapshotItem { + t.Helper() + output, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Policy: policy, + Filter: owl.SnapshotFilter{All: true}, + }) + require.NoError(t, err) + return output.Envs +} + +func checkStore(t *testing.T, store *owl.Store) owl.CheckOutput { + t.Helper() + output, err := store.Check(context.Background(), owl.CheckInput{}) + require.NoError(t, err) + return output +} + func TestNewStoreSeedsObservedSourceWithCallerProvenance(t *testing.T) { t.Parallel() @@ -40,8 +57,7 @@ func TestNewStoreSeedsObservedSourceWithCallerProvenance(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{}) env := snapshotItemByName(items)["KERNEL_ONLY"] assert.Equal(t, "[hidden]", env.Value) assert.Equal(t, owl.Source{Name: "[kernel]", Kind: "runme-kernel"}, env.Source) @@ -78,8 +94,7 @@ func TestNewStoreAttributesMatchingObservedEnvToDirenv(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{}) env := snapshotItemByName(items)["DIRENV_WINS"] assert.Equal(t, "from-direnv", env.Value) assert.Equal(t, owl.Source{Name: ".envrc", Kind: "direnv"}, env.Source) @@ -111,8 +126,7 @@ func TestNewStoreDefaultsDirenvToWarn(t *testing.T) { }) require.NoError(t, err) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{}) env := snapshotItemByName(items)["DIRENV_DEFAULT"] assert.Equal(t, "from-direnv", env.Value) assert.Equal(t, owl.Source{Name: ".envrc", Kind: "direnv"}, env.Source) @@ -215,8 +229,7 @@ func TestNewRawValueStoreSkipsMissingEnvFiles(t *testing.T) { }, false) require.NoError(t, err) - items, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + items := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) env := snapshotItemByName(items)["PRESENT"] assert.Equal(t, "from-dotenv", env.Value) assert.Equal(t, owl.VisibilityLiteral, env.Visibility) @@ -236,8 +249,7 @@ func TestNewRawValueStoreUsesDefaultEnvFilesInOrder(t *testing.T) { store, err := NewRawValueStore(Options{WorkDir: dir}, false) require.NoError(t, err) - items, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + items := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) env := snapshotItemByName(items)["DEFAULT_ORDER"] assert.Equal(t, "from-env-dev", env.Value) assert.Equal(t, ".env.dev", env.Source.Name) @@ -307,7 +319,7 @@ func TestStoreBuildersUseConfiguredTypeProvider(t *testing.T) { require.NoError(t, err) _, err = result.Store.Resolve(context.Background(), owl.ResolveInput{Process: result.Catalog.ProcessResolverInput()}) require.NoError(t, err) - _ = result.Store.CheckState() + _ = checkStore(t, result.Store) assert.Positive(t, seededProvider.validations) rawProvider := &seedTrackingTypeProvider{BuiltInRegistry: registry.NewBuiltInRegistry()} @@ -317,7 +329,7 @@ func TestStoreBuildersUseConfiguredTypeProvider(t *testing.T) { TypeProvider: rawProvider, }, false) require.NoError(t, err) - _ = rawStore.CheckState() + _ = checkStore(t, rawStore) assert.Positive(t, rawProvider.validations) } diff --git a/internal/seed/store.go b/internal/seed/store.go index 951a6c7..19c871b 100644 --- a/internal/seed/store.go +++ b/internal/seed/store.go @@ -2,6 +2,7 @@ package seed import ( "bytes" + "context" "errors" "os" "sort" @@ -99,12 +100,14 @@ func seedInheritedValues(store *owl.Store, catalog Catalog) error { } func explicitSnapshotKeys(store *owl.Store) (map[string]struct{}, error) { - items, err := store.SnapshotItems(owl.SnapshotPolicy{}) + snapshot, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Filter: owl.SnapshotFilter{All: true}, + }) if err != nil { return nil, err } - keys := make(map[string]struct{}, len(items)) - for _, item := range items { + keys := make(map[string]struct{}, len(snapshot.Envs)) + for _, item := range snapshot.Envs { if item.Explicit { keys[item.Name] = struct{}{} } @@ -133,7 +136,10 @@ func loadInheritedVariables(store *owl.Store, vars []owl.DotenvVariable, explici groups[index].vars = append(groups[index].vars, variable) } for _, group := range groups { - if err := store.LoadDotenv(group.source, group.vars); err != nil { + if err := store.ApplyUpdate(context.Background(), owl.UpdateInput{ + Source: group.source, + Dotenv: group.vars, + }); err != nil { return err } } @@ -141,12 +147,14 @@ func loadInheritedVariables(store *owl.Store, vars []owl.DotenvVariable, explici } func projectionKeys(store *owl.Store) map[string]struct{} { - items, err := store.SnapshotItems(owl.SnapshotPolicy{}) + snapshot, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Filter: owl.SnapshotFilter{All: true}, + }) if err != nil { return nil } - keys := make(map[string]struct{}, len(items)) - for _, item := range items { + keys := make(map[string]struct{}, len(snapshot.Envs)) + for _, item := range snapshot.Envs { if item.Name != "" { keys[item.Name] = struct{}{} } diff --git a/pkg/owl/api.go b/pkg/owl/api.go index bedb171..c7dcfcf 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "strings" "time" "github.com/runmedev/owl/internal/graph" @@ -37,6 +36,8 @@ type ( PromptAnswer = resolver.PromptAnswer TypeID = model.TypeID + TypeDef = model.TypeDef + TypeProvider = registry.TypeProvider FieldRef = model.FieldRef ConfigInput = model.ConfigInput NeedInput = model.NeedInput @@ -49,6 +50,7 @@ type ( LoadInput = state.LoadInput StateEnvelope = state.StateEnvelope StateProvenance = state.StateProvenance + Sensitivity = model.Sensitivity Visibility = model.Visibility Exposure = model.Exposure Diagnostic = model.Diagnostic @@ -66,6 +68,18 @@ type ( ProposedValue = resolver.ProposedValue ) +func NewBuiltInTypeProvider() TypeProvider { + return registry.NewBuiltInRegistry() +} + +func NewTypeProviderFromDirectory(root string) (TypeProvider, error) { + return registry.NewBuiltInRegistryFromDirectory(root) +} + +func ReadConfigFile(path string) (ConfigInput, error) { + return requirements.ReadConfigFile(path) +} + const ( TypeCoreOpaque = model.TypeCoreOpaque TypeCorePlain = model.TypeCorePlain @@ -73,6 +87,10 @@ const ( TypeCoreURL = model.TypeCoreURL TypeUniverseRedis = model.TypeUniverseRedis + SensitivityUnknown = model.SensitivityUnknown + SensitivityPlaintext = model.SensitivityPlaintext + SensitivitySensitive = model.SensitivitySensitive + VisibilityLiteral = model.VisibilityLiteral VisibilityUnresolved = model.VisibilityUnresolved VisibilityMasked = model.VisibilityMasked @@ -99,6 +117,8 @@ const ( UnresolvedMissing = model.UnresolvedReasonMissing UnresolvedInvalid = model.UnresolvedReasonInvalid + + GeneratedDotenvSpecHeaderPrefix = requirements.GeneratedDotenvSpecHeaderPrefix ) type Store struct { @@ -338,6 +358,20 @@ func WithConfigSource(source string, input ConfigInput) StoreOption { } } +func WithConfigFile(path string) StoreOption { + return func(cfg *config) error { + input, err := requirements.ReadConfigFile(path) + if err != nil { + return err + } + cfg.configs = append(cfg.configs, configInputSource{ + source: model.Source{Name: path, Kind: "owl-config"}, + input: input, + }) + return nil + } +} + func WithEnvContract(contract EnvContract) StoreOption { return func(cfg *config) error { cfg.contracts = append(cfg.contracts, contract) @@ -399,14 +433,6 @@ func (s *Store) BuildSnapshotOperation(ctx context.Context, input SnapshotInput) return graphOperation(graph.SnapshotOperation(load, input.Policy)), nil } -func (s *Store) SnapshotItems(policy SnapshotPolicy) ([]SnapshotItem, error) { - output, err := s.Snapshot(context.Background(), SnapshotInput{Policy: policy, Filter: SnapshotFilter{All: true}}) - if err != nil { - return nil, err - } - return output.Envs, nil -} - func snapshotEnvsForInput(items []SnapshotItem, input SnapshotInput) []SnapshotEnv { envs := make([]SnapshotEnv, 0, len(items)) limit := input.Filter.Limit @@ -445,14 +471,6 @@ func (s *Store) BuildSourceOperation(ctx context.Context, input SourceInput) (Gr return graphOperation(graph.DotenvOperation(load, input.Policy)), nil } -func (s *Store) Dotenv(policy DotenvPolicy) ([]string, error) { - output, err := s.Source(context.Background(), SourceInput{Policy: policy}) - if err != nil { - return nil, err - } - return output.Envs, nil -} - func (s *Store) DotenvSpec(ctx context.Context, input DotenvSpecInput) (DotenvSpecOutput, error) { if ctx == nil { ctx = context.Background() @@ -605,18 +623,6 @@ func (s *Store) BuildCheckOperation(ctx context.Context, input CheckInput) (Grap return graphOperation(graph.CheckOperation(load)), nil } -func (s *Store) CheckState() state.CheckResult { - envelope, err := s.StateEnvelope(context.Background()) - if err != nil { - return state.CheckResult{} - } - check, err := s.runtime.Check(context.Background(), LoadInput{Envelope: &envelope}) - if err != nil { - return state.CheckResult{} - } - return check -} - func (s *Store) ExecuteGraphQL(ctx context.Context, req GraphQLRequest) (GraphQLResult, error) { if ctx == nil { ctx = context.Background() @@ -752,42 +758,11 @@ func (s *Store) resolverState(ctx context.Context) (model.EffectiveState, error) return envelope.State, nil } -func (s *Store) LoadDotenv(source Source, vars []DotenvVariable) error { - return s.applyDotenv(source, vars, nil) -} - -func (s *Store) LoadDotenvLines(source string, envs ...string) error { - raw := strings.Join(envs, "\n") - if raw != "" { - raw += "\n" - } - input, err := state.LoadInputFromSourceBytes([]state.SourceBytes{{Name: source, Raw: []byte(raw)}}, nil) - if err != nil { - return err - } - return s.LoadDotenv(input.DotenvSource, input.Dotenv) -} - -func (s *Store) Update(ctx context.Context, newOrUpdated, deleted []string) error { +func (s *Store) ApplyUpdate(ctx context.Context, input UpdateInput) error { if ctx == nil { ctx = context.Background() } - raw := strings.Join(newOrUpdated, "\n") - if raw != "" { - raw += "\n" - } - input, err := state.LoadInputFromSourceBytes([]state.SourceBytes{{Name: "[update]", Raw: []byte(raw)}}, nil) - if err != nil { - return err - } - return s.applyDotenvWithContext(ctx, sourceFromContext(ctx, input.DotenvSource), input.Dotenv, deleted) -} - -func (s *Store) Delete(ctx context.Context, keys ...string) error { - if ctx == nil { - ctx = context.Background() - } - return s.applyDotenvWithContext(ctx, sourceFromContext(ctx, Source{}), nil, keys) + return s.applyUpdateWithContext(ctx, input) } func (s *Store) BuildUpdateOperation(ctx context.Context, input UpdateInput) (GraphOperation, error) { @@ -883,36 +858,36 @@ func Diagnostics(err error) []Diagnostic { return state.Diagnostics(err) } -func (s *Store) applyDotenv(source Source, vars []DotenvVariable, deleted []string) error { - return s.applyDotenvWithContext(context.Background(), source, vars, deleted) -} - -func (s *Store) applyDotenvWithContext(ctx context.Context, source Source, vars []DotenvVariable, deleted []string) error { +func (s *Store) applyUpdateWithContext(ctx context.Context, input UpdateInput) error { if ctx == nil { ctx = context.Background() } - if len(vars) == 0 && len(deleted) == 0 { + if len(input.Dotenv) == 0 && len(input.Delete) == 0 { return nil } - if len(vars) > 0 { + source := input.Source + if source == (Source{}) { + source = sourceFromContext(ctx, Source{Name: "[update]", Kind: "dotenv"}) + } + if len(input.Dotenv) > 0 { timestamp := s.clock() s.operations = append(s.operations, state.OperationRecord{ Kind: state.OperationRecordUpdate, Timestamp: timestamp, Update: state.UpdateOperation{ Source: source, - Dotenv: vars, + Dotenv: append([]DotenvVariable{}, input.Dotenv...), Timestamp: timestamp, }, }) } - if len(deleted) > 0 { + if len(input.Delete) > 0 { timestamp := s.clock() s.operations = append(s.operations, state.OperationRecord{ Kind: state.OperationRecordDelete, Timestamp: timestamp, Delete: state.DeleteOperation{ - Keys: append([]string{}, deleted...), + Keys: append([]string{}, input.Delete...), Source: source, Timestamp: timestamp, }, diff --git a/pkg/owl/api_internal_test.go b/pkg/owl/api_internal_test.go index c8dc827..1034718 100644 --- a/pkg/owl/api_internal_test.go +++ b/pkg/owl/api_internal_test.go @@ -10,6 +10,31 @@ import ( "github.com/stretchr/testify/require" ) +func snapshotItems(t *testing.T, store *Store, policy SnapshotPolicy) []SnapshotItem { + t.Helper() + output, err := store.Snapshot(context.Background(), SnapshotInput{ + Policy: policy, + Filter: SnapshotFilter{All: true}, + }) + require.NoError(t, err) + return output.Envs +} + +func applyUpdateLines(ctx context.Context, t *testing.T, store *Store, source Source, lines []string, deleted []string) { + t.Helper() + var vars []DotenvVariable + for _, line := range lines { + key, value, ok := strings.Cut(line, "=") + require.True(t, ok, "update line must be KEY=value") + vars = append(vars, DotenvVariable{Key: key, Value: value, Source: source}) + } + require.NoError(t, store.ApplyUpdate(ctx, UpdateInput{ + Source: source, + Dotenv: vars, + Delete: deleted, + })) +} + func TestPublicAPIUpdateTimestampsOnlyChangedItems(t *testing.T) { t.Parallel() @@ -25,22 +50,19 @@ func TestPublicAPIUpdateTimestampsOnlyChangedItems(t *testing.T) { ) require.NoError(t, err) - initial, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + initial := snapshotItems(t, store, SnapshotPolicy{Reveal: true}) initialByName := snapshotItemsByName(initial) assert.Equal(t, timestamps.At(0), initialByName["API_URL"].UpdatedAt) assert.Equal(t, timestamps.At(0), initialByName["TOKEN"].UpdatedAt) - require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://two.example.com"}, nil)) - afterAPIURLUpdate, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + applyUpdateLines(context.Background(), t, store, Source{Name: "[update]", Kind: "dotenv"}, []string{"API_URL=https://two.example.com"}, nil) + afterAPIURLUpdate := snapshotItems(t, store, SnapshotPolicy{Reveal: true}) afterAPIURLUpdateByName := snapshotItemsByName(afterAPIURLUpdate) assert.Equal(t, timestamps.At(1), afterAPIURLUpdateByName["API_URL"].UpdatedAt) assert.Equal(t, timestamps.At(0), afterAPIURLUpdateByName["TOKEN"].UpdatedAt) - require.NoError(t, store.Update(context.Background(), []string{"TOKEN=two"}, nil)) - afterTokenUpdate, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + applyUpdateLines(context.Background(), t, store, Source{Name: "[update]", Kind: "dotenv"}, []string{"TOKEN=two"}, nil) + afterTokenUpdate := snapshotItems(t, store, SnapshotPolicy{Reveal: true}) afterTokenUpdateByName := snapshotItemsByName(afterTokenUpdate) assert.Equal(t, timestamps.At(1), afterTokenUpdateByName["API_URL"].UpdatedAt) assert.Equal(t, timestamps.At(2), afterTokenUpdateByName["TOKEN"].UpdatedAt) @@ -59,17 +81,15 @@ func TestPublicAPIUpdateClearsResolvedRequiredDiagnostics(t *testing.T) { ) require.NoError(t, err) - initial, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + initial := snapshotItems(t, store, SnapshotPolicy{Reveal: true}) initialByName := snapshotItemsByName(initial) require.Contains(t, initialByName, "TOKEN") assert.Contains(t, diagnosticCodes(initialByName["TOKEN"].Diagnostics), "dotenv.unresolved-required") ctx := ContextWithExecutionInfo(context.Background(), ExecutionInfo{ExecContext: "direnv"}) - require.NoError(t, store.Update(ctx, []string{"TOKEN=secret"}, nil)) + applyUpdateLines(ctx, t, store, Source{Name: "[direnv]", Kind: "direnv"}, []string{"TOKEN=secret"}, nil) - updated, err := store.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + updated := snapshotItems(t, store, SnapshotPolicy{Reveal: true}) updatedByName := snapshotItemsByName(updated) assert.Equal(t, "secret", updatedByName["TOKEN"].Value) assert.Equal(t, "[direnv]", updatedByName["TOKEN"].Source.Name) @@ -91,7 +111,7 @@ func TestPublicAPIStateEnvelopePreservesOperationTimestamps(t *testing.T) { withClock(timestamps.Next), ) require.NoError(t, err) - require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://two.example.com"}, nil)) + applyUpdateLines(context.Background(), t, store, Source{Name: "[update]", Kind: "dotenv"}, []string{"API_URL=https://two.example.com"}, nil) envelope, err := store.StateEnvelope(context.Background()) require.NoError(t, err) @@ -109,9 +129,8 @@ func TestPublicAPIStateEnvelopePreservesOperationTimestamps(t *testing.T) { assert.Contains(t, operationTimestamps(roundTrippedEnvelope.Provenance.Operations), timestamps.At(0)) assert.Contains(t, operationTimestamps(roundTrippedEnvelope.Provenance.Operations), timestamps.At(1)) - require.NoError(t, roundTripped.Update(context.Background(), []string{"API_URL=https://three.example.com"}, nil)) - snapshot, err := roundTripped.SnapshotItems(SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + applyUpdateLines(context.Background(), t, roundTripped, Source{Name: "[update]", Kind: "dotenv"}, []string{"API_URL=https://three.example.com"}, nil) + snapshot := snapshotItems(t, roundTripped, SnapshotPolicy{Reveal: true}) assert.Equal(t, timestamps.At(2), snapshotItemsByName(snapshot)["API_URL"].UpdatedAt) } diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index a98454c..d428f7a 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -22,12 +22,10 @@ func TestV2PublicAPI(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) assert.Equal(t, "[masked]", snapshotByName(snapshot)["API_KEY"].Value) - envs, err := store.Dotenv(owl.DotenvPolicy{Insecure: true}) - require.NoError(t, err) + envs := dotenvLines(t, store, owl.DotenvPolicy{Insecure: true}) assert.Equal(t, []string{ "API_KEY=secret", "API_URL=https://api.example.com", @@ -49,19 +47,58 @@ func TestV2PublicAPI(t *testing.T) { next, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - require.NoError(t, next.LoadDotenvLines("[override]", "API_URL=https://next.example.com")) + applyUpdateLines(context.Background(), t, next, owl.Source{Name: "[override]", Kind: "dotenv"}, []string{"API_URL=https://next.example.com"}, nil) got, ok, err = next.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://next.example.com", got.Value) - require.NoError(t, next.Delete(context.Background(), "API_KEY")) + applyUpdateLines(context.Background(), t, next, owl.Source{Name: "[update]", Kind: "dotenv"}, nil, []string{"API_KEY"}) _, ok, err = next.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) } +func snapshotItems(t *testing.T, store *owl.Store, policy owl.SnapshotPolicy) []owl.SnapshotItem { + t.Helper() + output, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Policy: policy, + Filter: owl.SnapshotFilter{All: true}, + }) + require.NoError(t, err) + return output.Envs +} + +func dotenvLines(t *testing.T, store *owl.Store, policy owl.DotenvPolicy) []string { + t.Helper() + output, err := store.Source(context.Background(), owl.SourceInput{Policy: policy}) + require.NoError(t, err) + return output.Envs +} + +func checkStore(t *testing.T, store *owl.Store) owl.CheckOutput { + t.Helper() + output, err := store.Check(context.Background(), owl.CheckInput{}) + require.NoError(t, err) + return output +} + +func applyUpdateLines(ctx context.Context, t *testing.T, store *owl.Store, source owl.Source, lines []string, deleted []string) { + t.Helper() + var vars []owl.DotenvVariable + for _, line := range lines { + key, value, ok := strings.Cut(line, "=") + require.True(t, ok, "update line must be KEY=value") + vars = append(vars, owl.DotenvVariable{Key: key, Value: value, Source: source}) + } + require.NoError(t, store.ApplyUpdate(ctx, owl.UpdateInput{ + Source: source, + Dotenv: vars, + Delete: deleted, + })) +} + func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { t.Parallel() @@ -212,8 +249,7 @@ func TestPublicAPISnapshotOrderSurvivesStateEnvelopeRoundTrip(t *testing.T) { roundTripped, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - snapshot, err := roundTripped.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + snapshot := snapshotItems(t, roundTripped, owl.SnapshotPolicy{Reveal: true}) assert.Equal(t, []string{"ZETA", "BETA", "APPLE", "OMEGA"}, snapshotNames(snapshot)) } @@ -226,8 +262,7 @@ func TestPublicAPIVisibilityAndExposure(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) assert.Equal(t, "https://api.example.com", byName["API_URL"].Value) @@ -250,8 +285,7 @@ func TestPublicAPIVisibilityAndExposure(t *testing.T) { assert.Empty(t, byName["MISSING_TOKEN"].Source) assert.Equal(t, ".env.spec", byName["MISSING_TOKEN"].Origin.Name) - revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + revealed := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) revealedByName := snapshotByName(revealed) assert.Equal(t, "secret", revealedByName["API_KEY"].Value) assert.Equal(t, owl.VisibilityLiteral, revealedByName["API_KEY"].Visibility) @@ -268,8 +302,7 @@ func TestPublicAPIUndeclaredOpaqueKeysStayHidden(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) for _, name := range []string{"OPENAI_API_KEY", "SOMETHING_TOKEN", "REDIS_PASSWORD"} { @@ -281,8 +314,7 @@ func TestPublicAPIUndeclaredOpaqueKeysStayHidden(t *testing.T) { assert.Equal(t, "[process]", byName[name].Source.Name) } - revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + revealed := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) revealedByName := snapshotByName(revealed) assert.Equal(t, "sk-example", revealedByName["OPENAI_API_KEY"].Value) assert.Equal(t, "token-value", revealedByName["SOMETHING_TOKEN"].Value) @@ -298,8 +330,7 @@ func TestPublicAPIObservedEmptyValuesArePresent(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) assert.Equal(t, "[masked]", byName["RUNME_TEST_TOKEN"].Value) @@ -316,15 +347,14 @@ func TestPublicAPIObservedEmptyValuesArePresent(t *testing.T) { assert.Equal(t, "[process]", byName["EMPTY_OPAQUE"].Source.Name) assert.Equal(t, ".env.spec", byName["EMPTY_OPAQUE"].Origin.Name) - revealed, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + revealed := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) revealedByName := snapshotByName(revealed) assert.Equal(t, "", revealedByName["RUNME_TEST_TOKEN"].Value) assert.Equal(t, owl.VisibilityLiteral, revealedByName["RUNME_TEST_TOKEN"].Visibility) assert.Equal(t, "", revealedByName["EMPTY_OPAQUE"].Value) assert.Equal(t, owl.VisibilityLiteral, revealedByName["EMPTY_OPAQUE"].Visibility) - check := store.CheckState() + check := checkStore(t, store) assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-secret") assert.NotContains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") @@ -376,23 +406,21 @@ func TestPublicAPIDotenvSecureAndInsecure(t *testing.T) { ) require.NoError(t, err) - safe, err := store.Dotenv(owl.DotenvPolicy{}) - require.NoError(t, err) + safe := dotenvLines(t, store, owl.DotenvPolicy{}) assert.Equal(t, []string{ "API_KEY=[masked]", "API_URL=https://api.example.com", "DATABASE_URL=[hidden]", }, safe) - insecure, err := store.Dotenv(owl.DotenvPolicy{Insecure: true}) - require.NoError(t, err) + insecure := dotenvLines(t, store, owl.DotenvPolicy{Insecure: true}) assert.Equal(t, []string{ "API_KEY=secret", "API_URL=https://api.example.com", "DATABASE_URL=postgres://example", }, insecure) - check := store.CheckState() + check := checkStore(t, store) assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") } @@ -413,20 +441,18 @@ func TestPublicAPIStateEnvelopeRoundTrip(t *testing.T) { roundTripped, err := owl.NewStore(owl.WithStateEnvelope(envelope)) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) - roundTrippedSnapshot, err := roundTripped.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) + roundTrippedSnapshot := snapshotItems(t, roundTripped, owl.SnapshotPolicy{}) assert.Equal(t, snapshotByName(snapshot)["API_KEY"].Visibility, snapshotByName(roundTrippedSnapshot)["API_KEY"].Visibility) assert.Equal(t, snapshotByName(snapshot)["DATABASE_URL"].Exposure, snapshotByName(roundTrippedSnapshot)["DATABASE_URL"].Exposure) - require.NoError(t, roundTripped.LoadDotenvLines("[override]", "API_URL=https://next.example.com")) + applyUpdateLines(context.Background(), t, roundTripped, owl.Source{Name: "[override]", Kind: "dotenv"}, []string{"API_URL=https://next.example.com"}, nil) got, ok, err := roundTripped.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://next.example.com", got.Value) - require.NoError(t, roundTripped.Delete(context.Background(), "API_KEY")) + applyUpdateLines(context.Background(), t, roundTripped, owl.Source{Name: "[update]", Kind: "dotenv"}, nil, []string{"API_KEY"}) _, ok, err = roundTripped.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) @@ -518,14 +544,14 @@ func TestPublicAPIUpdatesMaterializeFromOperationLog(t *testing.T) { ) require.NoError(t, err) - require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://one.example.com"}, nil)) - require.NoError(t, store.Update(context.Background(), []string{"API_URL=https://two.example.com"}, nil)) + applyUpdateLines(context.Background(), t, store, owl.Source{Name: "[update]", Kind: "dotenv"}, []string{"API_URL=https://one.example.com"}, nil) + applyUpdateLines(context.Background(), t, store, owl.Source{Name: "[update]", Kind: "dotenv"}, []string{"API_URL=https://two.example.com"}, nil) got, ok, err := store.Get(context.Background(), owl.GetInput{Key: "API_URL", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) require.True(t, ok) assert.Equal(t, "https://two.example.com", got.Value) - require.NoError(t, store.Delete(context.Background(), "API_KEY")) + applyUpdateLines(context.Background(), t, store, owl.Source{Name: "[update]", Kind: "dotenv"}, nil, []string{"API_KEY"}) _, ok, err = store.Get(context.Background(), owl.GetInput{Key: "API_KEY", Policy: owl.GetPolicy{Reveal: true}}) require.NoError(t, err) assert.False(t, ok) @@ -554,13 +580,12 @@ func TestPublicAPIExecutionInfoSetsUpdateSource(t *testing.T) { KnownName: "cell-name", ExecContext: "direnv", }) - require.NoError(t, store.Update(ctx, []string{ + applyUpdateLines(ctx, t, store, owl.Source{}, []string{ "API_URL=https://next.example.com", "TOKEN=secret", - }, nil)) + }, nil) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{Reveal: true}) byName := snapshotByName(snapshot) assert.Equal(t, "[direnv]", byName["API_URL"].Source.Name) @@ -592,8 +617,7 @@ func TestPublicAPIWithEnvContractMapsBindings(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) item := snapshotByName(snapshot)["DATABASE_URL"] assert.Equal(t, "postgres://example", item.Value) assert.Equal(t, owl.TypeCoreURL, item.Type) @@ -626,8 +650,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) assert.Equal(t, "localhost", byName["QUEUES_REDIS_HOST"].Value) @@ -657,7 +680,7 @@ func TestPublicAPIWithConfigMapsRedisRequirement(t *testing.T) { "", }, "\n"), dotenvSpec.Rendered) - check := store.CheckState() + check := checkStore(t, store) assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "dotenv.unresolved-required") } @@ -679,7 +702,7 @@ func TestPublicAPIWithConfigValidatesRedisPort(t *testing.T) { ) require.NoError(t, err) - check := store.CheckState() + check := checkStore(t, store) assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-port") @@ -706,7 +729,7 @@ func TestPublicAPIWithConfigValidatesRedisHostRequiredByRedis(t *testing.T) { ) require.NoError(t, err) - check := store.CheckState() + check := checkStore(t, store) assert.False(t, check.OK) assert.Contains(t, diagnosticCodes(check.Diagnostics), "type.invalid-host") } @@ -728,8 +751,7 @@ func TestPublicAPIWithConfigIncludesRedisHostDiagnosticsInSnapshot(t *testing.T) ) require.NoError(t, err) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) assert.Contains(t, diagnosticCodes(byName["QUEUES_REDIS_HOST"].Diagnostics), "type.invalid-host") } @@ -765,8 +787,7 @@ func TestPublicAPIResolveReturnsPromptActionsAndAppliesAnswers(t *testing.T) { require.Len(t, applied.Attempts, 1) assert.Equal(t, owl.ResolverResolved, applied.Attempts[0].Outcome) - snapshot, err := store.SnapshotItems(owl.SnapshotPolicy{}) - require.NoError(t, err) + snapshot := snapshotItems(t, store, owl.SnapshotPolicy{}) byName := snapshotByName(snapshot) assert.Equal(t, "[masked]", byName["API_KEY"].Value) assert.Equal(t, "[interactive]", byName["API_KEY"].Source.Name) diff --git a/pkg/owl/seed/seed.go b/pkg/owl/seed/seed.go index 0d0e7a6..e028427 100644 --- a/pkg/owl/seed/seed.go +++ b/pkg/owl/seed/seed.go @@ -16,6 +16,7 @@ type Options struct { WorkDir string Direnv DirenvPolicy DirenvRunner DirenvExportRunner + TypeProvider owl.TypeProvider } // ObservedSource is an environment-shaped snapshot with caller-provided provenance. @@ -42,9 +43,12 @@ type DirenvExportRunner = internalseed.DirenvExportRunner // Result is the seeded store plus source diagnostics used to build it. type Result struct { Store *owl.Store + Catalog Catalog Diagnostics []owl.Diagnostic } +type Catalog = internalseed.Catalog + // NewStore creates an Owl store and resolves values from observed sources. func NewStore(ctx context.Context, opts Options) (*Result, error) { result, err := internalseed.NewStore(ctx, internalOptions(opts)) @@ -59,10 +63,19 @@ func NewStore(ctx context.Context, opts Options) (*Result, error) { } return &Result{ Store: result.Store, + Catalog: result.Catalog, Diagnostics: result.Diagnostics, }, nil } +func NewRawValueStore(opts Options, allowMissingSpec bool) (*owl.Store, error) { + return internalseed.NewRawValueStore(internalOptions(opts), allowMissingSpec) +} + +func BuildCatalog(ctx context.Context, opts Options) (Catalog, []owl.Diagnostic, error) { + return internalseed.BuildCatalog(ctx, internalOptions(opts)) +} + func internalOptions(opts Options) internalseed.Options { observed := make([]internalseed.ObservedSource, 0, len(opts.Observed)) for _, source := range opts.Observed { @@ -79,5 +92,6 @@ func internalOptions(opts Options) internalseed.Options { WorkDir: opts.WorkDir, Direnv: opts.Direnv, DirenvRunner: opts.DirenvRunner, + TypeProvider: opts.TypeProvider, } } diff --git a/pkg/owl/seed/seed_test.go b/pkg/owl/seed/seed_test.go index 800ea06..0ea7828 100644 --- a/pkg/owl/seed/seed_test.go +++ b/pkg/owl/seed/seed_test.go @@ -12,6 +12,16 @@ import ( "github.com/runmedev/owl/pkg/owl" ) +func snapshotItems(t *testing.T, store *owl.Store, policy owl.SnapshotPolicy) []owl.SnapshotItem { + t.Helper() + output, err := store.Snapshot(context.Background(), owl.SnapshotInput{ + Policy: policy, + Filter: owl.SnapshotFilter{All: true}, + }) + require.NoError(t, err) + return output.Envs +} + func TestNewStoreReturnsSeededStore(t *testing.T) { t.Parallel() @@ -28,8 +38,7 @@ func TestNewStoreReturnsSeededStore(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{Reveal: true}) require.Len(t, items, 1) assert.Equal(t, "API_KEY", items[0].Name) assert.Equal(t, "secret", items[0].Value) @@ -52,8 +61,7 @@ func TestNewStoreSkipsMissingEnvFiles(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{Reveal: true}) require.Len(t, items, 1) assert.Equal(t, "PRESENT", items[0].Name) assert.Equal(t, "from-dotenv", items[0].Value) @@ -79,8 +87,7 @@ func TestNewStoreUsesDefaultEnvFilesInOrder(t *testing.T) { require.NoError(t, err) require.Empty(t, result.Diagnostics) - items, err := result.Store.SnapshotItems(owl.SnapshotPolicy{Reveal: true}) - require.NoError(t, err) + items := snapshotItems(t, result.Store, owl.SnapshotPolicy{Reveal: true}) require.Len(t, items, 1) assert.Equal(t, "DEFAULT_ORDER", items[0].Name) assert.Equal(t, "from-env-dev", items[0].Value) diff --git a/internal/version/version.go b/pkg/owl/version.go similarity index 92% rename from internal/version/version.go rename to pkg/owl/version.go index 1ed6e93..6ec617a 100644 --- a/internal/version/version.go +++ b/pkg/owl/version.go @@ -1,4 +1,4 @@ -package version +package owl import "fmt" From 2815ff4c037d6cf7ae268d6bea186d6197ca0080 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Tue, 11 Aug 2026 09:41:31 -0700 Subject: [PATCH 12/13] refactor: move type catalog selection behind owl facade Signed-off-by: Sebastian (Tiedtke) Huckleberry --- cmd/cue_catalog.go | 10 ++-------- cmd/cue_catalog_test.go | 6 ++---- pkg/owl/api.go | 11 +++++++++++ pkg/owl/api_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/cmd/cue_catalog.go b/cmd/cue_catalog.go index bf3b39a..057b769 100644 --- a/cmd/cue_catalog.go +++ b/cmd/cue_catalog.go @@ -12,14 +12,8 @@ const cueRootEnv = "OWL_CUE_ROOT" var lookupEnv = os.LookupEnv func commandTypeProvider() (owl.TypeProvider, error) { - root, configured := lookupEnv(cueRootEnv) - if !configured { - return owl.NewBuiltInTypeProvider(), nil - } - if root == "" { - return nil, fmt.Errorf("%s is set but empty", cueRootEnv) - } - types, err := owl.NewTypeProviderFromDirectory(root) + root, _ := lookupEnv(cueRootEnv) + types, err := owl.TypeProviderFromCatalogInput(owl.TypeCatalogInput{Root: root}) if err != nil { return nil, fmt.Errorf("load CUE catalog from %s=%q: %w", cueRootEnv, root, err) } diff --git a/cmd/cue_catalog_test.go b/cmd/cue_catalog_test.go index 88ce42b..a670edc 100644 --- a/cmd/cue_catalog_test.go +++ b/cmd/cue_catalog_test.go @@ -50,11 +50,9 @@ func TestCommandCUECatalogPrecedence(t *testing.T) { require.Error(t, runCheck(t)) }) - t.Run("empty is rejected", func(t *testing.T) { + t.Run("empty uses embedded", func(t *testing.T) { withLookupEnv(t, func(string) (string, bool) { return "", true }) - err := runCheck(t) - require.Error(t, err) - assert.Contains(t, err.Error(), "OWL_CUE_ROOT is set but empty") + require.NoError(t, runCheck(t)) }) t.Run("invalid never falls back", func(t *testing.T) { diff --git a/pkg/owl/api.go b/pkg/owl/api.go index c7dcfcf..3a4eb4a 100644 --- a/pkg/owl/api.go +++ b/pkg/owl/api.go @@ -76,6 +76,17 @@ func NewTypeProviderFromDirectory(root string) (TypeProvider, error) { return registry.NewBuiltInRegistryFromDirectory(root) } +type TypeCatalogInput struct { + Root string +} + +func TypeProviderFromCatalogInput(input TypeCatalogInput) (TypeProvider, error) { + if input.Root == "" { + return NewBuiltInTypeProvider(), nil + } + return NewTypeProviderFromDirectory(input.Root) +} + func ReadConfigFile(path string) (ConfigInput, error) { return requirements.ReadConfigFile(path) } diff --git a/pkg/owl/api_test.go b/pkg/owl/api_test.go index d428f7a..f586e93 100644 --- a/pkg/owl/api_test.go +++ b/pkg/owl/api_test.go @@ -3,6 +3,8 @@ package owl_test import ( "context" "errors" + "os" + "path/filepath" "strings" "testing" "time" @@ -99,6 +101,17 @@ func applyUpdateLines(ctx context.Context, t *testing.T, store *owl.Store, sourc })) } +func copyCUECatalog(t *testing.T) string { + t.Helper() + root, err := filepath.Abs("../..") + require.NoError(t, err) + destination := t.TempDir() + for _, name := range []string{"cue.mod", "schema", "types"} { + require.NoError(t, os.CopyFS(filepath.Join(destination, name), os.DirFS(filepath.Join(root, name)))) + } + return destination +} + func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { t.Parallel() @@ -149,6 +162,36 @@ func TestPublicAPIOperationsUseGraphShapedLoadInput(t *testing.T) { } } +func TestTypeProviderFromCatalogInput(t *testing.T) { + t.Parallel() + + builtin, err := owl.TypeProviderFromCatalogInput(owl.TypeCatalogInput{}) + require.NoError(t, err) + _, ok, err := builtin.ResolveTypeRef("github.com/runmedev/owl/types/universe/redis") + require.NoError(t, err) + assert.True(t, ok) + + _, err = owl.TypeProviderFromCatalogInput(owl.TypeCatalogInput{Root: filepath.Join(t.TempDir(), "missing")}) + require.Error(t, err) + + root := copyCUECatalog(t) + redisType := filepath.Join(root, "types/universe/redis/type.cue") + raw, err := os.ReadFile(redisType) + require.NoError(t, err) + raw = []byte(strings.Replace(string(raw), "(uint & >=1 & <=65535)", "6380", 1)) + require.NoError(t, os.WriteFile(redisType, raw, 0o600)) + + types, err := owl.TypeProviderFromCatalogInput(owl.TypeCatalogInput{Root: root}) + require.NoError(t, err) + validator, ok := types.(interface { + ValidateFieldValue(owl.FieldRef, string) error + }) + require.True(t, ok) + ref := owl.FieldRef{TypeID: owl.TypeUniverseRedis, Instance: "queues", Field: "port"} + require.NoError(t, validator.ValidateFieldValue(ref, "6380")) + require.Error(t, validator.ValidateFieldValue(ref, "6379")) +} + func mustBuildSnapshotOperation(t *testing.T, store *owl.Store) owl.GraphOperation { t.Helper() op, err := store.BuildSnapshotOperation(context.Background(), owl.SnapshotInput{}) From 50c9f33b5c35e4efabb348b14dba87f13c3e6db0 Mon Sep 17 00:00:00 2001 From: "Sebastian (Tiedtke) Huckleberry" Date: Tue, 11 Aug 2026 09:51:34 -0700 Subject: [PATCH 13/13] chore: rename graph context type Signed-off-by: Sebastian (Tiedtke) Huckleberry --- internal/graph/runtime.go | 2 +- internal/graph/schema.go | 46 +++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/graph/runtime.go b/internal/graph/runtime.go index c879615..61fdfd5 100644 --- a/internal/graph/runtime.go +++ b/internal/graph/runtime.go @@ -19,7 +19,7 @@ type Runtime struct { types registry.TypeProvider } -type Context struct { +type GraphContext struct { State model.EffectiveState Types registry.TypeProvider } diff --git a/internal/graph/schema.go b/internal/graph/schema.go index 5c07bf2..d74bbd0 100644 --- a/internal/graph/schema.go +++ b/internal/graph/schema.go @@ -337,7 +337,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "envelope": &graphql.Field{ Type: stateEnvelopeType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) return stateEnvelopeView(state.MachineFromState(gctx.State, gctx.Types).StateEnvelope()), nil }, }, @@ -461,7 +461,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "all": &graphql.ArgumentConfig{Type: graphql.Boolean, DefaultValue: false}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) all, _ := p.Args["all"].(bool) result, err := state.MachineFromState(gctx.State, gctx.Types).Type(state.TypePolicy{All: all}) if err != nil { @@ -484,7 +484,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "reveal": &graphql.ArgumentConfig{Type: graphql.Boolean, DefaultValue: false}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) reveal, _ := p.Args["reveal"].(bool) return state.MachineFromState(gctx.State, gctx.Types).Snapshot(state.SnapshotPolicy{Reveal: reveal}) }, @@ -495,7 +495,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "insecure": &graphql.ArgumentConfig{Type: graphql.Boolean, DefaultValue: false}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) insecure, _ := p.Args["insecure"].(bool) return state.MachineFromState(gctx.State, gctx.Types).Dotenv(state.DotenvPolicy{Insecure: insecure}) }, @@ -503,7 +503,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "check": &graphql.Field{ Type: checkType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) check := state.MachineFromState(gctx.State, gctx.Types).Check() check.Diagnostics = sortedDiagnostics(check.Diagnostics) return check, nil @@ -512,7 +512,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "dotenvSpec": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) return requirements.RenderDotenvSpec(contractsFromState(gctx.State), gctx.Types) }, }, @@ -523,7 +523,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "reveal": &graphql.ArgumentConfig{Type: graphql.Boolean, DefaultValue: false}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) key := p.Args["key"].(string) reveal, _ := p.Args["reveal"].(bool) result, ok, err := state.MachineFromState(gctx.State, gctx.Types).Get(key, state.GetPolicy{Reveal: reveal}) @@ -536,7 +536,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "sensitiveKeys": &graphql.Field{ Type: graphql.NewList(graphql.String), Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) return state.MachineFromState(gctx.State, gctx.Types).SensitiveKeys() }, }, @@ -555,13 +555,13 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { input := decodeLoadInput(p.Args["input"].(map[string]interface{})) - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.LoadOperation{Input: input, Timestamp: input.Timestamp}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "update": &graphql.Field{ @@ -570,14 +570,14 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "dotenv": &graphql.ArgumentConfig{Type: dotenvInput}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) input := decodeDotenvInput(p.Args["dotenv"]) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.UpdateOperation{Source: input.DotenvSource, Dotenv: input.Dotenv, Timestamp: input.Timestamp}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "delete": &graphql.Field{ @@ -586,13 +586,13 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "keys": &graphql.ArgumentConfig{Type: graphql.NewList(graphql.NewNonNull(graphql.String))}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.DeleteOperation{Keys: decodeStringList(p.Args["keys"])}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "recordResolverAttempt": &graphql.Field{ @@ -601,14 +601,14 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "attempt": &graphql.ArgumentConfig{Type: graphql.NewNonNull(resolverAttemptInput)}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) attempt := decodeResolverAttemptInput(p.Args["attempt"]) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.RecordResolverAttemptOperation{Attempt: attempt}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "applyResolverProposal": &graphql.Field{ @@ -618,7 +618,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "timestamp": &graphql.ArgumentConfig{Type: graphql.String}, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) proposal := decodeResolverProposalInput(p.Args["proposal"]) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.ApplyResolverProposalOperation{ @@ -628,31 +628,31 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "normalize": &graphql.Field{ Type: environmentType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.NormalizeOperation{}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "validate": &graphql.Field{ Type: environmentType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - gctx := p.Source.(Context) + gctx := p.Source.(GraphContext) s := state.MachineFromState(gctx.State, gctx.Types) state, err := s.Apply(contextFromParams(p), state.IntegrityOperation{Types: gctx.Types}) if err != nil { return nil, err } - return Context{State: state, Types: gctx.Types}, nil + return GraphContext{State: state, Types: gctx.Types}, nil }, }, "render": &graphql.Field{ @@ -683,7 +683,7 @@ func (r *Runtime) newSchema() (graphql.Schema, error) { "Environment": &graphql.Field{ Type: environmentType, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - return Context{State: model.NewEffectiveState(), Types: r.types}, nil + return GraphContext{State: model.NewEffectiveState(), Types: r.types}, nil }, }, },