ro 4.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +42 -16
  3. data/MIGRATION.md +320 -0
  4. data/README.md +30 -18
  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 +46 -5
  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/ro.gemspec +207 -16
  17. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  18. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  19. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  20. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  21. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  22. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  23. data/specs/001-simplify-asset-structure/plan.md +90 -0
  24. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  25. data/specs/001-simplify-asset-structure/research.md +333 -0
  26. data/specs/001-simplify-asset-structure/spec.md +127 -0
  27. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  28. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  29. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  30. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  31. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  32. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  33. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  34. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  35. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  36. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  37. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  38. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  39. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  40. data/test/integration/ro_integration_test.rb +165 -0
  41. data/test/test_helper.rb +149 -0
  42. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  43. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  44. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  45. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  46. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  47. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  48. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  49. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  50. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  51. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  52. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  53. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  54. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  55. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  56. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  57. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  58. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  59. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  60. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  61. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  62. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  63. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  64. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  65. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  66. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  67. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  68. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  69. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  70. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  71. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  72. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  73. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  74. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  75. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  76. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  77. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  78. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  79. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  80. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  81. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  82. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  83. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  84. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  85. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  86. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  87. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  88. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  89. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  90. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  91. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  92. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  93. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  94. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  95. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  96. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  97. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  98. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  99. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  100. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  101. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  102. data/test/unit/asset_test.rb +90 -0
  103. data/test/unit/collection_test.rb +127 -0
  104. data/test/unit/migrator_test.rb +209 -0
  105. data/test/unit/node_test.rb +138 -0
  106. metadata +111 -18
  107. /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
  108. /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
  109. /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
  110. /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
  111. /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
  112. /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
  113. /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
  114. /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
  115. /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
  116. /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
  117. /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
  118. /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
  119. /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
  120. /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
  121. /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
@@ -0,0 +1,333 @@
1
+ # Research: Simplify Asset Directory Structure
2
+
3
+ **Date**: 2025-10-17
4
+ **Feature**: 001-simplify-asset-structure
5
+
6
+ ## Overview
7
+
8
+ This document contains research findings and technical decisions for simplifying the ro gem's asset directory structure from nested format to flattened format.
9
+
10
+ ## Key Technical Decisions
11
+
12
+ ### Decision 1: Node Discovery Pattern
13
+
14
+ **What was chosen**: Change from directory-based node discovery to file-based node discovery
15
+
16
+ **Rationale**:
17
+ - **Old approach**: Collections iterate subdirectories, each subdirectory is a node containing `attributes.yml`
18
+ - **New approach**: Collections scan for `.yml`/`.json`/`.toml` files, derive node identifier from filename
19
+ - **Why**: The new structure places metadata at the collection level (`identifier.yml`) rather than nested (`identifier/attributes.yml`), so node discovery must change from "find directories with attributes files" to "find metadata files, derive corresponding asset directories"
20
+
21
+ **Alternatives considered**:
22
+ - **Keep directory-based discovery**: Would require maintaining both `identifier/` directory AND `identifier.yml` file, defeating the purpose of simplification
23
+ - **Dual mode (support both)**: Adds complexity and makes migration harder; spec specifies preference for old structure until migration, not simultaneous support
24
+
25
+ **Implementation impact**:
26
+ - Modify `Collection#each` in `lib/ro/collection.rb` (lines 45-67)
27
+ - Modify `Collection#node_for` in `lib/ro/collection.rb` (line 33-35)
28
+ - Change from `subdirectories.each` to scanning for metadata files with supported extensions
29
+
30
+ ### Decision 2: Node Initialization Pattern
31
+
32
+ **What was chosen**: Decouple node path from metadata file location
33
+
34
+ **Rationale**:
35
+ - **Old pattern**: `Node.new(path)` where `path` IS the node directory containing `attributes.yml`
36
+ - **New pattern**: `Node.new(path, metadata_file)` where `path` is the collection, `metadata_file` points to `identifier.yml`
37
+ - **Why**: In the old structure, the node's path was the directory containing everything. In the new structure, the metadata file is at the collection level, and the asset directory is a sibling. The node needs to know both locations.
38
+
39
+ **Alternatives considered**:
40
+ - **Metadata file only**: Pass only the metadata file path, derive everything from it. Rejected because it would require more path manipulation and lose context of the collection.
41
+ - **Keep single path parameter**: Would require complex logic to detect whether path points to old or new structure. Rejected for clarity.
42
+
43
+ **Implementation impact**:
44
+ - Modify `Node#initialize` in `lib/ro/node.rb` (lines ~20-30)
45
+ - Modify `Node#_load_base_attributes` in `lib/ro/node.rb` (lines 57-63) to load from external metadata file
46
+ - Update all Node instantiation call sites in Collection and Root classes
47
+
48
+ ### Decision 3: Asset Directory Resolution
49
+
50
+ **What was chosen**: Remove `assets/` subdirectory nesting
51
+
52
+ **Rationale**:
53
+ - **Old approach**: `node.path.join('assets')` returns `identifier/assets/`
54
+ - **New approach**: `node.asset_dir` returns `identifier/` (same as node directory)
55
+ - **Why**: The new structure eliminates the `assets/` subdirectory, placing all files directly in the `identifier/` directory alongside the metadata file
56
+
57
+ **Alternatives considered**:
58
+ - **Keep `assets/` subdirectory**: Would not achieve the simplification goal from the spec
59
+ - **Flat structure (no directory at all)**: Would make it impossible to have multiple files per asset. Rejected because spec explicitly includes `identifier/` directory for "additional data"
60
+
61
+ **Implementation impact**:
62
+ - Modify `Node#asset_dir` in `lib/ro/node.rb` (line 209-211)
63
+ - Modify `Node#_ignored_files` in `lib/ro/node.rb` (lines 153-165) to update ignore patterns
64
+ - Modify `Asset` path splitting in `lib/ro/asset.rb` (line 12) - currently splits on `/assets/`
65
+
66
+ ### Decision 4: Backward Compatibility Strategy
67
+
68
+ **What was chosen**: Prefer old structure until explicit migration, then breaking change in v5.0
69
+
70
+ **Rationale**:
71
+ - **User decision**: Spec FR-011 states "prefer old structure when both exist until migration"
72
+ - **User decision**: Spec FR-012 states "breaking change with major version bump"
73
+ - **Why**: Safest approach for existing users - they can test the new structure without risking data loss, then migrate when ready
74
+
75
+ **Alternatives considered**:
76
+ - **Simultaneous support**: Would require complex detection logic and feature flags. Rejected per user decision.
77
+ - **Automatic migration on load**: Too risky - could modify user data unexpectedly. Rejected for safety.
78
+
79
+ **Implementation impact**:
80
+ - Migration tool must be run explicitly by users
81
+ - Version bump from 4.4.0 → 5.0.0
82
+ - Update README and CHANGELOG to document breaking change
83
+ - Potentially add deprecation warnings in 4.x versions (out of scope for this feature)
84
+
85
+ ### Decision 5: Migration Tool Design
86
+
87
+ **What was chosen**: Standalone migration script with dry-run mode
88
+
89
+ **Rationale**:
90
+ - **Pattern**: `ro migrate [collection_path] [--dry-run] [--backup]`
91
+ - **Why**:
92
+ - Explicit operation reduces risk of accidental data modification
93
+ - Dry-run mode allows users to preview changes
94
+ - Backup option provides safety net
95
+ - Can be run incrementally on specific collections
96
+
97
+ **Alternatives considered**:
98
+ - **In-place detection and migration**: Auto-migrate on first load. Rejected as too dangerous.
99
+ - **Rake task only**: Would work but less discoverable. CLI command is more user-friendly.
100
+ - **Bidirectional migration**: Support going back to old structure. Rejected as unnecessary - this is a one-way breaking change.
101
+
102
+ **Implementation impact**:
103
+ - Create `lib/ro/script/migrator.rb`
104
+ - Add CLI command in `bin/ro` or Rakefile
105
+ - Must handle:
106
+ - Moving `identifier/attributes.yml` → `identifier.yml`
107
+ - Moving `identifier/assets/**/*` → `identifier/`
108
+ - Moving `identifier/body.md` and other root files → `identifier/`
109
+ - Preserving file permissions and timestamps
110
+ - Handling errors gracefully with rollback capability
111
+
112
+ ### Decision 6: Testing Strategy
113
+
114
+ **What was chosen**: TDD approach with comprehensive test coverage before implementation
115
+
116
+ **Rationale**:
117
+ - **Current state**: No tests exist in the repository
118
+ - **Risk**: Core refactoring without tests is extremely dangerous
119
+ - **Approach**:
120
+ 1. Create tests for OLD structure first (document current behavior)
121
+ 2. Ensure all tests pass (baseline)
122
+ 3. Implement new structure
123
+ 4. Create tests for NEW structure
124
+ 5. Ensure both pass during transition
125
+ 6. Remove old structure support after migration
126
+
127
+ **Alternatives considered**:
128
+ - **Write tests after implementation**: Rejected - too risky for core refactoring
129
+ - **Minimal test coverage**: Rejected - this is a breaking change affecting all users
130
+
131
+ **Implementation impact**:
132
+ - Create `test/` directory with unit, functional, integration subdirectories
133
+ - Create test fixtures in both old and new structure formats
134
+ - Write tests for Node, Collection, Asset, Root, and Migrator classes
135
+ - Integration tests for end-to-end asset loading/writing/migration
136
+
137
+ ## Technical Constraints
138
+
139
+ ### File System Compatibility
140
+
141
+ **Research**: Need to support cross-platform file paths (Linux, macOS, Windows)
142
+
143
+ **Findings**:
144
+ - Ruby's `Pathname` class (already used in ro codebase) handles cross-platform paths
145
+ - YAML/JSON filename extensions are case-sensitive on Linux, case-insensitive on Windows
146
+ - Must test migration tool on all platforms
147
+
148
+ **Decision**: Continue using `Pathname`, add explicit case-handling for file extension detection
149
+
150
+ ### Metadata Format Support
151
+
152
+ **Research**: Which metadata formats must be supported?
153
+
154
+ **Findings from codebase**:
155
+ - `Node#_load_base_attributes` (lib/ro/node.rb:57) uses glob: `"attributes.{yml,yaml,json}"`
156
+ - Only YAML and JSON are currently supported
157
+ - Spec mentions TOML support (FR-005)
158
+
159
+ **Decision**:
160
+ - Phase 1: Support existing formats (YAML, JSON)
161
+ - Phase 2 (optional): Add TOML support if requested
162
+ - Extension detection must be explicit: `.yml`, `.yaml`, `.json`, (`.toml`)
163
+
164
+ ### Performance Considerations
165
+
166
+ **Research**: How to maintain <100ms asset lookup performance (SC-001)?
167
+
168
+ **Findings**:
169
+ - Current implementation uses `Pathname#glob` for file discovery
170
+ - Ruby's `Dir.glob` is generally fast for up to 10,000 files
171
+ - No caching is currently implemented
172
+ - Performance bottleneck is likely I/O, not directory structure
173
+
174
+ **Decision**:
175
+ - New structure should be FASTER (less nesting = fewer stat calls)
176
+ - No caching needed initially
177
+ - If performance issues arise, add memoization to Collection#nodes
178
+
179
+ ## Migration Edge Cases
180
+
181
+ ### Case 1: Identifier with Special Characters
182
+
183
+ **Scenario**: Asset identifier contains spaces, unicode, or special chars
184
+ **Example**: `my post!.yml` and `my post!/`
185
+
186
+ **Research**: How do filesystems handle these?
187
+ - **Linux/macOS**: Allow most characters except `/` and null
188
+ - **Windows**: Disallows `< > : " | ? * \`
189
+ - **Assumption from spec**: "Asset identifiers are valid filenames"
190
+
191
+ **Decision**: Trust the spec assumption - if it exists in old format, it should migrate cleanly
192
+
193
+ ### Case 2: Metadata-Only Assets
194
+
195
+ **Scenario**: Asset has `identifier.yml` but no `identifier/` directory
196
+ **Spec reference**: FR-007 "handle metadata only"
197
+
198
+ **Decision**: Perfectly valid in new structure, no special handling needed
199
+
200
+ ### Case 3: Files-Only Assets
201
+
202
+ **Scenario**: Asset has `identifier/` directory but no metadata file
203
+ **Spec reference**: FR-008 "handle files only"
204
+
205
+ **Research**: In old structure, would be `identifier/` with no `attributes.yml`
206
+
207
+ **Decision**:
208
+ - In new structure, create empty `identifier.yml` with minimal metadata
209
+ - Or: Skip during migration, log as warning
210
+ - **Requires clarification**: Ask user preference during implementation
211
+
212
+ ### Case 4: Both Structures Exist
213
+
214
+ **Scenario**: Migration interrupted, both `identifier/attributes.yml` AND `identifier.yml` exist
215
+ **Spec reference**: FR-011 "prefer old structure until migrated"
216
+
217
+ **Decision**:
218
+ - Detection logic: If `identifier/attributes.yml` exists, treat as old structure (ignore `identifier.yml`)
219
+ - Migration tool should check for this and warn user
220
+ - Post-migration cleanup: Remove old structure only after verification
221
+
222
+ ### Case 5: Nested Asset Directories
223
+
224
+ **Scenario**: Assets in subdirectories like `identifier/assets/images/photo.jpg`
225
+ **Edge case from spec**: "nested directories within asset directory"
226
+
227
+ **Decision**:
228
+ - Old: `identifier/assets/images/photo.jpg`
229
+ - New: `identifier/images/photo.jpg`
230
+ - Migration preserves directory structure, just removes `assets/` prefix
231
+
232
+ ### Case 6: Non-Asset Files in Node Directory
233
+
234
+ **Scenario**: Files like `body.md`, `samples/`, etc. in node directory
235
+ **Example**: `public/ro/pages/disco/` has `body.md`, `samples/`, and `assets/`
236
+
237
+ **Decision**:
238
+ - Old: `identifier/body.md`, `identifier/assets/`, `identifier/samples/`
239
+ - New: All go in `identifier/`: `identifier/body.md`, `identifier/samples/`
240
+ - These are NOT in `assets/` currently, so they stay at same relative position
241
+ - `Node#_ignored_files` explicitly ignores `assets/**/**`, so these files are already treated as node content
242
+
243
+ ## Dependencies and Integration Points
244
+
245
+ ### Dependency 1: Pathname Library
246
+
247
+ **Usage**: Core path manipulation throughout codebase
248
+ **Impact**: None - Pathname will continue to work with new structure
249
+ **Action**: No changes needed
250
+
251
+ ### Dependency 2: Front Matter Parser
252
+
253
+ **Usage**: `front_matter_parser` gem extracts YAML frontmatter from markdown
254
+ **Impact**: None - frontmatter parsing is independent of file location
255
+ **Action**: No changes needed
256
+
257
+ ### Dependency 3: Kramdown
258
+
259
+ **Usage**: Markdown rendering for body content
260
+ **Impact**: None - markdown rendering is independent of file location
261
+ **Action**: No changes needed
262
+
263
+ ### Integration Point 1: Static API Builder
264
+
265
+ **Location**: `lib/ro/script/builder.rb`
266
+ **Current behavior**: Iterates nodes, generates JSON API
267
+ **Impact**: Should work transparently if Node interface unchanged
268
+ **Action**: Verify builder still works after Node refactoring, add integration test
269
+
270
+ ### Integration Point 2: Dev Server
271
+
272
+ **Location**: `lib/ro/script/server.rb`
273
+ **Current behavior**: Serves content from ro directory structure
274
+ **Impact**: Should work transparently if Node interface unchanged
275
+ **Action**: Test server manually after implementation
276
+
277
+ ### Integration Point 3: GitHub Pages Workflow
278
+
279
+ **Location**: `.github/workflows/gh-pages.yml`
280
+ **Current behavior**: Builds static API and deploys to GitHub Pages
281
+ **Impact**: Must work with migrated `public/ro/` structure
282
+ **Action**: Migrate `public/ro/` as part of this feature, test workflow
283
+
284
+ ## Best Practices
285
+
286
+ ### Ruby Gem Versioning
287
+
288
+ **Research**: Semantic versioning for breaking changes
289
+ **Best practice**: Major version bump for breaking changes (4.x → 5.0)
290
+ **Action**: Update version in `lib/ro.rb` and `ro.gemspec`
291
+
292
+ ### Ruby Testing Patterns
293
+
294
+ **Research**: Conventions for Ruby testing without RSpec/Minitest
295
+ **Findings**: The Rakefile already defines a custom test runner
296
+ **Best practice**: Follow existing convention (test/**/*_test.rb)
297
+ **Action**: Create tests matching existing Rake task pattern
298
+
299
+ ### File System Operations
300
+
301
+ **Research**: Safe file operations in Ruby
302
+ **Best practices**:
303
+ - Use `FileUtils.mv` for moves (atomic on same filesystem)
304
+ - Use `FileUtils.cp_r` for backups
305
+ - Wrap in transactions with rollback capability
306
+ - Verify checksums before/after migration
307
+
308
+ **Action**: Implement migration with:
309
+ - Pre-migration validation
310
+ - Atomic operations where possible
311
+ - Error handling with rollback
312
+ - Post-migration verification
313
+
314
+ ### Changelog and Documentation
315
+
316
+ **Research**: Documenting breaking changes
317
+ **Best practice**:
318
+ - CHANGELOG.md with clear breaking changes section
319
+ - README.md with migration guide
320
+ - Version upgrade guide
321
+
322
+ **Action**: Update documentation as part of this feature
323
+
324
+ ## Open Questions
325
+
326
+ None - all clarifications were resolved during specification phase.
327
+
328
+ ## References
329
+
330
+ - [Semantic Versioning](https://semver.org/) - Version numbering for breaking changes
331
+ - [Ruby Pathname Documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/pathname/rdoc/Pathname.html) - Path manipulation
332
+ - [FileUtils Documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/fileutils/rdoc/FileUtils.html) - File operations
333
+ - ro gem codebase exploration findings (see plan.md Phase 0 notes)
@@ -0,0 +1,127 @@
1
+ # Feature Specification: Simplify Asset Directory Structure
2
+
3
+ **Feature Branch**: `001-simplify-asset-structure`
4
+ **Created**: 2025-10-17
5
+ **Status**: In Progress
6
+ **Input**: User description: "attm, ro stores assets in a directory structure like ./ro/posts/teh-slug/attributes.yml, ./ro/posts/teh-slug/assets/**.**. we want to, instead, use a more simple structure like ./ro/posts/teh-slug.yml and ./ro/posts/teh-slug/assets/**.**. that is to say '$identifier.yml' (or json, etc) for the main data and '$identifier/assets/**.**' for asset files"
7
+
8
+ **Clarification**: Assets remain in the `assets/` subdirectory to prevent non-asset files (like markdown, ERB templates) from being treated as assets. Only the metadata file moves from `$identifier/attributes.yml` to `$identifier.yml`.
9
+
10
+ ## User Scenarios & Testing *(mandatory)*
11
+
12
+ ### User Story 1 - Read Asset Data (Priority: P1)
13
+
14
+ Users need to access asset metadata and associated files using a simpler, more intuitive directory structure where the metadata file sits alongside the asset directory instead of being nested inside it.
15
+
16
+ **Why this priority**: This is the core structural change that affects all asset operations. Without this, no other functionality can work with the new structure.
17
+
18
+ **Independent Test**: Can be fully tested by creating an asset with the new structure (identifier.yml + identifier/ directory) and verifying that metadata can be read correctly. Delivers immediate value by making asset organization clearer.
19
+
20
+ **Acceptance Scenarios**:
21
+
22
+ 1. **Given** an asset with identifier "my-post", **When** the system reads the asset, **Then** it loads metadata from `my-post.yml` and assets from `my-post/assets/` directory
23
+ 2. **Given** multiple assets exist, **When** the system lists assets, **Then** it correctly identifies each asset by matching `identifier.yml` files with corresponding `identifier/assets/` directories
24
+
25
+ ---
26
+
27
+ ### User Story 2 - Write Asset Data (Priority: P2)
28
+
29
+ Users need to create and update assets in the new simplified structure, with metadata stored in a single file at the root level rather than nested inside a subdirectory.
30
+
31
+ **Why this priority**: Creating and modifying assets is essential for practical use, but depends on the ability to read assets correctly (P1).
32
+
33
+ **Independent Test**: Can be tested by creating a new asset, updating its metadata, and verifying the structure matches the expected pattern (`identifier.yml` + `identifier/` for files).
34
+
35
+ **Acceptance Scenarios**:
36
+
37
+ 1. **Given** a new asset identifier "new-post", **When** the system creates the asset, **Then** it creates `new-post.yml` for metadata and `new-post/assets/` directory for associated files
38
+ 2. **Given** an existing asset, **When** metadata is updated, **Then** changes are written to the `identifier.yml` file
39
+ 3. **Given** new files are added to an asset, **When** the system saves them, **Then** files are stored in the `identifier/assets/` directory
40
+
41
+ ---
42
+
43
+ ### User Story 3 - Migrate Existing Assets (Priority: P3)
44
+
45
+ Users need to migrate assets from the old structure (`identifier/attributes.yml` + `identifier/assets/`) to the new structure (`identifier.yml` + `identifier/`) without data loss.
46
+
47
+ **Why this priority**: Migration is necessary for existing installations but can happen after the new structure is fully functional. New users won't need this.
48
+
49
+ **Independent Test**: Can be tested by creating assets in the old format, running migration, and verifying all data is preserved in the new format.
50
+
51
+ **Acceptance Scenarios**:
52
+
53
+ 1. **Given** assets in old format (`slug/attributes.yml` + `slug/assets/`), **When** migration runs, **Then** metadata is moved to `slug.yml` and asset files remain in `slug/assets/`
54
+ 2. **Given** an asset with both metadata and files, **When** migration completes, **Then** all data is accessible in the new structure with only the metadata file location changed
55
+ 3. **Given** migration fails partway through, **When** the error occurs, **Then** the system can resume or rollback without data corruption
56
+
57
+ ---
58
+
59
+ ### Edge Cases
60
+
61
+ - What happens when both old and new structures exist for the same identifier (e.g., `post.yml` and `post/attributes.yml` both present)?
62
+ - How does the system handle assets with only metadata (no associated files directory)?
63
+ - How does the system handle assets with only files (no metadata file)?
64
+ - What happens when identifier contains special characters that might be invalid in filenames?
65
+ - How does the system handle nested directories within the asset directory (e.g., `identifier/subdir/file.txt`)?
66
+
67
+ ## Requirements *(mandatory)*
68
+
69
+ ### Functional Requirements
70
+
71
+ - **FR-001**: System MUST read asset metadata from files named `{identifier}.yml` (or .json, .toml, etc.) located at the collection level
72
+ - **FR-002**: System MUST read asset files from directories named `{identifier}/assets/` located at the same collection level
73
+ - **FR-003**: System MUST write new asset metadata to `{identifier}.yml` format at the collection level
74
+ - **FR-004**: System MUST write asset files to `{identifier}/assets/` directory structure
75
+ - **FR-005**: System MUST support multiple metadata formats (YAML, JSON, TOML, etc.) based on file extension
76
+ - **FR-006**: System MUST correctly identify assets by detecting metadata files with supported extensions
77
+ - **FR-007**: System MUST handle assets that have metadata only (no directory)
78
+ - **FR-008**: System MUST handle assets that have files only (no metadata file)
79
+ - **FR-009**: System MUST provide migration capability to convert from old structure (`identifier/attributes.yml` + `identifier/assets/`) to new structure (`identifier.yml` + `identifier/assets/`)
80
+ - **FR-010**: System MUST preserve all data during migration without loss
81
+ - **FR-011**: System MUST prefer old structure (`identifier/attributes.yml`) when both old and new structures exist for the same identifier, until explicit migration is performed
82
+ - **FR-012**: This is a one-time breaking change requiring a major version bump; migration must be completed before upgrading to the new version
83
+
84
+ ### Key Entities
85
+
86
+ - **Asset**: Represents a content item with an identifier, metadata (stored in `{identifier}.yml`), and optional associated files (stored in `{identifier}/` directory)
87
+ - **Collection**: A directory containing multiple assets, where each asset consists of a metadata file and optional asset directory at the same level
88
+ - **Identifier**: Unique name for an asset within a collection, used as the base name for both the metadata file and asset directory
89
+
90
+ ## Success Criteria *(mandatory)*
91
+
92
+ ### Measurable Outcomes
93
+
94
+ - **SC-001**: Assets can be located by identifier in under 100ms for collections containing up to 10,000 assets
95
+ - **SC-002**: Migration completes without data loss for 100% of assets tested
96
+ - **SC-003**: New asset structure reduces directory nesting depth by one level (from 3 to 2 levels)
97
+ - **SC-004**: Developers can understand the asset structure without documentation within 30 seconds of viewing the directory tree
98
+ - **SC-005**: Asset operations (read/write) work correctly for metadata-only assets, file-only assets, and combined assets in 100% of test cases
99
+
100
+ ## Assumptions
101
+
102
+ - Asset identifiers are valid filenames in the target filesystem (no special characters that would cause filesystem errors)
103
+ - Collections are organized as directories containing multiple assets
104
+ - The system already has mechanisms for reading/writing YAML, JSON, or other structured data formats
105
+ - The old structure pattern is `{identifier}/attributes.yml` for metadata and `{identifier}/assets/` for files
106
+ - Migration is a one-time operation that can be run as a maintenance task before upgrading to the new major version
107
+ - System has file I/O capabilities to move/copy files and directories
108
+ - Users will complete migration of all assets before upgrading to the new major version
109
+ - This breaking change warrants a major version bump (e.g., 1.x.x → 2.0.0)
110
+
111
+ ## Scope
112
+
113
+ ### In Scope
114
+
115
+ - Reading assets in the new structure format
116
+ - Writing assets in the new structure format
117
+ - Supporting multiple metadata formats (YAML, JSON, TOML, etc.)
118
+ - Migrating existing assets from old to new structure
119
+ - Handling edge cases (metadata-only, files-only, missing identifiers)
120
+
121
+ ### Out of Scope
122
+
123
+ - Changes to metadata content schema (only structure changes, not content changes)
124
+ - Performance optimization beyond basic file I/O
125
+ - Validation of metadata content (validation rules remain unchanged)
126
+ - User interface changes (this is a structural change only)
127
+ - Access control or permissions (security model remains unchanged)