ro 4.4.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +42 -16
- data/MIGRATION.md +320 -0
- data/README.md +30 -18
- data/a.yml +60 -0
- data/bin/ro +10 -0
- data/lib/ro/_lib.rb +1 -1
- data/lib/ro/asset.rb +46 -5
- data/lib/ro/collection.rb +51 -13
- data/lib/ro/migrator.rb +285 -0
- data/lib/ro/node.rb +53 -13
- data/lib/ro/root.rb +75 -1
- data/lib/ro/script/migrate.rb +204 -0
- data/lib/ro/script/server.rb +1 -1
- data/lib/ro.rb +1 -0
- data/ro.gemspec +207 -16
- data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
- data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
- data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
- data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
- data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
- data/specs/001-simplify-asset-structure/data-model.md +381 -0
- data/specs/001-simplify-asset-structure/plan.md +90 -0
- data/specs/001-simplify-asset-structure/quickstart.md +575 -0
- data/specs/001-simplify-asset-structure/research.md +333 -0
- data/specs/001-simplify-asset-structure/spec.md +127 -0
- data/specs/001-simplify-asset-structure/tasks.md +349 -0
- data/test/fixtures/new_structure/mixed/test-json.json +5 -0
- data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
- data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
- data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
- data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
- data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
- data/test/integration/ro_integration_test.rb +165 -0
- data/test/test_helper.rb +149 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
- data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
- data/test/unit/asset_test.rb +90 -0
- data/test/unit/collection_test.rb +127 -0
- data/test/unit/migrator_test.rb +209 -0
- data/test/unit/node_test.rb +138 -0
- metadata +111 -18
- /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
- /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
- /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
- /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
- /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
- /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
- /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
- /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
- /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
- /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
- /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
- /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
- /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
- /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
- /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
|
@@ -0,0 +1,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.
|