refactor: 共有ノーマライザを configNormalizers.ts に抽出

globalConfig.ts と projectConfig.ts に重複していた
normalizeProviderProfiles / denormalizeProviderProfiles /
normalizePieceOverrides / denormalizePieceOverrides を
configNormalizers.ts に集約した。
This commit is contained in:
nrslib 2026-03-04 01:30:02 +09:00
parent ecf0b02684
commit 8aa79d909c
3 changed files with 93 additions and 144 deletions

View File

@ -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<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
export function normalizePieceOverrides(
raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | 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<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
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;
}

View File

@ -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<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
/** Normalize piece_overrides from snake_case (YAML) to camelCase (internal) */
function normalizePieceOverrides(
raw: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | 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<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
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().

View File

@ -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<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }> | 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<string, { default_permission_mode: string; movement_permission_overrides?: Record<string, string> }>;
}
function normalizeAnalytics(raw: Record<string, unknown> | 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<strin
return Object.keys(raw).length > 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<string, { quality_gates?: string[] }> } | 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<string, { quality_gates?: string[] }> } | undefined {
if (!overrides) return undefined;
const result: { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } = {};
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
*/