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,349 @@
1
+ # Tasks: Simplify Asset Directory Structure
2
+
3
+ **Input**: Design documents from `/specs/001-simplify-asset-structure/`
4
+ **Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
5
+
6
+ **Tests**: Tests are included per TDD approach - this feature creates comprehensive test coverage for a codebase that currently has no tests.
7
+
8
+ **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
9
+
10
+ ## Format: `[ID] [P?] [Story] Description`
11
+ - **[P]**: Can run in parallel (different files, no dependencies)
12
+ - **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
13
+ - Include exact file paths in descriptions
14
+
15
+ ## Path Conventions
16
+ - **Ruby gem**: `lib/ro/`, `test/` at repository root
17
+ - Test fixtures in `test/fixtures/`
18
+ - Public examples in `public/ro/`
19
+
20
+ ---
21
+
22
+ ## Phase 1: Setup (Shared Infrastructure)
23
+
24
+ **Purpose**: Project initialization, test infrastructure, and basic structure
25
+
26
+ - [X] T001 Create test directory structure: test/unit/, test/integration/, test/fixtures/
27
+ - [X] T002 [P] Create test fixtures directory with subdirectories: test/fixtures/old_structure/ and test/fixtures/new_structure/
28
+ - [X] T003 [P] Create test helper file test/test_helper.rb with common setup and assertions
29
+ - [X] T004 [P] Update Rakefile to enable test tasks (verify test/**/*_test.rb pattern works)
30
+ - [X] T005 [P] Create .gitignore entries for test artifacts: test/tmp/, *.log, .backup.*
31
+
32
+ ---
33
+
34
+ ## Phase 2: Foundational (Blocking Prerequisites)
35
+
36
+ **Purpose**: Test fixtures and baseline tests that ALL user stories depend on
37
+
38
+ **⚠️ CRITICAL**: No user story work can begin until this phase is complete
39
+
40
+ - [X] T006 [P] Create old structure test fixture in test/fixtures/old_structure/posts/sample-post/ with attributes.yml, body.md, and assets/image.jpg
41
+ - [X] T007 [P] Create new structure test fixture in test/fixtures/new_structure/posts/ with sample-post.yml and sample-post/body.md
42
+ - [X] T008 [P] Create test fixture with metadata-only node (no asset directory) in test/fixtures/new_structure/posts/metadata-only.yml
43
+ - [X] T009 [P] Create test fixture with nested assets in test/fixtures/new_structure/posts/nested-test/ with subdirectory/image.png
44
+ - [X] T010 [P] Create test fixture with multiple metadata formats: test/fixtures/new_structure/mixed/test.yml, test.json
45
+
46
+ **Checkpoint**: ✓ Test fixtures ready - user story implementation can now begin in parallel
47
+
48
+ ---
49
+
50
+ ## Phase 3: User Story 1 - Read Asset Data (Priority: P1) 🎯 MVP
51
+
52
+ **Goal**: Enable reading assets from the new simplified structure (identifier.yml + identifier/ directory)
53
+
54
+ **Independent Test**: Create an asset with new structure and verify metadata loads from identifier.yml and assets load from identifier/ directory
55
+
56
+ ### Tests for User Story 1
57
+
58
+ **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
59
+
60
+ - [ ] T011 [P] [US1] Create unit test for Collection#metadata_files in test/unit/collection_test.rb
61
+ - [ ] T012 [P] [US1] Create unit test for Collection#each with new structure in test/unit/collection_test.rb
62
+ - [ ] T013 [P] [US1] Create unit test for Node initialization with metadata_file parameter in test/unit/node_test.rb
63
+ - [ ] T014 [P] [US1] Create unit test for Node#id derived from metadata filename in test/unit/node_test.rb
64
+ - [ ] T015 [P] [US1] Create unit test for Node#asset_dir returning node directory (not assets/ subdirectory) in test/unit/node_test.rb
65
+ - [ ] T016 [P] [US1] Create unit test for Node#_load_base_attributes loading from external metadata file in test/unit/node_test.rb
66
+ - [ ] T017 [P] [US1] Create unit test for Asset path resolution without assets/ prefix in test/unit/asset_test.rb
67
+ - [ ] T018 [US1] Create integration test for loading collection with new structure in test/integration/ro_integration_test.rb
68
+ - [ ] T019 [US1] Create integration test for metadata-only nodes (FR-007) in test/integration/ro_integration_test.rb
69
+
70
+ ### Implementation for User Story 1
71
+
72
+ - [ ] T020 [US1] Modify Collection#each in lib/ro/collection.rb to discover nodes by metadata files (.yml, .yaml, .json) instead of subdirectories
73
+ - [ ] T021 [US1] Add Collection#metadata_files method in lib/ro/collection.rb to scan for *.{yml,yaml,json,toml} files
74
+ - [ ] T022 [US1] Modify Collection#node_for in lib/ro/collection.rb to find nodes by metadata filename instead of directory name
75
+ - [ ] T023 [US1] Update Node#initialize in lib/ro/node.rb to accept (collection, metadata_file) parameters instead of (collection, node_directory)
76
+ - [ ] T024 [US1] Add Node#metadata_file attribute in lib/ro/node.rb to store path to metadata file
77
+ - [ ] T025 [US1] Update Node#id in lib/ro/node.rb to derive from metadata filename (without extension) instead of directory name
78
+ - [ ] T026 [US1] Modify Node#_load_base_attributes in lib/ro/node.rb to load from @metadata_file instead of searching for attributes.yml in @path
79
+ - [ ] T027 [US1] Update Node#asset_dir in lib/ro/node.rb to return @path (node directory) instead of @path/assets/
80
+ - [ ] T028 [US1] Update Node#_ignored_files in lib/ro/node.rb to remove assets/**/** from ignore list and add *.yml/*.json/*.toml (metadata files)
81
+ - [ ] T029 [US1] Modify Asset initialization in lib/ro/asset.rb to split paths on node ID instead of /assets/ segment
82
+ - [ ] T030 [US1] Update Asset#relative_path calculation in lib/ro/asset.rb for new structure (no assets/ prefix to strip)
83
+ - [ ] T031 [US1] Run all US1 tests and verify they pass
84
+
85
+ **Checkpoint**: At this point, User Story 1 should be fully functional - assets can be read from new structure
86
+
87
+ ---
88
+
89
+ ## Phase 4: User Story 2 - Write Asset Data (Priority: P2)
90
+
91
+ **Goal**: Enable creating and updating assets in the new structure
92
+
93
+ **Independent Test**: Create a new asset programmatically, update its metadata, and verify structure matches identifier.yml + identifier/ pattern
94
+
95
+ ### Tests for User Story 2
96
+
97
+ - [ ] T032 [P] [US2] Create unit test for Node#update_attributes! writing to correct metadata file in test/unit/node_test.rb
98
+ - [ ] T033 [P] [US2] Create unit test for creating new node with metadata file in test/unit/node_test.rb
99
+ - [ ] T034 [P] [US2] Create integration test for creating new asset in new structure in test/integration/ro_integration_test.rb
100
+ - [ ] T035 [P] [US2] Create integration test for updating existing node metadata in test/integration/ro_integration_test.rb
101
+ - [ ] T036 [P] [US2] Create integration test for adding assets to existing node in test/integration/ro_integration_test.rb
102
+
103
+ ### Implementation for User Story 2
104
+
105
+ - [ ] T037 [US2] Update Node#update_attributes! in lib/ro/node.rb to save to @metadata_file instead of @path/attributes.yml
106
+ - [ ] T038 [US2] Verify Node can create new metadata files at collection level (test with new node creation)
107
+ - [ ] T039 [US2] Verify Asset files can be added to node directory (test file copy to asset_dir)
108
+ - [ ] T040 [US2] Test metadata format support (.yml, .json) - verify both formats work for read/write
109
+ - [ ] T041 [US2] Run all US2 tests and verify they pass
110
+
111
+ **Checkpoint**: At this point, User Stories 1 AND 2 should both work - assets can be read AND written in new structure
112
+
113
+ ---
114
+
115
+ ## Phase 5: User Story 3 - Migrate Existing Assets (Priority: P3)
116
+
117
+ **Goal**: Provide migration tool to convert from old structure to new structure without data loss
118
+
119
+ **Independent Test**: Create assets in old format, run migration, verify all data is preserved in new format and old structure is removed
120
+
121
+ ### Tests for User Story 3
122
+
123
+ - [ ] T042 [P] [US3] Create unit test for Migrator#initialize in test/unit/migrator_test.rb
124
+ - [ ] T043 [P] [US3] Create unit test for Migrator#validate detecting old structure in test/unit/migrator_test.rb
125
+ - [ ] T044 [P] [US3] Create unit test for Migrator#validate detecting new structure in test/unit/migrator_test.rb
126
+ - [ ] T045 [P] [US3] Create unit test for Migrator#preview generating migration plan in test/unit/migrator_test.rb
127
+ - [ ] T046 [P] [US3] Create unit test for Migrator#migrate moving attributes.yml to identifier.yml in test/unit/migrator_test.rb
128
+ - [ ] T047 [P] [US3] Create unit test for Migrator#migrate moving assets/ files to identifier/ in test/unit/migrator_test.rb
129
+ - [ ] T048 [P] [US3] Create unit test for Migrator#migrate preserving nested asset directories in test/unit/migrator_test.rb
130
+ - [ ] T049 [P] [US3] Create unit test for Migrator#backup creating timestamped backup in test/unit/migrator_test.rb
131
+ - [ ] T050 [P] [US3] Create unit test for Migrator#rollback restoring from backup in test/unit/migrator_test.rb
132
+ - [ ] T051 [US3] Create integration test for full collection migration in test/integration/ro_integration_test.rb
133
+ - [ ] T052 [US3] Create integration test for migration with nested assets in test/integration/ro_integration_test.rb
134
+ - [ ] T053 [US3] Create integration test for migration error handling and recovery in test/integration/ro_integration_test.rb
135
+
136
+ ### Implementation for User Story 3
137
+
138
+ - [ ] T054 [US3] Create lib/ro/script/migrator.rb with Migrator class skeleton
139
+ - [ ] T055 [US3] Implement Migrator#initialize in lib/ro/script/migrator.rb with path and options (dry_run, backup, force, verbose)
140
+ - [ ] T056 [US3] Implement Migrator#validate in lib/ro/script/migrator.rb to check for old/new/mixed structures
141
+ - [ ] T057 [US3] Implement Migrator#preview in lib/ro/script/migrator.rb to generate migration plan (dry run)
142
+ - [ ] T058 [US3] Implement Migrator#backup in lib/ro/script/migrator.rb to create timestamped backup using FileUtils.cp_r
143
+ - [ ] T059 [US3] Implement Migrator#migrate_node in lib/ro/script/migrator.rb to migrate single node (move attributes.yml, move assets/, cleanup)
144
+ - [ ] T060 [US3] Implement Migrator#migrate in lib/ro/script/migrator.rb to iterate all nodes and migrate each
145
+ - [ ] T061 [US3] Implement Migrator#rollback in lib/ro/script/migrator.rb to restore from backup
146
+ - [ ] T062 [US3] Add error handling and recovery logic to Migrator#migrate in lib/ro/script/migrator.rb
147
+ - [ ] T063 [US3] Create CLI command 'ro migrate' in bin/ro or add Rake task for migration
148
+ - [ ] T064 [US3] Add migration logging with detailed progress output
149
+ - [ ] T065 [US3] Run all US3 tests and verify they pass
150
+
151
+ **Checkpoint**: All user stories should now be independently functional - read, write, and migrate all work
152
+
153
+ ---
154
+
155
+ ## Phase 6: Integration & Validation
156
+
157
+ **Purpose**: Ensure all user stories work together and with existing codebase
158
+
159
+ - [ ] T066 [P] Test that old structure still works if present (backward compatibility during transition)
160
+ - [ ] T067 [P] Test mixed structure detection (both old and new for same identifier) raises appropriate error/warning
161
+ - [ ] T068 Test full workflow: migrate public/ro/ examples, verify builder/server still work
162
+ - [ ] T069 [P] Create integration test for Root → Collection → Node chain with new structure
163
+ - [ ] T070 [P] Create integration test for multiple metadata formats (.yml, .json) in same collection
164
+ - [ ] T071 Verify performance: test asset lookup speed with 100+ nodes (target <100ms per SC-001)
165
+ - [ ] T072 Run existing examples through new code (if any example code exists in repo)
166
+
167
+ ---
168
+
169
+ ## Phase 7: Example Migration & Documentation
170
+
171
+ **Purpose**: Migrate example content and update documentation
172
+
173
+ - [ ] T073 Create backup of public/ro/ before migration: cp -r public/ro public/ro.backup.pre-v5
174
+ - [ ] T074 Run migration on public/ro/posts/ using the migration tool
175
+ - [ ] T075 Run migration on public/ro/pages/ using the migration tool
176
+ - [ ] T076 Run migration on public/ro/nerd/ using the migration tool
177
+ - [ ] T077 Verify all migrated examples load correctly via ro console
178
+ - [ ] T078 [P] Update README.md with new structure examples and migration instructions
179
+ - [ ] T079 [P] Create MIGR ATION_GUIDE.md with step-by-step migration instructions
180
+ - [ ] T080 [P] Update CHANGELOG.md with breaking changes for v5.0.0
181
+ - [ ] T081 [P] Update gem version in lib/ro.rb from 4.4.0 to 5.0.0
182
+
183
+ ---
184
+
185
+ ## Phase 8: Polish & Cross-Cutting Concerns
186
+
187
+ **Purpose**: Final cleanup and verification
188
+
189
+ - [ ] T082 [P] Add inline documentation (YARD comments) to modified methods in lib/ro/node.rb
190
+ - [ ] T083 [P] Add inline documentation to new Migrator class in lib/ro/script/migrator.rb
191
+ - [ ] T084 [P] Add inline documentation to modified Collection methods in lib/ro/collection.rb
192
+ - [ ] T085 Run all tests (rake test) and ensure 100% pass
193
+ - [ ] T086 Validate quickstart.md examples work with new structure
194
+ - [ ] T087 [P] Code cleanup: remove any debug logging or commented-out old code
195
+ - [ ] T088 [P] Verify .gitignore covers all test artifacts and backup directories
196
+ - [ ] T089 Final integration test: build static API with ro builder, verify output matches expectations
197
+ - [ ] T090 Performance benchmark: measure asset lookup time with 1000+ nodes, verify <100ms (SC-001)
198
+
199
+ ---
200
+
201
+ ## Dependencies & Execution Order
202
+
203
+ ### Phase Dependencies
204
+
205
+ - **Setup (Phase 1)**: No dependencies - can start immediately
206
+ - **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
207
+ - **User Stories (Phase 3-5)**: All depend on Foundational phase completion
208
+ - US1 (Read) → Can start immediately after Foundational
209
+ - US2 (Write) → Can start immediately after Foundational, but logically should follow US1
210
+ - US3 (Migrate) → Can start immediately after Foundational, but logically should follow US1+US2 since it needs to verify both reading and writing work
211
+ - **Integration (Phase 6)**: Depends on all user stories (US1, US2, US3)
212
+ - **Examples & Docs (Phase 7)**: Depends on US3 (migration tool) being complete
213
+ - **Polish (Phase 8)**: Depends on all previous phases
214
+
215
+ ### User Story Dependencies
216
+
217
+ - **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
218
+ - **User Story 2 (P2)**: Can start after Foundational (Phase 2) - Logically depends on US1 (need to read before write), but could be developed in parallel
219
+ - **User Story 3 (P3)**: Can start after Foundational (Phase 2) - Logically depends on US1+US2 (needs to verify both work), but could be developed in parallel
220
+
221
+ ### Within Each User Story
222
+
223
+ - Tests MUST be written and FAIL before implementation
224
+ - Tests can run in parallel (all marked [P])
225
+ - Implementation tasks run sequentially (Collection → Node → Asset) due to dependencies
226
+ - Each story must be complete and independently testable before moving to next priority
227
+
228
+ ### Parallel Opportunities
229
+
230
+ - **Setup tasks**: T002, T003, T004, T005 can run in parallel
231
+ - **Foundational fixtures**: T006-T010 can all run in parallel
232
+ - **US1 tests**: T011-T017, T019 can run in parallel (T018 may depend on others)
233
+ - **US2 tests**: T032-T036 can all run in parallel
234
+ - **US3 tests**: T042-T050, T052, T053 can run in parallel
235
+ - **Integration tests**: T066, T067, T069, T070 can run in parallel
236
+ - **Documentation**: T078-T084 can run in parallel
237
+ - **Polish**: T082-T084, T087, T088 can run in parallel
238
+
239
+ ### Sequential Requirements
240
+
241
+ - T020-T030 (US1 implementation) must run in order due to class dependencies
242
+ - T037-T040 (US2 implementation) must run in order
243
+ - T054-T065 (US3 implementation) must run in order
244
+ - T073-T077 (example migration) must run in order (backup before migrate)
245
+
246
+ ---
247
+
248
+ ## Parallel Example: User Story 1 Tests
249
+
250
+ ```bash
251
+ # Launch all US1 unit tests together:
252
+ Task: "Create unit test for Collection#metadata_files in test/unit/collection_test.rb"
253
+ Task: "Create unit test for Collection#each with new structure in test/unit/collection_test.rb"
254
+ Task: "Create unit test for Node initialization with metadata_file parameter in test/unit/node_test.rb"
255
+ Task: "Create unit test for Node#id derived from metadata filename in test/unit/node_test.rb"
256
+ Task: "Create unit test for Node#asset_dir returning node directory in test/unit/node_test.rb"
257
+ Task: "Create unit test for Asset path resolution without assets/ prefix in test/unit/asset_test.rb"
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Parallel Example: User Story 3 Tests
263
+
264
+ ```bash
265
+ # Launch all US3 unit tests together:
266
+ Task: "Create unit test for Migrator#initialize in test/unit/migrator_test.rb"
267
+ Task: "Create unit test for Migrator#validate detecting old structure in test/unit/migrator_test.rb"
268
+ Task: "Create unit test for Migrator#preview generating migration plan in test/unit/migrator_test.rb"
269
+ Task: "Create unit test for Migrator#backup creating timestamped backup in test/unit/migrator_test.rb"
270
+ Task: "Create unit test for Migrator#rollback restoring from backup in test/unit/migrator_test.rb"
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Implementation Strategy
276
+
277
+ ### MVP First (User Story 1 Only)
278
+
279
+ 1. Complete Phase 1: Setup (T001-T005)
280
+ 2. Complete Phase 2: Foundational (T006-T010) - CRITICAL
281
+ 3. Complete Phase 3: User Story 1 (T011-T031)
282
+ 4. **STOP and VALIDATE**: Run all US1 tests, verify assets load from new structure
283
+ 5. This gives you a working MVP that can read assets in the new simplified format
284
+
285
+ ### Incremental Delivery
286
+
287
+ 1. Complete Setup + Foundational → Test infrastructure ready
288
+ 2. Add User Story 1 (Read) → Test independently → MVP working! Can read new structure
289
+ 3. Add User Story 2 (Write) → Test independently → Can create/update assets in new structure
290
+ 4. Add User Story 3 (Migrate) → Test independently → Can migrate old data to new structure
291
+ 5. Complete Integration, Examples, Polish → Production ready for v5.0.0 release
292
+
293
+ ### Parallel Team Strategy
294
+
295
+ With multiple developers:
296
+
297
+ 1. Team completes Setup (Phase 1) + Foundational (Phase 2) together
298
+ 2. Once Foundational is done:
299
+ - Developer A: User Story 1 (Read) - T011-T031
300
+ - Developer B: User Story 2 (Write) - T032-T041 (may need to wait for some US1 completion)
301
+ - Developer C: User Story 3 (Migrate) - T042-T065
302
+ 3. Stories complete and integrate independently
303
+ 4. Team collaborates on Integration (Phase 6) and Polish (Phases 7-8)
304
+
305
+ ### TDD Workflow (Required for this feature)
306
+
307
+ This feature follows strict TDD due to zero existing test coverage:
308
+
309
+ 1. **Red**: Write tests first (T011-T019 for US1)
310
+ 2. **Verify Red**: Run tests, ensure they FAIL (proves tests are testing something)
311
+ 3. **Green**: Implement code (T020-T030 for US1)
312
+ 4. **Verify Green**: Run tests, ensure they PASS
313
+ 5. **Refactor**: Clean up code while keeping tests green
314
+ 6. **Repeat** for each user story
315
+
316
+ ---
317
+
318
+ ## Notes
319
+
320
+ - [P] tasks = different files, no dependencies - can run in parallel
321
+ - [Story] label maps task to specific user story for traceability
322
+ - Each user story should be independently completable and testable
323
+ - Verify tests fail before implementing (TDD approach)
324
+ - Commit after each task or logical group
325
+ - Stop at any checkpoint to validate story independently
326
+ - **CRITICAL**: This is a breaking change (v4.x → v5.0.0) - all existing ro users must migrate their data
327
+ - **Performance target**: <100ms asset lookup for 10,000 assets (SC-001)
328
+ - **Migration requirement**: Zero data loss (SC-002)
329
+ - Backward compatibility: Old structure should still work during transition period (FR-011)
330
+
331
+ ---
332
+
333
+ ## Task Count Summary
334
+
335
+ - **Total Tasks**: 90
336
+ - **Setup (Phase 1)**: 5 tasks
337
+ - **Foundational (Phase 2)**: 5 tasks
338
+ - **User Story 1 (Phase 3)**: 21 tasks (9 tests + 12 implementation)
339
+ - **User Story 2 (Phase 4)**: 10 tasks (5 tests + 5 implementation)
340
+ - **User Story 3 (Phase 5)**: 24 tasks (12 tests + 12 implementation)
341
+ - **Integration (Phase 6)**: 7 tasks
342
+ - **Examples & Docs (Phase 7)**: 9 tasks
343
+ - **Polish (Phase 8)**: 9 tasks
344
+
345
+ **Parallel Opportunities**: 40+ tasks can run in parallel (marked with [P])
346
+
347
+ **MVP Scope**: Phases 1-3 only (31 tasks) delivers working read capability with new structure
348
+
349
+ **Full Feature**: All 90 tasks delivers complete v5.0.0 with read, write, migrate, tests, examples, and documentation
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "JSON Format Test",
3
+ "format": "json",
4
+ "extension": "json"
5
+ }
@@ -0,0 +1,3 @@
1
+ title: "YAML Format Test"
2
+ format: "yaml"
3
+ extension: "yml"
@@ -0,0 +1,7 @@
1
+ title: "Metadata Only Node"
2
+ author: "Test Author"
3
+ published_at: "2025-01-02"
4
+ tags:
5
+ - test
6
+ - metadata-only
7
+ description: "This node has no asset directory - testing FR-007"
@@ -0,0 +1,2 @@
1
+ FAKE_PNG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file in a nested subdirectory.
@@ -0,0 +1,7 @@
1
+ title: "Nested Assets Test"
2
+ author: "Test Author"
3
+ published_at: "2025-01-03"
4
+ tags:
5
+ - test
6
+ - nested-assets
7
+ description: "This node has nested subdirectories with assets"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the new structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the new structure (no assets/ subdirectory).
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (New Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - new-structure
7
+ description: "This is a test fixture in the new structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ # Integration tests for Ro with new structure support
3
+
4
+ require_relative '../test_helper'
5
+
6
+ class RoIntegrationTest < RoTestCase
7
+ def setup
8
+ @root = Ro::Root.new(new_structure_path)
9
+ end
10
+
11
+ # T018: Test loading collection with new structure
12
+ def test_load_collection_with_new_structure
13
+ posts = @root['posts']
14
+
15
+ assert_not_nil posts, "Should load posts collection"
16
+ assert posts.is_a?(Ro::Collection), "Should return Collection instance"
17
+ end
18
+
19
+ def test_collection_discovers_nodes_from_metadata_files
20
+ posts = @root['posts']
21
+ nodes = posts.nodes
22
+
23
+ assert nodes.any?, "Collection should discover nodes"
24
+
25
+ # Should find all nodes with metadata files
26
+ node_ids = nodes.map(&:id)
27
+ assert node_ids.include?('sample-post'), "Should find sample-post"
28
+ assert node_ids.include?('metadata-only'), "Should find metadata-only"
29
+ assert node_ids.include?('nested-test'), "Should find nested-test"
30
+ end
31
+
32
+ def test_node_loads_metadata_from_external_file
33
+ posts = @root['posts']
34
+ node = posts['sample-post']
35
+
36
+ assert_not_nil node, "Should find sample-post node"
37
+ assert_equal 'Sample Post (New Structure)', node[:title], "Should load metadata from identifier.yml"
38
+ end
39
+
40
+ def test_node_loads_assets_from_node_directory
41
+ posts = @root['posts']
42
+ node = posts['sample-post']
43
+
44
+ asset_paths = node.asset_paths
45
+
46
+ assert asset_paths.any?, "Node should have assets"
47
+
48
+ # Assets should be in node/assets/ subdirectory
49
+ asset_paths.each do |path|
50
+ expected_dir = new_structure_path / 'posts' / 'sample-post' / 'assets'
51
+ assert path.to_s.start_with?(expected_dir.to_s), "Assets should be in assets/ subdirectory"
52
+ assert path.to_s.include?('/assets/'), "Asset paths should contain /assets/ subdirectory"
53
+ end
54
+ end
55
+
56
+ def test_nested_assets_preserved
57
+ posts = @root['posts']
58
+ node = posts['nested-test']
59
+
60
+ asset_paths = node.asset_paths
61
+
62
+ # Should find nested asset
63
+ nested_asset = asset_paths.find { |p| p.to_s.include?('subdirectory/image.png') }
64
+ assert_not_nil nested_asset, "Should find nested asset in subdirectory"
65
+ end
66
+
67
+ def test_multiple_collections
68
+ posts = @root['posts']
69
+ mixed = @root['mixed']
70
+
71
+ assert_not_nil posts, "Should load posts collection"
72
+ assert_not_nil mixed, "Should load mixed collection"
73
+
74
+ assert posts.nodes.size > 0, "Posts should have nodes"
75
+ assert mixed.nodes.size > 0, "Mixed should have nodes"
76
+ end
77
+
78
+ def test_multiple_metadata_formats
79
+ mixed = @root['mixed']
80
+ nodes = mixed.nodes
81
+
82
+ # Should discover both .yml and .json files
83
+ node_ids = nodes.map(&:id)
84
+ assert node_ids.include?('test-yaml'), "Should find test-yaml.yml"
85
+ assert node_ids.include?('test-json'), "Should find test-json.json"
86
+ end
87
+
88
+ # T019: Test metadata-only nodes (FR-007)
89
+ def test_metadata_only_node
90
+ posts = @root['posts']
91
+ node = posts['metadata-only']
92
+
93
+ assert_not_nil node, "Should find metadata-only node"
94
+ assert_equal 'Metadata Only Node', node[:title], "Should load metadata"
95
+ end
96
+
97
+ def test_metadata_only_node_has_no_assets
98
+ posts = @root['posts']
99
+ node = posts['metadata-only']
100
+
101
+ asset_paths = node.asset_paths
102
+
103
+ # Metadata-only node should have no assets (directory doesn't exist)
104
+ assert_equal 0, asset_paths.size, "Metadata-only node should have no assets"
105
+ end
106
+
107
+ def test_metadata_only_node_asset_dir_not_required
108
+ posts = @root['posts']
109
+ node = posts['metadata-only']
110
+
111
+ # Node should work fine even without asset directory existing
112
+ assert_not_nil node.asset_dir, "asset_dir should still return a path"
113
+
114
+ # But the directory doesn't have to exist
115
+ # (This is valid per FR-007: handle metadata-only nodes)
116
+ end
117
+
118
+ def test_full_workflow_read
119
+ # Full integration: Root → Collection → Node → Assets
120
+ root = Ro::Root.new(new_structure_path)
121
+ posts = root['posts']
122
+ node = posts['sample-post']
123
+
124
+ # Should load everything correctly
125
+ assert_equal 'Sample Post (New Structure)', node[:title]
126
+ assert_equal 'Test Author', node[:author]
127
+ assert node.asset_paths.any?
128
+
129
+ # Assets should be accessible
130
+ image_asset = node.asset_paths.find { |p| p.basename.to_s == 'image.jpg' }
131
+ assert_not_nil image_asset, "Should find image.jpg asset"
132
+ end
133
+ end
134
+
135
+ # Run the tests
136
+ if __FILE__ == $0
137
+ test = RoIntegrationTest.new
138
+
139
+ puts "Running Ro integration tests..."
140
+
141
+ tests = [
142
+ :test_load_collection_with_new_structure,
143
+ :test_collection_discovers_nodes_from_metadata_files,
144
+ :test_node_loads_metadata_from_external_file,
145
+ :test_node_loads_assets_from_node_directory,
146
+ :test_nested_assets_preserved,
147
+ :test_multiple_collections,
148
+ :test_multiple_metadata_formats,
149
+ :test_metadata_only_node,
150
+ :test_metadata_only_node_has_no_assets,
151
+ :test_metadata_only_node_asset_dir_not_required,
152
+ :test_full_workflow_read
153
+ ]
154
+
155
+ tests.each do |test_method|
156
+ begin
157
+ test.setup
158
+ test.send(test_method)
159
+ puts "✓ #{test_method}"
160
+ rescue => e
161
+ puts "✗ #{test_method}: #{e.message}"
162
+ puts " #{e.backtrace.first}"
163
+ end
164
+ end
165
+ end