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.
Files changed (161) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +42 -16
  3. data/MIGRATION.md +320 -0
  4. data/README.md +31 -19
  5. data/a.yml +60 -0
  6. data/bin/ro +10 -0
  7. data/lib/ro/_lib.rb +1 -1
  8. data/lib/ro/asset.rb +48 -6
  9. data/lib/ro/collection.rb +51 -13
  10. data/lib/ro/migrator.rb +285 -0
  11. data/lib/ro/node.rb +53 -13
  12. data/lib/ro/root.rb +75 -1
  13. data/lib/ro/script/migrate.rb +204 -0
  14. data/lib/ro/script/server.rb +1 -1
  15. data/lib/ro.rb +1 -0
  16. data/public/api/ro/index-1.json +82 -148
  17. data/public/api/ro/index.json +82 -148
  18. data/public/api/ro/nerd/fastest-possible-embeddings/index.json +7 -8
  19. data/public/api/ro/nerd/ima/index.json +3 -4
  20. data/public/api/ro/nerd/index/index.json +5 -6
  21. data/public/api/ro/nerd/index-1.json +15 -18
  22. data/public/api/ro/nerd/index.json +15 -18
  23. data/public/api/ro/pages/contact/index.json +4 -5
  24. data/public/api/ro/pages/cv/index.json +3 -4
  25. data/public/api/ro/pages/disco/index.json +9 -10
  26. data/public/api/ro/pages/index/index.json +2 -3
  27. data/public/api/ro/pages/index-1.json +25 -82
  28. data/public/api/ro/pages/index.json +25 -82
  29. data/public/api/ro/pages/jess/index.json +4 -5
  30. data/public/api/ro/pages/now/index.json +3 -4
  31. data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +21 -22
  32. data/public/api/ro/posts/facebook-and-global-extremism/index.json +8 -9
  33. data/public/api/ro/posts/index-1.json +42 -48
  34. data/public/api/ro/posts/index.json +42 -48
  35. data/public/api/ro/posts/lemmings-considered-harmful/index.json +3 -4
  36. data/public/api/ro/posts/lost-in-the-desert/index.json +3 -4
  37. data/public/api/ro/posts/mission/index.json +3 -4
  38. data/public/api/ro/posts/return-your-laptop/index.json +4 -5
  39. data/ro.gemspec +247 -18
  40. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  41. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  42. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  43. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  44. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  45. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  46. data/specs/001-simplify-asset-structure/plan.md +90 -0
  47. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  48. data/specs/001-simplify-asset-structure/research.md +333 -0
  49. data/specs/001-simplify-asset-structure/spec.md +127 -0
  50. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  51. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  52. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  53. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  54. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  55. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  56. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  57. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  58. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  59. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  60. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  61. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  62. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  63. data/test/integration/ro_integration_test.rb +165 -0
  64. data/test/test_helper.rb +149 -0
  65. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  66. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  67. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  68. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  69. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  70. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  71. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  72. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  73. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  74. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  75. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  76. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  77. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  78. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  79. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  80. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  81. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  82. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  83. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  84. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  85. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  86. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  87. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  88. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  89. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  90. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  91. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  92. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  93. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  94. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  95. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  96. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  97. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  98. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  99. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  100. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  101. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  102. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  103. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  104. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  105. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  106. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  107. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  108. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  109. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  110. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  111. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  112. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  113. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  114. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  115. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  116. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  117. data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/assets-only/assets/test.txt +1 -0
  118. data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/assets/body.md +5 -0
  119. data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/assets/image.jpg +2 -0
  120. data/test/tmp/migration_test_1760940939.backup.20251020061539/migration_test_1760940939/posts/sample-post/attributes.yml +2 -0
  121. data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/assets-only/assets/test.txt +1 -0
  122. data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/assets/body.md +5 -0
  123. data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/assets/image.jpg +2 -0
  124. data/test/tmp/migration_test_1760940939.backup.20251020061539/posts/sample-post/attributes.yml +2 -0
  125. data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/assets-only/assets/test.txt +1 -0
  126. data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/assets/body.md +5 -0
  127. data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/assets/image.jpg +2 -0
  128. data/test/tmp/migration_test_1760941048.backup.20251020061728/migration_test_1760941048/posts/sample-post/attributes.yml +2 -0
  129. data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/assets-only/assets/test.txt +1 -0
  130. data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/sample-post/assets/body.md +5 -0
  131. data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/sample-post/assets/image.jpg +2 -0
  132. data/test/tmp/migration_test_1760941048.backup.20251020061728/posts/sample-post/attributes.yml +2 -0
  133. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  134. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  135. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  136. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  137. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  138. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  139. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  140. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  141. data/test/unit/asset_test.rb +90 -0
  142. data/test/unit/collection_test.rb +127 -0
  143. data/test/unit/migrator_test.rb +209 -0
  144. data/test/unit/node_test.rb +138 -0
  145. metadata +127 -19
  146. data/public/api/ro/pages/about/index.json +0 -60
  147. /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
  148. /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
  149. /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
  150. /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
  151. /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
  152. /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
  153. /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
  154. /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
  155. /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
  156. /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
  157. /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
  158. /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
  159. /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
  160. /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
  161. /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
@@ -0,0 +1,90 @@
1
+ # Implementation Plan: Simplify Asset Directory Structure
2
+
3
+ **Branch**: `001-simplify-asset-structure` | **Date**: 2025-10-17 | **Spec**: [spec.md](./spec.md)
4
+ **Input**: Feature specification from `/specs/001-simplify-asset-structure/spec.md`
5
+
6
+ **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
7
+
8
+ ## Summary
9
+
10
+ Refactor the ro gem's asset directory structure from nested format (`identifier/attributes.yml` + `identifier/assets/`) to a flattened format (`identifier.yml` + `identifier/`). This breaking change (v4.x → v5.0) simplifies the directory hierarchy by one level, making asset organization more intuitive. Implementation requires modifying core Node, Collection, and Asset classes, creating a migration tool, and establishing comprehensive test coverage (currently no tests exist).
11
+
12
+ ## Technical Context
13
+
14
+ **Language/Version**: Ruby 3.0+
15
+ **Primary Dependencies**: map (~> 6.6), kramdown (~> 2.4), front_matter_parser (~> 1.0), nokogiri (~> 1)
16
+ **Storage**: File system (YAML/JSON/TOML metadata files + asset directories)
17
+ **Testing**: Custom test runner via Rake (test/unit, test/functional, test/integration)
18
+ **Target Platform**: Cross-platform (Linux, macOS, Windows) - Ruby gem
19
+ **Project Type**: Single project (Ruby gem library + CLI tool)
20
+ **Performance Goals**: <100ms asset lookup for collections with 10,000 assets (per spec SC-001)
21
+ **Constraints**: Zero data loss during migration (per spec SC-002), backward compatibility preference for old structure until migration
22
+ **Scale/Scope**: Codebase has ~10 core classes, 10+ example assets in public/ro/, breaking change affects all ro users
23
+
24
+ ## Constitution Check
25
+
26
+ *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
27
+
28
+ **Status**: SKIPPED - No project constitution file found at `.specify/memory/constitution.md`. This feature proceeds without constitutional constraints.
29
+
30
+ **Note**: If this project adopts a constitution in the future, this feature should be reviewed against those principles.
31
+
32
+ ## Project Structure
33
+
34
+ ### Documentation (this feature)
35
+
36
+ ```
37
+ specs/[###-feature]/
38
+ ├── plan.md # This file (/speckit.plan command output)
39
+ ├── research.md # Phase 0 output (/speckit.plan command)
40
+ ├── data-model.md # Phase 1 output (/speckit.plan command)
41
+ ├── quickstart.md # Phase 1 output (/speckit.plan command)
42
+ ├── contracts/ # Phase 1 output (/speckit.plan command)
43
+ └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
44
+ ```
45
+
46
+ ### Source Code (repository root)
47
+
48
+ ```
49
+ lib/ro/
50
+ ├── node.rb # MODIFY: Core node class - loads attributes, manages assets
51
+ ├── collection.rb # MODIFY: Collection class - discovers nodes by new pattern
52
+ ├── asset.rb # MODIFY: Asset class - path resolution for new structure
53
+ ├── root.rb # MINOR: May need updates for collection discovery
54
+ ├── methods.rb # REVIEW: URL generation, may need path updates
55
+ ├── path.rb # REVIEW: Path utilities, may need helpers
56
+ ├── template.rb # NO CHANGE: Template rendering (md, yml, etc)
57
+ ├── script/
58
+ │ └── migrator.rb # NEW: Migration script for old → new structure
59
+ └── ... (other files unchanged)
60
+
61
+ test/ # NEW: Test directory (currently doesn't exist)
62
+ ├── unit/
63
+ │ ├── node_test.rb # NEW: Unit tests for Node class
64
+ │ ├── collection_test.rb # NEW: Unit tests for Collection class
65
+ │ ├── asset_test.rb # NEW: Unit tests for Asset class
66
+ │ └── migrator_test.rb # NEW: Unit tests for migration tool
67
+ ├── integration/
68
+ │ └── ro_integration_test.rb # NEW: End-to-end tests
69
+ └── fixtures/ # NEW: Test data in both old and new structures
70
+ ├── old_structure/
71
+ └── new_structure/
72
+
73
+ public/ro/ # MIGRATE: Example content (test migration here)
74
+ ├── posts/
75
+ │ └── almost-died-in-an-ice-cave.yml # MIGRATED: Was attributes.yml
76
+ │ └── almost-died-in-an-ice-cave/ # MIGRATED: Was assets/ + other files
77
+ │ ├── body.md
78
+ │ ├── image1.png
79
+ │ └── og.jpg
80
+ └── ... (other collections migrated similarly)
81
+ ```
82
+
83
+ **Structure Decision**: Single project (Ruby gem). This is a library that provides both programmatic API and CLI interface. The core logic lives in `lib/ro/`, with the main entry point at `lib/ro.rb`. Tests will be created in a new `test/` directory following the Rake test convention (unit, functional, integration). The `public/ro/` directory contains example content that will be migrated as part of this feature implementation.
84
+
85
+ ## Complexity Tracking
86
+
87
+ *Fill ONLY if Constitution Check has violations that must be justified*
88
+
89
+ **Status**: N/A - No constitution defined, no violations to track.
90
+
@@ -0,0 +1,575 @@
1
+ # Quickstart: Simplify Asset Directory Structure
2
+
3
+ **Feature**: 001-simplify-asset-structure
4
+ **Version**: 5.0.0
5
+ **Date**: 2025-10-17
6
+
7
+ ## Overview
8
+
9
+ This guide walks you through migrating from the old nested asset structure to the new simplified structure, and demonstrates how to use the new API.
10
+
11
+ ## For Existing Users: Migration Guide
12
+
13
+ ### Step 1: Backup Your Data
14
+
15
+ Before migrating, create a backup of your ro directory:
16
+
17
+ ```bash
18
+ cp -r ./public/ro ./public/ro.backup
19
+ ```
20
+
21
+ ### Step 2: Validate Your Structure
22
+
23
+ Check if your assets are in the old format:
24
+
25
+ ```bash
26
+ # Look for the old pattern:
27
+ find ./public/ro -name "attributes.yml" -o -name "attributes.yaml"
28
+
29
+ # If you see results like:
30
+ # ./public/ro/posts/my-post/attributes.yml
31
+ # ./public/ro/pages/about/attributes.yml
32
+ # Then you have the old structure and need to migrate.
33
+ ```
34
+
35
+ ### Step 3: Run Migration (Dry Run First)
36
+
37
+ Preview the migration without making changes:
38
+
39
+ ```bash
40
+ ro migrate ./public/ro --dry-run --verbose
41
+ ```
42
+
43
+ Review the output to ensure it looks correct:
44
+
45
+ ```
46
+ [DRY RUN] Migrating collection: posts
47
+ [DRY RUN] Node: my-post
48
+ [DRY RUN] MOVE: posts/my-post/attributes.yml → posts/my-post.yml
49
+ [DRY RUN] MOVE: posts/my-post/assets/cover.jpg → posts/my-post/cover.jpg
50
+ [DRY RUN] MOVE: posts/my-post/body.md → posts/my-post/body.md
51
+ [DRY RUN] REMOVE: posts/my-post/assets/ (empty)
52
+ [DRY RUN] Node: another-post
53
+ [DRY RUN] ...
54
+ [DRY RUN] Summary: 15 nodes would be migrated
55
+ ```
56
+
57
+ ### Step 4: Run Actual Migration
58
+
59
+ If the dry run looks good, run the real migration with backup:
60
+
61
+ ```bash
62
+ ro migrate ./public/ro --backup --verbose
63
+ ```
64
+
65
+ Output:
66
+
67
+ ```
68
+ Creating backup at: ./public/ro.backup.20250117-143022
69
+ Migrating collection: posts
70
+ ✓ my-post migrated successfully
71
+ ✓ another-post migrated successfully
72
+ ✓ ...
73
+ Migrating collection: pages
74
+ ✓ about migrated successfully
75
+ ✓ ...
76
+ ✓ Migration complete!
77
+ Total nodes: 15
78
+ Migrated: 15
79
+ Failed: 0
80
+ Backup: ./public/ro.backup.20250117-143022
81
+ ```
82
+
83
+ ### Step 5: Verify Migration
84
+
85
+ Check that your assets are now in the new format:
86
+
87
+ ```bash
88
+ # Look for the new pattern:
89
+ ls -la ./public/ro/posts/
90
+
91
+ # You should see:
92
+ # my-post.yml ← Metadata at collection level
93
+ # my-post/ ← Asset directory (no more assets/ subdirectory)
94
+ # ├── body.md
95
+ # └── cover.jpg
96
+ ```
97
+
98
+ Test that the ro gem can load your assets:
99
+
100
+ ```bash
101
+ ro console
102
+ ```
103
+
104
+ ```ruby
105
+ root = Ro::Root.new('./public/ro')
106
+ posts = root.collection('posts')
107
+ post = posts.node_for('my-post')
108
+
109
+ puts post.attributes[:title] # Should print the title
110
+ puts post.asset_paths # Should show cover.jpg, etc.
111
+ ```
112
+
113
+ ### Step 6: Remove Old Backup (Optional)
114
+
115
+ Once you've verified everything works, you can remove the old backup:
116
+
117
+ ```bash
118
+ rm -rf ./public/ro.backup.20250117-143022
119
+ ```
120
+
121
+ ---
122
+
123
+ ## For New Users: Using the New Structure
124
+
125
+ ### Creating a New ro Directory
126
+
127
+ ```bash
128
+ mkdir -p ./my-ro/posts
129
+ cd ./my-ro
130
+ ```
131
+
132
+ ### Creating Your First Asset
133
+
134
+ Create a metadata file at the collection level:
135
+
136
+ **posts/my-first-post.yml**:
137
+ ```yaml
138
+ title: "My First Post"
139
+ author: "Your Name"
140
+ published_at: 2025-01-17
141
+ tags:
142
+ - tutorial
143
+ - ro
144
+ ```
145
+
146
+ Create the asset directory and add files:
147
+
148
+ ```bash
149
+ mkdir posts/my-first-post
150
+ echo "# Hello World" > posts/my-first-post/body.md
151
+ cp ~/cover-image.jpg posts/my-first-post/cover.jpg
152
+ ```
153
+
154
+ Your structure should look like:
155
+
156
+ ```
157
+ my-ro/
158
+ └── posts/
159
+ ├── my-first-post.yml ← Metadata
160
+ └── my-first-post/ ← Assets
161
+ ├── body.md
162
+ └── cover.jpg
163
+ ```
164
+
165
+ ### Loading Assets with the ro Gem
166
+
167
+ ```ruby
168
+ require 'ro'
169
+
170
+ root = Ro::Root.new('./my-ro')
171
+ posts = root.collection('posts')
172
+
173
+ # Get a specific post
174
+ post = posts.node_for('my-first-post')
175
+
176
+ # Access metadata
177
+ puts post[:title] # => "My First Post"
178
+ puts post[:author] # => "Your Name"
179
+ puts post.attributes # => { title: "My First Post", ... }
180
+
181
+ # Access assets
182
+ post.asset_paths.each do |asset_path|
183
+ puts asset_path # => .../posts/my-first-post/body.md
184
+ # => .../posts/my-first-post/cover.jpg
185
+ end
186
+
187
+ # Iterate all posts
188
+ posts.each do |post|
189
+ puts "#{post.id}: #{post[:title]}"
190
+ end
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Common Workflows
196
+
197
+ ### Workflow 1: Adding a New Post (Programmatic)
198
+
199
+ ```ruby
200
+ require 'ro'
201
+ require 'fileutils'
202
+ require 'yaml'
203
+
204
+ root = Ro::Root.new('./my-ro')
205
+ posts = root.collection('posts')
206
+
207
+ # Define new post ID and metadata
208
+ post_id = 'my-new-post'
209
+ metadata = {
210
+ title: "My New Post",
211
+ author: "Your Name",
212
+ published_at: Date.today.to_s,
213
+ tags: ['ruby', 'gems']
214
+ }
215
+
216
+ # Create metadata file
217
+ metadata_file = posts.path / "#{post_id}.yml"
218
+ File.write(metadata_file, metadata.to_yaml)
219
+
220
+ # Create asset directory and add files
221
+ asset_dir = posts.path / post_id
222
+ FileUtils.mkdir_p(asset_dir)
223
+
224
+ body_content = "# My New Post\n\nThis is the content."
225
+ File.write(asset_dir / 'body.md', body_content)
226
+
227
+ # Verify
228
+ node = posts.node_for(post_id)
229
+ puts node[:title] # => "My New Post"
230
+ ```
231
+
232
+ ### Workflow 2: Updating Post Metadata
233
+
234
+ ```ruby
235
+ require 'ro'
236
+
237
+ root = Ro::Root.new('./my-ro')
238
+ post = root.collection('posts').node_for('my-first-post')
239
+
240
+ # Update attributes
241
+ post.update_attributes!(
242
+ title: "Updated Title",
243
+ tags: post[:tags] + ['updated']
244
+ )
245
+
246
+ # Reload to verify
247
+ updated_post = root.collection('posts').node_for('my-first-post')
248
+ puts updated_post[:title] # => "Updated Title"
249
+ ```
250
+
251
+ ### Workflow 3: Adding Assets to Existing Post
252
+
253
+ ```ruby
254
+ require 'ro'
255
+ require 'fileutils'
256
+
257
+ root = Ro::Root.new('./my-ro')
258
+ post = root.collection('posts').node_for('my-first-post')
259
+
260
+ # Copy a new image to the post's asset directory
261
+ source_image = './new-diagram.png'
262
+ dest_image = post.asset_dir / 'diagram.png'
263
+ FileUtils.cp(source_image, dest_image)
264
+
265
+ # Verify
266
+ puts post.asset_paths.map(&:basename) # => ["body.md", "cover.jpg", "diagram.png"]
267
+ ```
268
+
269
+ ### Workflow 4: Listing All Posts with Assets
270
+
271
+ ```ruby
272
+ require 'ro'
273
+
274
+ root = Ro::Root.new('./my-ro')
275
+ posts = root.collection('posts')
276
+
277
+ posts.each do |post|
278
+ puts "Post: #{post[:title]}"
279
+ puts " ID: #{post.id}"
280
+ puts " Assets: #{post.asset_paths.size} files"
281
+ post.asset_paths.each do |asset|
282
+ puts " - #{asset.basename}"
283
+ end
284
+ puts
285
+ end
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Integration Scenarios
291
+
292
+ ### Scenario 1: Building a Static Site
293
+
294
+ ```ruby
295
+ require 'ro'
296
+ require 'json'
297
+ require 'fileutils'
298
+
299
+ # Load ro data
300
+ root = Ro::Root.new('./content')
301
+ posts = root.collection('posts')
302
+
303
+ # Build static JSON API
304
+ api_dir = './public/api'
305
+ FileUtils.mkdir_p(api_dir)
306
+
307
+ # Generate index
308
+ index = posts.map do |post|
309
+ {
310
+ id: post.id,
311
+ title: post[:title],
312
+ author: post[:author],
313
+ published_at: post[:published_at],
314
+ url: "/posts/#{post.id}"
315
+ }
316
+ end
317
+ File.write("#{api_dir}/posts.json", JSON.pretty_generate(index))
318
+
319
+ # Generate individual post files
320
+ posts.each do |post|
321
+ post_data = {
322
+ id: post.id,
323
+ attributes: post.attributes,
324
+ assets: post.asset_paths.map { |p| "/assets/#{post.id}/#{p.basename}" }
325
+ }
326
+
327
+ post_dir = "#{api_dir}/posts"
328
+ FileUtils.mkdir_p(post_dir)
329
+ File.write("#{post_dir}/#{post.id}.json", JSON.pretty_generate(post_data))
330
+
331
+ # Copy assets
332
+ asset_dest_dir = "./public/assets/#{post.id}"
333
+ FileUtils.mkdir_p(asset_dest_dir)
334
+ post.asset_paths.each do |asset|
335
+ FileUtils.cp(asset, asset_dest_dir / asset.basename)
336
+ end
337
+ end
338
+
339
+ puts "Static site built in ./public"
340
+ ```
341
+
342
+ ### Scenario 2: Markdown Blog Integration
343
+
344
+ ```ruby
345
+ require 'ro'
346
+ require 'kramdown'
347
+
348
+ root = Ro::Root.new('./blog')
349
+ posts = root.collection('posts')
350
+
351
+ # Render posts to HTML
352
+ posts.each do |post|
353
+ # Read markdown body
354
+ body_path = post.asset_dir / 'body.md'
355
+ next unless body_path.exist?
356
+
357
+ markdown = File.read(body_path)
358
+
359
+ # Render to HTML
360
+ html = Kramdown::Document.new(markdown, input: 'GFM').to_html
361
+
362
+ # Combine with metadata
363
+ output = <<~HTML
364
+ <!DOCTYPE html>
365
+ <html>
366
+ <head>
367
+ <title>#{post[:title]}</title>
368
+ <meta name="author" content="#{post[:author]}">
369
+ </head>
370
+ <body>
371
+ <h1>#{post[:title]}</h1>
372
+ <p>By #{post[:author]} on #{post[:published_at]}</p>
373
+ #{html}
374
+ </body>
375
+ </html>
376
+ HTML
377
+
378
+ # Write HTML file
379
+ html_dir = './output/posts'
380
+ FileUtils.mkdir_p(html_dir)
381
+ File.write("#{html_dir}/#{post.id}.html", output)
382
+ end
383
+
384
+ puts "Blog rendered to ./output/posts"
385
+ ```
386
+
387
+ ### Scenario 3: API Server with Sinatra
388
+
389
+ ```ruby
390
+ require 'sinatra'
391
+ require 'ro'
392
+ require 'json'
393
+
394
+ # Initialize ro
395
+ set :ro_root, Ro::Root.new('./content')
396
+
397
+ # List all posts
398
+ get '/api/posts' do
399
+ content_type :json
400
+ posts = settings.ro_root.collection('posts')
401
+
402
+ posts.map do |post|
403
+ {
404
+ id: post.id,
405
+ title: post[:title],
406
+ author: post[:author],
407
+ url: "/api/posts/#{post.id}"
408
+ }
409
+ end.to_json
410
+ end
411
+
412
+ # Get single post
413
+ get '/api/posts/:id' do
414
+ content_type :json
415
+ posts = settings.ro_root.collection('posts')
416
+ post = posts.node_for(params[:id])
417
+
418
+ halt 404, { error: 'Not found' }.to_json unless post
419
+
420
+ {
421
+ id: post.id,
422
+ attributes: post.attributes,
423
+ assets: post.asset_paths.map { |p| "/assets/#{post.id}/#{p.basename}" }
424
+ }.to_json
425
+ end
426
+
427
+ # Serve assets
428
+ get '/assets/:post_id/:filename' do
429
+ posts = settings.ro_root.collection('posts')
430
+ post = posts.node_for(params[:post_id])
431
+
432
+ halt 404 unless post
433
+
434
+ asset = post.asset_dir / params[:filename]
435
+ halt 404 unless asset.exist?
436
+
437
+ send_file asset
438
+ end
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Troubleshooting
444
+
445
+ ### Issue: "Metadata file not found"
446
+
447
+ **Symptom**: `collection.node_for('my-post')` returns `nil`
448
+
449
+ **Cause**: Metadata file doesn't exist or has wrong extension
450
+
451
+ **Solution**:
452
+ ```bash
453
+ # Check if metadata file exists
454
+ ls ./my-ro/posts/my-post.yml
455
+
456
+ # If not, create it:
457
+ cat > ./my-ro/posts/my-post.yml <<EOF
458
+ title: "My Post"
459
+ EOF
460
+ ```
461
+
462
+ ### Issue: "No assets found"
463
+
464
+ **Symptom**: `node.asset_paths` is empty
465
+
466
+ **Cause**: Asset directory doesn't exist
467
+
468
+ **Solution**:
469
+ ```bash
470
+ # Create asset directory
471
+ mkdir ./my-ro/posts/my-post
472
+
473
+ # Add files
474
+ echo "# Content" > ./my-ro/posts/my-post/body.md
475
+ ```
476
+
477
+ ### Issue: "Migration failed partway through"
478
+
479
+ **Symptom**: Some nodes migrated, some didn't
480
+
481
+ **Solution**:
482
+ ```bash
483
+ # Check migration log for errors
484
+ cat ./migration.log
485
+
486
+ # If safe, re-run migration (will skip already-migrated nodes)
487
+ ro migrate ./public/ro --verbose
488
+
489
+ # Or rollback to backup:
490
+ ro migrate --rollback ./public/ro.backup.20250117-143022
491
+ ```
492
+
493
+ ### Issue: "Both old and new structures exist"
494
+
495
+ **Symptom**: Migration warnings about duplicate structures
496
+
497
+ **Solution**:
498
+ ```bash
499
+ # The new structure takes precedence in v5.0
500
+ # Manually remove old structure if migration completed:
501
+ rm -rf ./public/ro/posts/my-post/attributes.yml
502
+ rm -rf ./public/ro/posts/my-post/assets/
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Best Practices
508
+
509
+ ### 1. Metadata Naming
510
+
511
+ Use kebab-case for post IDs:
512
+ - ✓ `my-first-post.yml`
513
+ - ✗ `My First Post.yml` (spaces)
514
+ - ✗ `my_first_post.yml` (underscores okay but less common)
515
+
516
+ ### 2. Asset Organization
517
+
518
+ Keep assets organized in subdirectories:
519
+ ```
520
+ my-post/
521
+ ├── body.md
522
+ ├── images/
523
+ │ ├── hero.jpg
524
+ │ └── diagram.png
525
+ └── downloads/
526
+ └── code-sample.zip
527
+ ```
528
+
529
+ ### 3. Metadata Format
530
+
531
+ Prefer YAML for human-edited metadata:
532
+ - YAML: Best for hand-editing (`.yml`)
533
+ - JSON: Best for programmatic generation (`.json`)
534
+ - TOML: Alternative (`.toml`, if supported)
535
+
536
+ ### 4. Version Control
537
+
538
+ In `.gitignore`, track metadata and markdown, ignore generated files:
539
+ ```gitignore
540
+ # Track these:
541
+ # *.yml
542
+ # *.md
543
+
544
+ # Ignore these:
545
+ public/api/
546
+ .backup.*
547
+ ```
548
+
549
+ ### 5. Backup Before Migration
550
+
551
+ Always create a backup before migrating:
552
+ ```bash
553
+ # Timestamp your backups
554
+ cp -r ./ro "./ro.backup.$(date +%Y%m%d-%H%M%S)"
555
+ ```
556
+
557
+ ---
558
+
559
+ ## What's Next?
560
+
561
+ - **Spec**: Read [spec.md](./spec.md) for full feature requirements
562
+ - **Implementation Plan**: See [plan.md](./plan.md) for technical architecture
563
+ - **Data Model**: Review [data-model.md](./data-model.md) for entity relationships
564
+ - **API Contracts**: Check [contracts/](./contracts/) for detailed API documentation
565
+ - **Tasks**: Execute [tasks.md](./tasks.md) for implementation checklist (after running `/speckit.tasks`)
566
+
567
+ ---
568
+
569
+ ## Version Info
570
+
571
+ **Feature Version**: 5.0.0 (breaking change from 4.x)
572
+ **Migration Required**: Yes (one-time, run `ro migrate`)
573
+ **Backward Compatibility**: No (major version bump)
574
+
575
+ For questions or issues, refer to the main ro gem documentation or file an issue on GitHub.