xiaohongshu
xiaohongshu
config-schema-migrator
Expert at evolving Pydantic configuration schemas with backward compatibility and automated migrations
Full skill instructions
Activate this skill when you need to:
adapters Section While Keeping cli_toolsProblem: You need to add a new config section (adapters) to replace an old one (cli_tools) without breaking existing configs.
Solution Pattern (from models/config.py):
from typing import Optional
from pydantic import BaseModel
import warnings
class Config(BaseModel):
"""Root configuration model."""
# New section (preferred)
adapters: Optional[dict[str, AdapterConfig]] = None
# Legacy section (deprecated)
cli_tools: Optional[dict[str, CLIToolConfig]] = None
def model_post_init(self, __context):
"""Post-initialization validation."""
# Ensure at least one section exists
if self.adapters is None and self.cli_tools is None:
raise ValueError(
"Configuration must include either 'adapters' or 'cli_tools' section"
)
# Emit deprecation warning for old section
if self.cli_tools is not None and self.adapters is None:
warnings.warn(
"The 'cli_tools' configuration section is deprecated. "
"Please migrate to 'adapters' section with explicit 'type' field. "
"See migration guide: docs/migration/cli_tools_to_adapters.md",
DeprecationWarning,
stacklevel=2,
)
Key Techniques:
Optional for both old and new sectionsmodel_post_init() that at least one existsDeprecationWarning when old section is usedProblem: You have config objects that can be one of several types (CLI adapter, HTTP adapter, etc).
Solution Pattern (from models/config.py):
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
class CLIAdapterConfig(BaseModel):
"""Configuration for CLI-based adapter."""
type: Literal["cli"] = "cli"
command: str
args: list[str]
timeout: int = 60
class HTTPAdapterConfig(BaseModel):
"""Configuration for HTTP-based adapter."""
type: Literal["http"] = "http"
base_url: str
api_key: Optional[str] = None
timeout: int = 60
max_retries: int = 3
# Discriminated union - Pydantic uses 'type' field to determine which model
AdapterConfig = Annotated[
Union[CLIAdapterConfig, HTTPAdapterConfig],
Field(discriminator="type")
]
YAML Usage:
adapters:
claude:
type: cli # Discriminator field
command: "claude"
args: ["-p", "{prompt}"]
timeout: 60
ollama:
type: http # Different type triggers HTTPAdapterConfig
base_url: "http://localhost:11434"
timeout: 120
Key Techniques:
Literal["value"] for discriminator field with default valueAnnotated[Union[...], Field(discriminator="type")]type fieldProblem: You need to inject secrets from environment variables without hardcoding in YAML.
Solution Pattern (from models/config.py):
import os
import re
from pydantic import BaseModel, field_validator
class HTTPAdapterConfig(BaseModel):
"""Configuration for HTTP-based adapter."""
base_url: str
api_key: Optional[str] = None
@field_validator("api_key", "base_url")
@classmethod
def resolve_env_vars(cls, v: Optional[str], info) -> Optional[str]:
"""Resolve ${ENV_VAR} references in string fields."""
if v is None:
return v
# Pattern: ${VAR_NAME}
pattern = r"\$\{([^}]+)\}"
is_api_key = info.field_name == "api_key"
def replacer(match):
env_var = match.group(1)
value = os.getenv(env_var)
if value is None:
# For optional fields like api_key, use sentinel
if is_api_key:
return "__MISSING_API_KEY__"
# For required fields, raise error
raise ValueError(
f"Environment variable '{env_var}' is not set. "
f"Required for configuration."
)
return value
result = re.sub(pattern, replacer, v)
# If api_key has sentinel marker, return None (graceful degradation)
if is_api_key and "__MISSING_API_KEY__" in result:
return None
return result
YAML Usage:
adapters:
openrouter:
type: http
base_url: "https://openrouter.ai/api/v1"
api_key: "${OPENROUTER_API_KEY}" # Resolved from environment
Key Techniques:
@field_validator on fields that may contain env varsinfo.field_name to customize behavior per fieldr"\$\{([^}]+)\}" to find ${VAR_NAME}None if env var missingValueError if env var missing.env file first with dotenv.load_dotenv() in load_config()Problem: You need to migrate existing config files from old format to new format automatically.
Solution Pattern (from scripts/migrate_config.py):
#!/usr/bin/env python3
"""
Migration script: cli_tools → adapters
Migrates config.yaml from legacy cli_tools format to new adapters format
with explicit type fields.
Usage:
python scripts/migrate_config.py [path/to/config.yaml]
"""
import shutil
import sys
from pathlib import Path
from typing import Any, Dict
import yaml
def migrate_config_dict(config: Dict[str, Any]) -> Dict[str, Any]:
"""
Migrate config dictionary from cli_tools to adapters format.
Returns:
Migrated config with adapters section
"""
# If already migrated, return as-is
if "adapters" in config and "cli_tools" not in config:
print("Info: Config already migrated (has 'adapters' section)")
return config
# If no cli_tools, nothing to migrate
if "cli_tools" not in config:
print("Warning: No 'cli_tools' section found, nothing to migrate")
return config
# Create new config with adapters
migrated = config.copy()
# Transform cli_tools → adapters
adapters = {}
for name, cli_config in config["cli_tools"].items():
adapters[name] = {
"type": "cli", # Add explicit type discriminator
"command": cli_config["command"],
"args": cli_config["args"],
"timeout": cli_config["timeout"],
}
migrated["adapters"] = adapters
del migrated["cli_tools"]
print(f"Success: Migrated {len(adapters)} CLI tools to adapters format")
return migrated
def migrate_config_file(path: str) -> None:
"""
Migrate config file from cli_tools to adapters format.
Creates a backup at {path}.bak before modifying.
"""
config_path = Path(path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
# Create backup BEFORE modifying
backup_path = Path(f"{path}.bak")
shutil.copy2(config_path, backup_path)
print(f"Created backup: {backup_path}")
# Load config
with open(config_path, "r") as f:
config = yaml.safe_load(f)
# Migrate
migrated = migrate_config_dict(config)
# Write migrated config
with open(config_path, "w") as f:
yaml.dump(migrated, f, default_flow_style=False, sort_keys=False)
print(f"Migrated config written to: {config_path}")
print(f"\nInfo: Review the changes and delete {backup_path} when satisfied.")
def main():
"""Main entry point."""
config_path = sys.argv[1] if len(sys.argv) > 1 else "config.yaml"
print(f"Migrating config: {config_path}")
print("-" * 50)
try:
migrate_config_file(config_path)
print("\nMigration complete!")
print("\nNext steps:")
print("1. Review the migrated config.yaml")
print("2. Test loading: python -c 'from models.config import load_config; load_config()'")
print("3. Delete backup if satisfied: rm config.yaml.bak")
except Exception as e:
print(f"\nError: Migration failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Key Techniques:
shutil.copy2)migrate_config_dict() for logic, migrate_config_file() for I/Osort_keys=False to preserve key orderProblem: You need to resolve relative paths and environment variables for config fields.
Solution Pattern (from models/config.py):
import os
import re
from pathlib import Path
from pydantic import BaseModel, field_validator
class DecisionGraphConfig(BaseModel):
"""Configuration for decision graph memory."""
db_path: str = "decision_graph.db"
@field_validator("db_path")
@classmethod
def resolve_db_path(cls, v: str) -> str:
"""
Resolve db_path to absolute path relative to project root.
Processing steps:
1. Resolve ${ENV_VAR} environment variable references
2. Convert relative paths to absolute paths relative to project root
3. Keep absolute paths unchanged
4. Return normalized absolute path as string
Examples:
"decision_graph.db" → "/path/to/project/decision_graph.db"
"/tmp/foo.db" → "/tmp/foo.db" (unchanged)
"${DATA_DIR}/graph.db" → "/var/data/graph.db" (if DATA_DIR=/var/data)
"""
# Step 1: Resolve environment variables
pattern = r"\$\{([^}]+)\}"
def replacer(match):
env_var = match.group(1)
value = os.getenv(env_var)
if value is None:
raise ValueError(
f"Environment variable '{env_var}' is not set. "
f"Required for db_path configuration."
)
return value
resolved = re.sub(pattern, replacer, v)
# Step 2: Convert to Path object
path = Path(resolved)
# Step 3: If relative, make it relative to project root
if not path.is_absolute():
# This file is at: project_root/models/config.py
# Project root is two levels up from this file
project_root = Path(__file__).parent.parent
path = (project_root / path).resolve()
# Step 4: Return as string (normalized, absolute)
return str(path)
Key Techniques:
Path(__file__).parent.parent to find project rootsimilarity_threshold in Favor of tier_boundariesProblem: You need to replace a single config field with a more complex structure.
Solution Pattern (from models/config.py):
from pydantic import BaseModel, Field, field_validator
class DecisionGraphConfig(BaseModel):
"""Configuration for decision graph memory."""
# DEPRECATED field (kept for backward compatibility)
similarity_threshold: float = Field(
0.7,
ge=0.0,
le=1.0,
description="DEPRECATED: Use tier_boundaries instead.",
)
# NEW field (preferred)
tier_boundaries: dict[str, float] = Field(
default_factory=lambda: {"strong": 0.75, "moderate": 0.60},
description="Similarity score boundaries for tiered injection"
)
@field_validator("tier_boundaries")
@classmethod
def validate_tier_boundaries(cls, v: dict[str, float]) -> dict[str, float]:
"""Validate tier boundaries: strong > moderate > 0."""
if not isinstance(v, dict) or "strong" not in v or "moderate" not in v:
raise ValueError("tier_boundaries must have 'strong' and 'moderate' keys")
if not (0.0 < v["moderate"] < v["strong"] <= 1.0):
raise ValueError(
f"tier_boundaries must satisfy: 0 < moderate ({v['moderate']}) "
f"< strong ({v['strong']}) <= 1"
)
return v
YAML Usage:
decision_graph:
# OLD (still works, but deprecated in field description)
similarity_threshold: 0.7
# NEW (preferred)
tier_boundaries:
strong: 0.75
moderate: 0.60
Key Techniques:
@field_validatorUnit Test the Migration Logic:
def test_migrate_config_dict():
"""Test migration transforms cli_tools to adapters."""
old_config = {
"cli_tools": {
"claude": {
"command": "claude",
"args": ["-p", "{prompt}"],
"timeout": 60
}
}
}
migrated = migrate_config_dict(old_config)
assert "adapters" in migrated
assert "cli_tools" not in migrated
assert migrated["adapters"]["claude"]["type"] == "cli"
assert migrated["adapters"]["claude"]["command"] == "claude"
Test Idempotency:
def test_migrate_idempotent():
"""Test migrating already-migrated config is safe."""
already_migrated = {
"adapters": {
"claude": {"type": "cli", "command": "claude"}
}
}
result = migrate_config_dict(already_migrated)
assert result == already_migrated # No changes
Manual Testing Steps:
# 1. Create test config
cp config.yaml config.test.yaml
# 2. Run migration
python scripts/migrate_config.py config.test.yaml
# 3. Verify backup created
ls -la config.test.yaml.bak
# 4. Test loading migrated config
python -c "from models.config import load_config; c = load_config('config.test.yaml'); print('OK')"
# 5. Compare files
diff config.test.yaml.bak config.test.yaml
# 6. Clean up
rm config.test.yaml config.test.yaml.bak
Load-Time Validation:
# After migration, always test that config loads without errors
python -c "from models.config import load_config; load_config()"
When you need to evolve a config schema:
Optional (don't break existing configs)@field_validator for new field validationmodel_post_init()scripts/migrate_*.py with clear docstringmigrate_config_dict() for logic (testable)migrate_config_file() for I/O (backup, load, migrate, save)main() with CLI argument parsingmigrate_config_dict()Context: AI Counsel needed to support both CLI and HTTP adapters, requiring type discrimination.
Changes Made:
Schema Evolution (models/config.py):
CLIAdapterConfig and HTTPAdapterConfig with type discriminatoradapters and cli_tools both optionalcli_toolsMigration Script (scripts/migrate_config.py):
cli_tools → adapters with type: "cli"Testing:
migrate_config_dict()Documentation:
Result: Users can migrate seamlessly with one command, and old configs continue working with a warning.
| Pattern | Use Case | Key Technique |
|---|---|---|
| Optional Sections | Add new section while keeping old | Optional[T] + validation |
| Discriminated Union | Type discrimination (CLI vs HTTP) | Literal["type"] + Field(discriminator="type") |
| Env Var Substitution | Inject secrets from environment | @field_validator + regex \$\{VAR\} |
| Path Resolution | Resolve relative paths | Path(__file__).parent + resolve() |
| Deprecation Warnings | Signal old patterns | warnings.warn() in model_post_init() |
| Migration Scripts | Automate config updates | Backup + dict transform + YAML dump |
| Field Validation | Complex field constraints | @field_validator + custom logic |
/Users/harrison/Github/ai-counsel/models/config.py/Users/harrison/Github/ai-counsel/scripts/migrate_config.py/Users/harrison/Github/ai-counsel/config.yaml/Users/harrison/Github/ai-counsel/CLAUDE.mdWhen you detect config schema evolution needs, activate this skill and follow these patterns to ensure smooth, backward-compatible migrations.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience