ro 4.4.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/Gemfile.lock +42 -16
 - data/MIGRATION.md +320 -0
 - data/README.md +30 -18
 - data/a.yml +60 -0
 - data/bin/ro +10 -0
 - data/lib/ro/_lib.rb +1 -1
 - data/lib/ro/asset.rb +46 -5
 - data/lib/ro/collection.rb +51 -13
 - data/lib/ro/migrator.rb +285 -0
 - data/lib/ro/node.rb +53 -13
 - data/lib/ro/root.rb +75 -1
 - data/lib/ro/script/migrate.rb +204 -0
 - data/lib/ro/script/server.rb +1 -1
 - data/lib/ro.rb +1 -0
 - data/ro.gemspec +207 -16
 - data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
 - data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
 - data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
 - data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
 - data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
 - data/specs/001-simplify-asset-structure/data-model.md +381 -0
 - data/specs/001-simplify-asset-structure/plan.md +90 -0
 - data/specs/001-simplify-asset-structure/quickstart.md +575 -0
 - data/specs/001-simplify-asset-structure/research.md +333 -0
 - data/specs/001-simplify-asset-structure/spec.md +127 -0
 - data/specs/001-simplify-asset-structure/tasks.md +349 -0
 - data/test/fixtures/new_structure/mixed/test-json.json +5 -0
 - data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
 - data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
 - data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
 - data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
 - data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
 - data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
 - data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
 - data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
 - data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
 - data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
 - data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
 - data/test/integration/ro_integration_test.rb +165 -0
 - data/test/test_helper.rb +149 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
 - data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
 - data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
 - data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
 - data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
 - data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
 - data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
 - data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
 - data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
 - data/test/unit/asset_test.rb +90 -0
 - data/test/unit/collection_test.rb +127 -0
 - data/test/unit/migrator_test.rb +209 -0
 - data/test/unit/node_test.rb +138 -0
 - metadata +111 -18
 - /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
 - /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
 - /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
 - /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
 - /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
 - /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
 - /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
 - /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
 - /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
 - /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
 - /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
 - /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
 - /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
 - /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
 - /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
 
| 
         @@ -0,0 +1,461 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Contract: Migrator API
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            **Version**: 5.0.0
         
     | 
| 
      
 4 
     | 
    
         
            +
            **Date**: 2025-10-17
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            ## Overview
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            Defines the interface for the migration tool that converts assets from the old structure (`identifier/attributes.yml` + `identifier/assets/`) to the new structure (`identifier.yml` + `identifier/`).
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            ## Command Line Interface
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### `ro migrate [PATH] [OPTIONS]`
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            **Purpose**: Migrate a collection or entire ro directory from old to new structure.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            **Arguments**:
         
     | 
| 
      
 17 
     | 
    
         
            +
            - `PATH` (optional): Path to collection or root directory (defaults to current directory)
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            **Options**:
         
     | 
| 
      
 20 
     | 
    
         
            +
            - `--dry-run`: Preview migration without making changes
         
     | 
| 
      
 21 
     | 
    
         
            +
            - `--backup [PATH]`: Create backup before migration (defaults to `PATH.backup.TIMESTAMP`)
         
     | 
| 
      
 22 
     | 
    
         
            +
            - `--force`: Proceed even if backup exists or other warnings
         
     | 
| 
      
 23 
     | 
    
         
            +
            - `--verbose`: Show detailed progress information
         
     | 
| 
      
 24 
     | 
    
         
            +
            - `--rollback [BACKUP_PATH]`: Restore from a previous backup
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            **Examples**:
         
     | 
| 
      
 27 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 28 
     | 
    
         
            +
            # Migrate current directory (dry run)
         
     | 
| 
      
 29 
     | 
    
         
            +
            ro migrate --dry-run
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            # Migrate specific collection with backup
         
     | 
| 
      
 32 
     | 
    
         
            +
            ro migrate ./public/ro/posts --backup
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            # Migrate entire ro directory
         
     | 
| 
      
 35 
     | 
    
         
            +
            ro migrate ./public/ro --verbose
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            # Rollback a migration
         
     | 
| 
      
 38 
     | 
    
         
            +
            ro migrate --rollback ./public/ro.backup.20250117-120000
         
     | 
| 
      
 39 
     | 
    
         
            +
            ```
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            **Exit Codes**:
         
     | 
| 
      
 42 
     | 
    
         
            +
            - `0`: Success (all nodes migrated)
         
     | 
| 
      
 43 
     | 
    
         
            +
            - `1`: Partial success (some nodes failed, see log)
         
     | 
| 
      
 44 
     | 
    
         
            +
            - `2`: Fatal error (migration aborted, no changes made)
         
     | 
| 
      
 45 
     | 
    
         
            +
            - `3`: Validation error (invalid structure, cannot migrate)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            ---
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            ## Programmatic API
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            ### `Ro::Migrator.new(path, options = {})`
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            **Purpose**: Create a migrator instance for a given path.
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            **Parameters**:
         
     | 
| 
      
 56 
     | 
    
         
            +
            - `path` (String | Pathname): Path to collection or root directory
         
     | 
| 
      
 57 
     | 
    
         
            +
            - `options` (Hash, optional):
         
     | 
| 
      
 58 
     | 
    
         
            +
              - `:dry_run` (Boolean): Preview mode (default: false)
         
     | 
| 
      
 59 
     | 
    
         
            +
              - `:backup` (Boolean | String): Create backup, optionally at specific path (default: false)
         
     | 
| 
      
 60 
     | 
    
         
            +
              - `:force` (Boolean): Skip safety checks (default: false)
         
     | 
| 
      
 61 
     | 
    
         
            +
              - `:verbose` (Boolean): Enable detailed logging (default: false)
         
     | 
| 
      
 62 
     | 
    
         
            +
              - `:logger` (Logger): Custom logger instance (default: STDOUT)
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
            **Returns**: `Ro::Migrator` instance
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 67 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 68 
     | 
    
         
            +
            migrator = Ro::Migrator.new('./public/ro/posts', dry_run: true, verbose: true)
         
     | 
| 
      
 69 
     | 
    
         
            +
            ```
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            ---
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            ### `#migrate` → Ro::MigrationResult
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
            **Purpose**: Execute the migration.
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
            **Returns**: `Ro::MigrationResult` object with:
         
     | 
| 
      
 78 
     | 
    
         
            +
              - `#success?` (Boolean): Whether migration completed successfully
         
     | 
| 
      
 79 
     | 
    
         
            +
              - `#total_nodes` (Integer): Total nodes processed
         
     | 
| 
      
 80 
     | 
    
         
            +
              - `#migrated_nodes` (Integer): Successfully migrated nodes
         
     | 
| 
      
 81 
     | 
    
         
            +
              - `#failed_nodes` (Integer): Nodes that failed to migrate
         
     | 
| 
      
 82 
     | 
    
         
            +
              - `#skipped_nodes` (Integer): Nodes already in new structure (skipped)
         
     | 
| 
      
 83 
     | 
    
         
            +
              - `#errors` (Array<Hash>): Error details for failed nodes
         
     | 
| 
      
 84 
     | 
    
         
            +
              - `#backup_path` (Pathname): Path to backup (if created)
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            **Raises**:
         
     | 
| 
      
 87 
     | 
    
         
            +
            - `Ro::MigrationError`: If fatal error occurs during migration
         
     | 
| 
      
 88 
     | 
    
         
            +
            - `Ro::ValidationError`: If path is invalid or structure is ambiguous
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 91 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 92 
     | 
    
         
            +
            migrator = Ro::Migrator.new('./public/ro/posts', backup: true)
         
     | 
| 
      
 93 
     | 
    
         
            +
            result = migrator.migrate
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
            if result.success?
         
     | 
| 
      
 96 
     | 
    
         
            +
              puts "Migrated #{result.migrated_nodes} nodes successfully"
         
     | 
| 
      
 97 
     | 
    
         
            +
            else
         
     | 
| 
      
 98 
     | 
    
         
            +
              puts "Migration failed: #{result.errors.count} errors"
         
     | 
| 
      
 99 
     | 
    
         
            +
              result.errors.each do |error|
         
     | 
| 
      
 100 
     | 
    
         
            +
                puts "#{error[:node_id]}: #{error[:message]}"
         
     | 
| 
      
 101 
     | 
    
         
            +
              end
         
     | 
| 
      
 102 
     | 
    
         
            +
            end
         
     | 
| 
      
 103 
     | 
    
         
            +
            ```
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
            ---
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            ### `#validate` → Boolean
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            **Purpose**: Validate that the path can be migrated without actually migrating.
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
            **Returns**: Boolean (true if valid, false otherwise)
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            **Checks**:
         
     | 
| 
      
 114 
     | 
    
         
            +
            - Path exists and is readable
         
     | 
| 
      
 115 
     | 
    
         
            +
            - Path contains nodes in old structure
         
     | 
| 
      
 116 
     | 
    
         
            +
            - No duplicate node IDs
         
     | 
| 
      
 117 
     | 
    
         
            +
            - No permission issues
         
     | 
| 
      
 118 
     | 
    
         
            +
            - Sufficient disk space (if backup enabled)
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 121 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 122 
     | 
    
         
            +
            migrator = Ro::Migrator.new('./public/ro/posts')
         
     | 
| 
      
 123 
     | 
    
         
            +
            if migrator.validate
         
     | 
| 
      
 124 
     | 
    
         
            +
              puts "Ready to migrate"
         
     | 
| 
      
 125 
     | 
    
         
            +
            else
         
     | 
| 
      
 126 
     | 
    
         
            +
              puts "Validation failed: #{migrator.validation_errors.join(', ')}"
         
     | 
| 
      
 127 
     | 
    
         
            +
            end
         
     | 
| 
      
 128 
     | 
    
         
            +
            ```
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            ---
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
            ### `#preview` → Array<Hash>
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
            **Purpose**: Generate a preview of what will be migrated (dry run).
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
            **Returns**: Array of hashes describing each migration step
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 139 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 140 
     | 
    
         
            +
            migrator = Ro::Migrator.new('./public/ro/posts')
         
     | 
| 
      
 141 
     | 
    
         
            +
            preview = migrator.preview
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
            preview.each do |step|
         
     | 
| 
      
 144 
     | 
    
         
            +
              puts "#{step[:action]}: #{step[:source]} → #{step[:destination]}"
         
     | 
| 
      
 145 
     | 
    
         
            +
            end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
            # Output:
         
     | 
| 
      
 148 
     | 
    
         
            +
            # MOVE: posts/my-post/attributes.yml → posts/my-post.yml
         
     | 
| 
      
 149 
     | 
    
         
            +
            # MOVE: posts/my-post/assets/cover.jpg → posts/my-post/cover.jpg
         
     | 
| 
      
 150 
     | 
    
         
            +
            # MOVE: posts/my-post/body.md → posts/my-post/body.md
         
     | 
| 
      
 151 
     | 
    
         
            +
            # REMOVE: posts/my-post/ (empty directory)
         
     | 
| 
      
 152 
     | 
    
         
            +
            ```
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
            ---
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
            ### `#rollback(backup_path)` → Boolean
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
            **Purpose**: Restore from a backup created during migration.
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
            **Parameters**:
         
     | 
| 
      
 161 
     | 
    
         
            +
            - `backup_path` (String | Pathname): Path to backup directory
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
            **Returns**: Boolean (true if rollback successful)
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
            **Raises**:
         
     | 
| 
      
 166 
     | 
    
         
            +
            - `Ro::RollbackError`: If backup is invalid or rollback fails
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 169 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 170 
     | 
    
         
            +
            migrator = Ro::Migrator.new('./public/ro/posts')
         
     | 
| 
      
 171 
     | 
    
         
            +
            result = migrator.migrate
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
            if result.failed_nodes > 0
         
     | 
| 
      
 174 
     | 
    
         
            +
              puts "Migration failed, rolling back..."
         
     | 
| 
      
 175 
     | 
    
         
            +
              if migrator.rollback(result.backup_path)
         
     | 
| 
      
 176 
     | 
    
         
            +
                puts "Rollback successful"
         
     | 
| 
      
 177 
     | 
    
         
            +
              else
         
     | 
| 
      
 178 
     | 
    
         
            +
                puts "Rollback failed!"
         
     | 
| 
      
 179 
     | 
    
         
            +
              end
         
     | 
| 
      
 180 
     | 
    
         
            +
            end
         
     | 
| 
      
 181 
     | 
    
         
            +
            ```
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
            ---
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
            ## Migration Algorithm
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
            ### Pre-Migration Phase
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
            1. **Validation**:
         
     | 
| 
      
 190 
     | 
    
         
            +
               ```
         
     | 
| 
      
 191 
     | 
    
         
            +
               For each potential node in path:
         
     | 
| 
      
 192 
     | 
    
         
            +
                 ✓ Check if directory contains attributes.yml (old structure)
         
     | 
| 
      
 193 
     | 
    
         
            +
                 ✓ Check if metadata file already exists (new structure - skip)
         
     | 
| 
      
 194 
     | 
    
         
            +
                 ✓ Check for duplicate IDs
         
     | 
| 
      
 195 
     | 
    
         
            +
                 ✓ Verify write permissions
         
     | 
| 
      
 196 
     | 
    
         
            +
               ```
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
            2. **Backup** (if enabled):
         
     | 
| 
      
 199 
     | 
    
         
            +
               ```
         
     | 
| 
      
 200 
     | 
    
         
            +
               Create backup directory: {path}.backup.{timestamp}
         
     | 
| 
      
 201 
     | 
    
         
            +
               Copy entire structure to backup using FileUtils.cp_r
         
     | 
| 
      
 202 
     | 
    
         
            +
               Verify backup integrity (checksums)
         
     | 
| 
      
 203 
     | 
    
         
            +
               ```
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
            3. **Plan Migration**:
         
     | 
| 
      
 206 
     | 
    
         
            +
               ```
         
     | 
| 
      
 207 
     | 
    
         
            +
               For each node in old structure:
         
     | 
| 
      
 208 
     | 
    
         
            +
                 - Identify: {identifier}/attributes.yml
         
     | 
| 
      
 209 
     | 
    
         
            +
                 - Plan: Move attributes.yml → {identifier}.yml
         
     | 
| 
      
 210 
     | 
    
         
            +
                 - Plan: Move {identifier}/assets/* → {identifier}/*
         
     | 
| 
      
 211 
     | 
    
         
            +
                 - Plan: Move {identifier}/* (non-assets) → {identifier}/*
         
     | 
| 
      
 212 
     | 
    
         
            +
                 - Plan: Remove {identifier}/assets/ (empty)
         
     | 
| 
      
 213 
     | 
    
         
            +
                 - Plan: Remove {identifier}/ (if empty after above)
         
     | 
| 
      
 214 
     | 
    
         
            +
               ```
         
     | 
| 
      
 215 
     | 
    
         
            +
             
     | 
| 
      
 216 
     | 
    
         
            +
            ---
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
            ### Migration Phase
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
            For each node (in dependency order):
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
            1. **Create Metadata File**:
         
     | 
| 
      
 223 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 224 
     | 
    
         
            +
               source = "#{identifier}/attributes.yml"
         
     | 
| 
      
 225 
     | 
    
         
            +
               dest = "#{identifier}.yml"
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
               FileUtils.mv(source, dest)
         
     | 
| 
      
 228 
     | 
    
         
            +
               verify_file(dest)
         
     | 
| 
      
 229 
     | 
    
         
            +
               ```
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
            2. **Move Asset Files**:
         
     | 
| 
      
 232 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 233 
     | 
    
         
            +
               source_dir = "#{identifier}/assets/"
         
     | 
| 
      
 234 
     | 
    
         
            +
               dest_dir = "#{identifier}/"
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
      
 236 
     | 
    
         
            +
               if source_dir.exist?
         
     | 
| 
      
 237 
     | 
    
         
            +
                 # Move all files from assets/ to identifier/
         
     | 
| 
      
 238 
     | 
    
         
            +
                 source_dir.children.each do |child|
         
     | 
| 
      
 239 
     | 
    
         
            +
                   dest_path = dest_dir / child.basename
         
     | 
| 
      
 240 
     | 
    
         
            +
                   FileUtils.mv(child, dest_path)
         
     | 
| 
      
 241 
     | 
    
         
            +
                   verify_file(dest_path)
         
     | 
| 
      
 242 
     | 
    
         
            +
                 end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
                 # Remove empty assets/ directory
         
     | 
| 
      
 245 
     | 
    
         
            +
                 source_dir.rmdir
         
     | 
| 
      
 246 
     | 
    
         
            +
               end
         
     | 
| 
      
 247 
     | 
    
         
            +
               ```
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
            3. **Move Other Content Files**:
         
     | 
| 
      
 250 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 251 
     | 
    
         
            +
               # Files like body.md, samples/, etc. already in identifier/
         
     | 
| 
      
 252 
     | 
    
         
            +
               # These stay in place (already at correct location)
         
     | 
| 
      
 253 
     | 
    
         
            +
               ```
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
      
 255 
     | 
    
         
            +
            4. **Cleanup**:
         
     | 
| 
      
 256 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 257 
     | 
    
         
            +
               # If identifier/ directory is now empty, remove it
         
     | 
| 
      
 258 
     | 
    
         
            +
               # (This only happens if node had ONLY attributes.yml, no other files)
         
     | 
| 
      
 259 
     | 
    
         
            +
               if "#{identifier}/".children.empty?
         
     | 
| 
      
 260 
     | 
    
         
            +
                 "#{identifier}/".rmdir
         
     | 
| 
      
 261 
     | 
    
         
            +
               end
         
     | 
| 
      
 262 
     | 
    
         
            +
               ```
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
            5. **Verification**:
         
     | 
| 
      
 265 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 266 
     | 
    
         
            +
               # Verify node can be loaded in new structure
         
     | 
| 
      
 267 
     | 
    
         
            +
               node = Ro::Node.new(collection, "#{identifier}.yml")
         
     | 
| 
      
 268 
     | 
    
         
            +
               assert node.attributes.any?, "Metadata loaded successfully"
         
     | 
| 
      
 269 
     | 
    
         
            +
               assert node.asset_paths.sort == original_asset_paths.sort, "All assets present"
         
     | 
| 
      
 270 
     | 
    
         
            +
               ```
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
            ---
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
            ### Post-Migration Phase
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
            1. **Verify All Nodes**:
         
     | 
| 
      
 277 
     | 
    
         
            +
               ```
         
     | 
| 
      
 278 
     | 
    
         
            +
               For each migrated node:
         
     | 
| 
      
 279 
     | 
    
         
            +
                 ✓ Metadata file exists at correct location
         
     | 
| 
      
 280 
     | 
    
         
            +
                 ✓ All assets are accessible
         
     | 
| 
      
 281 
     | 
    
         
            +
                 ✓ Node can be loaded via Collection API
         
     | 
| 
      
 282 
     | 
    
         
            +
               ```
         
     | 
| 
      
 283 
     | 
    
         
            +
             
     | 
| 
      
 284 
     | 
    
         
            +
            2. **Cleanup Old Structure**:
         
     | 
| 
      
 285 
     | 
    
         
            +
               ```
         
     | 
| 
      
 286 
     | 
    
         
            +
               For each migrated node:
         
     | 
| 
      
 287 
     | 
    
         
            +
                 ✓ Verify old attributes.yml is gone
         
     | 
| 
      
 288 
     | 
    
         
            +
                 ✓ Verify old assets/ directory is gone
         
     | 
| 
      
 289 
     | 
    
         
            +
                 ✓ Verify old node directory is gone (if was emptied)
         
     | 
| 
      
 290 
     | 
    
         
            +
               ```
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
            3. **Generate Report**:
         
     | 
| 
      
 293 
     | 
    
         
            +
               ```ruby
         
     | 
| 
      
 294 
     | 
    
         
            +
               {
         
     | 
| 
      
 295 
     | 
    
         
            +
                 total_nodes: 50,
         
     | 
| 
      
 296 
     | 
    
         
            +
                 migrated_nodes: 48,
         
     | 
| 
      
 297 
     | 
    
         
            +
                 failed_nodes: 2,
         
     | 
| 
      
 298 
     | 
    
         
            +
                 skipped_nodes: 5,  # Already in new structure
         
     | 
| 
      
 299 
     | 
    
         
            +
                 errors: [
         
     | 
| 
      
 300 
     | 
    
         
            +
                   { node_id: 'broken-post', message: 'Invalid YAML in attributes.yml' },
         
     | 
| 
      
 301 
     | 
    
         
            +
                   { node_id: 'locked-post', message: 'Permission denied' }
         
     | 
| 
      
 302 
     | 
    
         
            +
                 ],
         
     | 
| 
      
 303 
     | 
    
         
            +
                 backup_path: './public/ro/posts.backup.20250117-120000'
         
     | 
| 
      
 304 
     | 
    
         
            +
               }
         
     | 
| 
      
 305 
     | 
    
         
            +
               ```
         
     | 
| 
      
 306 
     | 
    
         
            +
             
     | 
| 
      
 307 
     | 
    
         
            +
            ---
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
            ## Error Handling
         
     | 
| 
      
 310 
     | 
    
         
            +
             
     | 
| 
      
 311 
     | 
    
         
            +
            ### Recoverable Errors
         
     | 
| 
      
 312 
     | 
    
         
            +
             
     | 
| 
      
 313 
     | 
    
         
            +
            **Scenario**: Individual node fails (e.g., permission error)
         
     | 
| 
      
 314 
     | 
    
         
            +
             
     | 
| 
      
 315 
     | 
    
         
            +
            **Behavior**:
         
     | 
| 
      
 316 
     | 
    
         
            +
            - Log error with node ID and details
         
     | 
| 
      
 317 
     | 
    
         
            +
            - Continue with next node
         
     | 
| 
      
 318 
     | 
    
         
            +
            - Report error in final result
         
     | 
| 
      
 319 
     | 
    
         
            +
            - Do NOT rollback (partial migration is acceptable)
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 322 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 323 
     | 
    
         
            +
            # Node 1: Success
         
     | 
| 
      
 324 
     | 
    
         
            +
            # Node 2: Failed (permission error) ← Log and continue
         
     | 
| 
      
 325 
     | 
    
         
            +
            # Node 3: Success
         
     | 
| 
      
 326 
     | 
    
         
            +
            # ...
         
     | 
| 
      
 327 
     | 
    
         
            +
            # Report: 48/50 migrated, 2 failed
         
     | 
| 
      
 328 
     | 
    
         
            +
            ```
         
     | 
| 
      
 329 
     | 
    
         
            +
             
     | 
| 
      
 330 
     | 
    
         
            +
            ---
         
     | 
| 
      
 331 
     | 
    
         
            +
             
     | 
| 
      
 332 
     | 
    
         
            +
            ### Fatal Errors
         
     | 
| 
      
 333 
     | 
    
         
            +
             
     | 
| 
      
 334 
     | 
    
         
            +
            **Scenario**: Catastrophic failure (e.g., disk full, backup failed)
         
     | 
| 
      
 335 
     | 
    
         
            +
             
     | 
| 
      
 336 
     | 
    
         
            +
            **Behavior**:
         
     | 
| 
      
 337 
     | 
    
         
            +
            - Halt migration immediately
         
     | 
| 
      
 338 
     | 
    
         
            +
            - Attempt rollback to backup (if exists)
         
     | 
| 
      
 339 
     | 
    
         
            +
            - Exit with error code 2
         
     | 
| 
      
 340 
     | 
    
         
            +
            - Preserve backup for manual recovery
         
     | 
| 
      
 341 
     | 
    
         
            +
             
     | 
| 
      
 342 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 343 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 344 
     | 
    
         
            +
            # Node 1: Success
         
     | 
| 
      
 345 
     | 
    
         
            +
            # Node 2: Disk full! ← Fatal error
         
     | 
| 
      
 346 
     | 
    
         
            +
            # → Attempt rollback
         
     | 
| 
      
 347 
     | 
    
         
            +
            # → Restore from backup
         
     | 
| 
      
 348 
     | 
    
         
            +
            # → Exit code 2
         
     | 
| 
      
 349 
     | 
    
         
            +
            ```
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
            ---
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
            ### Validation Errors
         
     | 
| 
      
 354 
     | 
    
         
            +
             
     | 
| 
      
 355 
     | 
    
         
            +
            **Scenario**: Pre-migration validation fails
         
     | 
| 
      
 356 
     | 
    
         
            +
             
     | 
| 
      
 357 
     | 
    
         
            +
            **Behavior**:
         
     | 
| 
      
 358 
     | 
    
         
            +
            - Do NOT start migration
         
     | 
| 
      
 359 
     | 
    
         
            +
            - Report all validation errors
         
     | 
| 
      
 360 
     | 
    
         
            +
            - Exit with error code 3
         
     | 
| 
      
 361 
     | 
    
         
            +
            - No rollback needed (no changes made)
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 364 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 365 
     | 
    
         
            +
            # Validation:
         
     | 
| 
      
 366 
     | 
    
         
            +
            # ✗ Duplicate node ID: "my-post" (both my-post.yml and my-post.json exist)
         
     | 
| 
      
 367 
     | 
    
         
            +
            # ✗ Insufficient disk space for backup
         
     | 
| 
      
 368 
     | 
    
         
            +
            # → Exit code 3, no changes made
         
     | 
| 
      
 369 
     | 
    
         
            +
            ```
         
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
            ---
         
     | 
| 
      
 372 
     | 
    
         
            +
             
     | 
| 
      
 373 
     | 
    
         
            +
            ## Test Requirements
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
            ### Unit Tests
         
     | 
| 
      
 376 
     | 
    
         
            +
             
     | 
| 
      
 377 
     | 
    
         
            +
            Must verify:
         
     | 
| 
      
 378 
     | 
    
         
            +
             
     | 
| 
      
 379 
     | 
    
         
            +
            1. **Initialization**:
         
     | 
| 
      
 380 
     | 
    
         
            +
               - ✓ Creates migrator with valid path
         
     | 
| 
      
 381 
     | 
    
         
            +
               - ✓ Applies options (dry_run, backup, force, verbose)
         
     | 
| 
      
 382 
     | 
    
         
            +
               - ✓ Raises error for invalid path
         
     | 
| 
      
 383 
     | 
    
         
            +
             
     | 
| 
      
 384 
     | 
    
         
            +
            2. **Validation**:
         
     | 
| 
      
 385 
     | 
    
         
            +
               - ✓ Detects old structure correctly
         
     | 
| 
      
 386 
     | 
    
         
            +
               - ✓ Detects new structure correctly
         
     | 
| 
      
 387 
     | 
    
         
            +
               - ✓ Detects mixed structures (error)
         
     | 
| 
      
 388 
     | 
    
         
            +
               - ✓ Detects duplicate node IDs
         
     | 
| 
      
 389 
     | 
    
         
            +
               - ✓ Checks write permissions
         
     | 
| 
      
 390 
     | 
    
         
            +
               - ✓ Validates sufficient disk space
         
     | 
| 
      
 391 
     | 
    
         
            +
             
     | 
| 
      
 392 
     | 
    
         
            +
            3. **Migration**:
         
     | 
| 
      
 393 
     | 
    
         
            +
               - ✓ Moves attributes.yml correctly
         
     | 
| 
      
 394 
     | 
    
         
            +
               - ✓ Moves assets/ files correctly
         
     | 
| 
      
 395 
     | 
    
         
            +
               - ✓ Preserves other content files
         
     | 
| 
      
 396 
     | 
    
         
            +
               - ✓ Removes empty directories
         
     | 
| 
      
 397 
     | 
    
         
            +
               - ✓ Handles nested asset directories
         
     | 
| 
      
 398 
     | 
    
         
            +
               - ✓ Preserves file timestamps and permissions
         
     | 
| 
      
 399 
     | 
    
         
            +
             
     | 
| 
      
 400 
     | 
    
         
            +
            4. **Backup**:
         
     | 
| 
      
 401 
     | 
    
         
            +
               - ✓ Creates backup before migration
         
     | 
| 
      
 402 
     | 
    
         
            +
               - ✓ Backup contains complete copy of original
         
     | 
| 
      
 403 
     | 
    
         
            +
               - ✓ Backup path is timestamped correctly
         
     | 
| 
      
 404 
     | 
    
         
            +
             
     | 
| 
      
 405 
     | 
    
         
            +
            5. **Rollback**:
         
     | 
| 
      
 406 
     | 
    
         
            +
               - ✓ Restores from backup correctly
         
     | 
| 
      
 407 
     | 
    
         
            +
               - ✓ Removes partial migration artifacts
         
     | 
| 
      
 408 
     | 
    
         
            +
               - ✓ Validates backup before restoring
         
     | 
| 
      
 409 
     | 
    
         
            +
             
     | 
| 
      
 410 
     | 
    
         
            +
            6. **Error Handling**:
         
     | 
| 
      
 411 
     | 
    
         
            +
               - ✓ Continues on recoverable errors
         
     | 
| 
      
 412 
     | 
    
         
            +
               - ✓ Halts on fatal errors
         
     | 
| 
      
 413 
     | 
    
         
            +
               - ✓ Logs errors with details
         
     | 
| 
      
 414 
     | 
    
         
            +
               - ✓ Generates accurate error reports
         
     | 
| 
      
 415 
     | 
    
         
            +
             
     | 
| 
      
 416 
     | 
    
         
            +
            ### Integration Tests
         
     | 
| 
      
 417 
     | 
    
         
            +
             
     | 
| 
      
 418 
     | 
    
         
            +
            Must verify end-to-end migration:
         
     | 
| 
      
 419 
     | 
    
         
            +
             
     | 
| 
      
 420 
     | 
    
         
            +
            1. **Full Collection Migration**:
         
     | 
| 
      
 421 
     | 
    
         
            +
               - ✓ Migrate collection with 10+ nodes
         
     | 
| 
      
 422 
     | 
    
         
            +
               - ✓ All nodes accessible via new Collection API
         
     | 
| 
      
 423 
     | 
    
         
            +
               - ✓ All assets accessible via new Node API
         
     | 
| 
      
 424 
     | 
    
         
            +
               - ✓ Old structure completely removed
         
     | 
| 
      
 425 
     | 
    
         
            +
             
     | 
| 
      
 426 
     | 
    
         
            +
            2. **Partial Migration**:
         
     | 
| 
      
 427 
     | 
    
         
            +
               - ✓ Some nodes succeed, some fail
         
     | 
| 
      
 428 
     | 
    
         
            +
               - ✓ Successful nodes are in new structure
         
     | 
| 
      
 429 
     | 
    
         
            +
               - ✓ Failed nodes remain in old structure (if safe)
         
     | 
| 
      
 430 
     | 
    
         
            +
               - ✓ Errors reported accurately
         
     | 
| 
      
 431 
     | 
    
         
            +
             
     | 
| 
      
 432 
     | 
    
         
            +
            3. **Edge Cases**:
         
     | 
| 
      
 433 
     | 
    
         
            +
               - ✓ Metadata-only node (no assets/)
         
     | 
| 
      
 434 
     | 
    
         
            +
               - ✓ Assets-only node (no attributes.yml) - handle gracefully
         
     | 
| 
      
 435 
     | 
    
         
            +
               - ✓ Node with nested asset subdirectories
         
     | 
| 
      
 436 
     | 
    
         
            +
               - ✓ Node with non-asset files (body.md, samples/, etc.)
         
     | 
| 
      
 437 
     | 
    
         
            +
               - ✓ Empty collection (no nodes)
         
     | 
| 
      
 438 
     | 
    
         
            +
             
     | 
| 
      
 439 
     | 
    
         
            +
            4. **Rollback**:
         
     | 
| 
      
 440 
     | 
    
         
            +
               - ✓ Failed migration triggers rollback
         
     | 
| 
      
 441 
     | 
    
         
            +
               - ✓ Post-rollback structure matches pre-migration
         
     | 
| 
      
 442 
     | 
    
         
            +
               - ✓ All data preserved during rollback
         
     | 
| 
      
 443 
     | 
    
         
            +
             
     | 
| 
      
 444 
     | 
    
         
            +
            ---
         
     | 
| 
      
 445 
     | 
    
         
            +
             
     | 
| 
      
 446 
     | 
    
         
            +
            ## Success Criteria
         
     | 
| 
      
 447 
     | 
    
         
            +
             
     | 
| 
      
 448 
     | 
    
         
            +
            Per spec SC-002: Migration must complete without data loss for 100% of assets tested.
         
     | 
| 
      
 449 
     | 
    
         
            +
             
     | 
| 
      
 450 
     | 
    
         
            +
            **Verification**:
         
     | 
| 
      
 451 
     | 
    
         
            +
            1. Count files before migration: N
         
     | 
| 
      
 452 
     | 
    
         
            +
            2. Run migration
         
     | 
| 
      
 453 
     | 
    
         
            +
            3. Count files after migration: M
         
     | 
| 
      
 454 
     | 
    
         
            +
            4. Assert: M == N (no files lost)
         
     | 
| 
      
 455 
     | 
    
         
            +
            5. Verify: All files accessible via new API
         
     | 
| 
      
 456 
     | 
    
         
            +
             
     | 
| 
      
 457 
     | 
    
         
            +
            **Additional Checks**:
         
     | 
| 
      
 458 
     | 
    
         
            +
            - File checksums match before/after (content unchanged)
         
     | 
| 
      
 459 
     | 
    
         
            +
            - File permissions preserved
         
     | 
| 
      
 460 
     | 
    
         
            +
            - Directory structure simplified (nesting depth reduced by 1)
         
     | 
| 
      
 461 
     | 
    
         
            +
            - No orphaned files or directories
         
     |