ro 4.4.0 → 5.1.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 +31 -19
 - data/a.yml +60 -0
 - data/bin/ro +10 -0
 - data/lib/ro/_lib.rb +1 -1
 - data/lib/ro/asset.rb +48 -6
 - 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/public/api/ro/index-1.json +82 -148
 - data/public/api/ro/index.json +82 -148
 - data/public/api/ro/nerd/fastest-possible-embeddings/index.json +7 -8
 - data/public/api/ro/nerd/ima/index.json +3 -4
 - data/public/api/ro/nerd/index/index.json +5 -6
 - data/public/api/ro/nerd/index-1.json +15 -18
 - data/public/api/ro/nerd/index.json +15 -18
 - data/public/api/ro/pages/contact/index.json +4 -5
 - data/public/api/ro/pages/cv/index.json +3 -4
 - data/public/api/ro/pages/disco/index.json +9 -10
 - data/public/api/ro/pages/index/index.json +2 -3
 - data/public/api/ro/pages/index-1.json +25 -82
 - data/public/api/ro/pages/index.json +25 -82
 - data/public/api/ro/pages/jess/index.json +4 -5
 - data/public/api/ro/pages/now/index.json +3 -4
 - data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +21 -22
 - data/public/api/ro/posts/facebook-and-global-extremism/index.json +8 -9
 - data/public/api/ro/posts/index-1.json +42 -48
 - data/public/api/ro/posts/index.json +42 -48
 - data/public/api/ro/posts/lemmings-considered-harmful/index.json +3 -4
 - data/public/api/ro/posts/lost-in-the-desert/index.json +3 -4
 - data/public/api/ro/posts/mission/index.json +3 -4
 - data/public/api/ro/posts/return-your-laptop/index.json +4 -5
 - data/ro.gemspec +247 -18
 - 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/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/attributes.yml +2 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/assets-only/assets/test.txt +1 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/sample-post/assets/body.md +5 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/sample-post/assets/image.jpg +2 -0
 - data/test/tmp/migration_test_1760941048.backup.20251020061728/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 +127 -19
 - data/public/api/ro/pages/about/index.json +0 -60
 - /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,294 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Contract: Node API
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            **Version**: 5.0.0 (new structure)
         
     | 
| 
      
 4 
     | 
    
         
            +
            **Date**: 2025-10-17
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            ## Overview
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            Defines the programmatic interface for the `Ro::Node` class in the new simplified asset structure. This contract ensures that the Node API remains stable and predictable for library consumers.
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            ## Constructor
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### `Node.new(collection, metadata_file)`
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            **Purpose**: Initialize a new Node instance from a metadata file in the new structure.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            **Parameters**:
         
     | 
| 
      
 17 
     | 
    
         
            +
            - `collection` (Ro::Collection): Parent collection object
         
     | 
| 
      
 18 
     | 
    
         
            +
            - `metadata_file` (Pathname): Path to the metadata file (e.g., `posts/my-post.yml`)
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            **Returns**: `Ro::Node` instance
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            **Raises**:
         
     | 
| 
      
 23 
     | 
    
         
            +
            - `Errno::ENOENT`: If metadata_file does not exist
         
     | 
| 
      
 24 
     | 
    
         
            +
            - `YAML::SyntaxError`, `JSON::ParserError`: If metadata file is malformed
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 27 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 28 
     | 
    
         
            +
            collection = Ro::Collection.new(root, 'posts')
         
     | 
| 
      
 29 
     | 
    
         
            +
            metadata_file = Pathname.new('/path/to/ro/posts/my-post.yml')
         
     | 
| 
      
 30 
     | 
    
         
            +
            node = Ro::Node.new(collection, metadata_file)
         
     | 
| 
      
 31 
     | 
    
         
            +
            ```
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            **Backward Compatibility Note**:
         
     | 
| 
      
 34 
     | 
    
         
            +
            In the old structure (v4.x), Node was initialized with a directory path:
         
     | 
| 
      
 35 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 36 
     | 
    
         
            +
            # OLD (v4.x):
         
     | 
| 
      
 37 
     | 
    
         
            +
            node = Ro::Node.new(collection, '/path/to/ro/posts/my-post')
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            # NEW (v5.0):
         
     | 
| 
      
 40 
     | 
    
         
            +
            node = Ro::Node.new(collection, '/path/to/ro/posts/my-post.yml')
         
     | 
| 
      
 41 
     | 
    
         
            +
            ```
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            ---
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            ## Instance Methods
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            ### `#id` → String
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            **Purpose**: Returns the unique identifier for this node (derived from metadata filename).
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            **Returns**: String (e.g., "my-post")
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 54 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 55 
     | 
    
         
            +
            node.id  # => "my-post"
         
     | 
| 
      
 56 
     | 
    
         
            +
            ```
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
            **Derivation**:
         
     | 
| 
      
 59 
     | 
    
         
            +
            - Old structure: Basename of node directory (e.g., `posts/my-post/` → "my-post")
         
     | 
| 
      
 60 
     | 
    
         
            +
            - New structure: Basename of metadata file without extension (e.g., `posts/my-post.yml` → "my-post")
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            ---
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
            ### `#path` → Pathname
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            **Purpose**: Returns the path to the node's asset directory.
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
            **Returns**: Pathname
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 71 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 72 
     | 
    
         
            +
            node.path  # => #<Pathname:/path/to/ro/posts/my-post>
         
     | 
| 
      
 73 
     | 
    
         
            +
            ```
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
            **Behavior Change**:
         
     | 
| 
      
 76 
     | 
    
         
            +
            - Old structure: Path to directory containing `attributes.yml` (e.g., `/posts/my-post/`)
         
     | 
| 
      
 77 
     | 
    
         
            +
            - New structure: Path to directory containing assets (e.g., `/posts/my-post/`), derived from metadata filename
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
            ---
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            ### `#metadata_file` → Pathname
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
            **Purpose**: Returns the path to the metadata file (NEW in v5.0).
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
            **Returns**: Pathname
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 88 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 89 
     | 
    
         
            +
            node.metadata_file  # => #<Pathname:/path/to/ro/posts/my-post.yml>
         
     | 
| 
      
 90 
     | 
    
         
            +
            ```
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
            **Note**: This is a new method in v5.0. In v4.x, metadata was always at `node.path / 'attributes.yml'`.
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
            ---
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
            ### `#attributes` → Hash
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
            **Purpose**: Returns the parsed metadata as a hash.
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
            **Returns**: Hash with symbol keys
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 103 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 104 
     | 
    
         
            +
            node.attributes  # => { title: "My Post", author: "John", tags: ["ruby"] }
         
     | 
| 
      
 105 
     | 
    
         
            +
            ```
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            **Unchanged**: This method works the same in both old and new structures.
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            ---
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
            ### `#[]` (alias: `#get`, `#fetch`) → Object
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            **Purpose**: Access a specific attribute by key.
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            **Parameters**:
         
     | 
| 
      
 116 
     | 
    
         
            +
            - `key` (String or Symbol): Attribute name
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
            **Returns**: Attribute value or nil
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 121 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 122 
     | 
    
         
            +
            node[:title]        # => "My Post"
         
     | 
| 
      
 123 
     | 
    
         
            +
            node.get(:author)   # => "John"
         
     | 
| 
      
 124 
     | 
    
         
            +
            ```
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
            **Unchanged**: This method works the same in both old and new structures.
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
            ---
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            ### `#asset_dir` → Pathname
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
            **Purpose**: Returns the path to the directory containing assets.
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
            **Returns**: Pathname
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 137 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 138 
     | 
    
         
            +
            node.asset_dir  # => #<Pathname:/path/to/ro/posts/my-post>
         
     | 
| 
      
 139 
     | 
    
         
            +
            ```
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
            **Behavior Change**:
         
     | 
| 
      
 142 
     | 
    
         
            +
            - Old structure: `node.path / 'assets'` (e.g., `/posts/my-post/assets/`)
         
     | 
| 
      
 143 
     | 
    
         
            +
            - New structure: `node.path` (e.g., `/posts/my-post/`)
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
            ---
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
            ### `#asset_paths` → Array<Pathname>
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
            **Purpose**: Returns paths to all files in the asset directory.
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
            **Returns**: Array of Pathname objects (sorted)
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 154 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 155 
     | 
    
         
            +
            node.asset_paths  # => [#<Pathname:.../cover.jpg>, #<Pathname:.../diagram.png>]
         
     | 
| 
      
 156 
     | 
    
         
            +
            ```
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
            **Behavior Change**:
         
     | 
| 
      
 159 
     | 
    
         
            +
            - Old structure: Files from `node.path / 'assets'`
         
     | 
| 
      
 160 
     | 
    
         
            +
            - New structure: Files from `node.path` (excluding ignored files)
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
            ---
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
            ### `#assets` → Array<Ro::Asset>
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
            **Purpose**: Returns Asset objects for all files in the asset directory.
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
            **Returns**: Array of `Ro::Asset` instances
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 171 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 172 
     | 
    
         
            +
            node.assets  # => [#<Ro::Asset path=.../cover.jpg>, #<Ro::Asset path=.../diagram.png>]
         
     | 
| 
      
 173 
     | 
    
         
            +
            ```
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
            **Unchanged**: Returns Asset instances regardless of structure.
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
            ---
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
            ### `#update_attributes!(attrs, file: nil)` → void
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            **Purpose**: Updates node metadata and saves to disk.
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
            **Parameters**:
         
     | 
| 
      
 184 
     | 
    
         
            +
            - `attrs` (Hash): New attribute values
         
     | 
| 
      
 185 
     | 
    
         
            +
            - `file` (Pathname, optional): Specific file to save to (defaults to metadata_file)
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
            **Returns**: void
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
            **Raises**:
         
     | 
| 
      
 190 
     | 
    
         
            +
            - `Errno::EACCES`: If metadata file is not writable
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
            **Example**:
         
     | 
| 
      
 193 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 194 
     | 
    
         
            +
            node.update_attributes!(title: "Updated Title", author: "Jane")
         
     | 
| 
      
 195 
     | 
    
         
            +
            ```
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
            **Behavior Change**:
         
     | 
| 
      
 198 
     | 
    
         
            +
            - Old structure: Saves to `node.path / 'attributes.yml'`
         
     | 
| 
      
 199 
     | 
    
         
            +
            - New structure: Saves to `node.metadata_file` (e.g., `posts/my-post.yml`)
         
     | 
| 
      
 200 
     | 
    
         
            +
             
     | 
| 
      
 201 
     | 
    
         
            +
            ---
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
            ## Test Requirements
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
            ### Unit Tests
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
            Must verify for the NEW structure:
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
            1. **Initialization**:
         
     | 
| 
      
 210 
     | 
    
         
            +
               - ✓ Creates node from metadata file
         
     | 
| 
      
 211 
     | 
    
         
            +
               - ✓ Derives ID from filename (without extension)
         
     | 
| 
      
 212 
     | 
    
         
            +
               - ✓ Raises error if metadata file doesn't exist
         
     | 
| 
      
 213 
     | 
    
         
            +
               - ✓ Raises error if metadata file is malformed
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
            2. **Attribute Access**:
         
     | 
| 
      
 216 
     | 
    
         
            +
               - ✓ Loads attributes from metadata file
         
     | 
| 
      
 217 
     | 
    
         
            +
               - ✓ Returns correct values for `#[]`, `#get`, `#fetch`
         
     | 
| 
      
 218 
     | 
    
         
            +
               - ✓ Returns Hash for `#attributes`
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
            3. **Asset Management**:
         
     | 
| 
      
 221 
     | 
    
         
            +
               - ✓ `#asset_dir` returns correct path (node directory, not assets/ subdirectory)
         
     | 
| 
      
 222 
     | 
    
         
            +
               - ✓ `#asset_paths` returns files from node directory
         
     | 
| 
      
 223 
     | 
    
         
            +
               - ✓ `#assets` returns Asset instances
         
     | 
| 
      
 224 
     | 
    
         
            +
               - ✓ Ignores metadata file itself (don't treat as asset)
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
            4. **Metadata Updates**:
         
     | 
| 
      
 227 
     | 
    
         
            +
               - ✓ `#update_attributes!` saves to correct metadata file
         
     | 
| 
      
 228 
     | 
    
         
            +
               - ✓ Preserves existing attributes when updating subset
         
     | 
| 
      
 229 
     | 
    
         
            +
               - ✓ Handles file write errors gracefully
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
            ### Integration Tests
         
     | 
| 
      
 232 
     | 
    
         
            +
             
     | 
| 
      
 233 
     | 
    
         
            +
            Must verify interaction between Node, Collection, and Asset:
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
            1. **Node Discovery**:
         
     | 
| 
      
 236 
     | 
    
         
            +
               - ✓ Collection finds nodes by detecting metadata files
         
     | 
| 
      
 237 
     | 
    
         
            +
               - ✓ Node ID matches metadata filename (without extension)
         
     | 
| 
      
 238 
     | 
    
         
            +
               - ✓ Node path corresponds to asset directory
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
            2. **Asset Resolution**:
         
     | 
| 
      
 241 
     | 
    
         
            +
               - ✓ Assets resolve to correct URLs
         
     | 
| 
      
 242 
     | 
    
         
            +
               - ✓ Nested assets (subdirectories) work correctly
         
     | 
| 
      
 243 
     | 
    
         
            +
               - ✓ Asset paths are relative to node directory (no `assets/` prefix)
         
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
            3. **Metadata Formats**:
         
     | 
| 
      
 246 
     | 
    
         
            +
               - ✓ Supports YAML (`.yml`, `.yaml`)
         
     | 
| 
      
 247 
     | 
    
         
            +
               - ✓ Supports JSON (`.json`)
         
     | 
| 
      
 248 
     | 
    
         
            +
               - ✓ Handles missing optional asset directory
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
            ---
         
     | 
| 
      
 251 
     | 
    
         
            +
             
     | 
| 
      
 252 
     | 
    
         
            +
            ## Migration Compatibility
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
            ### Transition Period
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
            During migration from v4.x to v5.0, the Node class must:
         
     | 
| 
      
 257 
     | 
    
         
            +
             
     | 
| 
      
 258 
     | 
    
         
            +
            1. **Detect old structure**: If initialized with a directory path (old API), detect old structure
         
     | 
| 
      
 259 
     | 
    
         
            +
            2. **Prefer old structure**: Per FR-011, if both structures exist, use old structure
         
     | 
| 
      
 260 
     | 
    
         
            +
            3. **Fail gracefully**: If neither structure exists, raise clear error
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
            **Pseudo-code for compatibility**:
         
     | 
| 
      
 263 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 264 
     | 
    
         
            +
            def initialize(collection, path_or_metadata)
         
     | 
| 
      
 265 
     | 
    
         
            +
              if path_or_metadata.directory?
         
     | 
| 
      
 266 
     | 
    
         
            +
                # Old structure (v4.x compatibility)
         
     | 
| 
      
 267 
     | 
    
         
            +
                @path = path_or_metadata
         
     | 
| 
      
 268 
     | 
    
         
            +
                @metadata_file = @path / 'attributes.yml'
         
     | 
| 
      
 269 
     | 
    
         
            +
              elsif path_or_metadata.file?
         
     | 
| 
      
 270 
     | 
    
         
            +
                # New structure (v5.0)
         
     | 
| 
      
 271 
     | 
    
         
            +
                @metadata_file = path_or_metadata
         
     | 
| 
      
 272 
     | 
    
         
            +
                @path = derive_asset_directory_from_metadata_file
         
     | 
| 
      
 273 
     | 
    
         
            +
              else
         
     | 
| 
      
 274 
     | 
    
         
            +
                raise "Invalid node: #{path_or_metadata}"
         
     | 
| 
      
 275 
     | 
    
         
            +
              end
         
     | 
| 
      
 276 
     | 
    
         
            +
            end
         
     | 
| 
      
 277 
     | 
    
         
            +
            ```
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
            **NOTE**: This compatibility code is ONLY for migration period. In final v5.0 release, only the new signature should be supported.
         
     | 
| 
      
 280 
     | 
    
         
            +
             
     | 
| 
      
 281 
     | 
    
         
            +
            ---
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
            ## Breaking Changes from v4.x
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
            | Method | v4.x Behavior | v5.0 Behavior | Breaking? |
         
     | 
| 
      
 286 
     | 
    
         
            +
            |--------|---------------|---------------|-----------|
         
     | 
| 
      
 287 
     | 
    
         
            +
            | `Node.new` | Accepts directory path | Accepts metadata file path | YES |
         
     | 
| 
      
 288 
     | 
    
         
            +
            | `#path` | Returns node directory | Returns asset directory (same location) | NO |
         
     | 
| 
      
 289 
     | 
    
         
            +
            | `#asset_dir` | Returns `path/assets/` | Returns `path` | YES |
         
     | 
| 
      
 290 
     | 
    
         
            +
            | `#metadata_file` | N/A (implicit) | Returns explicit metadata file path | NEW |
         
     | 
| 
      
 291 
     | 
    
         
            +
            | `#id` | From directory name | From metadata filename | NO (same value) |
         
     | 
| 
      
 292 
     | 
    
         
            +
            | `#attributes` | Loaded from `path/attributes.yml` | Loaded from `metadata_file` | NO (same data) |
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
            **Migration Impact**: Code that calls `Node.new` directly must be updated. Code that uses Node instances should mostly work unchanged, except for `#asset_dir` which now points to the node directory instead of `assets/` subdirectory.
         
     | 
| 
         @@ -0,0 +1,381 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Data Model: Simplify Asset Directory Structure
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            **Date**: 2025-10-17
         
     | 
| 
      
 4 
     | 
    
         
            +
            **Feature**: 001-simplify-asset-structure
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            ## Overview
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            This document defines the data entities and their relationships for the simplified asset directory structure. The ro gem uses a file-based data model where entities are represented by directories and files on the filesystem.
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            ## Core Entities
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Entity: Root
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            **Description**: Top-level entry point representing the ro directory structure. Contains one or more Collections.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            **Attributes**:
         
     | 
| 
      
 17 
     | 
    
         
            +
            - `path` (Pathname): Absolute path to the root directory
         
     | 
| 
      
 18 
     | 
    
         
            +
            - `collections` (Array<Collection>): Child collections discovered by scanning subdirectories
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            **Relationships**:
         
     | 
| 
      
 21 
     | 
    
         
            +
            - **Has many** Collections (1:N)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            **File System Representation**:
         
     | 
| 
      
 24 
     | 
    
         
            +
            ```
         
     | 
| 
      
 25 
     | 
    
         
            +
            /path/to/ro/
         
     | 
| 
      
 26 
     | 
    
         
            +
            ├── posts/          # Collection
         
     | 
| 
      
 27 
     | 
    
         
            +
            ├── pages/          # Collection
         
     | 
| 
      
 28 
     | 
    
         
            +
            └── nerd/           # Collection
         
     | 
| 
      
 29 
     | 
    
         
            +
            ```
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            **Validation Rules**:
         
     | 
| 
      
 32 
     | 
    
         
            +
            - Path must exist and be a directory
         
     | 
| 
      
 33 
     | 
    
         
            +
            - Path must be readable
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            **State Transitions**: N/A (immutable once initialized)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ---
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            ### Entity: Collection
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            **Description**: A named category of related Assets (e.g., "posts", "pages"). Contains multiple Nodes. In the new structure, Collections are discovered as subdirectories of Root that contain metadata files.
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            **Attributes**:
         
     | 
| 
      
 44 
     | 
    
         
            +
            - `name` (String): Collection identifier (derived from directory name)
         
     | 
| 
      
 45 
     | 
    
         
            +
            - `path` (Pathname): Absolute path to the collection directory
         
     | 
| 
      
 46 
     | 
    
         
            +
            - `root` (Root): Parent root instance
         
     | 
| 
      
 47 
     | 
    
         
            +
            - `nodes` (Array<Node>): Child nodes discovered by scanning for metadata files
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            **Relationships**:
         
     | 
| 
      
 50 
     | 
    
         
            +
            - **Belongs to** Root (N:1)
         
     | 
| 
      
 51 
     | 
    
         
            +
            - **Has many** Nodes (1:N)
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            **File System Representation (New Structure)**:
         
     | 
| 
      
 54 
     | 
    
         
            +
            ```
         
     | 
| 
      
 55 
     | 
    
         
            +
            /path/to/ro/posts/
         
     | 
| 
      
 56 
     | 
    
         
            +
            ├── my-first-post.yml        # Node metadata
         
     | 
| 
      
 57 
     | 
    
         
            +
            ├── my-first-post/           # Node asset directory
         
     | 
| 
      
 58 
     | 
    
         
            +
            │   ├── body.md
         
     | 
| 
      
 59 
     | 
    
         
            +
            │   └── cover.jpg
         
     | 
| 
      
 60 
     | 
    
         
            +
            ├── another-post.yml         # Node metadata
         
     | 
| 
      
 61 
     | 
    
         
            +
            └── another-post/            # Node asset directory
         
     | 
| 
      
 62 
     | 
    
         
            +
                └── content.md
         
     | 
| 
      
 63 
     | 
    
         
            +
            ```
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            **File System Representation (Old Structure)**:
         
     | 
| 
      
 66 
     | 
    
         
            +
            ```
         
     | 
| 
      
 67 
     | 
    
         
            +
            /path/to/ro/posts/
         
     | 
| 
      
 68 
     | 
    
         
            +
            ├── my-first-post/           # Node directory
         
     | 
| 
      
 69 
     | 
    
         
            +
            │   ├── attributes.yml       # Metadata (inside node dir)
         
     | 
| 
      
 70 
     | 
    
         
            +
            │   ├── body.md
         
     | 
| 
      
 71 
     | 
    
         
            +
            │   └── assets/              # Asset subdirectory
         
     | 
| 
      
 72 
     | 
    
         
            +
            │       └── cover.jpg
         
     | 
| 
      
 73 
     | 
    
         
            +
            └── another-post/            # Node directory
         
     | 
| 
      
 74 
     | 
    
         
            +
                ├── attributes.yml
         
     | 
| 
      
 75 
     | 
    
         
            +
                └── assets/
         
     | 
| 
      
 76 
     | 
    
         
            +
                    └── content.md
         
     | 
| 
      
 77 
     | 
    
         
            +
            ```
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
            **Validation Rules**:
         
     | 
| 
      
 80 
     | 
    
         
            +
            - Name must be a valid directory name
         
     | 
| 
      
 81 
     | 
    
         
            +
            - Path must exist and be a directory
         
     | 
| 
      
 82 
     | 
    
         
            +
            - Must contain at least one metadata file (new structure) or subdirectory (old structure)
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
            **Discovery Logic Changes**:
         
     | 
| 
      
 85 
     | 
    
         
            +
            - **Old**: Iterate subdirectories, each is a potential Node
         
     | 
| 
      
 86 
     | 
    
         
            +
            - **New**: Scan for `*.{yml,yaml,json,toml}` files, derive Node from each metadata file
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
            **State Transitions**: N/A (immutable once initialized)
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
            ---
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
            ### Entity: Node
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
            **Description**: A single content item with metadata and optional associated files. Represents one logical asset (e.g., a blog post, page, or article).
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
            **Attributes**:
         
     | 
| 
      
 97 
     | 
    
         
            +
            - `id` (String): Node identifier (derived from metadata filename in new structure, directory name in old)
         
     | 
| 
      
 98 
     | 
    
         
            +
            - `path` (Pathname): Path to node location (directory in old structure, collection path in new)
         
     | 
| 
      
 99 
     | 
    
         
            +
            - `metadata_file` (Pathname): Path to metadata file (NEW: separate from path)
         
     | 
| 
      
 100 
     | 
    
         
            +
            - `attributes` (Hash): Parsed metadata content (YAML/JSON/TOML)
         
     | 
| 
      
 101 
     | 
    
         
            +
            - `collection` (Collection): Parent collection
         
     | 
| 
      
 102 
     | 
    
         
            +
            - `assets` (Array<Asset>): Associated files (images, documents, etc.)
         
     | 
| 
      
 103 
     | 
    
         
            +
            - `content_files` (Array<Pathname>): Non-asset files (e.g., body.md)
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
            **Relationships**:
         
     | 
| 
      
 106 
     | 
    
         
            +
            - **Belongs to** Collection (N:1)
         
     | 
| 
      
 107 
     | 
    
         
            +
            - **Has many** Assets (1:N)
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            **File System Representation (New Structure)**:
         
     | 
| 
      
 110 
     | 
    
         
            +
            ```
         
     | 
| 
      
 111 
     | 
    
         
            +
            Node ID: "my-post"
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            /path/to/ro/posts/my-post.yml    # Metadata file (at collection level)
         
     | 
| 
      
 114 
     | 
    
         
            +
            /path/to/ro/posts/my-post/       # Asset directory (sibling to metadata)
         
     | 
| 
      
 115 
     | 
    
         
            +
            ├── body.md                       # Content file
         
     | 
| 
      
 116 
     | 
    
         
            +
            ├── image1.png                    # Asset
         
     | 
| 
      
 117 
     | 
    
         
            +
            └── subdir/                       # Nested directory
         
     | 
| 
      
 118 
     | 
    
         
            +
                └── image2.jpg                # Nested asset
         
     | 
| 
      
 119 
     | 
    
         
            +
            ```
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
            **File System Representation (Old Structure)**:
         
     | 
| 
      
 122 
     | 
    
         
            +
            ```
         
     | 
| 
      
 123 
     | 
    
         
            +
            Node ID: "my-post"
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
            /path/to/ro/posts/my-post/       # Node directory
         
     | 
| 
      
 126 
     | 
    
         
            +
            ├── attributes.yml                # Metadata (inside node dir)
         
     | 
| 
      
 127 
     | 
    
         
            +
            ├── body.md                       # Content file (same level as attributes)
         
     | 
| 
      
 128 
     | 
    
         
            +
            └── assets/                       # Asset subdirectory
         
     | 
| 
      
 129 
     | 
    
         
            +
                ├── image1.png                # Asset
         
     | 
| 
      
 130 
     | 
    
         
            +
                └── subdir/                   # Nested directory
         
     | 
| 
      
 131 
     | 
    
         
            +
                    └── image2.jpg            # Nested asset
         
     | 
| 
      
 132 
     | 
    
         
            +
            ```
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
            **Validation Rules**:
         
     | 
| 
      
 135 
     | 
    
         
            +
            - ID must be a valid filename (for metadata file)
         
     | 
| 
      
 136 
     | 
    
         
            +
            - Metadata file must exist and contain valid YAML/JSON/TOML
         
     | 
| 
      
 137 
     | 
    
         
            +
            - Asset directory is optional (metadata-only nodes are valid per FR-007)
         
     | 
| 
      
 138 
     | 
    
         
            +
            - Metadata file is optional (files-only nodes may be supported per FR-008, but edge case)
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
            **State Transitions**:
         
     | 
| 
      
 141 
     | 
    
         
            +
            1. **Unloaded** → Load metadata → **Loaded**
         
     | 
| 
      
 142 
     | 
    
         
            +
            2. **Loaded** → Update attributes → **Modified** → Save → **Loaded**
         
     | 
| 
      
 143 
     | 
    
         
            +
            3. **Loaded** → Add asset → **Modified** → Save → **Loaded**
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
            **Key Behavioral Changes**:
         
     | 
| 
      
 146 
     | 
    
         
            +
            - **Old**: `Node.new(node_directory_path)` - path IS the node
         
     | 
| 
      
 147 
     | 
    
         
            +
            - **New**: `Node.new(collection_path, metadata_file_path)` - separate metadata from assets
         
     | 
| 
      
 148 
     | 
    
         
            +
            - **Old**: `node.asset_dir` returns `node_path/assets/`
         
     | 
| 
      
 149 
     | 
    
         
            +
            - **New**: `node.asset_dir` returns `collection_path/node_id/`
         
     | 
| 
      
 150 
     | 
    
         
            +
            - **Old**: `_load_base_attributes` searches for `./attributes.{yml,yaml,json}`
         
     | 
| 
      
 151 
     | 
    
         
            +
            - **New**: `_load_base_attributes` loads from explicit `@metadata_file` path
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
            ---
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
            ### Entity: Asset
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
            **Description**: A file associated with a Node (image, document, video, etc.). Assets are files within the node's asset directory.
         
     | 
| 
      
 158 
     | 
    
         
            +
             
     | 
| 
      
 159 
     | 
    
         
            +
            **Attributes**:
         
     | 
| 
      
 160 
     | 
    
         
            +
            - `path` (Pathname): Absolute path to the asset file
         
     | 
| 
      
 161 
     | 
    
         
            +
            - `relative_path` (Pathname): Path relative to asset directory
         
     | 
| 
      
 162 
     | 
    
         
            +
            - `node` (Node): Parent node
         
     | 
| 
      
 163 
     | 
    
         
            +
            - `url` (String): Generated URL for accessing the asset
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
            **Relationships**:
         
     | 
| 
      
 166 
     | 
    
         
            +
            - **Belongs to** Node (N:1)
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
            **File System Representation (New Structure)**:
         
     | 
| 
      
 169 
     | 
    
         
            +
            ```
         
     | 
| 
      
 170 
     | 
    
         
            +
            Asset: /path/to/ro/posts/my-post/images/cover.jpg
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
            Node: my-post
         
     | 
| 
      
 173 
     | 
    
         
            +
            Relative path: images/cover.jpg
         
     | 
| 
      
 174 
     | 
    
         
            +
            URL: /posts/my-post/images/cover.jpg
         
     | 
| 
      
 175 
     | 
    
         
            +
            ```
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
            **File System Representation (Old Structure)**:
         
     | 
| 
      
 178 
     | 
    
         
            +
            ```
         
     | 
| 
      
 179 
     | 
    
         
            +
            Asset: /path/to/ro/posts/my-post/assets/images/cover.jpg
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            Node: my-post
         
     | 
| 
      
 182 
     | 
    
         
            +
            Relative path: images/cover.jpg  (assets/ prefix stripped)
         
     | 
| 
      
 183 
     | 
    
         
            +
            URL: /posts/my-post/images/cover.jpg
         
     | 
| 
      
 184 
     | 
    
         
            +
            ```
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
            **Validation Rules**:
         
     | 
| 
      
 187 
     | 
    
         
            +
            - Path must exist and be a file (not directory)
         
     | 
| 
      
 188 
     | 
    
         
            +
            - Must be within node's asset directory
         
     | 
| 
      
 189 
     | 
    
         
            +
            - URL must be generated correctly regardless of structure
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
            **Key Behavioral Changes**:
         
     | 
| 
      
 192 
     | 
    
         
            +
            - **Old**: Asset paths include `/assets/` segment that must be stripped for URLs
         
     | 
| 
      
 193 
     | 
    
         
            +
            - **New**: Asset paths are already relative to node, no stripping needed
         
     | 
| 
      
 194 
     | 
    
         
            +
            - **Old**: `Asset` splits path on `/assets/` to find node (lib/ro/asset.rb:12)
         
     | 
| 
      
 195 
     | 
    
         
            +
            - **New**: `Asset` splits path on node ID to find node and relative path
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
            ---
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
            ## Entity Relationships Diagram
         
     | 
| 
      
 200 
     | 
    
         
            +
             
     | 
| 
      
 201 
     | 
    
         
            +
            ```
         
     | 
| 
      
 202 
     | 
    
         
            +
            Root (1)
         
     | 
| 
      
 203 
     | 
    
         
            +
              │
         
     | 
| 
      
 204 
     | 
    
         
            +
              └─ has many ─→ Collection (N)
         
     | 
| 
      
 205 
     | 
    
         
            +
                                │
         
     | 
| 
      
 206 
     | 
    
         
            +
                                └─ has many ─→ Node (N)
         
     | 
| 
      
 207 
     | 
    
         
            +
                                                 │
         
     | 
| 
      
 208 
     | 
    
         
            +
                                                 └─ has many ─→ Asset (N)
         
     | 
| 
      
 209 
     | 
    
         
            +
            ```
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
            **Cardinality**:
         
     | 
| 
      
 212 
     | 
    
         
            +
            - 1 Root : N Collections
         
     | 
| 
      
 213 
     | 
    
         
            +
            - 1 Collection : N Nodes
         
     | 
| 
      
 214 
     | 
    
         
            +
            - 1 Node : N Assets (0..N, assets are optional)
         
     | 
| 
      
 215 
     | 
    
         
            +
             
     | 
| 
      
 216 
     | 
    
         
            +
            ## Migration State Model
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
            ### Entity: MigrationState
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
            **Description**: Tracks the migration status of a single node from old structure to new structure. Used by the migration tool to ensure safe, resumable migrations.
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
            **Attributes**:
         
     | 
| 
      
 223 
     | 
    
         
            +
            - `node_id` (String): Identifier of the node being migrated
         
     | 
| 
      
 224 
     | 
    
         
            +
            - `collection_name` (String): Name of parent collection
         
     | 
| 
      
 225 
     | 
    
         
            +
            - `source_structure` (Symbol): `:old` or `:new`
         
     | 
| 
      
 226 
     | 
    
         
            +
            - `status` (Symbol): `:pending`, `:in_progress`, `:completed`, `:failed`, `:rolled_back`
         
     | 
| 
      
 227 
     | 
    
         
            +
            - `metadata_migrated` (Boolean): Whether metadata file has been moved
         
     | 
| 
      
 228 
     | 
    
         
            +
            - `assets_migrated` (Boolean): Whether asset files have been moved
         
     | 
| 
      
 229 
     | 
    
         
            +
            - `backup_path` (Pathname): Location of backup (if created)
         
     | 
| 
      
 230 
     | 
    
         
            +
            - `error` (String): Error message if status is `:failed`
         
     | 
| 
      
 231 
     | 
    
         
            +
             
     | 
| 
      
 232 
     | 
    
         
            +
            **State Transitions**:
         
     | 
| 
      
 233 
     | 
    
         
            +
            ```
         
     | 
| 
      
 234 
     | 
    
         
            +
            :pending
         
     | 
| 
      
 235 
     | 
    
         
            +
              ↓
         
     | 
| 
      
 236 
     | 
    
         
            +
            :in_progress (metadata copied)
         
     | 
| 
      
 237 
     | 
    
         
            +
              ↓
         
     | 
| 
      
 238 
     | 
    
         
            +
            :in_progress (assets copied)
         
     | 
| 
      
 239 
     | 
    
         
            +
              ↓
         
     | 
| 
      
 240 
     | 
    
         
            +
            :completed (old structure removed)
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
            OR
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
            :in_progress
         
     | 
| 
      
 245 
     | 
    
         
            +
              ↓
         
     | 
| 
      
 246 
     | 
    
         
            +
            :failed (error occurred)
         
     | 
| 
      
 247 
     | 
    
         
            +
              ↓
         
     | 
| 
      
 248 
     | 
    
         
            +
            :rolled_back (restored from backup)
         
     | 
| 
      
 249 
     | 
    
         
            +
            ```
         
     | 
| 
      
 250 
     | 
    
         
            +
             
     | 
| 
      
 251 
     | 
    
         
            +
            **Validation Rules**:
         
     | 
| 
      
 252 
     | 
    
         
            +
            - Cannot transition to `:completed` unless both `metadata_migrated` and `assets_migrated` are true
         
     | 
| 
      
 253 
     | 
    
         
            +
            - Backup must exist before attempting rollback
         
     | 
| 
      
 254 
     | 
    
         
            +
            - Cannot re-migrate a node in `:completed` state (skip if already migrated)
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
            ---
         
     | 
| 
      
 257 
     | 
    
         
            +
             
     | 
| 
      
 258 
     | 
    
         
            +
            ## Data Constraints
         
     | 
| 
      
 259 
     | 
    
         
            +
             
     | 
| 
      
 260 
     | 
    
         
            +
            ### Uniqueness Constraints
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
            1. **Node ID within Collection**: Each node ID must be unique within its collection
         
     | 
| 
      
 263 
     | 
    
         
            +
               - **Old structure**: Directory names are unique by filesystem constraint
         
     | 
| 
      
 264 
     | 
    
         
            +
               - **New structure**: Metadata filenames (without extension) must be unique
         
     | 
| 
      
 265 
     | 
    
         
            +
               - **Validation**: Migration tool must detect duplicate IDs before migrating
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
            2. **Collection Name within Root**: Each collection name must be unique within root
         
     | 
| 
      
 268 
     | 
    
         
            +
               - Enforced by filesystem (directory names are unique)
         
     | 
| 
      
 269 
     | 
    
         
            +
             
     | 
| 
      
 270 
     | 
    
         
            +
            ### Referential Integrity
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
            1. **Asset → Node**: Every asset must belong to a valid node
         
     | 
| 
      
 273 
     | 
    
         
            +
               - **Old structure**: Assets in `node_dir/assets/` are children of `node_dir`
         
     | 
| 
      
 274 
     | 
    
         
            +
               - **New structure**: Assets in `node_id/` are children of node with `node_id.yml`
         
     | 
| 
      
 275 
     | 
    
         
            +
               - **Validation**: Orphaned asset directories (no corresponding metadata file) should be flagged
         
     | 
| 
      
 276 
     | 
    
         
            +
             
     | 
| 
      
 277 
     | 
    
         
            +
            2. **Node → Collection**: Every node must belong to a valid collection
         
     | 
| 
      
 278 
     | 
    
         
            +
               - Enforced by file system structure (nodes are discovered from collection directories)
         
     | 
| 
      
 279 
     | 
    
         
            +
             
     | 
| 
      
 280 
     | 
    
         
            +
            ### Format Constraints
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
            1. **Metadata File Extensions**: Only `.yml`, `.yaml`, `.json`, `.toml` are recognized
         
     | 
| 
      
 283 
     | 
    
         
            +
            2. **Metadata Content**: Must be valid YAML/JSON/TOML, parse errors should fail gracefully
         
     | 
| 
      
 284 
     | 
    
         
            +
            3. **Path Characters**: Must be valid filesystem paths (platform-specific constraints)
         
     | 
| 
      
 285 
     | 
    
         
            +
             
     | 
| 
      
 286 
     | 
    
         
            +
            ---
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
            ## Data Format Examples
         
     | 
| 
      
 289 
     | 
    
         
            +
             
     | 
| 
      
 290 
     | 
    
         
            +
            ### Metadata File Content (Unchanged Between Structures)
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
            **YAML Format** (`my-post.yml`):
         
     | 
| 
      
 293 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 294 
     | 
    
         
            +
            title: "My First Post"
         
     | 
| 
      
 295 
     | 
    
         
            +
            author: "John Doe"
         
     | 
| 
      
 296 
     | 
    
         
            +
            published_at: 2025-01-15
         
     | 
| 
      
 297 
     | 
    
         
            +
            tags:
         
     | 
| 
      
 298 
     | 
    
         
            +
              - ruby
         
     | 
| 
      
 299 
     | 
    
         
            +
              - programming
         
     | 
| 
      
 300 
     | 
    
         
            +
            featured_image: cover.jpg
         
     | 
| 
      
 301 
     | 
    
         
            +
            ```
         
     | 
| 
      
 302 
     | 
    
         
            +
             
     | 
| 
      
 303 
     | 
    
         
            +
            **JSON Format** (`my-post.json`):
         
     | 
| 
      
 304 
     | 
    
         
            +
            ```json
         
     | 
| 
      
 305 
     | 
    
         
            +
            {
         
     | 
| 
      
 306 
     | 
    
         
            +
              "title": "My First Post",
         
     | 
| 
      
 307 
     | 
    
         
            +
              "author": "John Doe",
         
     | 
| 
      
 308 
     | 
    
         
            +
              "published_at": "2025-01-15",
         
     | 
| 
      
 309 
     | 
    
         
            +
              "tags": ["ruby", "programming"],
         
     | 
| 
      
 310 
     | 
    
         
            +
              "featured_image": "cover.jpg"
         
     | 
| 
      
 311 
     | 
    
         
            +
            }
         
     | 
| 
      
 312 
     | 
    
         
            +
            ```
         
     | 
| 
      
 313 
     | 
    
         
            +
             
     | 
| 
      
 314 
     | 
    
         
            +
            ### File Structure Comparison
         
     | 
| 
      
 315 
     | 
    
         
            +
             
     | 
| 
      
 316 
     | 
    
         
            +
            **Old Structure**:
         
     | 
| 
      
 317 
     | 
    
         
            +
            ```
         
     | 
| 
      
 318 
     | 
    
         
            +
            posts/
         
     | 
| 
      
 319 
     | 
    
         
            +
            └── my-post/
         
     | 
| 
      
 320 
     | 
    
         
            +
                ├── attributes.yml       # Metadata
         
     | 
| 
      
 321 
     | 
    
         
            +
                ├── body.md              # Content
         
     | 
| 
      
 322 
     | 
    
         
            +
                └── assets/              # Assets subdirectory
         
     | 
| 
      
 323 
     | 
    
         
            +
                    ├── cover.jpg
         
     | 
| 
      
 324 
     | 
    
         
            +
                    └── diagram.png
         
     | 
| 
      
 325 
     | 
    
         
            +
            ```
         
     | 
| 
      
 326 
     | 
    
         
            +
             
     | 
| 
      
 327 
     | 
    
         
            +
            **New Structure**:
         
     | 
| 
      
 328 
     | 
    
         
            +
            ```
         
     | 
| 
      
 329 
     | 
    
         
            +
            posts/
         
     | 
| 
      
 330 
     | 
    
         
            +
            ├── my-post.yml              # Metadata (moved up one level)
         
     | 
| 
      
 331 
     | 
    
         
            +
            └── my-post/                 # Asset directory (no more assets/ nesting)
         
     | 
| 
      
 332 
     | 
    
         
            +
                ├── body.md              # Content
         
     | 
| 
      
 333 
     | 
    
         
            +
                ├── cover.jpg
         
     | 
| 
      
 334 
     | 
    
         
            +
                └── diagram.png
         
     | 
| 
      
 335 
     | 
    
         
            +
            ```
         
     | 
| 
      
 336 
     | 
    
         
            +
             
     | 
| 
      
 337 
     | 
    
         
            +
            **Key Differences**:
         
     | 
| 
      
 338 
     | 
    
         
            +
            1. Metadata file moved from `posts/my-post/attributes.yml` → `posts/my-post.yml`
         
     | 
| 
      
 339 
     | 
    
         
            +
            2. Assets moved from `posts/my-post/assets/` → `posts/my-post/`
         
     | 
| 
      
 340 
     | 
    
         
            +
            3. Content files (`body.md`) moved from `posts/my-post/body.md` → `posts/my-post/body.md` (same relative location, but different absolute due to restructure)
         
     | 
| 
      
 341 
     | 
    
         
            +
             
     | 
| 
      
 342 
     | 
    
         
            +
            ---
         
     | 
| 
      
 343 
     | 
    
         
            +
             
     | 
| 
      
 344 
     | 
    
         
            +
            ## Performance Considerations
         
     | 
| 
      
 345 
     | 
    
         
            +
             
     | 
| 
      
 346 
     | 
    
         
            +
            ### Lookup Performance
         
     | 
| 
      
 347 
     | 
    
         
            +
             
     | 
| 
      
 348 
     | 
    
         
            +
            **Old Structure**:
         
     | 
| 
      
 349 
     | 
    
         
            +
            - Collection discovery: O(N) where N = subdirectories in root
         
     | 
| 
      
 350 
     | 
    
         
            +
            - Node discovery: O(M) where M = subdirectories in collection
         
     | 
| 
      
 351 
     | 
    
         
            +
            - Metadata loading: 1 file read per node (`attributes.yml`)
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
            **New Structure**:
         
     | 
| 
      
 354 
     | 
    
         
            +
            - Collection discovery: O(N) where N = subdirectories in root (unchanged)
         
     | 
| 
      
 355 
     | 
    
         
            +
            - Node discovery: O(M) where M = metadata files in collection (file glob instead of directory iteration)
         
     | 
| 
      
 356 
     | 
    
         
            +
            - Metadata loading: 1 file read per node (unchanged)
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
            **Analysis**: New structure should have similar or slightly better performance because:
         
     | 
| 
      
 359 
     | 
    
         
            +
            1. File glob for `*.yml` is typically faster than directory stat for subdirectories
         
     | 
| 
      
 360 
     | 
    
         
            +
            2. Fewer levels of nesting = fewer filesystem operations
         
     | 
| 
      
 361 
     | 
    
         
            +
            3. No need to look inside directories to find `attributes.yml`
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
            **Success Criteria**: Must maintain <100ms asset lookup for 10,000 assets (SC-001)
         
     | 
| 
      
 364 
     | 
    
         
            +
             
     | 
| 
      
 365 
     | 
    
         
            +
            ### Memory Footprint
         
     | 
| 
      
 366 
     | 
    
         
            +
             
     | 
| 
      
 367 
     | 
    
         
            +
            - **Old structure**: Minimal - only loaded node metadata is kept in memory
         
     | 
| 
      
 368 
     | 
    
         
            +
            - **New structure**: Same - no change to memory model
         
     | 
| 
      
 369 
     | 
    
         
            +
            - **Migration tool**: May need to load multiple nodes simultaneously, estimate ~1MB per 100 nodes
         
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
            ---
         
     | 
| 
      
 372 
     | 
    
         
            +
             
     | 
| 
      
 373 
     | 
    
         
            +
            ## Index Requirements
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
            No database indexes are required (file-based system). However, for optimal performance:
         
     | 
| 
      
 376 
     | 
    
         
            +
             
     | 
| 
      
 377 
     | 
    
         
            +
            1. **Collection-level caching**: Consider memoizing the list of metadata files in a collection
         
     | 
| 
      
 378 
     | 
    
         
            +
            2. **Node-level caching**: Consider memoizing parsed attributes to avoid re-parsing on each access
         
     | 
| 
      
 379 
     | 
    
         
            +
            3. **Asset-level caching**: File stats could be cached if performance becomes an issue
         
     | 
| 
      
 380 
     | 
    
         
            +
             
     | 
| 
      
 381 
     | 
    
         
            +
            **Note**: Current implementation does NOT use caching. Add only if performance benchmarks indicate need.
         
     |