Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions framework/configstore/complexityconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,66 @@ func MergeComplexityAnalyzerConfig(base, file *ComplexityAnalyzerConfig) (*Compl
return &merged, nil
}

// MergeComplexityAnalyzerConfigWithFileSnapshot overlays file boundaries, removes
// keywords removed from config.json since the previous file snapshot, and keeps
// runtime-only keywords.
func MergeComplexityAnalyzerConfigWithFileSnapshot(base, previousFile, file *ComplexityAnalyzerConfig) (*ComplexityAnalyzerConfig, error) {
if previousFile == nil {
return MergeComplexityAnalyzerConfig(base, file)
}
if file == nil {
return MergeComplexityAnalyzerConfig(base, nil)
}

normalizedFile := file.Normalized()
if err := normalizedFile.Validate(); err != nil {
return nil, err
}
normalizedPreviousFile := previousFile.Normalized()
if err := normalizedPreviousFile.Validate(); err != nil {
return nil, err
}

var normalizedBase ComplexityAnalyzerConfig
if base != nil {
normalizedBase = base.Normalized()
if err := normalizedBase.Validate(); err != nil {
return nil, err
}
}

merged := ComplexityAnalyzerConfig{
TierBoundaries: normalizedFile.TierBoundaries,
Keywords: ComplexityEditableKeywordConfig{
CodeKeywords: mergeComplexityKeywordListsWithFileSnapshot(
normalizedBase.Keywords.CodeKeywords,
normalizedPreviousFile.Keywords.CodeKeywords,
normalizedFile.Keywords.CodeKeywords,
),
ReasoningKeywords: mergeComplexityKeywordListsWithFileSnapshot(
normalizedBase.Keywords.ReasoningKeywords,
normalizedPreviousFile.Keywords.ReasoningKeywords,
normalizedFile.Keywords.ReasoningKeywords,
),
TechnicalKeywords: mergeComplexityKeywordListsWithFileSnapshot(
normalizedBase.Keywords.TechnicalKeywords,
normalizedPreviousFile.Keywords.TechnicalKeywords,
normalizedFile.Keywords.TechnicalKeywords,
),
SimpleKeywords: mergeComplexityKeywordListsWithFileSnapshot(
normalizedBase.Keywords.SimpleKeywords,
normalizedPreviousFile.Keywords.SimpleKeywords,
normalizedFile.Keywords.SimpleKeywords,
),
},
ConfigHash: normalizedFile.ConfigHash,
}
if err := merged.Validate(); err != nil {
return nil, err
}
return &merged, nil
}

// DecodeComplexityAnalyzerConfig decodes raw JSON into a normalized, validated config.
func DecodeComplexityAnalyzerConfig(data []byte) (*ComplexityAnalyzerConfig, error) {
if len(data) == 0 {
Expand Down Expand Up @@ -202,3 +262,26 @@ func mergeComplexityKeywordLists(base, overlay []string) []string {
values = append(values, overlay...)
return normalizeComplexityKeywordList(values)
}

func mergeComplexityKeywordListsWithFileSnapshot(current []string, previousFile []string, file []string) []string {
normalizedFile := normalizeComplexityKeywordList(file)
fileSet := make(map[string]struct{}, len(normalizedFile))
for _, value := range normalizedFile {
fileSet[value] = struct{}{}
}

removedFromFile := make(map[string]struct{}, len(previousFile))
for _, value := range normalizeComplexityKeywordList(previousFile) {
if _, stillInFile := fileSet[value]; !stillInFile {
removedFromFile[value] = struct{}{}
}
}

preserved := make([]string, 0, len(current))
for _, value := range normalizeComplexityKeywordList(current) {
if _, removed := removedFromFile[value]; !removed {
preserved = append(preserved, value)
}
}
return mergeComplexityKeywordLists(preserved, normalizedFile)
}
55 changes: 55 additions & 0 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,17 @@ func (s *RDBConfigStore) UpdateConfig(ctx context.Context, config *tables.TableG
return txDB.WithContext(ctx).Save(config).Error
}

// DeleteConfig deletes a specific config key from the database.
func (s *RDBConfigStore) DeleteConfig(ctx context.Context, key string, tx ...*gorm.DB) error {
var txDB *gorm.DB
if len(tx) > 0 && tx[0] != nil {
txDB = tx[0]
} else {
txDB = s.DB()
}
return txDB.WithContext(ctx).Delete(&tables.TableGovernanceConfig{}, "key = ?", key).Error
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// GetModelPrices retrieves all model pricing records from the database.
func (s *RDBConfigStore) GetModelPrices(ctx context.Context) ([]tables.TableModelPricing, error) {
var modelPrices []tables.TableModelPricing
Expand Down Expand Up @@ -5065,6 +5076,36 @@ func (s *RDBConfigStore) UpdateComplexityAnalyzerConfig(ctx context.Context, con
return s.updateComplexityAnalyzerConfigTx(ctx, normalized, tx...)
}

// ApplyComplexityAnalyzerConfigFromFile persists a file-reconciled analyzer config and its file snapshot.
func (s *RDBConfigStore) ApplyComplexityAnalyzerConfigFromFile(ctx context.Context, config *ComplexityAnalyzerConfig, fileSnapshot *ComplexityAnalyzerConfig, tx ...*gorm.DB) error {
if config == nil {
return fmt.Errorf("complexity analyzer config is nil")
}
if fileSnapshot == nil {
return fmt.Errorf("complexity analyzer file snapshot is nil")
}

normalized := config.Normalized()
if err := normalized.Validate(); err != nil {
return err
}
if normalized.ConfigHash == "" {
return fmt.Errorf("complexity analyzer config hash is required for file apply")
}
normalizedSnapshot := fileSnapshot.Normalized()
if err := normalizedSnapshot.Validate(); err != nil {
return err
}
normalizedSnapshot.ConfigHash = ""

if len(tx) == 0 || tx[0] == nil {
return s.DB().WithContext(ctx).Transaction(func(transaction *gorm.DB) error {
return s.applyComplexityAnalyzerConfigFromFileTx(ctx, normalized, normalizedSnapshot, transaction)
})
}
return s.applyComplexityAnalyzerConfigFromFileTx(ctx, normalized, normalizedSnapshot, tx...)
}

func (s *RDBConfigStore) updateComplexityAnalyzerConfigTx(ctx context.Context, normalized ComplexityAnalyzerConfig, tx ...*gorm.DB) error {
raw, err := json.Marshal(normalized)
if err != nil {
Expand All @@ -5085,6 +5126,20 @@ func (s *RDBConfigStore) updateComplexityAnalyzerConfigTx(ctx context.Context, n
}, tx...)
}

func (s *RDBConfigStore) applyComplexityAnalyzerConfigFromFileTx(ctx context.Context, normalized ComplexityAnalyzerConfig, snapshot ComplexityAnalyzerConfig, tx ...*gorm.DB) error {
if err := s.updateComplexityAnalyzerConfigTx(ctx, normalized, tx...); err != nil {
return err
}
rawSnapshot, err := json.Marshal(snapshot)
if err != nil {
return fmt.Errorf("failed to marshal complexity analyzer config snapshot: %w", err)
}
return s.UpdateConfig(ctx, &tables.TableGovernanceConfig{
Key: tables.ConfigComplexityAnalyzerConfigSnapshotKey,
Value: string(rawSnapshot),
}, tx...)
}

// GetAuthConfig retrieves the auth configuration from the database.
func (s *RDBConfigStore) GetAuthConfig(ctx context.Context) (*AuthConfig, error) {
var username *string
Expand Down
64 changes: 63 additions & 1 deletion framework/configstore/rdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestRDBConfigStore_UpdateComplexityAnalyzerConfigPreservesExistingHashOnRun

fileConfig := testComplexityAnalyzerConfig()
fileConfig.ConfigHash = "file-hash-1"
require.NoError(t, store.UpdateComplexityAnalyzerConfig(ctx, fileConfig))
require.NoError(t, store.ApplyComplexityAnalyzerConfigFromFile(ctx, fileConfig, fileConfig))

runtimeConfig := testComplexityAnalyzerConfig()
runtimeConfig.TierBoundaries.SimpleMedium = 0.12
Expand All @@ -125,6 +125,13 @@ func TestRDBConfigStore_UpdateComplexityAnalyzerConfigPreservesExistingHashOnRun
require.NotNil(t, got)
assert.Equal(t, 0.12, got.TierBoundaries.SimpleMedium)
assert.Equal(t, "file-hash-1", got.ConfigHash)

snapshot, err := store.GetConfig(ctx, tables.ConfigComplexityAnalyzerConfigSnapshotKey)
require.NoError(t, err)
require.NotNil(t, snapshot)
var snapshotCfg ComplexityAnalyzerConfig
require.NoError(t, json.Unmarshal([]byte(snapshot.Value), &snapshotCfg))
assert.Equal(t, 0.10, snapshotCfg.TierBoundaries.SimpleMedium)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestRDBConfigStore_GetGovernanceConfigIncludesComplexityAnalyzerConfig(t *testing.T) {
Expand Down Expand Up @@ -182,6 +189,61 @@ func TestMergeComplexityAnalyzerConfigAddsKeywordsAndOverlaysBoundaries(t *testi
assert.Equal(t, []string{"hello", "thanks"}, merged.Keywords.SimpleKeywords)
}

func TestMergeComplexityAnalyzerConfigWithFileSnapshotRemovesFileOwnedKeywords(t *testing.T) {
tests := []struct {
name string
current []string
previousFile []string
file []string
want []string
}{
{
name: "removes file-owned keyword removed from file",
current: []string{"api", "function", "ui-code"},
previousFile: []string{"api", "function"},
file: []string{"function", "new-file-code"},
want: []string{"function", "new-file-code", "ui-code"},
},
{
name: "keeps current-only keyword when file is unchanged",
current: []string{"api", "ui-code"},
previousFile: []string{"api"},
file: []string{"api"},
want: []string{"api", "ui-code"},
},
{
name: "removes all stale file-owned keywords and adds new file keyword",
current: []string{"api", "function", "ui-code"},
previousFile: []string{"api", "function"},
file: []string{"new-file-code"},
want: []string{"new-file-code", "ui-code"},
},
{
name: "normalizes case and duplicates",
current: []string{"API", "api", "Ui-Code"},
previousFile: []string{"API"},
file: []string{"api", "GraphQL", "graphql"},
want: []string{"api", "graphql", "ui-code"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
current := testComplexityAnalyzerConfig()
current.Keywords.CodeKeywords = tt.current
previousFile := testComplexityAnalyzerConfig()
previousFile.Keywords.CodeKeywords = tt.previousFile
file := testComplexityAnalyzerConfig()
file.Keywords.CodeKeywords = tt.file

merged, err := MergeComplexityAnalyzerConfigWithFileSnapshot(current, previousFile, file)
require.NoError(t, err)
require.NotNil(t, merged)
assert.Equal(t, tt.want, merged.Keywords.CodeKeywords)
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestRDBConfigStore_UpdateComplexityAnalyzerConfigRejectsInvalidConfig(t *testing.T) {
store := setupRDBTestStore(t)
ctx := context.Background()
Expand Down
19 changes: 11 additions & 8 deletions framework/configstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ type MCPClientsQueryParams struct {
// parameters for MCP library catalog queries. All fields are optional — an
// empty struct returns the first default-sized page ordered by name.
type MCPLibraryQueryParams struct {
Limit int
Offset int
Search string // matches name/description/publisher (case-insensitive)
Categories []string // exact category filter(s), OR semantics
Limit int
Offset int
Search string // matches name/description/publisher (case-insensitive)
Categories []string // exact category filter(s), OR semantics
ConnectionTypes []string // exact connection_type filter(s) (http | stdio | sse)
AuthTypes []string // exact auth_type filter(s)
Tags []string // match rows carrying any of these tags
SortBy string // name, category, publisher, created_at, updated_at (default: name)
Order string // asc, desc (default: asc)
AuthTypes []string // exact auth_type filter(s)
Tags []string // match rows carrying any of these tags
SortBy string // name, category, publisher, created_at, updated_at (default: name)
Order string // asc, desc (default: asc)
}

// MCPLibraryFilterData holds the distinct facet values surfaced by the filter
Expand Down Expand Up @@ -215,10 +215,13 @@ type ConfigStore interface {
// Config CRUD
GetConfig(ctx context.Context, key string) (*tables.TableGovernanceConfig, error)
UpdateConfig(ctx context.Context, config *tables.TableGovernanceConfig, tx ...*gorm.DB) error
DeleteConfig(ctx context.Context, key string, tx ...*gorm.DB) error
// GetComplexityAnalyzerConfig retrieves the persisted analyzer config, if configured.
GetComplexityAnalyzerConfig(ctx context.Context) (*ComplexityAnalyzerConfig, error)
// UpdateComplexityAnalyzerConfig persists the normalized analyzer config.
UpdateComplexityAnalyzerConfig(ctx context.Context, config *ComplexityAnalyzerConfig, tx ...*gorm.DB) error
// ApplyComplexityAnalyzerConfigFromFile persists the reconciled analyzer config and file snapshot.
ApplyComplexityAnalyzerConfigFromFile(ctx context.Context, config *ComplexityAnalyzerConfig, fileSnapshot *ComplexityAnalyzerConfig, tx ...*gorm.DB) error

// Plugins CRUD
GetPlugins(ctx context.Context) ([]*tables.TablePlugin, error)
Expand Down
6 changes: 4 additions & 2 deletions framework/configstore/tables/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ const (
ConfigComplexityAnalyzerConfigKey = "complexity_analyzer_config"
// ConfigComplexityAnalyzerConfigHashKey stores the last config.json hash synced for the analyzer config.
ConfigComplexityAnalyzerConfigHashKey = "complexity_analyzer_config_hash"
ConfigRestartRequiredKey = "restart_required"
ConfigHeaderFilterKey = "header_filter_config"
// ConfigComplexityAnalyzerConfigSnapshotKey stores the last config.json analyzer config used for split-mode reconciliation.
ConfigComplexityAnalyzerConfigSnapshotKey = "complexity_analyzer_config_snapshot"
ConfigRestartRequiredKey = "restart_required"
ConfigHeaderFilterKey = "header_filter_config"
)

// Keys for the ClientConfig.MetadataJSON blob.
Expand Down
Loading
Loading