diff --git a/src/infra/config/configNormalizers.ts b/src/infra/config/configNormalizers.ts new file mode 100644 index 0000000..455fe92 --- /dev/null +++ b/src/infra/config/configNormalizers.ts @@ -0,0 +1,79 @@ +/** + * Shared normalizer/denormalizer functions for config snake_case <-> camelCase conversion. + * + * Used by both globalConfig.ts and projectConfig.ts. + */ + +import type { ProviderPermissionProfiles } from '../../core/models/provider-profiles.js'; +import type { PieceOverrides } from '../../core/models/persisted-global-config.js'; + +export function normalizeProviderProfiles( + raw: Record }> | undefined, +): ProviderPermissionProfiles | undefined { + if (!raw) return undefined; + + const entries = Object.entries(raw).map(([provider, profile]) => [provider, { + defaultPermissionMode: profile.default_permission_mode, + movementPermissionOverrides: profile.movement_permission_overrides, + }]); + + return Object.fromEntries(entries) as ProviderPermissionProfiles; +} + +export function denormalizeProviderProfiles( + profiles: ProviderPermissionProfiles | undefined, +): Record }> | undefined { + if (!profiles) return undefined; + const entries = Object.entries(profiles); + if (entries.length === 0) return undefined; + + return Object.fromEntries(entries.map(([provider, profile]) => [provider, { + default_permission_mode: profile.defaultPermissionMode, + ...(profile.movementPermissionOverrides + ? { movement_permission_overrides: profile.movementPermissionOverrides } + : {}), + }])) as Record }>; +} + +export function normalizePieceOverrides( + raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined, +): PieceOverrides | undefined { + if (!raw) return undefined; + return { + qualityGates: raw.quality_gates, + qualityGatesEditOnly: raw.quality_gates_edit_only, + movements: raw.movements + ? Object.fromEntries( + Object.entries(raw.movements).map(([name, override]) => [ + name, + { qualityGates: override.quality_gates }, + ]) + ) + : undefined, + }; +} + +export function denormalizePieceOverrides( + overrides: PieceOverrides | undefined, +): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined { + if (!overrides) return undefined; + const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; + if (overrides.qualityGates !== undefined) { + result.quality_gates = overrides.qualityGates; + } + if (overrides.qualityGatesEditOnly !== undefined) { + result.quality_gates_edit_only = overrides.qualityGatesEditOnly; + } + if (overrides.movements) { + result.movements = Object.fromEntries( + Object.entries(overrides.movements).map(([name, override]) => { + const movementOverride: { quality_gates?: string[] } = {}; + if (override.qualityGates !== undefined) { + movementOverride.quality_gates = override.qualityGates; + } + return [name, movementOverride]; + }) + ); + } + return Object.keys(result).length > 0 ? result : undefined; +} diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index 778170b..658b3eb 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -10,12 +10,17 @@ import { isAbsolute } from 'node:path'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { GlobalConfigSchema } from '../../../core/models/index.js'; import type { Language } from '../../../core/models/index.js'; -import type { PersistedGlobalConfig, PersonaProviderEntry, PieceOverrides } from '../../../core/models/persisted-global-config.js'; -import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js'; +import type { PersistedGlobalConfig, PersonaProviderEntry } from '../../../core/models/persisted-global-config.js'; import { normalizeConfigProviderReference, type ConfigProviderReference, } from '../providerReference.js'; +import { + normalizeProviderProfiles, + denormalizeProviderProfiles, + normalizePieceOverrides, + denormalizePieceOverrides, +} from '../configNormalizers.js'; import { getGlobalConfigPath } from '../paths.js'; import { DEFAULT_LANGUAGE } from '../../../shared/constants.js'; import { parseProviderModel } from '../../../shared/utils/providerModel.js'; @@ -112,79 +117,6 @@ function normalizePersonaProviders( ); } -function normalizeProviderProfiles( - raw: Record }> | undefined, -): ProviderPermissionProfiles | undefined { - if (!raw) return undefined; - - const entries = Object.entries(raw).map(([provider, profile]) => [provider, { - defaultPermissionMode: profile.default_permission_mode, - movementPermissionOverrides: profile.movement_permission_overrides, - }]); - - return Object.fromEntries(entries) as ProviderPermissionProfiles; -} - -function denormalizeProviderProfiles( - profiles: ProviderPermissionProfiles | undefined, -): Record }> | undefined { - if (!profiles) return undefined; - const entries = Object.entries(profiles); - if (entries.length === 0) return undefined; - - return Object.fromEntries(entries.map(([provider, profile]) => [provider, { - default_permission_mode: profile.defaultPermissionMode, - ...(profile.movementPermissionOverrides - ? { movement_permission_overrides: profile.movementPermissionOverrides } - : {}), - }])) as Record }>; -} - -/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */ -function normalizePieceOverrides( - raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined, -): PieceOverrides | undefined { - if (!raw) return undefined; - return { - qualityGates: raw.quality_gates, - qualityGatesEditOnly: raw.quality_gates_edit_only, - movements: raw.movements - ? Object.fromEntries( - Object.entries(raw.movements).map(([name, override]) => [ - name, - { qualityGates: override.quality_gates }, - ]) - ) - : undefined, - }; -} - -/** Denormalize piece_overrides from camelCase (internal) to snake_case (YAML) */ -function denormalizePieceOverrides( - overrides: PieceOverrides | undefined, -): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined { - if (!overrides) return undefined; - const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; - if (overrides.qualityGates !== undefined) { - result.quality_gates = overrides.qualityGates; - } - if (overrides.qualityGatesEditOnly !== undefined) { - result.quality_gates_edit_only = overrides.qualityGatesEditOnly; - } - if (overrides.movements) { - result.movements = Object.fromEntries( - Object.entries(overrides.movements).map(([name, override]) => { - const movementOverride: { quality_gates?: string[] } = {}; - if (override.qualityGates !== undefined) { - movementOverride.quality_gates = override.qualityGates; - } - return [name, movementOverride]; - }) - ); - } - return Object.keys(result).length > 0 ? result : undefined; -} - /** * Manages global configuration loading and caching. * Singleton — use GlobalConfigManager.getInstance(). diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index ce5beed..c86bd40 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -10,13 +10,18 @@ import { parse, stringify } from 'yaml'; import { ProjectConfigSchema } from '../../../core/models/index.js'; import { copyProjectResourcesToDir } from '../../resources/index.js'; import type { ProjectLocalConfig } from '../types.js'; -import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js'; -import type { AnalyticsConfig, PieceOverrides, SubmoduleSelection } from '../../../core/models/persisted-global-config.js'; +import type { AnalyticsConfig, SubmoduleSelection } from '../../../core/models/persisted-global-config.js'; import { applyProjectConfigEnvOverrides } from '../env/config-env-overrides.js'; import { normalizeConfigProviderReference, type ConfigProviderReference, } from '../providerReference.js'; +import { + normalizeProviderProfiles, + denormalizeProviderProfiles, + normalizePieceOverrides, + denormalizePieceOverrides, +} from '../configNormalizers.js'; import { invalidateResolvedConfigCache } from '../resolutionCache.js'; export type { ProjectLocalConfig } from '../types.js'; @@ -86,28 +91,6 @@ function getConfigPath(projectDir: string): string { return join(getConfigDir(projectDir), 'config.yaml'); } -function normalizeProviderProfiles(raw: Record }> | undefined): ProviderPermissionProfiles | undefined { - if (!raw) return undefined; - return Object.fromEntries( - Object.entries(raw).map(([provider, profile]) => [provider, { - defaultPermissionMode: profile.default_permission_mode, - movementPermissionOverrides: profile.movement_permission_overrides, - }]), - ) as ProviderPermissionProfiles; -} - -function denormalizeProviderProfiles(profiles: ProviderPermissionProfiles | undefined): Record }> | undefined { - if (!profiles) return undefined; - const entries = Object.entries(profiles); - if (entries.length === 0) return undefined; - return Object.fromEntries(entries.map(([provider, profile]) => [provider, { - default_permission_mode: profile.defaultPermissionMode, - ...(profile.movementPermissionOverrides - ? { movement_permission_overrides: profile.movementPermissionOverrides } - : {}), - }])) as Record }>; -} - function normalizeAnalytics(raw: Record | undefined): AnalyticsConfig | undefined { if (!raw) return undefined; const enabled = typeof raw.enabled === 'boolean' ? raw.enabled : undefined; @@ -133,51 +116,6 @@ function denormalizeAnalytics(config: AnalyticsConfig | undefined): Record 0 ? raw : undefined; } -/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */ -function normalizePieceOverrides( - raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined, -): PieceOverrides | undefined { - if (!raw) return undefined; - return { - qualityGates: raw.quality_gates, - qualityGatesEditOnly: raw.quality_gates_edit_only, - movements: raw.movements - ? Object.fromEntries( - Object.entries(raw.movements).map(([name, override]) => [ - name, - { qualityGates: override.quality_gates }, - ]) - ) - : undefined, - }; -} - -/** Denormalize piece_overrides from camelCase (internal) to snake_case (YAML) */ -function denormalizePieceOverrides( - overrides: PieceOverrides | undefined, -): { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } | undefined { - if (!overrides) return undefined; - const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record } = {}; - if (overrides.qualityGates !== undefined) { - result.quality_gates = overrides.qualityGates; - } - if (overrides.qualityGatesEditOnly !== undefined) { - result.quality_gates_edit_only = overrides.qualityGatesEditOnly; - } - if (overrides.movements) { - result.movements = Object.fromEntries( - Object.entries(overrides.movements).map(([name, override]) => { - const movementOverride: { quality_gates?: string[] } = {}; - if (override.qualityGates !== undefined) { - movementOverride.quality_gates = override.qualityGates; - } - return [name, movementOverride]; - }) - ); - } - return Object.keys(result).length > 0 ? result : undefined; -} - /** * Load project configuration from .takt/config.yaml */