ace-support-markdown 0.3.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 +7 -0
- data/CHANGELOG.md +121 -0
- data/CONTRIBUTING.md +221 -0
- data/LICENSE +21 -0
- data/README.md +29 -0
- data/Rakefile +13 -0
- data/lib/ace/support/markdown/atoms/document_validator.rb +129 -0
- data/lib/ace/support/markdown/atoms/frontmatter_extractor.rb +117 -0
- data/lib/ace/support/markdown/atoms/frontmatter_serializer.rb +88 -0
- data/lib/ace/support/markdown/atoms/section_extractor.rb +174 -0
- data/lib/ace/support/markdown/models/markdown_document.rb +187 -0
- data/lib/ace/support/markdown/models/section.rb +114 -0
- data/lib/ace/support/markdown/molecules/document_builder.rb +151 -0
- data/lib/ace/support/markdown/molecules/frontmatter_editor.rb +108 -0
- data/lib/ace/support/markdown/molecules/kramdown_processor.rb +124 -0
- data/lib/ace/support/markdown/molecules/section_editor.rb +158 -0
- data/lib/ace/support/markdown/organisms/document_editor.rb +190 -0
- data/lib/ace/support/markdown/organisms/safe_file_writer.rb +203 -0
- data/lib/ace/support/markdown/version.rb +9 -0
- data/lib/ace/support/markdown.rb +34 -0
- metadata +148 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: caaad5b4c68beae09ec7d4e7d6c09b0b0060cb09eeff550d72473edcb315e87e
|
|
4
|
+
data.tar.gz: 150a17fa755dbacf16be654f325fcc9eaa520f2e7f69e77150304aeedb8c3e81
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 55dd180392b834db3db26c312f193b68c8ffa2b3ccb9b9099a99ecc365c4b0e3c91153d7329829e88ba323722519727ea62268cc3b9f054beea1d66ce5cced37
|
|
7
|
+
data.tar.gz: ab08ad8cd7290c10c53021121b247c2eec7db7939bdf2f58356fd7f33690623cbc081b611daf60b8823bc1c03ec1a08e742458f95223671e0f1d3fac855c0aa6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Technical
|
|
13
|
+
- Removed phantom `handbook/**/*` glob from gemspec (no handbook directory exists).
|
|
14
|
+
|
|
15
|
+
## [0.2.2] - 2026-03-22
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Refreshed README structure with an explicit Purpose section and consistent support-library framing.
|
|
19
|
+
- Updated README testing commands to use `ace-test` instead of `bundle exec` invocations.
|
|
20
|
+
- Added a "Part of ACE" footer link to the package README.
|
|
21
|
+
|
|
22
|
+
## [0.2.1] - 2026-02-23
|
|
23
|
+
|
|
24
|
+
### Technical
|
|
25
|
+
- Updated internal dependency version constraints to current releases
|
|
26
|
+
|
|
27
|
+
## [0.2.0] - 2026-01-03
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **BREAKING**: Minimum Ruby version raised to 3.3.0 (was 3.1.0)
|
|
31
|
+
- Standardized gemspec file patterns with deterministic Dir.glob
|
|
32
|
+
- Added MIT LICENSE file
|
|
33
|
+
|
|
34
|
+
## [0.1.3] - 2025-11-01
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **Dependency Migration**: Updated to use renamed infrastructure gems
|
|
39
|
+
- Changed dependency from `ace-core` to `ace-support-core`
|
|
40
|
+
- Part of ecosystem-wide naming convention alignment for infrastructure gems
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## [0.1.2] - 2025-10-23
|
|
44
|
+
|
|
45
|
+
### Improved
|
|
46
|
+
|
|
47
|
+
- **README examples now include educational comments**
|
|
48
|
+
- Added "why" explanations to all 6 real-world examples
|
|
49
|
+
- Comments explain reasoning behind patterns (validation, error handling, etc.)
|
|
50
|
+
- Clarifies ATOM architecture usage and best practices
|
|
51
|
+
- Explains StandardError rescue patterns and ensure block usage
|
|
52
|
+
|
|
53
|
+
- **Example 5 refactored to use begin/rescue/ensure pattern**
|
|
54
|
+
- Replaced multiple rescue blocks with ensure block for rollback
|
|
55
|
+
- Uses success flag to track operation status
|
|
56
|
+
- Cleaner, more maintainable error handling pattern
|
|
57
|
+
- Guarantees cleanup even with non-linear control flow
|
|
58
|
+
|
|
59
|
+
- **Documentation sync strategy documented**
|
|
60
|
+
- Added "Maintaining Documentation" section to README
|
|
61
|
+
- Created comprehensive CONTRIBUTING.md with API sync guidelines
|
|
62
|
+
- Documents the automated README validation approach
|
|
63
|
+
- Provides checklist for API changes and documentation updates
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Automated README example validation** (`test/integration/readme_examples_test.rb`)
|
|
68
|
+
- 8 test cases validating all README code examples
|
|
69
|
+
- Tests run as part of standard test suite
|
|
70
|
+
- Catches documentation/code mismatches automatically
|
|
71
|
+
- Ensures examples stay in sync with API evolution
|
|
72
|
+
- Validates Quick Start, API Documentation, and Real-World Examples
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **Corrected API parameter names in README**
|
|
77
|
+
- Fixed `validate: true` → `validate_before: true` for DocumentEditor.save!()
|
|
78
|
+
- Ensures documentation accurately reflects actual API
|
|
79
|
+
- Caught by new automated README validation tests
|
|
80
|
+
|
|
81
|
+
## [0.1.1] - 2025-10-23
|
|
82
|
+
|
|
83
|
+
### Documentation
|
|
84
|
+
|
|
85
|
+
- **Enhanced README with comprehensive real-world examples** (390+ lines)
|
|
86
|
+
- Example 1: Task management system (auto-fixing with backup/validation)
|
|
87
|
+
- Example 2: Documentation updates (bulk operations, nested frontmatter)
|
|
88
|
+
- Example 3: Complex multi-section operations (complete task workflow)
|
|
89
|
+
- Example 4: Safe file writing with custom validation
|
|
90
|
+
- Example 5: Error handling and recovery (rollback, retry logic)
|
|
91
|
+
- Example 6: Batch operations with progress tracking
|
|
92
|
+
- All examples based on actual ace-taskflow and ace-docs implementations
|
|
93
|
+
- Demonstrates error handling patterns, validation rules, and safety features
|
|
94
|
+
|
|
95
|
+
## [0.1.0] - 2025-10-18
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
- Initial release
|
|
99
|
+
- Frontmatter extraction and serialization atoms
|
|
100
|
+
- Section extraction using Kramdown AST
|
|
101
|
+
- Document validation with hardcoded rules
|
|
102
|
+
- Frontmatter and section editing molecules
|
|
103
|
+
- Safe file writing with backup/rollback
|
|
104
|
+
- Document editor with fluent API
|
|
105
|
+
- Immutable document models
|
|
106
|
+
- Test suite with 100% atom coverage
|
|
107
|
+
|
|
108
|
+
### Features
|
|
109
|
+
- ATOM architecture (Atoms, Molecules, Organisms, Models)
|
|
110
|
+
- Exact string matching for section identification
|
|
111
|
+
- Atomic file operations with temp file + move pattern
|
|
112
|
+
- Performance: <10ms frontmatter updates, <50ms section edits
|
|
113
|
+
- Zero-corruption design with validation and rollback
|
|
114
|
+
|
|
115
|
+
[Unreleased]: https://github.com/cs3b/ace/compare/v0.2.2...HEAD
|
|
116
|
+
[0.2.2]: https://github.com/cs3b/ace/compare/v0.2.1...v0.2.2
|
|
117
|
+
[0.2.1]: https://github.com/cs3b/ace/compare/v0.2.0...v0.2.1
|
|
118
|
+
[0.2.0]: https://github.com/cs3b/ace/compare/v0.1.2...v0.2.0
|
|
119
|
+
[0.1.2]: https://github.com/cs3b/ace/compare/v0.1.1...v0.1.2
|
|
120
|
+
[0.1.1]: https://github.com/cs3b/ace/compare/v0.1.0...v0.1.1
|
|
121
|
+
[0.1.0]: https://github.com/cs3b/ace/releases/tag/v0.1.0
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Contributing to ace-support-markdown
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to ace-support-markdown! This document provides guidelines for maintaining code quality and documentation accuracy.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/your-org/ace-support-markdown.git
|
|
9
|
+
cd ace-support-markdown
|
|
10
|
+
bundle install
|
|
11
|
+
bundle exec rake test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Code Architecture
|
|
15
|
+
|
|
16
|
+
This gem follows the **ATOM architecture pattern**:
|
|
17
|
+
|
|
18
|
+
- **Atoms**: Pure functions with no side effects (`lib/ace/support/markdown/atoms/`)
|
|
19
|
+
- **Molecules**: Composed operations (`lib/ace/support/markdown/molecules/`)
|
|
20
|
+
- **Organisms**: High-level orchestration (`lib/ace/support/markdown/organisms/`)
|
|
21
|
+
- **Models**: Immutable data structures (`lib/ace/support/markdown/models/`)
|
|
22
|
+
|
|
23
|
+
### Design Principles
|
|
24
|
+
|
|
25
|
+
1. **Immutability**: Models return new instances on transformation
|
|
26
|
+
2. **Safety First**: All file operations include backup and rollback
|
|
27
|
+
3. **Validation**: Pre-write and post-write validation prevents corruption
|
|
28
|
+
4. **Atomicity**: Operations succeed completely or fail completely
|
|
29
|
+
5. **Clear Errors**: Detailed error messages with actionable information
|
|
30
|
+
|
|
31
|
+
## Making Changes
|
|
32
|
+
|
|
33
|
+
### 1. Adding New Features
|
|
34
|
+
|
|
35
|
+
When adding new functionality:
|
|
36
|
+
|
|
37
|
+
1. **Identify the layer**: Determine if it's an atom, molecule, organism, or model
|
|
38
|
+
2. **Write tests first**: Add test cases before implementation
|
|
39
|
+
3. **Implement the feature**: Follow existing patterns and conventions
|
|
40
|
+
4. **Update documentation**: Add examples to README.md
|
|
41
|
+
5. **Add README tests**: Create tests in `test/integration/readme_examples_test.rb`
|
|
42
|
+
6. **Run full test suite**: `bundle exec rake test`
|
|
43
|
+
7. **Update CHANGELOG.md**: Document the new feature
|
|
44
|
+
|
|
45
|
+
### 2. Fixing Bugs
|
|
46
|
+
|
|
47
|
+
When fixing bugs:
|
|
48
|
+
|
|
49
|
+
1. **Write a failing test**: Reproduce the bug in a test case
|
|
50
|
+
2. **Fix the implementation**: Resolve the issue
|
|
51
|
+
3. **Verify the fix**: Ensure the test passes
|
|
52
|
+
4. **Check for regressions**: Run full test suite
|
|
53
|
+
5. **Update CHANGELOG.md**: Document the fix
|
|
54
|
+
|
|
55
|
+
### 3. Updating API
|
|
56
|
+
|
|
57
|
+
**IMPORTANT**: API changes require updating both code AND documentation.
|
|
58
|
+
|
|
59
|
+
When modifying the public API:
|
|
60
|
+
|
|
61
|
+
1. **Update implementation** in `lib/`
|
|
62
|
+
2. **Update corresponding tests** in `test/`
|
|
63
|
+
3. **Update README examples** - this is critical!
|
|
64
|
+
4. **Update README example tests** in `test/integration/readme_examples_test.rb`
|
|
65
|
+
5. **Run test suite** - README tests will catch doc/code mismatches
|
|
66
|
+
6. **Update CHANGELOG.md**
|
|
67
|
+
7. **Consider semver implications**:
|
|
68
|
+
- Breaking changes → MAJOR version bump
|
|
69
|
+
- New features → MINOR version bump
|
|
70
|
+
- Bug fixes → PATCH version bump
|
|
71
|
+
|
|
72
|
+
### 4. Documentation Sync Strategy
|
|
73
|
+
|
|
74
|
+
**The Problem**: Documentation can become stale as code evolves.
|
|
75
|
+
|
|
76
|
+
**Our Solution**: Automated validation of README examples.
|
|
77
|
+
|
|
78
|
+
The test file `test/integration/readme_examples_test.rb` validates that:
|
|
79
|
+
- Code examples in README.md are syntactically correct
|
|
80
|
+
- Examples work against the current API
|
|
81
|
+
- API behavior matches what's documented
|
|
82
|
+
|
|
83
|
+
**When you change the API:**
|
|
84
|
+
1. Update the README examples
|
|
85
|
+
2. Update the README example tests
|
|
86
|
+
3. Run `bundle exec rake test`
|
|
87
|
+
4. If tests fail, either:
|
|
88
|
+
- Fix the README (if docs are wrong)
|
|
89
|
+
- Fix the test (if test is wrong)
|
|
90
|
+
- Fix the implementation (if code is wrong)
|
|
91
|
+
|
|
92
|
+
**This ensures documentation never gets out of sync with reality.**
|
|
93
|
+
|
|
94
|
+
## Testing Guidelines
|
|
95
|
+
|
|
96
|
+
### Test Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
test/
|
|
100
|
+
├── atoms/ # Unit tests for pure functions
|
|
101
|
+
├── molecules/ # Integration tests for operations
|
|
102
|
+
├── organisms/ # End-to-end tests for orchestration
|
|
103
|
+
├── models/ # Tests for data structures
|
|
104
|
+
└── integration/ # Full workflow tests + README validation
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Writing Tests
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class MyFeatureTest < Minitest::Test
|
|
111
|
+
include TestHelpers
|
|
112
|
+
|
|
113
|
+
def setup
|
|
114
|
+
# Setup test environment
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def teardown
|
|
118
|
+
# Clean up
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_feature_behavior
|
|
122
|
+
# Arrange: Set up test data
|
|
123
|
+
# Act: Execute the feature
|
|
124
|
+
# Assert: Verify behavior
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Test Coverage Requirements
|
|
130
|
+
|
|
131
|
+
- **Atoms**: 100% coverage (pure functions, easy to test)
|
|
132
|
+
- **Molecules**: 95%+ coverage
|
|
133
|
+
- **Organisms**: 95%+ coverage
|
|
134
|
+
- **Integration**: All major workflows covered
|
|
135
|
+
|
|
136
|
+
Run coverage report:
|
|
137
|
+
```bash
|
|
138
|
+
bundle exec rake test
|
|
139
|
+
# Coverage report in coverage/index.html
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Documentation Guidelines
|
|
143
|
+
|
|
144
|
+
### README Examples
|
|
145
|
+
|
|
146
|
+
Examples should be:
|
|
147
|
+
1. **Real-world**: Based on actual use cases from ACE gems
|
|
148
|
+
2. **Complete**: Include setup, execution, and verification
|
|
149
|
+
3. **Commented**: Explain *why*, not just *what*
|
|
150
|
+
4. **Tested**: Have corresponding tests in `readme_examples_test.rb`
|
|
151
|
+
|
|
152
|
+
### Code Comments
|
|
153
|
+
|
|
154
|
+
Add comments that explain:
|
|
155
|
+
- **Why** decisions were made (not what the code does)
|
|
156
|
+
- **Edge cases** and special handling
|
|
157
|
+
- **Performance** considerations
|
|
158
|
+
- **Safety** guarantees
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
```ruby
|
|
162
|
+
# Use ensure block for rollback to guarantee cleanup even if control flow is interrupted
|
|
163
|
+
# This handles early returns, raised exceptions, and other non-linear execution paths
|
|
164
|
+
ensure
|
|
165
|
+
editor.rollback if original_backup && !success_flag
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### CHANGELOG Format
|
|
170
|
+
|
|
171
|
+
Follow [Keep a Changelog](https://keepachangelog.com/) format:
|
|
172
|
+
|
|
173
|
+
```markdown
|
|
174
|
+
## [0.1.2] - 2025-10-23
|
|
175
|
+
|
|
176
|
+
### Added
|
|
177
|
+
- New feature description
|
|
178
|
+
|
|
179
|
+
### Changed
|
|
180
|
+
- Modified behavior description
|
|
181
|
+
|
|
182
|
+
### Fixed
|
|
183
|
+
- Bug fix description
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Pull Request Process
|
|
187
|
+
|
|
188
|
+
1. **Create a feature branch**: `git checkout -b feature/my-feature`
|
|
189
|
+
2. **Make your changes**: Follow guidelines above
|
|
190
|
+
3. **Run tests**: `bundle exec rake test`
|
|
191
|
+
4. **Update CHANGELOG.md**: Document your changes
|
|
192
|
+
5. **Commit your changes**: Use clear commit messages
|
|
193
|
+
6. **Push to your fork**: `git push origin feature/my-feature`
|
|
194
|
+
7. **Create Pull Request**: With detailed description
|
|
195
|
+
|
|
196
|
+
### PR Checklist
|
|
197
|
+
|
|
198
|
+
- [ ] Tests added/updated and passing
|
|
199
|
+
- [ ] README updated (if API changed)
|
|
200
|
+
- [ ] README example tests updated (if examples changed)
|
|
201
|
+
- [ ] CHANGELOG.md updated
|
|
202
|
+
- [ ] Code follows ATOM architecture
|
|
203
|
+
- [ ] Comments explain *why*, not *what*
|
|
204
|
+
- [ ] No breaking changes (or clearly documented)
|
|
205
|
+
|
|
206
|
+
## Code Style
|
|
207
|
+
|
|
208
|
+
Follow Ruby community conventions:
|
|
209
|
+
- 2 spaces for indentation
|
|
210
|
+
- Snake_case for methods and variables
|
|
211
|
+
- CamelCase for classes and modules
|
|
212
|
+
- Descriptive names over short names
|
|
213
|
+
- Limit line length to 120 characters
|
|
214
|
+
|
|
215
|
+
## Questions?
|
|
216
|
+
|
|
217
|
+
- Open an issue for bugs or feature requests
|
|
218
|
+
- Start a discussion for questions or ideas
|
|
219
|
+
- Check existing issues and PRs first
|
|
220
|
+
|
|
221
|
+
Thank you for contributing to ace-support-markdown!
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ACE Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1> ACE - Support Markdown </h1>
|
|
3
|
+
|
|
4
|
+
Safe, composable markdown editing tools for ACE libraries and docs.
|
|
5
|
+
|
|
6
|
+
<img src="https://raw.githubusercontent.com/cs3b/ace/main/docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
|
|
7
|
+
<br><br>
|
|
8
|
+
|
|
9
|
+
<a href="https://rubygems.org/gems/ace-support-markdown"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-support-markdown.svg" /></a>
|
|
10
|
+
<a href="https://www.ruby-lang.org"><img alt="Ruby" src="https://img.shields.io/badge/Ruby-3.2+-CC342D?logo=ruby" /></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg" /></a>
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
|
|
16
|
+
|
|
17
|
+
`ace-support-markdown` provides atomic document editing with frontmatter (YAML metadata blocks at the top of markdown files) and section-level updates while preserving document integrity. It is the shared editing layer that packages like [ace-task](../ace-task) and [ace-docs](../ace-docs) depend on for safe, repeatable content mutations.
|
|
18
|
+
|
|
19
|
+
## Use Cases
|
|
20
|
+
|
|
21
|
+
**Safely update task and docs content** - use frontmatter and section editing primitives to apply metadata and content changes without risking accidental corruption during automated or agent-driven edits.
|
|
22
|
+
|
|
23
|
+
**Generate documents programmatically** - build reproducible markdown output with builder-like APIs used by [ace-task](../ace-task) for spec files and [ace-docs](../ace-docs) for generated documentation.
|
|
24
|
+
|
|
25
|
+
**Preserve history during edits** - maintain backup and rollback safety for write-heavy operations so that tools like [`ace-task`](../ace-task) can update specs confidently in batch workflows.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
Part of [ACE](https://github.com/cs3b/ace)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.warning = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Markdown
|
|
6
|
+
module Atoms
|
|
7
|
+
# Pure function to validate markdown documents and frontmatter
|
|
8
|
+
# v0.1.0: Uses hardcoded validation rules
|
|
9
|
+
# v0.2.0: Will support JSON Schema validation (see task 080)
|
|
10
|
+
class DocumentValidator
|
|
11
|
+
# Validate complete markdown content (frontmatter + body)
|
|
12
|
+
# @param content [String] The complete markdown content
|
|
13
|
+
# @param rules [Hash] Optional validation rules
|
|
14
|
+
# @return [Hash] Result with :valid (Boolean), :errors (Array), :warnings (Array)
|
|
15
|
+
def self.validate(content, rules: {})
|
|
16
|
+
return invalid_result(["Empty content"]) if content.nil? || content.empty?
|
|
17
|
+
|
|
18
|
+
errors = []
|
|
19
|
+
warnings = []
|
|
20
|
+
|
|
21
|
+
# Parse frontmatter
|
|
22
|
+
extractor_result = FrontmatterExtractor.extract(content)
|
|
23
|
+
|
|
24
|
+
unless extractor_result[:valid]
|
|
25
|
+
errors.concat(extractor_result[:errors])
|
|
26
|
+
return {valid: false, errors: errors, warnings: warnings}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Validate frontmatter structure
|
|
30
|
+
fm_errors = validate_frontmatter(extractor_result[:frontmatter], rules)
|
|
31
|
+
errors.concat(fm_errors)
|
|
32
|
+
|
|
33
|
+
# Validate body exists
|
|
34
|
+
if extractor_result[:body].nil? || extractor_result[:body].strip.empty?
|
|
35
|
+
warnings << "Empty body content"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
valid: errors.empty?,
|
|
40
|
+
errors: errors,
|
|
41
|
+
warnings: warnings
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Validate only frontmatter hash
|
|
46
|
+
# @param frontmatter [Hash] The frontmatter data
|
|
47
|
+
# @param rules [Hash] Optional validation rules
|
|
48
|
+
# @return [Hash] Result with :valid (Boolean), :errors (Array)
|
|
49
|
+
def self.validate_frontmatter(frontmatter, rules = {})
|
|
50
|
+
errors = []
|
|
51
|
+
|
|
52
|
+
unless frontmatter.is_a?(Hash)
|
|
53
|
+
errors << "Frontmatter must be a hash"
|
|
54
|
+
return errors
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Apply required fields validation
|
|
58
|
+
if rules[:required_fields]
|
|
59
|
+
rules[:required_fields].each do |field|
|
|
60
|
+
unless frontmatter.key?(field) || frontmatter.key?(field.to_s)
|
|
61
|
+
errors << "Missing required field: #{field}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Apply field type validation
|
|
67
|
+
if rules[:field_types]
|
|
68
|
+
rules[:field_types].each do |field, expected_type|
|
|
69
|
+
value = frontmatter[field] || frontmatter[field.to_s]
|
|
70
|
+
next unless value
|
|
71
|
+
|
|
72
|
+
unless value.is_a?(expected_type)
|
|
73
|
+
errors << "Field '#{field}' must be #{expected_type}, got #{value.class}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Apply enum validation
|
|
79
|
+
if rules[:enums]
|
|
80
|
+
rules[:enums].each do |field, allowed_values|
|
|
81
|
+
value = frontmatter[field] || frontmatter[field.to_s]
|
|
82
|
+
next unless value
|
|
83
|
+
|
|
84
|
+
unless allowed_values.include?(value)
|
|
85
|
+
errors << "Field '#{field}' must be one of #{allowed_values.join(", ")}, got '#{value}'"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
errors
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate that content can be round-trip parsed
|
|
94
|
+
# @param content [String] The markdown content
|
|
95
|
+
# @return [Boolean] true if content can be safely parsed and rebuilt
|
|
96
|
+
def self.can_round_trip?(content)
|
|
97
|
+
return false if content.nil? || content.empty?
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
# Extract frontmatter
|
|
101
|
+
result = FrontmatterExtractor.extract(content)
|
|
102
|
+
return false unless result[:valid]
|
|
103
|
+
|
|
104
|
+
# Serialize back
|
|
105
|
+
serialized = FrontmatterSerializer.serialize(
|
|
106
|
+
result[:frontmatter],
|
|
107
|
+
body: result[:body]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
serialized[:valid]
|
|
111
|
+
rescue
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def self.invalid_result(errors)
|
|
119
|
+
{
|
|
120
|
+
valid: false,
|
|
121
|
+
errors: errors,
|
|
122
|
+
warnings: []
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module Markdown
|
|
8
|
+
module Atoms
|
|
9
|
+
# Pure function to extract YAML frontmatter from markdown content
|
|
10
|
+
# Separates frontmatter and body content for safe editing
|
|
11
|
+
class FrontmatterExtractor
|
|
12
|
+
# Extract frontmatter from markdown content
|
|
13
|
+
# @param content [String] The markdown content with optional frontmatter
|
|
14
|
+
# @return [Hash] Result with :frontmatter (Hash), :body (String), :valid (Boolean), :errors (Array)
|
|
15
|
+
def self.extract(content)
|
|
16
|
+
return empty_result if content.nil? || content.empty?
|
|
17
|
+
|
|
18
|
+
# Check if content starts with YAML frontmatter delimiter
|
|
19
|
+
unless content.start_with?("---\n")
|
|
20
|
+
return {
|
|
21
|
+
frontmatter: {},
|
|
22
|
+
body: content,
|
|
23
|
+
valid: false,
|
|
24
|
+
errors: ["No frontmatter found"]
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find the ending delimiter (searching from position 4 to skip the opening ---)
|
|
29
|
+
end_index = content.index("\n---\n", 4)
|
|
30
|
+
|
|
31
|
+
unless end_index
|
|
32
|
+
return {
|
|
33
|
+
frontmatter: {},
|
|
34
|
+
body: content[4..-1] || "",
|
|
35
|
+
valid: false,
|
|
36
|
+
errors: ["Missing closing '---' delimiter for frontmatter"]
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Extract YAML content (between delimiters) and body content (after delimiters)
|
|
41
|
+
yaml_content = content[4...end_index]
|
|
42
|
+
body_content = content[(end_index + 5)..-1] || ""
|
|
43
|
+
|
|
44
|
+
# Parse YAML with safe_load
|
|
45
|
+
begin
|
|
46
|
+
frontmatter = YAML.safe_load(
|
|
47
|
+
yaml_content,
|
|
48
|
+
permitted_classes: [Date, Time, Symbol],
|
|
49
|
+
permitted_symbols: [],
|
|
50
|
+
aliases: true
|
|
51
|
+
) || {}
|
|
52
|
+
|
|
53
|
+
# Ensure frontmatter is a hash
|
|
54
|
+
unless frontmatter.is_a?(Hash)
|
|
55
|
+
return {
|
|
56
|
+
frontmatter: {},
|
|
57
|
+
body: body_content,
|
|
58
|
+
valid: false,
|
|
59
|
+
errors: ["Frontmatter must be a hash/object, got #{frontmatter.class}"]
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
frontmatter: frontmatter,
|
|
65
|
+
body: body_content,
|
|
66
|
+
valid: true,
|
|
67
|
+
errors: []
|
|
68
|
+
}
|
|
69
|
+
rescue Psych::SyntaxError => e
|
|
70
|
+
{
|
|
71
|
+
frontmatter: {},
|
|
72
|
+
body: body_content,
|
|
73
|
+
valid: false,
|
|
74
|
+
errors: ["YAML syntax error: #{e.message}"]
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Extract only the frontmatter hash
|
|
80
|
+
# @param content [String] The markdown content
|
|
81
|
+
# @return [Hash] The frontmatter data or empty hash
|
|
82
|
+
def self.frontmatter_only(content)
|
|
83
|
+
result = extract(content)
|
|
84
|
+
result[:frontmatter]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Extract only the body content without frontmatter
|
|
88
|
+
# @param content [String] The markdown content
|
|
89
|
+
# @return [String] The body content
|
|
90
|
+
def self.body_only(content)
|
|
91
|
+
result = extract(content)
|
|
92
|
+
result[:body]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if content has valid frontmatter
|
|
96
|
+
# @param content [String] The markdown content
|
|
97
|
+
# @return [Boolean] true if valid frontmatter exists
|
|
98
|
+
def self.has_frontmatter?(content)
|
|
99
|
+
result = extract(content)
|
|
100
|
+
result[:valid]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def self.empty_result
|
|
106
|
+
{
|
|
107
|
+
frontmatter: {},
|
|
108
|
+
body: "",
|
|
109
|
+
valid: false,
|
|
110
|
+
errors: ["Empty content"]
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|