Compare commits
6 Commits
b5f510f9c7
...
3034b90a0e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3034b90a0e | |||
| c89a26f6d4 | |||
| db60a1f3aa | |||
| a8d9b9289c | |||
| 0004fa2a0f | |||
| 0f37900222 |
207
NEXT_SESSION_BRIEFING.md
Normal file
207
NEXT_SESSION_BRIEFING.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Next Session Briefing - MarkiTect Development
|
||||
|
||||
## 🎯 Current Status: Phase 2 Complete!
|
||||
|
||||
**Completed Issues:**
|
||||
- ✅ Issue #51: Add outline mode to schema generation
|
||||
- ✅ Issue #52: Capture actual heading text in schemas
|
||||
- ✅ Issue #54: Add content field instruction capabilities
|
||||
|
||||
**Next Phase Goals**: Continue with remaining schema generation enhancements
|
||||
**Mode**: Autonomous TDD8 implementation following established patterns
|
||||
**Expected Outcome**: Continue building advanced schema generation features
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Project Status
|
||||
|
||||
### ✅ Recently Completed
|
||||
- **Issue #50**: Metaschema definition COMPLETE (TDD8 cycle successful)
|
||||
- Files: `markitect/metaschema.py`, `markitect/schemas/markitect-metaschema.json`
|
||||
- CLI integration in `schema-ingest` command
|
||||
- 15 comprehensive tests (100% passing)
|
||||
- Foundation established for advanced schema features
|
||||
|
||||
### 🎯 Next Target: Issue #51 - Add outline mode to schema generation
|
||||
- **Priority**: High
|
||||
- **Dependencies**: ✅ Metaschema definition (Issue #50) COMPLETE
|
||||
- **Goal**: `markitect schema-generate --mode outline --depth 3 --outfile invoice.json example.md`
|
||||
- **Status**: Ready to start
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Development Environment & Tooling
|
||||
|
||||
### Working Directory
|
||||
```
|
||||
/mnt/c/Users/bernd.worsch/Documents/binky/2025/250915b-markitectAdvancedMarkdownEngine/markitect_project
|
||||
```
|
||||
|
||||
### Key Commands
|
||||
```bash
|
||||
# Start new issue workspace
|
||||
make tdd-start NUM=51
|
||||
|
||||
# Run tests
|
||||
PYTHONPATH=. python3 -m pytest tests/ --tb=short -q --maxfail=5
|
||||
|
||||
# Close issue when complete
|
||||
make close-issue NUM=51
|
||||
|
||||
# Show issues
|
||||
make show-issue NUM=51
|
||||
make list-issues
|
||||
```
|
||||
|
||||
### Available Subagents
|
||||
- **general-purpose**: Multi-step research and complex tasks
|
||||
- **tddai-assistant**: TDD8 workflow expert
|
||||
- **refactoring-assistant**: Code quality and improvement (use proactively)
|
||||
- **repository-assistant**: Directory structure optimization (use proactively)
|
||||
- **project-assistant**: Project tracking and planning
|
||||
|
||||
### TDD8 Workflow Protocol
|
||||
1. **ISSUE** - Understand requirements and analyze existing code
|
||||
2. **TEST** - Write failing tests first (RED state required)
|
||||
3. **RED** - Verify tests fail before implementation
|
||||
4. **GREEN** - Implement minimal code to pass tests
|
||||
5. **REFACTOR** - Clean up while maintaining green tests
|
||||
6. **DOCUMENT** - Update CLI help and documentation
|
||||
7. **REFINE** - Polish and optimize with comprehensive validation
|
||||
8. **PUBLISH** - Commit with descriptive message and close issue
|
||||
|
||||
---
|
||||
|
||||
## 📋 Issue #51 Implementation Plan
|
||||
|
||||
### Requirements Analysis
|
||||
- Add `--mode outline` option to `schema-generate` command
|
||||
- Add `--depth` parameter for outline depth control
|
||||
- Schema title should be "Schema from example.md" (not "for")
|
||||
- Capture actual heading text in generated schemas
|
||||
- Integrate with metaschema extensions from Issue #50
|
||||
|
||||
### Key Files to Examine/Modify
|
||||
- `markitect/cli.py` - schema-generate command (around line 1800+)
|
||||
- `markitect/schema_generator.py` - SchemaGenerator class
|
||||
- `tests/test_l4_service_schema_generation.py` - Existing schema tests
|
||||
- Create new test file for Issue #51 functionality
|
||||
|
||||
### Expected CLI Usage Pattern
|
||||
```bash
|
||||
# Current command
|
||||
markitect schema-generate document.md --max-depth 2 --output schema.json
|
||||
|
||||
# New outline mode
|
||||
markitect schema-generate --mode outline --depth 3 --outfile invoice.json example.md
|
||||
```
|
||||
|
||||
### Technical Integration Points
|
||||
- Use `x-markitect-outline-mode: true` in generated schemas
|
||||
- Use `x-markitect-outline-depth` to record depth setting
|
||||
- Integrate `x-markitect-heading-text` for actual heading capture
|
||||
- Ensure backward compatibility with existing `--max-depth` option
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Test File Structure
|
||||
- Create `.markitect_workspace/issue_51/tests/test_issue_51_outline_mode.py`
|
||||
- Follow established test patterns from Issue #50
|
||||
- Include CLI integration tests
|
||||
- Test backward compatibility
|
||||
|
||||
### Critical Test Cases
|
||||
1. Outline mode flag acceptance and processing
|
||||
2. Depth parameter validation and functionality
|
||||
3. Schema title format ("from" vs "for")
|
||||
4. Heading text capture in outline mode
|
||||
5. Metaschema compliance (validate against Issue #50 metaschema)
|
||||
6. Backward compatibility with existing schema-generate
|
||||
7. CLI help text and error handling
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Notes
|
||||
|
||||
### Code Patterns to Follow
|
||||
- Use existing SchemaGenerator class as foundation
|
||||
- Add mode parameter to `generate_schema_from_file()` method
|
||||
- Implement outline-specific logic in separate methods
|
||||
- Follow existing CLI argument patterns in Click
|
||||
- Use metaschema validation from Issue #50
|
||||
|
||||
### Quality Standards
|
||||
- Maintain 100% test pass rate for existing 478 tests
|
||||
- Follow project conventions (no unnecessary comments unless asked)
|
||||
- Use TodoWrite tool proactively for task tracking
|
||||
- Commit frequently with descriptive messages
|
||||
- Update CLI help text appropriately
|
||||
|
||||
### Git Workflow
|
||||
- Current branch: `main` (2 commits ahead of origin)
|
||||
- Last commit: "feat: Complete Issue #50 - Define metaschema for JSON schema structure"
|
||||
- Modified files since last session: `Makefile`, `markitect/cli.py`, `markitect/database.py`
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Autonomous Work Protocols
|
||||
|
||||
### Do NOT Forget
|
||||
- ✅ Use TodoWrite tool to track all tasks and phases
|
||||
- ✅ Run tests after each change to verify state
|
||||
- ✅ Follow complete TDD8 cycle (don't skip steps)
|
||||
- ✅ Update CLI help when adding new features
|
||||
- ✅ Maintain backward compatibility
|
||||
- ✅ Use proper PYTHONPATH=. for all test runs
|
||||
- ✅ Commit frequently with descriptive messages
|
||||
|
||||
### Success Criteria for Issue #51
|
||||
- [ ] `--mode outline` option implemented and functional
|
||||
- [ ] `--depth` parameter working with validation
|
||||
- [ ] Schema title format updated ("from" not "for")
|
||||
- [ ] Actual heading text captured in schemas
|
||||
- [ ] Metaschema integration working
|
||||
- [ ] All tests passing (new + existing 478)
|
||||
- [ ] CLI help updated and comprehensive
|
||||
- [ ] Backward compatibility maintained
|
||||
- [ ] Issue closed via `make close-issue NUM=51`
|
||||
|
||||
### Next Issues After #51
|
||||
- **Issue #52**: Capture actual heading text in schemas (may overlap with #51)
|
||||
- **Issue #54**: Add content field instruction capabilities
|
||||
- **Issue #55**: Schema-based draft generation
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Session Success Metrics
|
||||
|
||||
### Minimum Viable Success
|
||||
- Issue #51 basic functionality implemented
|
||||
- Core tests passing
|
||||
- CLI integration working
|
||||
|
||||
### Optimal Success
|
||||
- Complete TDD8 cycle for Issue #51
|
||||
- All 478+ tests passing
|
||||
- Issue #51 closed and committed
|
||||
- Foundation laid for Issue #52
|
||||
|
||||
### Stretch Goals
|
||||
- Begin Issue #52 if time permits
|
||||
- Proactive refactoring improvements
|
||||
- Documentation enhancements
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Start Command
|
||||
|
||||
When beginning the session:
|
||||
```bash
|
||||
make tdd-start NUM=51
|
||||
```
|
||||
|
||||
Then follow TDD8 methodology autonomously, using TodoWrite for task tracking and committing/closing when complete.
|
||||
|
||||
**Remember**: This is autonomous work mode - proceed through full TDD8 cycle without checking in unless hitting technical blockers or token limits.
|
||||
@@ -1454,8 +1454,11 @@ def ast_stats(config, file_path, format):
|
||||
@click.option('--format', 'output_format', type=click.Choice(['json', 'yaml']), default='json', help='Output format')
|
||||
@click.option('--mode', type=click.Choice(['outline']), help='Generation mode: outline for structure-focused schemas')
|
||||
@click.option('--depth', type=int, help='Maximum depth for outline mode (similar to --max-depth)')
|
||||
@click.option('--capture-heading-text', is_flag=True, help='Capture exact heading text as schema constraints')
|
||||
@click.option('--include-content-instructions', is_flag=True, help='Include content field instructions for document generation')
|
||||
@click.option('--instruction-type', type=click.Choice(['description', 'example', 'constraint', 'template']), default='description', help='Type of content instructions to generate')
|
||||
@pass_config
|
||||
def generate_schema(config, file_path, max_depth, output, outfile, output_format, mode, depth):
|
||||
def generate_schema(config, file_path, max_depth, output, outfile, output_format, mode, depth, capture_heading_text, include_content_instructions, instruction_type):
|
||||
"""
|
||||
Generate a JSON schema from a markdown file's AST structure.
|
||||
|
||||
@@ -1470,9 +1473,30 @@ def generate_schema(config, file_path, max_depth, output, outfile, output_format
|
||||
markitect schema-generate --mode outline document.md
|
||||
markitect schema-generate --mode outline --depth 3 --outfile schema.json document.md
|
||||
|
||||
# Heading text capture for validation constraints
|
||||
markitect schema-generate --capture-heading-text document.md
|
||||
markitect schema-generate --mode outline --capture-heading-text --depth 2 document.md
|
||||
|
||||
# Content instructions for document generation guidance
|
||||
markitect schema-generate --include-content-instructions document.md
|
||||
markitect schema-generate --include-content-instructions --instruction-type example document.md
|
||||
markitect schema-generate --mode outline --include-content-instructions --instruction-type template document.md
|
||||
|
||||
Modes:
|
||||
Default: Standard schema generation with structural analysis
|
||||
Outline: Structure-focused schema with heading text capture and metaschema extensions
|
||||
|
||||
Heading Text Capture:
|
||||
When --capture-heading-text is enabled, the schema will include exact heading text
|
||||
as enum constraints, enabling validation to enforce specific heading text requirements.
|
||||
|
||||
Content Instructions:
|
||||
When --include-content-instructions is enabled, the schema will include guidance fields
|
||||
for document generation. Use --instruction-type to specify the type of instructions:
|
||||
- description: Descriptive guidance for content authors
|
||||
- example: Example-based content guidance
|
||||
- constraint: Content constraint specifications
|
||||
- template: Template-based content structure
|
||||
"""
|
||||
try:
|
||||
# Handle parameter conflicts and defaults
|
||||
@@ -1507,7 +1531,10 @@ def generate_schema(config, file_path, max_depth, output, outfile, output_format
|
||||
file_path,
|
||||
max_depth=final_depth,
|
||||
mode=mode,
|
||||
outline_depth=depth if mode == 'outline' else None
|
||||
outline_depth=depth if mode == 'outline' else None,
|
||||
capture_heading_text=capture_heading_text,
|
||||
include_content_instructions=include_content_instructions,
|
||||
instruction_type=instruction_type
|
||||
)
|
||||
|
||||
# Format output
|
||||
@@ -1957,7 +1984,9 @@ def generate_stub(config, schema_file, output, style, title):
|
||||
Generate a markdown stub/template from a JSON schema.
|
||||
|
||||
Creates a markdown document with proper heading hierarchy and placeholder
|
||||
content based on the structural definitions in the JSON schema.
|
||||
content based on the structural definitions in the JSON schema. When schemas
|
||||
include content instructions (x-markitect-content-instructions), the generated
|
||||
stub will use specific guidance text instead of generic placeholders.
|
||||
|
||||
SCHEMA_FILE: Path to the JSON schema file
|
||||
|
||||
@@ -1965,6 +1994,19 @@ def generate_stub(config, schema_file, output, style, title):
|
||||
markitect generate-stub blog_schema.json
|
||||
markitect generate-stub schema.json --output template.md
|
||||
markitect generate-stub schema.json --style detailed --title "My Document"
|
||||
|
||||
# Content instructions will be used automatically when present in schema
|
||||
markitect generate-stub schema_with_instructions.json
|
||||
|
||||
Content Instructions:
|
||||
When a schema contains x-markitect-content-instructions-enabled: true,
|
||||
the generated stub will include specific content guidance from the schema
|
||||
instead of generic "TODO" placeholders. This is especially useful with
|
||||
schemas created using the --include-content-instructions option.
|
||||
|
||||
Schema Reference:
|
||||
Generated stubs include a comment referencing the source schema file
|
||||
for validation and traceability purposes.
|
||||
"""
|
||||
try:
|
||||
if config.get('verbose'):
|
||||
@@ -1982,7 +2024,7 @@ def generate_stub(config, schema_file, output, style, title):
|
||||
schema = json.load(f)
|
||||
|
||||
stub_content = generator.generate_stub_from_schema(
|
||||
schema, placeholder_style=style, title=title
|
||||
schema, placeholder_style=style, title=title, schema_file_path=schema_file
|
||||
)
|
||||
|
||||
# Mode-based output logic
|
||||
@@ -1994,7 +2036,7 @@ def generate_stub(config, schema_file, output, style, title):
|
||||
|
||||
# Output to file or stdout
|
||||
if output:
|
||||
generator.generate_stub_to_file(schema, output, style, title)
|
||||
generator.generate_stub_to_file(schema, output, style, title, schema_file)
|
||||
click.echo(f"✅ Stub generated: {output}")
|
||||
|
||||
if config.get('verbose'):
|
||||
|
||||
@@ -168,4 +168,15 @@ class InvalidSchemaError(MarkitectError):
|
||||
- Schema doesn't conform to JSON Schema specification
|
||||
- Schema file cannot be loaded or parsed
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidInstructionTypeError(MarkitectError):
|
||||
"""Errors related to invalid content instruction types.
|
||||
|
||||
Raised when:
|
||||
- Instruction type is not one of the supported types
|
||||
- Instruction type parameter is malformed
|
||||
- Instruction type conflicts with other options
|
||||
"""
|
||||
pass
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Set
|
||||
|
||||
from .parser import parse_markdown_to_ast
|
||||
from .exceptions import FileNotFoundError, InvalidDepthError
|
||||
from .exceptions import FileNotFoundError, InvalidDepthError, InvalidInstructionTypeError
|
||||
|
||||
|
||||
class SchemaGenerator:
|
||||
@@ -33,7 +33,10 @@ class SchemaGenerator:
|
||||
file_path: Path,
|
||||
max_depth: Optional[int] = None,
|
||||
mode: Optional[str] = None,
|
||||
outline_depth: Optional[int] = None
|
||||
outline_depth: Optional[int] = None,
|
||||
capture_heading_text: bool = False,
|
||||
include_content_instructions: bool = False,
|
||||
instruction_type: str = 'description'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a JSON schema from a markdown file's AST structure.
|
||||
@@ -43,6 +46,9 @@ class SchemaGenerator:
|
||||
max_depth: Maximum heading depth to include (None = unlimited)
|
||||
mode: Generation mode ('outline' for structure-focused schemas)
|
||||
outline_depth: Depth limit for outline mode
|
||||
capture_heading_text: Whether to capture exact heading text as constraints
|
||||
include_content_instructions: Whether to include content instruction fields
|
||||
instruction_type: Type of content instructions ('description', 'example', 'constraint', 'template')
|
||||
|
||||
Returns:
|
||||
JSON schema as a dictionary
|
||||
@@ -58,6 +64,11 @@ class SchemaGenerator:
|
||||
if max_depth is not None and max_depth < 1:
|
||||
raise InvalidDepthError(f"max_depth must be >= 1, got: {max_depth}")
|
||||
|
||||
# Validate instruction type
|
||||
valid_instruction_types = {'description', 'example', 'constraint', 'template'}
|
||||
if instruction_type not in valid_instruction_types:
|
||||
raise InvalidInstructionTypeError(f"Invalid instruction type '{instruction_type}'. Must be one of: {', '.join(valid_instruction_types)}")
|
||||
|
||||
# Read and parse the markdown file
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
ast_tokens = parse_markdown_to_ast(content)
|
||||
@@ -66,7 +77,15 @@ class SchemaGenerator:
|
||||
structure_analysis = self._analyze_ast_structure(ast_tokens, max_depth)
|
||||
|
||||
# Generate the JSON schema
|
||||
schema = self._create_json_schema(structure_analysis, file_path.name, mode=mode, outline_depth=outline_depth)
|
||||
schema = self._create_json_schema(
|
||||
structure_analysis,
|
||||
file_path.name,
|
||||
mode=mode,
|
||||
outline_depth=outline_depth,
|
||||
capture_heading_text=capture_heading_text,
|
||||
include_content_instructions=include_content_instructions,
|
||||
instruction_type=instruction_type
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
@@ -183,7 +202,10 @@ class SchemaGenerator:
|
||||
analysis: Dict[str, Any],
|
||||
filename: str,
|
||||
mode: Optional[str] = None,
|
||||
outline_depth: Optional[int] = None
|
||||
outline_depth: Optional[int] = None,
|
||||
capture_heading_text: bool = False,
|
||||
include_content_instructions: bool = False,
|
||||
instruction_type: str = 'description'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a JSON schema from structural analysis.
|
||||
@@ -193,6 +215,9 @@ class SchemaGenerator:
|
||||
filename: Name of the source file
|
||||
mode: Generation mode ('outline' for structure-focused schemas)
|
||||
outline_depth: Depth limit for outline mode
|
||||
capture_heading_text: Whether to capture exact heading text as constraints
|
||||
include_content_instructions: Whether to include content instruction fields
|
||||
instruction_type: Type of content instructions to generate
|
||||
|
||||
Returns:
|
||||
JSON schema dictionary
|
||||
@@ -214,21 +239,57 @@ class SchemaGenerator:
|
||||
if outline_depth is not None:
|
||||
schema["x-markitect-outline-depth"] = outline_depth
|
||||
|
||||
# Add metaschema extension for heading text capture
|
||||
if capture_heading_text:
|
||||
schema["x-markitect-heading-text-capture"] = True
|
||||
|
||||
# Add metaschema extension for content instructions
|
||||
if include_content_instructions:
|
||||
schema["x-markitect-content-instructions-enabled"] = True
|
||||
|
||||
# Add heading structure
|
||||
if analysis['headings']:
|
||||
heading_properties = {}
|
||||
for level_key, headings in analysis['headings'].items():
|
||||
if headings: # Only include levels that have content
|
||||
# Configure content property based on heading text capture
|
||||
if capture_heading_text:
|
||||
# Extract actual heading texts in document order
|
||||
heading_texts = [heading['content'] for heading in headings]
|
||||
content_property = {"enum": heading_texts}
|
||||
else:
|
||||
content_property = {"type": "string"}
|
||||
|
||||
# Build properties for the heading item
|
||||
item_properties = {
|
||||
"content": content_property,
|
||||
"level": {"type": "integer"},
|
||||
"position": {"type": "integer"}
|
||||
}
|
||||
|
||||
# Add content instruction fields if enabled
|
||||
if include_content_instructions:
|
||||
# Generate appropriate instruction text based on heading level
|
||||
level_num = int(level_key.split('_')[1])
|
||||
section_name = f"level {level_num} heading"
|
||||
instruction_text = self._generate_content_instruction(section_name, instruction_type)
|
||||
|
||||
item_properties["x-markitect-content-instructions"] = {
|
||||
"type": "string",
|
||||
"const": instruction_text
|
||||
}
|
||||
|
||||
item_properties["x-markitect-instruction-type"] = {
|
||||
"type": "string",
|
||||
"enum": [instruction_type]
|
||||
}
|
||||
|
||||
heading_properties[level_key] = {
|
||||
"type": "array",
|
||||
"description": f"Headings at {level_key.replace('_', ' ')}",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"position": {"type": "integer"}
|
||||
},
|
||||
"properties": item_properties,
|
||||
"required": ["content", "level"]
|
||||
},
|
||||
"minItems": len(headings),
|
||||
@@ -256,13 +317,33 @@ class SchemaGenerator:
|
||||
|
||||
for element_name, (description, element_list) in structural_elements.items():
|
||||
if element_list:
|
||||
schema["properties"][element_name] = {
|
||||
# Build base schema for the element
|
||||
element_schema = {
|
||||
"type": "array",
|
||||
"description": description,
|
||||
"minItems": len(element_list),
|
||||
"maxItems": len(element_list)
|
||||
}
|
||||
|
||||
# Add content instructions for paragraphs and lists if enabled
|
||||
if include_content_instructions and element_name in ["paragraphs", "lists"]:
|
||||
element_schema["items"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": self._generate_content_instruction(element_name, instruction_type)
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": [instruction_type]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schema["properties"][element_name] = element_schema
|
||||
|
||||
# Add metadata
|
||||
schema["properties"]["metadata"] = {
|
||||
"type": "object",
|
||||
@@ -359,4 +440,27 @@ class SchemaGenerator:
|
||||
elif child_type in ['em_open', 'strong_open']:
|
||||
result['emphasis'].append({"type": child_type})
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
def _generate_content_instruction(self, heading_text: str, instruction_type: str) -> str:
|
||||
"""
|
||||
Generate appropriate content instruction text based on heading and instruction type.
|
||||
|
||||
Args:
|
||||
heading_text: The text of the heading
|
||||
instruction_type: Type of instruction to generate
|
||||
|
||||
Returns:
|
||||
Instruction text for the content field
|
||||
"""
|
||||
if instruction_type == "description":
|
||||
return f"Provide content for the '{heading_text}' section"
|
||||
elif instruction_type == "example":
|
||||
return f"Example content for the '{heading_text}' section"
|
||||
elif instruction_type == "constraint":
|
||||
return f"Content must be relevant to '{heading_text}'"
|
||||
elif instruction_type == "template":
|
||||
return f"Template content for '{heading_text}' section"
|
||||
else:
|
||||
# Default fallback
|
||||
return f"Content for the '{heading_text}' section"
|
||||
@@ -68,8 +68,13 @@ class SchemaValidator:
|
||||
except Exception as e:
|
||||
raise SchemaValidationError(f"Failed to generate document schema: {e}") from e
|
||||
|
||||
# Compare the document's structure against the expected schema
|
||||
return self._compare_structures(document_schema, schema)
|
||||
# Check if the expected schema has heading text constraints
|
||||
if self._has_heading_text_constraints(schema):
|
||||
# For heading text validation, we need to extract actual content and compare against enum constraints
|
||||
return self._validate_with_heading_text_constraints(file_path, schema, document_schema)
|
||||
else:
|
||||
# Use standard structure comparison for backward compatibility
|
||||
return self._compare_structures(document_schema, schema)
|
||||
|
||||
def validate_file_against_schema_string(self, file_path: Path, schema_json: str) -> bool:
|
||||
"""
|
||||
@@ -314,7 +319,13 @@ class SchemaValidator:
|
||||
return error_collector
|
||||
|
||||
# Compare the document's structure against the expected schema and collect errors
|
||||
self._compare_structures_with_errors(document_schema, schema, error_collector)
|
||||
if self._has_heading_text_constraints(schema):
|
||||
# For heading text validation, we need to handle enum constraints specially
|
||||
self._compare_structures_with_errors(document_schema, schema, error_collector)
|
||||
self._validate_heading_text_constraints_with_errors(file_path, schema, error_collector)
|
||||
else:
|
||||
# Use standard structure comparison for backward compatibility
|
||||
self._compare_structures_with_errors(document_schema, schema, error_collector)
|
||||
|
||||
return error_collector
|
||||
|
||||
@@ -562,4 +573,110 @@ class SchemaValidator:
|
||||
expected=f"At most {expected_max} {element_description}",
|
||||
actual=f"{actual_count} {element_description}",
|
||||
suggestion=f"Remove {actual_count - expected_max} {element_description}"
|
||||
)
|
||||
)
|
||||
|
||||
def _has_heading_text_constraints(self, schema: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the schema has heading text constraints (enum values on heading content).
|
||||
|
||||
Args:
|
||||
schema: JSON schema to check
|
||||
|
||||
Returns:
|
||||
True if schema has heading text constraints
|
||||
"""
|
||||
headings_props = schema.get('properties', {}).get('headings', {}).get('properties', {})
|
||||
|
||||
for level_props in headings_props.values():
|
||||
items = level_props.get('items', {})
|
||||
content_prop = items.get('properties', {}).get('content', {})
|
||||
if 'enum' in content_prop:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _validate_with_heading_text_constraints(
|
||||
self,
|
||||
file_path: Path,
|
||||
expected_schema: Dict[str, Any],
|
||||
document_schema: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Validate document with heading text constraints by comparing actual content against enum values.
|
||||
|
||||
Args:
|
||||
file_path: Path to the markdown file
|
||||
expected_schema: Schema with heading text constraints
|
||||
document_schema: Generated schema from the actual document
|
||||
|
||||
Returns:
|
||||
True if document meets all constraints including heading text
|
||||
"""
|
||||
# First check standard structure compliance
|
||||
if not self._compare_structures(document_schema, expected_schema):
|
||||
return False
|
||||
|
||||
# Then check heading text constraints
|
||||
expected_headings = expected_schema.get('properties', {}).get('headings', {}).get('properties', {})
|
||||
|
||||
# Generate document analysis with actual heading content
|
||||
from .parser import parse_markdown_to_ast
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
ast_tokens = parse_markdown_to_ast(content)
|
||||
structure_analysis = self.schema_generator._analyze_ast_structure(ast_tokens, None)
|
||||
|
||||
for level_key, expected_level_spec in expected_headings.items():
|
||||
content_constraints = expected_level_spec.get('items', {}).get('properties', {}).get('content', {})
|
||||
|
||||
if 'enum' in content_constraints:
|
||||
allowed_texts = content_constraints['enum']
|
||||
actual_headings = structure_analysis['headings'].get(level_key, [])
|
||||
|
||||
for heading in actual_headings:
|
||||
actual_text = heading['content']
|
||||
if actual_text not in allowed_texts:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _validate_heading_text_constraints_with_errors(
|
||||
self,
|
||||
file_path: Path,
|
||||
expected_schema: Dict[str, Any],
|
||||
error_collector: ValidationErrorCollector
|
||||
) -> None:
|
||||
"""
|
||||
Validate heading text constraints and collect detailed errors.
|
||||
|
||||
Args:
|
||||
file_path: Path to the markdown file
|
||||
expected_schema: Schema with heading text constraints
|
||||
error_collector: Collector for validation errors
|
||||
"""
|
||||
expected_headings = expected_schema.get('properties', {}).get('headings', {}).get('properties', {})
|
||||
|
||||
# Generate document analysis with actual heading content
|
||||
from .parser import parse_markdown_to_ast
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
ast_tokens = parse_markdown_to_ast(content)
|
||||
structure_analysis = self.schema_generator._analyze_ast_structure(ast_tokens, None)
|
||||
|
||||
for level_key, expected_level_spec in expected_headings.items():
|
||||
content_constraints = expected_level_spec.get('items', {}).get('properties', {}).get('content', {})
|
||||
|
||||
if 'enum' in content_constraints:
|
||||
allowed_texts = content_constraints['enum']
|
||||
actual_headings = structure_analysis['headings'].get(level_key, [])
|
||||
|
||||
for i, heading in enumerate(actual_headings):
|
||||
actual_text = heading['content']
|
||||
if actual_text not in allowed_texts:
|
||||
# Add detailed error about heading text mismatch
|
||||
error_collector.add_error(
|
||||
ValidationErrorType.HEADING_COUNT_MISMATCH,
|
||||
f"Heading text mismatch at {level_key.replace('_', ' ')} #{i+1}: expected one of {allowed_texts}, found '{actual_text}'",
|
||||
f"headings.{level_key}[{i}].content",
|
||||
expected=f"One of: {allowed_texts}",
|
||||
actual=actual_text,
|
||||
suggestion=f"Change heading text to one of the allowed values: {', '.join(allowed_texts)}"
|
||||
)
|
||||
@@ -34,7 +34,8 @@ class StubGenerator:
|
||||
|
||||
def generate_stub_from_schema(self, schema: Dict[str, Any],
|
||||
placeholder_style: str = 'default',
|
||||
title: Optional[str] = None) -> str:
|
||||
title: Optional[str] = None,
|
||||
schema_file_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generate a markdown stub from a JSON schema dictionary.
|
||||
|
||||
@@ -42,6 +43,7 @@ class StubGenerator:
|
||||
schema: JSON schema as dictionary
|
||||
placeholder_style: Style of placeholder content ('default', 'custom', 'detailed')
|
||||
title: Custom title for the document (overrides schema title)
|
||||
schema_file_path: Optional path to schema file for reference metadata
|
||||
|
||||
Returns:
|
||||
Generated markdown content as string
|
||||
@@ -49,9 +51,17 @@ class StubGenerator:
|
||||
# Extract title
|
||||
doc_title = title or schema.get('title', DEFAULT_TITLE)
|
||||
|
||||
# Check if schema has content instructions enabled
|
||||
content_instructions_enabled = schema.get('x-markitect-content-instructions-enabled', False)
|
||||
|
||||
# Start building the markdown content
|
||||
lines = []
|
||||
|
||||
# Add schema reference metadata if schema file path is provided
|
||||
if schema_file_path:
|
||||
lines.append(f"<!-- Generated from schema: {schema_file_path} -->")
|
||||
lines.append("")
|
||||
|
||||
# Extract heading structure from schema
|
||||
headings_schema = schema.get('properties', {}).get('headings', {})
|
||||
heading_properties = headings_schema.get('properties', {})
|
||||
@@ -60,12 +70,12 @@ class StubGenerator:
|
||||
# Create a minimal document if no heading structure is defined
|
||||
lines.append(f"# {doc_title}")
|
||||
lines.append("")
|
||||
lines.append(self._get_placeholder_content(placeholder_style, "main"))
|
||||
lines.append(self._get_placeholder_content(placeholder_style, "main", schema=schema))
|
||||
lines.append("")
|
||||
else:
|
||||
# Generate content based on heading structure
|
||||
lines.extend(self._generate_content_from_headings(
|
||||
heading_properties, doc_title, placeholder_style
|
||||
heading_properties, doc_title, placeholder_style, schema=schema
|
||||
))
|
||||
|
||||
return '\n'.join(lines)
|
||||
@@ -90,12 +100,13 @@ class StubGenerator:
|
||||
with open(schema_file, 'r', encoding='utf-8') as f:
|
||||
schema = json.load(f)
|
||||
|
||||
return self.generate_stub_from_schema(schema)
|
||||
return self.generate_stub_from_schema(schema, schema_file_path=str(schema_file))
|
||||
|
||||
def generate_stub_to_file(self, schema: Dict[str, Any],
|
||||
output_file: Path,
|
||||
placeholder_style: str = 'default',
|
||||
title: Optional[str] = None) -> None:
|
||||
title: Optional[str] = None,
|
||||
schema_file_path: Optional[str] = None) -> None:
|
||||
"""
|
||||
Generate a markdown stub and save it to a file.
|
||||
|
||||
@@ -104,8 +115,9 @@ class StubGenerator:
|
||||
output_file: Path where to save the generated markdown
|
||||
placeholder_style: Style of placeholder content
|
||||
title: Custom title for the document
|
||||
schema_file_path: Optional path to schema file for reference metadata
|
||||
"""
|
||||
content = self.generate_stub_from_schema(schema, placeholder_style, title)
|
||||
content = self.generate_stub_from_schema(schema, placeholder_style, title, schema_file_path)
|
||||
|
||||
# Ensure parent directory exists
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -114,7 +126,7 @@ class StubGenerator:
|
||||
f.write(content)
|
||||
|
||||
def _generate_content_from_headings(self, heading_properties: Dict[str, Any],
|
||||
doc_title: str, placeholder_style: str) -> List[str]:
|
||||
doc_title: str, placeholder_style: str, schema: Optional[Dict[str, Any]] = None) -> List[str]:
|
||||
"""Generate markdown content from heading structure."""
|
||||
lines = []
|
||||
|
||||
@@ -129,7 +141,14 @@ class StubGenerator:
|
||||
# Start with H1
|
||||
lines.append(f"# {doc_title}")
|
||||
lines.append("")
|
||||
lines.append(self._get_placeholder_content(placeholder_style, "introduction"))
|
||||
# Get the heading schema for level 1
|
||||
level_1_heading_schema = heading_properties.get('level_1', {})
|
||||
lines.append(self._get_placeholder_content(
|
||||
placeholder_style,
|
||||
"introduction",
|
||||
schema=schema,
|
||||
heading_schema=level_1_heading_schema
|
||||
))
|
||||
lines.append("")
|
||||
|
||||
# Generate H2+ headings
|
||||
@@ -144,7 +163,17 @@ class StubGenerator:
|
||||
|
||||
lines.append(f"{heading_prefix} {section_name}")
|
||||
lines.append("")
|
||||
lines.append(self._get_placeholder_content(placeholder_style, f"section_level_{level}"))
|
||||
|
||||
# Get the heading schema for this level
|
||||
level_key = f"level_{level}"
|
||||
heading_schema = heading_properties.get(level_key, {})
|
||||
|
||||
lines.append(self._get_placeholder_content(
|
||||
placeholder_style,
|
||||
f"section_level_{level}",
|
||||
schema=schema,
|
||||
heading_schema=heading_schema
|
||||
))
|
||||
lines.append("")
|
||||
else:
|
||||
# No H1, start with whatever level is available
|
||||
@@ -159,7 +188,17 @@ class StubGenerator:
|
||||
|
||||
lines.append(f"{heading_prefix} {section_name}")
|
||||
lines.append("")
|
||||
lines.append(self._get_placeholder_content(placeholder_style, f"section_level_{level}"))
|
||||
|
||||
# Get the heading schema for this level
|
||||
level_key = f"level_{level}"
|
||||
heading_schema = heading_properties.get(level_key, {})
|
||||
|
||||
lines.append(self._get_placeholder_content(
|
||||
placeholder_style,
|
||||
f"section_level_{level}",
|
||||
schema=schema,
|
||||
heading_schema=heading_schema
|
||||
))
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
@@ -194,8 +233,15 @@ class StubGenerator:
|
||||
else:
|
||||
return f"Section {index}"
|
||||
|
||||
def _get_placeholder_content(self, style: str, section_type: str) -> str:
|
||||
def _get_placeholder_content(self, style: str, section_type: str, schema: Optional[Dict[str, Any]] = None, heading_schema: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Get placeholder content based on style and section type."""
|
||||
# Check if we have content instructions from schema
|
||||
if schema and heading_schema and schema.get('x-markitect-content-instructions-enabled', False):
|
||||
content_instruction = self._extract_content_instruction_from_heading_schema(heading_schema)
|
||||
if content_instruction:
|
||||
return content_instruction
|
||||
|
||||
# Fall back to standard placeholder generation
|
||||
generator = self.placeholder_styles.get(style, self.placeholder_styles['default'])
|
||||
return generator(section_type)
|
||||
|
||||
@@ -250,4 +296,26 @@ TODO: Add detailed content for this subsection.""",
|
||||
return detailed_placeholders.get(
|
||||
section_type,
|
||||
f"<!-- {section_type.title()} Section -->\nTODO: Add content for {section_type}."
|
||||
)
|
||||
)
|
||||
|
||||
def _extract_content_instruction_from_heading_schema(self, heading_schema: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Extract content instruction from a heading schema items definition.
|
||||
|
||||
Args:
|
||||
heading_schema: The schema definition for a heading level
|
||||
|
||||
Returns:
|
||||
Content instruction text if found, None otherwise
|
||||
"""
|
||||
# Navigate through the schema structure to find content instructions
|
||||
# Schema structure: heading_schema -> items -> properties -> x-markitect-content-instructions -> const
|
||||
items_schema = heading_schema.get('items', {})
|
||||
if isinstance(items_schema, dict):
|
||||
properties = items_schema.get('properties', {})
|
||||
if isinstance(properties, dict):
|
||||
instruction_schema = properties.get('x-markitect-content-instructions', {})
|
||||
if isinstance(instruction_schema, dict):
|
||||
return instruction_schema.get('const')
|
||||
|
||||
return None
|
||||
346
tests/test_issue_50_metaschema_definition.py
Normal file
346
tests/test_issue_50_metaschema_definition.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Tests for Issue #50: Define metaschema for JSON schema structure
|
||||
|
||||
This test module defines comprehensive tests for the MarkiTect metaschema that extends
|
||||
standard JSON Schema with MarkiTect-specific features like heading text capture,
|
||||
content field instructions, and outline structure representation.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from markitect.metaschema import MetaschemaValidator, MARKITECT_METASCHEMA_PATH
|
||||
|
||||
|
||||
class TestIssue50MetaschemaDefinition:
|
||||
"""Test suite for MarkiTect metaschema definition and validation."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.metaschema_validator = MetaschemaValidator()
|
||||
|
||||
def test_metaschema_file_exists_and_is_valid_json(self):
|
||||
"""Test that the metaschema JSON file exists and contains valid JSON."""
|
||||
# Arrange & Act
|
||||
metaschema_path = Path(MARKITECT_METASCHEMA_PATH)
|
||||
|
||||
# Assert
|
||||
assert metaschema_path.exists(), f"Metaschema file should exist at {MARKITECT_METASCHEMA_PATH}"
|
||||
|
||||
with open(metaschema_path) as f:
|
||||
metaschema = json.load(f)
|
||||
|
||||
assert isinstance(metaschema, dict), "Metaschema should be a valid JSON object"
|
||||
assert "$schema" in metaschema, "Metaschema should have $schema property"
|
||||
assert "type" in metaschema, "Metaschema should have type property"
|
||||
|
||||
def test_metaschema_extends_json_schema_draft_07(self):
|
||||
"""Test that metaschema properly extends JSON Schema Draft 07."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert metaschema["$schema"] == "http://json-schema.org/draft-07/schema#"
|
||||
assert metaschema["type"] == "object"
|
||||
assert "allOf" in metaschema, "Should extend base JSON Schema using allOf"
|
||||
|
||||
# Should reference standard JSON Schema
|
||||
found_json_schema_ref = False
|
||||
for schema_ref in metaschema["allOf"]:
|
||||
if "$ref" in schema_ref and "json-schema.org" in schema_ref["$ref"]:
|
||||
found_json_schema_ref = True
|
||||
break
|
||||
assert found_json_schema_ref, "Should reference standard JSON Schema Draft 07"
|
||||
|
||||
def test_metaschema_supports_heading_text_capture(self):
|
||||
"""Test that metaschema supports heading text capture extensions."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for MarkiTect-specific heading text properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-heading-text" in markitect_properties, \
|
||||
"Should support x-markitect-heading-text property"
|
||||
|
||||
heading_text_schema = markitect_properties["x-markitect-heading-text"]
|
||||
assert heading_text_schema["type"] == "string"
|
||||
assert "description" in heading_text_schema
|
||||
assert "preserve actual heading text" in heading_text_schema["description"].lower()
|
||||
|
||||
def test_metaschema_supports_content_field_instructions(self):
|
||||
"""Test that metaschema supports content field instruction capabilities."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for content instruction properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-content-instructions" in markitect_properties, \
|
||||
"Should support x-markitect-content-instructions property"
|
||||
|
||||
instructions_schema = markitect_properties["x-markitect-content-instructions"]
|
||||
assert instructions_schema["type"] == "string"
|
||||
assert "description" in instructions_schema
|
||||
assert "content author" in instructions_schema["description"].lower()
|
||||
|
||||
def test_metaschema_supports_outline_structure_representation(self):
|
||||
"""Test that metaschema supports outline structure representation."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
|
||||
# Assert - Check for outline structure properties
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
assert "x-markitect-outline-mode" in markitect_properties, \
|
||||
"Should support x-markitect-outline-mode property"
|
||||
|
||||
outline_schema = markitect_properties["x-markitect-outline-mode"]
|
||||
assert outline_schema["type"] == "boolean"
|
||||
assert "description" in outline_schema
|
||||
|
||||
assert "x-markitect-outline-depth" in markitect_properties, \
|
||||
"Should support x-markitect-outline-depth property"
|
||||
|
||||
depth_schema = markitect_properties["x-markitect-outline-depth"]
|
||||
assert depth_schema["type"] == "integer"
|
||||
assert depth_schema["minimum"] == 1
|
||||
|
||||
def test_metaschema_validates_standard_json_schema(self):
|
||||
"""Test that metaschema accepts standard JSON Schema documents (backward compatibility)."""
|
||||
# Arrange
|
||||
standard_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Standard Schema",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer", "minimum": 0}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(standard_schema)
|
||||
assert is_valid, "Standard JSON Schema should be valid against metaschema"
|
||||
|
||||
def test_metaschema_validates_markitect_extended_schema(self):
|
||||
"""Test that metaschema accepts MarkiTect extended schemas."""
|
||||
# Arrange
|
||||
markitect_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "MarkiTect Extended Schema",
|
||||
"x-markitect-outline-mode": True,
|
||||
"x-markitect-outline-depth": 3,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-heading-text": "Introduction",
|
||||
"x-markitect-content-instructions": "Provide overview of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(markitect_schema)
|
||||
assert is_valid, "MarkiTect extended schema should be valid against metaschema"
|
||||
|
||||
def test_metaschema_rejects_invalid_markitect_extensions(self):
|
||||
"""Test that metaschema rejects invalid MarkiTect extension values."""
|
||||
# Arrange
|
||||
invalid_schemas = [
|
||||
# Invalid outline depth (negative)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-depth": -1
|
||||
},
|
||||
# Invalid outline mode (not boolean)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-mode": "true"
|
||||
},
|
||||
# Invalid heading text (not string)
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"x-markitect-heading-text": 123
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# Act & Assert
|
||||
for invalid_schema in invalid_schemas:
|
||||
is_valid = self.metaschema_validator.validate_schema(invalid_schema)
|
||||
assert not is_valid, f"Invalid schema should be rejected: {invalid_schema}"
|
||||
|
||||
def test_metaschema_validation_provides_detailed_errors(self):
|
||||
"""Test that metaschema validation provides detailed error messages."""
|
||||
# Arrange
|
||||
invalid_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-markitect-outline-depth": "not-a-number"
|
||||
}
|
||||
|
||||
# Act
|
||||
validation_result = self.metaschema_validator.validate_schema_with_errors(invalid_schema)
|
||||
|
||||
# Assert
|
||||
assert not validation_result.is_valid
|
||||
assert len(validation_result.errors) > 0
|
||||
|
||||
error_messages = [error.message for error in validation_result.errors]
|
||||
assert any("x-markitect-outline-depth" in msg for msg in error_messages), \
|
||||
"Error should mention the problematic MarkiTect extension"
|
||||
|
||||
def test_metaschema_supports_content_instruction_types(self):
|
||||
"""Test that metaschema supports different types of content instructions."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
# Assert - Check for instruction type support
|
||||
assert "x-markitect-instruction-type" in markitect_properties, \
|
||||
"Should support x-markitect-instruction-type property"
|
||||
|
||||
instruction_type_schema = markitect_properties["x-markitect-instruction-type"]
|
||||
assert instruction_type_schema["type"] == "string"
|
||||
assert "enum" in instruction_type_schema
|
||||
|
||||
expected_types = ["description", "example", "constraint", "template"]
|
||||
for instruction_type in expected_types:
|
||||
assert instruction_type in instruction_type_schema["enum"], \
|
||||
f"Should support {instruction_type} instruction type"
|
||||
|
||||
def test_metaschema_supports_schema_metadata(self):
|
||||
"""Test that metaschema supports MarkiTect-specific schema metadata."""
|
||||
# Arrange & Act
|
||||
metaschema = self.metaschema_validator.get_metaschema()
|
||||
markitect_properties = self._get_markitect_extensions(metaschema)
|
||||
|
||||
# Assert - Check for metadata properties
|
||||
assert "x-markitect-generated-from" in markitect_properties, \
|
||||
"Should support x-markitect-generated-from property"
|
||||
|
||||
assert "x-markitect-generation-mode" in markitect_properties, \
|
||||
"Should support x-markitect-generation-mode property"
|
||||
|
||||
generation_mode = markitect_properties["x-markitect-generation-mode"]
|
||||
assert "enum" in generation_mode
|
||||
assert "outline" in generation_mode["enum"]
|
||||
assert "full" in generation_mode["enum"]
|
||||
|
||||
def test_existing_schemas_validate_against_metaschema(self):
|
||||
"""Test that all existing MarkiTect schemas validate against the new metaschema."""
|
||||
# Arrange - Get sample existing schema structure
|
||||
existing_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema for example.md",
|
||||
"description": "JSON schema describing the structure of example.md",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"position": {"type": "integer"}
|
||||
},
|
||||
"required": ["content", "level"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total_elements": {"type": "integer"},
|
||||
"structure_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act & Assert
|
||||
is_valid = self.metaschema_validator.validate_schema(existing_schema)
|
||||
assert is_valid, "Existing MarkiTect schemas should remain valid (backward compatibility)"
|
||||
|
||||
def _get_markitect_extensions(self, metaschema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Helper to extract MarkiTect extension properties from metaschema."""
|
||||
# Look for MarkiTect extensions in the allOf extension
|
||||
for extension in metaschema.get("allOf", []):
|
||||
if "properties" in extension:
|
||||
markitect_props = {}
|
||||
for prop_name, prop_schema in extension["properties"].items():
|
||||
if prop_name.startswith("x-markitect-"):
|
||||
markitect_props[prop_name] = prop_schema
|
||||
if markitect_props:
|
||||
return markitect_props
|
||||
|
||||
# Fallback: look in patternProperties for x-markitect- patterns
|
||||
pattern_props = metaschema.get("patternProperties", {})
|
||||
for pattern, schema in pattern_props.items():
|
||||
if "x-markitect" in pattern:
|
||||
return {pattern: schema}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class TestMetaschemaValidator:
|
||||
"""Test the MetaschemaValidator utility class."""
|
||||
|
||||
def test_metaschema_validator_can_be_created(self):
|
||||
"""Test that MetaschemaValidator can be instantiated."""
|
||||
# Act & Assert
|
||||
validator = MetaschemaValidator()
|
||||
assert validator is not None
|
||||
|
||||
def test_metaschema_validator_loads_metaschema(self):
|
||||
"""Test that MetaschemaValidator properly loads the metaschema."""
|
||||
# Arrange & Act
|
||||
validator = MetaschemaValidator()
|
||||
metaschema = validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert isinstance(metaschema, dict)
|
||||
assert "$schema" in metaschema
|
||||
assert metaschema["$schema"] == "http://json-schema.org/draft-07/schema#"
|
||||
|
||||
def test_metaschema_validator_caches_metaschema(self):
|
||||
"""Test that MetaschemaValidator caches the loaded metaschema."""
|
||||
# Arrange & Act
|
||||
validator = MetaschemaValidator()
|
||||
metaschema1 = validator.get_metaschema()
|
||||
metaschema2 = validator.get_metaschema()
|
||||
|
||||
# Assert
|
||||
assert metaschema1 is metaschema2, "Should cache metaschema instance"
|
||||
381
tests/test_issue_52_heading_text_capture.py
Normal file
381
tests/test_issue_52_heading_text_capture.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
Tests for Issue #52: Capture actual heading text in schemas
|
||||
|
||||
This test module implements comprehensive tests for capturing actual heading text
|
||||
from documents and enforcing specific heading text requirements in validation.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
from markitect.schema_generator import SchemaGenerator
|
||||
from markitect.schema_validator import SchemaValidator
|
||||
from markitect.exceptions import FileNotFoundError
|
||||
|
||||
|
||||
class TestIssue52HeadingTextCapture:
|
||||
"""Test suite for heading text capture functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.schema_generator = SchemaGenerator()
|
||||
self.schema_validator = SchemaValidator()
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_schema_generation_with_heading_text_capture_option(self):
|
||||
"""Test that schema generation can capture exact heading text as constraints."""
|
||||
# Arrange
|
||||
markdown_content = """# Architecture Overview
|
||||
This document describes the system architecture.
|
||||
|
||||
## System Design
|
||||
The core system design principles.
|
||||
|
||||
## Implementation Strategy
|
||||
How we will implement the system.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema with heading text capture enabled
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Assert - Schema should contain exact heading text as constraints
|
||||
assert "properties" in schema
|
||||
assert "headings" in schema["properties"]
|
||||
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
|
||||
# Level 1 heading should have exact text constraint
|
||||
level_1 = headings["level_1"]
|
||||
assert level_1["items"]["properties"]["content"]["enum"] == ["Architecture Overview"]
|
||||
|
||||
# Level 2 headings should have exact text constraints
|
||||
level_2 = headings["level_2"]
|
||||
expected_level_2_texts = ["System Design", "Implementation Strategy"]
|
||||
assert level_2["items"]["properties"]["content"]["enum"] == expected_level_2_texts
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_cli_schema_generate_with_capture_heading_text_option(self):
|
||||
"""Test CLI supports --capture-heading-text option."""
|
||||
# Arrange
|
||||
markdown_content = """# Project Documentation
|
||||
|
||||
## Overview
|
||||
Project overview section.
|
||||
|
||||
## Requirements
|
||||
Project requirements section.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--capture-heading-text',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Check heading text constraints are present
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
assert "enum" in level_1["items"]["properties"]["content"]
|
||||
assert level_1["items"]["properties"]["content"]["enum"] == ["Project Documentation"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_schema_validation_enforces_exact_heading_text(self):
|
||||
"""Test that validation enforces specific heading text requirements."""
|
||||
# Arrange
|
||||
original_content = """# Architecture Overview
|
||||
System architecture description.
|
||||
|
||||
## System Design
|
||||
Core design principles.
|
||||
"""
|
||||
|
||||
wrong_heading_content = """# Different Title
|
||||
System architecture description.
|
||||
|
||||
## System Design
|
||||
Core design principles.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(original_content)
|
||||
original_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(wrong_heading_content)
|
||||
wrong_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Generate schema with heading text capture
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
original_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Act & Assert - Original should validate
|
||||
result1 = self.schema_validator.validate_file_against_schema(original_file, schema)
|
||||
assert result1 is True, "Original document should validate against its own schema"
|
||||
|
||||
# Act & Assert - Wrong heading text should fail validation
|
||||
result2 = self.schema_validator.validate_file_against_schema(wrong_file, schema)
|
||||
assert result2 is False, "Document with wrong heading text should fail validation"
|
||||
|
||||
finally:
|
||||
original_file.unlink()
|
||||
wrong_file.unlink()
|
||||
|
||||
def test_schema_includes_heading_text_capture_metaschema_extension(self):
|
||||
"""Test that schemas with heading text capture include metaschema extension."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section A
|
||||
Content for section A.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Assert - Should have metaschema extension
|
||||
assert "x-markitect-heading-text-capture" in schema
|
||||
assert schema["x-markitect-heading-text-capture"] is True
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_outline_mode_with_heading_text_capture_integration(self):
|
||||
"""Test that outline mode can be combined with heading text capture."""
|
||||
# Arrange
|
||||
markdown_content = """# Main Document
|
||||
|
||||
## Introduction
|
||||
Introduction content.
|
||||
|
||||
### Details
|
||||
Detailed information.
|
||||
|
||||
## Conclusion
|
||||
Conclusion content.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--capture-heading-text',
|
||||
'--depth', '2',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have both outline mode and heading text capture extensions
|
||||
assert schema.get("x-markitect-outline-mode") is True
|
||||
assert schema.get("x-markitect-heading-text-capture") is True
|
||||
|
||||
# Should only include headings up to depth 2
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
assert "level_1" in headings
|
||||
assert "level_2" in headings
|
||||
assert "level_3" not in headings
|
||||
|
||||
# Should have exact heading text constraints
|
||||
level_1 = headings["level_1"]
|
||||
assert level_1["items"]["properties"]["content"]["enum"] == ["Main Document"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_backward_compatibility_without_heading_text_capture(self):
|
||||
"""Test that existing behavior is maintained when heading text capture is not enabled."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section One
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema without heading text capture (default behavior)
|
||||
schema = self.schema_generator.generate_schema_from_file(temp_file)
|
||||
|
||||
# Assert - Should NOT have enum constraints on heading content
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
|
||||
# Should have string type but no enum constraint
|
||||
assert level_1["items"]["properties"]["content"]["type"] == "string"
|
||||
assert "enum" not in level_1["items"]["properties"]["content"]
|
||||
|
||||
# Should NOT have heading text capture extension
|
||||
assert "x-markitect-heading-text-capture" not in schema
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_validation_error_messages_for_heading_text_mismatches(self):
|
||||
"""Test that validation provides meaningful error messages for heading text mismatches."""
|
||||
# Arrange
|
||||
original_content = """# Expected Title
|
||||
|
||||
## Expected Section
|
||||
Content here.
|
||||
"""
|
||||
|
||||
wrong_content = """# Wrong Title
|
||||
|
||||
## Wrong Section
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(original_content)
|
||||
original_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(wrong_content)
|
||||
wrong_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Generate schema with heading text capture
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
original_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Act - Validate with detailed errors
|
||||
error_collector = self.schema_validator.validate_file_with_errors(wrong_file, schema)
|
||||
|
||||
# Assert - Should have specific errors about heading text mismatches
|
||||
errors = error_collector.errors
|
||||
assert len(errors) > 0
|
||||
|
||||
# Look for heading text mismatch errors
|
||||
heading_errors = [e for e in errors if "heading" in e.message.lower()]
|
||||
assert len(heading_errors) > 0
|
||||
|
||||
# Should mention expected vs actual heading text
|
||||
error_text = " ".join([e.message for e in heading_errors])
|
||||
assert "Expected Title" in error_text or "Wrong Title" in error_text
|
||||
|
||||
finally:
|
||||
original_file.unlink()
|
||||
wrong_file.unlink()
|
||||
|
||||
def test_schema_generation_preserves_heading_order_in_constraints(self):
|
||||
"""Test that heading text constraints preserve the order of headings."""
|
||||
# Arrange
|
||||
markdown_content = """# First Document
|
||||
|
||||
## Beta Section
|
||||
Second section alphabetically.
|
||||
|
||||
## Alpha Section
|
||||
First section alphabetically.
|
||||
|
||||
## Gamma Section
|
||||
Third section alphabetically.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Assert - Level 2 headings should preserve document order, not alphabetical
|
||||
level_2 = schema["properties"]["headings"]["properties"]["level_2"]
|
||||
expected_order = ["Beta Section", "Alpha Section", "Gamma Section"]
|
||||
assert level_2["items"]["properties"]["content"]["enum"] == expected_order
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_cli_help_includes_capture_heading_text_option(self):
|
||||
"""Test that CLI help includes documentation for the new option."""
|
||||
# Act
|
||||
result = self.runner.invoke(cli, ['schema-generate', '--help'])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
help_text = result.output
|
||||
assert "--capture-heading-text" in help_text
|
||||
assert "exact heading text" in help_text or "heading text constraints" in help_text
|
||||
|
||||
def test_empty_document_with_heading_text_capture(self):
|
||||
"""Test that heading text capture handles documents with no headings gracefully."""
|
||||
# Arrange
|
||||
markdown_content = """This is a document with no headings.
|
||||
|
||||
Just some regular paragraphs here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True
|
||||
)
|
||||
|
||||
# Assert - Should generate valid schema even with no headings
|
||||
assert "properties" in schema
|
||||
# Should still have the metaschema extension
|
||||
assert schema.get("x-markitect-heading-text-capture") is True
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
515
tests/test_issue_54_content_instructions.py
Normal file
515
tests/test_issue_54_content_instructions.py
Normal file
@@ -0,0 +1,515 @@
|
||||
"""
|
||||
Tests for Issue #54: Add content field instruction capabilities
|
||||
|
||||
This test module implements comprehensive tests for content field instructions
|
||||
that provide guidance for content authors during document generation.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
from markitect.schema_generator import SchemaGenerator
|
||||
from markitect.stub_generator import StubGenerator
|
||||
from markitect.exceptions import InvalidInstructionTypeError
|
||||
|
||||
|
||||
class TestIssue54ContentInstructions:
|
||||
"""Test suite for content field instruction functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.schema_generator = SchemaGenerator()
|
||||
self.stub_generator = StubGenerator()
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_cli_accepts_include_content_instructions_option(self):
|
||||
"""Test that CLI accepts --include-content-instructions option."""
|
||||
# Arrange
|
||||
markdown_content = """# Architecture Document
|
||||
|
||||
## Introduction
|
||||
This section provides an overview of the system.
|
||||
|
||||
## Design Principles
|
||||
Core principles guiding the design.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0, f"CLI should accept --include-content-instructions option, got: {result.output}"
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_schema_generation_with_content_instructions_includes_instruction_fields(self):
|
||||
"""Test that schema generation with content instructions includes instruction fields."""
|
||||
# Arrange
|
||||
markdown_content = """# Software Architecture Document
|
||||
|
||||
## Introduction
|
||||
This section provides an overview of the system architecture.
|
||||
|
||||
### Purpose
|
||||
Explain the purpose and goals of this document.
|
||||
|
||||
## System Design
|
||||
Describe the overall system design and architecture.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Schema should contain content instruction fields
|
||||
assert "properties" in schema
|
||||
assert "headings" in schema["properties"]
|
||||
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
|
||||
# Level 1 heading should have content instructions
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
assert items_props["x-markitect-content-instructions"]["type"] == "string"
|
||||
|
||||
# Level 2 headings should have content instructions
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_schema_includes_content_instruction_metaschema_extension(self):
|
||||
"""Test that schemas with content instructions include metaschema extension."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section A
|
||||
Content for section A.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Should have metaschema extension
|
||||
assert "x-markitect-content-instructions-enabled" in schema
|
||||
assert schema["x-markitect-content-instructions-enabled"] is True
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_support_different_instruction_types(self):
|
||||
"""Test that content instructions can specify different instruction types."""
|
||||
# Arrange
|
||||
markdown_content = """# Requirements Document
|
||||
|
||||
## Functional Requirements
|
||||
List all functional requirements.
|
||||
|
||||
## Non-Functional Requirements
|
||||
Describe performance, security, and usability requirements.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True,
|
||||
instruction_type="description"
|
||||
)
|
||||
|
||||
# Assert - Should have instruction type specified
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
|
||||
assert "x-markitect-instruction-type" in items_props
|
||||
assert items_props["x-markitect-instruction-type"]["enum"] == ["description"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_integration_with_outline_mode(self):
|
||||
"""Test that content instructions work with outline mode."""
|
||||
# Arrange
|
||||
markdown_content = """# Project Plan
|
||||
|
||||
## Phase 1: Planning
|
||||
Planning activities and deliverables.
|
||||
|
||||
### Requirements Gathering
|
||||
Gather and document all requirements.
|
||||
|
||||
### Architecture Design
|
||||
Design the system architecture.
|
||||
|
||||
## Phase 2: Implementation
|
||||
Implementation activities.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--mode', 'outline',
|
||||
'--include-content-instructions',
|
||||
'--depth', '2',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Should have both outline mode and content instructions extensions
|
||||
assert schema.get("x-markitect-outline-mode") is True
|
||||
assert schema.get("x-markitect-content-instructions-enabled") is True
|
||||
|
||||
# Should only include headings up to depth 2
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
assert "level_1" in headings
|
||||
assert "level_2" in headings
|
||||
assert "level_3" not in headings
|
||||
|
||||
# Should have content instructions in the headings
|
||||
level_2 = headings["level_2"]
|
||||
items_props = level_2["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_for_paragraphs_and_lists(self):
|
||||
"""Test that content instructions can be added for paragraphs and lists."""
|
||||
# Arrange
|
||||
markdown_content = """# User Guide
|
||||
|
||||
## Overview
|
||||
This guide explains how to use the system.
|
||||
|
||||
Some introductory text here.
|
||||
|
||||
- Feature A
|
||||
- Feature B
|
||||
- Feature C
|
||||
|
||||
## Getting Started
|
||||
Follow these steps to get started.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Paragraphs should have content instructions
|
||||
if "paragraphs" in schema["properties"]:
|
||||
paragraphs_schema = schema["properties"]["paragraphs"]
|
||||
items_props = paragraphs_schema["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
# Lists should have content instructions
|
||||
if "lists" in schema["properties"]:
|
||||
lists_schema = schema["properties"]["lists"]
|
||||
items_props = lists_schema["items"]["properties"]
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_stub_generation_includes_content_instruction_placeholders(self):
|
||||
"""Test that stub generation includes content instruction placeholders."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Test Schema",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the main title of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Describe each major section of the document"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
stub_content = self.stub_generator.generate_stub_from_schema(schema)
|
||||
|
||||
# Assert - Stub should include instruction placeholders
|
||||
assert "Provide the main title of the document" in stub_content
|
||||
assert "Describe each major section of the document" in stub_content
|
||||
|
||||
def test_cli_supports_instruction_type_parameter(self):
|
||||
"""Test that CLI supports --instruction-type parameter."""
|
||||
# Arrange
|
||||
markdown_content = """# API Documentation
|
||||
|
||||
## Authentication
|
||||
How to authenticate with the API.
|
||||
|
||||
## Endpoints
|
||||
Available API endpoints.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
'--instruction-type', 'example',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
schema = json.loads(result.output)
|
||||
|
||||
# Check that instruction type is set correctly
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
assert items_props["x-markitect-instruction-type"]["enum"] == ["example"]
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_backward_compatibility_without_content_instructions(self):
|
||||
"""Test that existing behavior is maintained when content instructions are not enabled."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section One
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate schema without content instructions (default behavior)
|
||||
schema = self.schema_generator.generate_schema_from_file(temp_file)
|
||||
|
||||
# Assert - Should NOT have content instruction fields
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
|
||||
# Should not have content instruction fields
|
||||
assert "x-markitect-content-instructions" not in items_props
|
||||
assert "x-markitect-instruction-type" not in items_props
|
||||
|
||||
# Should NOT have content instructions extension
|
||||
assert "x-markitect-content-instructions-enabled" not in schema
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_with_heading_text_capture_integration(self):
|
||||
"""Test that content instructions work with heading text capture."""
|
||||
# Arrange
|
||||
markdown_content = """# Architecture Overview
|
||||
|
||||
## System Components
|
||||
Core system components and their responsibilities.
|
||||
|
||||
## Data Flow
|
||||
How data flows through the system.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
capture_heading_text=True,
|
||||
include_content_instructions=True
|
||||
)
|
||||
|
||||
# Assert - Should have both heading text capture and content instructions
|
||||
assert schema.get("x-markitect-heading-text-capture") is True
|
||||
assert schema.get("x-markitect-content-instructions-enabled") is True
|
||||
|
||||
# Headings should have both enum constraints and content instructions
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
|
||||
# Should have enum constraint from heading text capture
|
||||
assert "enum" in items_props["content"]
|
||||
assert items_props["content"]["enum"] == ["Architecture Overview"]
|
||||
|
||||
# Should also have content instructions
|
||||
assert "x-markitect-content-instructions" in items_props
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_cli_help_includes_content_instructions_options(self):
|
||||
"""Test that CLI help includes documentation for content instruction options."""
|
||||
# Act
|
||||
result = self.runner.invoke(cli, ['schema-generate', '--help'])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
help_text = result.output
|
||||
assert "--include-content-instructions" in help_text
|
||||
assert "--instruction-type" in help_text
|
||||
assert "content instructions" in help_text or "content guidance" in help_text
|
||||
|
||||
def test_instruction_type_validation(self):
|
||||
"""Test that --instruction-type parameter validates input correctly."""
|
||||
# Arrange
|
||||
markdown_content = """# Test Document
|
||||
|
||||
## Section
|
||||
Content here.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Test invalid instruction type
|
||||
result = self.runner.invoke(cli, [
|
||||
'schema-generate',
|
||||
'--include-content-instructions',
|
||||
'--instruction-type', 'invalid-type',
|
||||
str(temp_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code != 0
|
||||
assert "Invalid instruction type" in result.output or "invalid-type" in result.output
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
|
||||
def test_content_instructions_generate_appropriate_default_text(self):
|
||||
"""Test that content instructions generate appropriate default guidance text."""
|
||||
# Arrange
|
||||
markdown_content = """# Development Guide
|
||||
|
||||
## Prerequisites
|
||||
System requirements and prerequisites.
|
||||
|
||||
### Software Requirements
|
||||
Required software and versions.
|
||||
|
||||
## Installation
|
||||
Step-by-step installation instructions.
|
||||
|
||||
## Configuration
|
||||
How to configure the system.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
f.write(markdown_content)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
schema = self.schema_generator.generate_schema_from_file(
|
||||
temp_file,
|
||||
include_content_instructions=True,
|
||||
instruction_type="description"
|
||||
)
|
||||
|
||||
# Assert - Content instructions should contain appropriate guidance
|
||||
headings = schema["properties"]["headings"]["properties"]
|
||||
|
||||
# Level 1 should have appropriate instructions
|
||||
level_1 = headings["level_1"]
|
||||
items_props = level_1["items"]["properties"]
|
||||
instructions = items_props["x-markitect-content-instructions"]["const"]
|
||||
assert len(instructions) > 0
|
||||
assert isinstance(instructions, str)
|
||||
|
||||
# Instructions should be contextually appropriate
|
||||
# (implementation will determine specific text)
|
||||
|
||||
finally:
|
||||
temp_file.unlink()
|
||||
632
tests/test_issue_55_schema_based_draft_generation.py
Normal file
632
tests/test_issue_55_schema_based_draft_generation.py
Normal file
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Tests for Issue #55: Schema-based draft generation
|
||||
|
||||
This test module implements comprehensive tests for enhanced schema-based draft
|
||||
generation that utilizes content field instructions and schema metadata.
|
||||
|
||||
Following TDD8 methodology - these tests are written before implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.cli import cli
|
||||
from markitect.stub_generator import StubGenerator
|
||||
|
||||
|
||||
class TestIssue55SchemaBasedDraftGeneration:
|
||||
"""Test suite for enhanced schema-based draft generation functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.stub_generator = StubGenerator()
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_generate_stub_uses_content_instructions_from_schema(self):
|
||||
"""Test that generate-stub uses content instructions instead of generic placeholders."""
|
||||
# Arrange
|
||||
schema_with_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "API Documentation",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Write the main API documentation title"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Describe each API endpoint section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use specific content instructions, not generic placeholders
|
||||
assert "Write the main API documentation title" in output
|
||||
assert "Describe each API endpoint section" in output
|
||||
|
||||
# Should NOT contain generic placeholder text
|
||||
assert "TODO: Add content for" not in output
|
||||
assert "section_level_2 section" not in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_includes_schema_reference_metadata(self):
|
||||
"""Test that generated drafts include reference to their source schema."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Requirements Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the document title"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should include schema reference metadata
|
||||
assert "<!-- Generated from schema:" in output or "Source schema:" in output
|
||||
assert str(schema_file.name) in output or schema_file.name in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_supports_different_instruction_types(self):
|
||||
"""Test that generate-stub handles different instruction types appropriately."""
|
||||
# Arrange
|
||||
schema_with_example_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Tutorial Guide",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Example: Getting Started with Our API"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["example"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_example_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use the example-type instruction
|
||||
assert "Example: Getting Started with Our API" in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_handles_schemas_without_content_instructions(self):
|
||||
"""Test that generate-stub gracefully handles schemas without content instructions."""
|
||||
# Arrange - Schema without content instructions (backward compatibility)
|
||||
schema_without_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Basic Document",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"level": {"type": "integer"}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_without_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert - Should still work with generic placeholders
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should fall back to generic placeholder behavior
|
||||
assert "Basic Document" in output # Should use schema title
|
||||
assert len(output) > 0 # Should generate some content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_supports_output_file_with_schema_reference(self):
|
||||
"""Test that generate-stub writes to output file with schema reference."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Project Plan",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide the project name and overview"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
output_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(output_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
content = output_file.read_text()
|
||||
assert "Provide the project name and overview" in content
|
||||
assert "Project Plan" in content
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
if output_file.exists():
|
||||
output_file.unlink()
|
||||
|
||||
def test_generate_stub_validates_generated_draft_against_schema(self):
|
||||
"""Test that generated drafts can be validated against their source schema."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Meeting Notes",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Meeting title and date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||
draft_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act - Generate draft
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file),
|
||||
'--output', str(draft_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Act - Validate generated draft against schema
|
||||
validate_result = self.runner.invoke(cli, [
|
||||
'validate',
|
||||
str(draft_file),
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert - Generated draft should be valid against its source schema
|
||||
assert validate_result.exit_code == 0
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
if draft_file.exists():
|
||||
draft_file.unlink()
|
||||
|
||||
def test_stub_generator_class_supports_content_instructions_directly(self):
|
||||
"""Test that StubGenerator class can be used directly with content instruction schemas."""
|
||||
# Arrange
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Architecture Document",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "System architecture overview title"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
stub_content = self.stub_generator.generate_stub_from_schema(schema)
|
||||
|
||||
# Assert
|
||||
assert "System architecture overview title" in stub_content
|
||||
assert "Architecture Document" in stub_content
|
||||
|
||||
def test_generate_stub_with_outline_mode_schema_integration(self):
|
||||
"""Test that generate-stub works with schemas created by outline mode + content instructions."""
|
||||
# Arrange - Schema that would be generated by outline mode with content instructions
|
||||
outline_schema_with_instructions = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema from user_guide.md",
|
||||
"x-markitect-outline-mode": True,
|
||||
"x-markitect-outline-depth": 2,
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide content for the 'level 1 heading' section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Provide content for the 'level 2 heading' section"
|
||||
},
|
||||
"x-markitect-instruction-type": {
|
||||
"type": "string",
|
||||
"enum": ["description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(outline_schema_with_instructions, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use content instructions from outline mode schema
|
||||
assert "Provide content for the 'level 1 heading' section" in output
|
||||
assert "Provide content for the 'level 2 heading' section" in output
|
||||
|
||||
# Should respect outline mode structure (depth limited to 2)
|
||||
assert output.count('##') >= 3 # Should have multiple level 2 headings
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_with_heading_text_capture_schema_integration(self):
|
||||
"""Test that generate-stub works with schemas that have heading text capture."""
|
||||
# Arrange - Schema with both heading text capture and content instructions
|
||||
schema_with_heading_capture = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Schema from api_docs.md",
|
||||
"x-markitect-heading-text-capture": True,
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"enum": ["API Reference"] # From heading text capture
|
||||
},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Complete API reference documentation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_heading_capture, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should use the specific heading text from enum constraint
|
||||
assert "# API Reference" in output
|
||||
|
||||
# Should use content instructions
|
||||
assert "Complete API reference documentation" in output
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
|
||||
def test_generate_stub_preserves_schema_metadata_in_output(self):
|
||||
"""Test that important schema metadata is preserved in the generated draft."""
|
||||
# Arrange
|
||||
schema_with_metadata = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Design Document",
|
||||
"description": "Template for system design documents",
|
||||
"x-markitect-content-instructions-enabled": True,
|
||||
"x-markitect-outline-mode": True,
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"x-markitect-content-instructions": {
|
||||
"type": "string",
|
||||
"const": "Design document title and scope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(schema_with_metadata, f, indent=2)
|
||||
schema_file = Path(f.name)
|
||||
|
||||
try:
|
||||
# Act
|
||||
result = self.runner.invoke(cli, [
|
||||
'generate-stub',
|
||||
str(schema_file)
|
||||
])
|
||||
|
||||
# Assert
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
|
||||
# Should include schema metadata information
|
||||
assert any(marker in output.lower() for marker in [
|
||||
"generated from", "source schema", "template for", "schema:", "outline mode"
|
||||
])
|
||||
|
||||
finally:
|
||||
schema_file.unlink()
|
||||
Reference in New Issue
Block a user