mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
231 lines
5.6 KiB
Markdown
231 lines
5.6 KiB
Markdown
# Config Schema Versioning Guide
|
|
|
|
## Overview
|
|
|
|
PicoClaw uses a schema versioning system for `config.json` to ensure smooth upgrades as the configuration format evolves.
|
|
|
|
## Version History
|
|
|
|
### Version 1
|
|
- **Introduction**: Initial version with version field support
|
|
- **Changes**: Added `version` field to Config struct
|
|
- **Migration**: No structural changes needed for existing configs
|
|
|
|
## How It Works
|
|
|
|
### Automatic Migration
|
|
When you load a config file:
|
|
1. The system first reads the `version` field from the JSON
|
|
2. Based on the detected version, it loads the appropriate config struct (`ConfigV0`, `ConfigV1`, etc.)
|
|
3. If the loaded version is less than the latest, migrations are applied incrementally
|
|
4. The version number is updated automatically
|
|
5. The migrated config is automatically saved back to disk
|
|
|
|
### Version Field
|
|
The `version` field in `config.json` indicates the schema version:
|
|
- `0` or missing: Legacy config (no version field)
|
|
- `1`: Current version with versioning support
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"agents": {...},
|
|
...
|
|
}
|
|
```
|
|
|
|
## Adding a New Migration
|
|
|
|
When making breaking changes to the config schema:
|
|
|
|
### Step 1: Define the New Version Struct
|
|
|
|
Create a new struct for the new version if the structure changes significantly:
|
|
|
|
```go
|
|
// ConfigV2 represents version 2 config structure
|
|
type ConfigV2 struct {
|
|
Version int `json:"version"`
|
|
Agents AgentsConfig `json:"agents"`
|
|
// ... other fields with new structure
|
|
}
|
|
```
|
|
|
|
### Step 2: Update Current Config Version
|
|
|
|
```go
|
|
const CurrentConfigVersion = 2 // Increment this
|
|
```
|
|
|
|
### Step 3: Add a Loader Function
|
|
|
|
```go
|
|
// loadConfigV2 loads a version 2 config
|
|
func loadConfigV2(data []byte) (*Config, error) {
|
|
cfg := DefaultConfig()
|
|
|
|
// Parse to ConfigV2 struct
|
|
var v2 ConfigV2
|
|
if err := json.Unmarshal(data, &v2); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to current Config
|
|
cfg.Version = v2.Version
|
|
cfg.Agents = v2.Agents
|
|
// ... map other fields
|
|
|
|
return cfg, nil
|
|
}
|
|
```
|
|
|
|
### Step 4: Add Migration Logic
|
|
|
|
```go
|
|
// applyMigration applies a single migration step from fromVersion to toVersion
|
|
func applyMigration(cfg *Config, fromVersion, toVersion int) (*Config, error) {
|
|
switch toVersion {
|
|
case 1:
|
|
// Migration from version 0 to 1
|
|
return &Config{
|
|
Version: 1,
|
|
Agents: cfg.Agents,
|
|
// ... copy all fields
|
|
}, nil
|
|
case 2:
|
|
// Migration from version 1 to 2
|
|
// Example: Move or rename fields
|
|
migrated := *cfg
|
|
migrated.Version = 2
|
|
// Apply structural changes
|
|
if cfg.SomeOldField != "" {
|
|
migrated.SomeNewField = cfg.SomeOldField
|
|
}
|
|
return &migrated, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported migration target version: %d", toVersion)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 5: Update LoadConfig Switch
|
|
|
|
```go
|
|
func LoadConfig(path string) (*Config, error) {
|
|
// ... read file ...
|
|
|
|
switch versionInfo.Version {
|
|
case 0:
|
|
cfg, err = loadConfigV0(data)
|
|
case 1:
|
|
cfg, err = loadConfigV1(data)
|
|
case 2:
|
|
cfg, err = loadConfigV2(data)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
|
|
}
|
|
|
|
// ... migrate and validate ...
|
|
}
|
|
```
|
|
|
|
### Step 6: Test Your Migration
|
|
|
|
Create a test in `config_migration_test.go`:
|
|
|
|
```go
|
|
func TestMigrateV1ToV2(t *testing.T) {
|
|
// Create a version 1 config
|
|
v1Config := Config{
|
|
Version: 1,
|
|
// ... set up test data
|
|
}
|
|
|
|
// Apply migration
|
|
migrated, err := applyMigration(&v1Config, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("Migration failed: %v", err)
|
|
}
|
|
|
|
// Verify version is updated
|
|
if migrated.Version != 2 {
|
|
t.Errorf("Expected version 2, got %d", migrated.Version)
|
|
}
|
|
|
|
// Verify data is preserved/transformed correctly
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Migration Best Practices
|
|
|
|
1. **Version-Specific Structs**: Define a separate struct for each version that has structural changes
|
|
2. **Backward Compatibility**: Ensure old configs can still be loaded with their specific structs
|
|
3. **No Data Loss**: Migrations should preserve all user settings
|
|
4. **Idempotent**: Running the same migration multiple times should be safe
|
|
5. **Auto-Save**: Migrated configs are automatically saved to update the user's file
|
|
6. **Test Thoroughly**: Test with real user config files
|
|
7. **Update Defaults**: Keep `defaults.go` in sync with the latest schema
|
|
|
|
## Example Migration
|
|
|
|
### Scenario: Adding a new field with default value
|
|
|
|
Old config (version 1):
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"agents": {
|
|
"defaults": {
|
|
"max_tokens": 32768
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Migration to version 2:
|
|
```go
|
|
case 2:
|
|
migrated := *cfg
|
|
migrated.Version = 2
|
|
|
|
// Add new field with default value if not set
|
|
if migrated.Agents.Defaults.NewFeatureEnabled == false {
|
|
// Use default value
|
|
}
|
|
|
|
return &migrated, nil
|
|
```
|
|
|
|
New config (version 2):
|
|
```json
|
|
{
|
|
"version": 2,
|
|
"agents": {
|
|
"defaults": {
|
|
"max_tokens": 32768,
|
|
"new_feature_enabled": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Config Not Upgrading
|
|
- Check that `CurrentConfigVersion` is incremented
|
|
- Verify migration logic in `applyMigration()` handles the target version
|
|
- Ensure `migrateConfig()` is called in `LoadConfig()`
|
|
|
|
### Migration Errors
|
|
- Check error messages for specific migration failures
|
|
- Review migration logic for edge cases
|
|
- Ensure all required fields are properly initialized
|
|
- Verify the loader function for the source version
|
|
|
|
### Data Loss After Migration
|
|
- Ensure all fields are copied during migration
|
|
- Check that the migration doesn't overwrite values with defaults unnecessarily
|
|
- Review the conversion logic in the loader functions
|
|
|