Compare commits
7 Commits
d68e762612
...
82c1a3ab65
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c1a3ab65 | |||
| da34303057 | |||
| d2cd2d22fd | |||
| 48e0b60be5 | |||
| 2b35fcde62 | |||
| c46d9f7a0b | |||
| 2b687a4ca8 |
17
TODO.md
17
TODO.md
@@ -76,6 +76,23 @@ The **capability-capability** includes:
|
||||
|
||||
*Recent completed tasks have been documented in _issue-tracking/issue-facade/CHANGELOG.md following Keep a Changelog format.*
|
||||
|
||||
### 2026-01-04 - Phase 2: Schema Refinement Tools
|
||||
- ✅ Implemented schema-analyze command to detect rigidity issues
|
||||
- ✅ Implemented schema-refine command with automatic loosening logic
|
||||
- ✅ Added interactive mode to schema-refine for fine-grained control
|
||||
- ✅ Created comprehensive test suite (33 unit tests, 100% passing)
|
||||
- ✅ Wrote user guide documentation with examples and workflows
|
||||
- ✅ Successfully tested on example schemas (reduced rigidity from 60/100 to 24/100)
|
||||
- ✅ Integrated into CLI with proper exit codes and error handling
|
||||
|
||||
**Key Features Delivered:**
|
||||
- Rigidity score calculation (0-100 scale)
|
||||
- Automatic detection of exact counts, const values, overly specific numbers
|
||||
- Path navigation for nested schema properties
|
||||
- Dry-run mode for previewing changes
|
||||
- Interactive approval workflow
|
||||
- Comprehensive reporting (normal and verbose modes)
|
||||
|
||||
### 2025-12-17 - Architecture Refactoring
|
||||
- ✅ Implemented ReusableCapabilitiesArchitecture v0.1
|
||||
- ✅ Added feedback capability to issue-facade
|
||||
|
||||
495
docs/user-guides/SCHEMA_REFINEMENT_TOOLS.md
Normal file
495
docs/user-guides/SCHEMA_REFINEMENT_TOOLS.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Schema Refinement Tools - User Guide
|
||||
|
||||
## Overview
|
||||
|
||||
MarkiTect Phase 2 introduces powerful schema refinement tools to help you analyze and improve JSON schemas for markdown validation. These tools detect rigidity issues and automatically apply fixes to make schemas more flexible and reusable.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Analyze a schema for rigidity issues
|
||||
markitect schema-analyze examples/manpages/markdown-manpage-schema.json
|
||||
|
||||
# Refine a schema automatically
|
||||
markitect schema-refine examples/manpages/markdown-manpage-schema.json --output refined-schema.json
|
||||
|
||||
# Review each fix interactively
|
||||
markitect schema-refine examples/manpages/markdown-manpage-schema.json --interactive
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### schema-analyze
|
||||
|
||||
Analyzes a JSON schema to detect rigidity issues and calculate a rigidity score (0-100).
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
markitect schema-analyze <schema-file> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
- `--verbose`, `-v`: Show detailed analysis with current and suggested values
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Basic analysis
|
||||
markitect schema-analyze schema.json
|
||||
|
||||
# Verbose output with details
|
||||
markitect schema-analyze schema.json --verbose
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
The analyzer provides:
|
||||
|
||||
- **Rigidity Score** (0-100): Higher scores indicate more rigid schemas
|
||||
- 0-40: LOW - Flexible, good design
|
||||
- 41-70: MEDIUM - Some rigidity detected
|
||||
- 71-100: HIGH - Very rigid, needs refinement
|
||||
|
||||
- **Phase 1 Features**: Checks for classification system and content control
|
||||
- **Issue Count**: Breakdown by severity (Errors, Warnings, Info)
|
||||
- **Detected Issues**: List of problems with suggestions
|
||||
|
||||
#### Exit Codes
|
||||
|
||||
- `0`: Schema is flexible (score ≤ 50)
|
||||
- `1`: Schema is rigid (score > 50)
|
||||
- `2`: Error occurred
|
||||
|
||||
### schema-refine
|
||||
|
||||
Automatically refines rigid schemas by applying fixes for detected issues.
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
markitect schema-refine <schema-file> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
- `--output`, `-o PATH`: Output file (default: overwrite input file)
|
||||
- `--loosen-counts`: Convert exact counts to flexible ranges (default: enabled)
|
||||
- `--no-loosen-counts`: Disable count loosening
|
||||
- `--round-numbers`: Round overly specific numbers (default: enabled)
|
||||
- `--no-round-numbers`: Disable number rounding
|
||||
- `--migrate-deprecated`: Document deprecated extensions (default: disabled)
|
||||
- `--dry-run`: Show changes without applying them
|
||||
- `--interactive`, `-i`: Prompt for each refinement interactively
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Refine schema in place
|
||||
markitect schema-refine schema.json
|
||||
|
||||
# Preview changes without applying
|
||||
markitect schema-refine schema.json --dry-run
|
||||
|
||||
# Save refined schema to new file
|
||||
markitect schema-refine schema.json --output refined-schema.json
|
||||
|
||||
# Review each fix interactively
|
||||
markitect schema-refine schema.json --interactive
|
||||
|
||||
# Disable specific refinements
|
||||
markitect schema-refine schema.json --no-loosen-counts
|
||||
```
|
||||
|
||||
#### Refinement Actions
|
||||
|
||||
The refiner automatically applies these fixes:
|
||||
|
||||
1. **Exact Count Loosening**: Converts exact counts to flexible ranges
|
||||
- Before: `"minItems": 5, "maxItems": 5`
|
||||
- After: `"minItems": 3, "maxItems": 10`
|
||||
|
||||
2. **Const Value Conversion**: Replaces exact value constraints with ranges
|
||||
- Before: `"const": 1`
|
||||
- After: `"minimum": 0, "maximum": 2`
|
||||
|
||||
3. **Number Rounding**: Rounds overly specific numbers
|
||||
- Before: `"minItems": 73`
|
||||
- After: `"minItems": 70`
|
||||
|
||||
4. **Range Widening**: Expands narrow integer ranges
|
||||
- Before: `"minimum": 5, "maximum": 6`
|
||||
- After: `"minimum": 0, "maximum": 11`
|
||||
|
||||
#### Exit Codes
|
||||
|
||||
- `0`: Success with changes applied
|
||||
- `1`: Success but no changes needed
|
||||
- `2`: Error occurred
|
||||
|
||||
## Issue Types
|
||||
|
||||
### Exact Count (WARNING)
|
||||
|
||||
**Problem**: Schema requires exact number of items, leaving no flexibility.
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Convert to a range
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Const Value (WARNING)
|
||||
|
||||
**Problem**: Property must have exact value.
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Replace with range for numeric values
|
||||
```json
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Overly Specific Numbers (INFO)
|
||||
|
||||
**Problem**: Numbers are too specific (like 73 instead of 70).
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 73
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Round to nearest 10
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 70
|
||||
}
|
||||
```
|
||||
|
||||
### No Flexibility (INFO)
|
||||
|
||||
**Problem**: Integer range is too narrow.
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 5,
|
||||
"maximum": 6
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Widen the range
|
||||
```json
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 11
|
||||
}
|
||||
```
|
||||
|
||||
### Missing Classifications (INFO)
|
||||
|
||||
**Problem**: Schema doesn't use the Phase 1 classification system.
|
||||
|
||||
**Suggestion**: Add `x-markitect-sections` to classify sections as required/recommended/optional/discouraged/improper.
|
||||
|
||||
### Missing Content Control (INFO)
|
||||
|
||||
**Problem**: Schema lacks content validation patterns and quality metrics.
|
||||
|
||||
**Suggestion**: Add `x-markitect-content-control` for pattern validation and quality requirements.
|
||||
|
||||
### Deprecated Extensions (WARNING)
|
||||
|
||||
**Problem**: Schema uses old extension format.
|
||||
|
||||
**Example**: `x-markitect-required-sections`
|
||||
|
||||
**Suggestion**: Migrate to `x-markitect-sections` with classification system.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Basic Workflow: Analyze and Refine
|
||||
|
||||
1. **Analyze** your schema to understand issues:
|
||||
```bash
|
||||
markitect schema-analyze my-schema.json --verbose
|
||||
```
|
||||
|
||||
2. **Preview** refinements before applying:
|
||||
```bash
|
||||
markitect schema-refine my-schema.json --dry-run
|
||||
```
|
||||
|
||||
3. **Apply** refinements:
|
||||
```bash
|
||||
markitect schema-refine my-schema.json --output my-schema-refined.json
|
||||
```
|
||||
|
||||
4. **Verify** improvements:
|
||||
```bash
|
||||
markitect schema-analyze my-schema-refined.json
|
||||
```
|
||||
|
||||
### Interactive Workflow
|
||||
|
||||
For fine-grained control, use interactive mode:
|
||||
|
||||
```bash
|
||||
markitect schema-refine my-schema.json --interactive
|
||||
```
|
||||
|
||||
The tool will:
|
||||
1. Show each detected issue
|
||||
2. Display current and suggested values
|
||||
3. Prompt for confirmation (y/N/q)
|
||||
4. Apply only approved fixes
|
||||
|
||||
Example session:
|
||||
```
|
||||
Issue 1/4
|
||||
Type: exact_count
|
||||
Path: properties.headings.level_1
|
||||
Array 'level_1' requires exactly 1 items
|
||||
Suggestion: Use a range like minItems: 0, maxItems: 6
|
||||
Current: {"minItems": 1, "maxItems": 1}
|
||||
Suggested: {"minItems": 0, "maxItems": 6}
|
||||
|
||||
Apply this fix? [y/N/q]: y
|
||||
✓ Applied
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Use exit codes to enforce schema quality in your pipeline:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Analyze schema and fail if rigid
|
||||
if ! markitect schema-analyze schema.json; then
|
||||
echo "Schema is too rigid (score > 50)"
|
||||
echo "Run: markitect schema-refine schema.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Schema quality check passed"
|
||||
```
|
||||
|
||||
### Schema Migration Workflow
|
||||
|
||||
Migrating from old format to Phase 1:
|
||||
|
||||
1. **Analyze** to identify deprecated extensions:
|
||||
```bash
|
||||
markitect schema-analyze old-schema.json
|
||||
```
|
||||
|
||||
2. **Document** deprecated extensions:
|
||||
```bash
|
||||
markitect schema-refine old-schema.json --migrate-deprecated
|
||||
```
|
||||
|
||||
3. **Manually migrate** to new format (automatic migration not implemented due to complexity)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use schema-analyze
|
||||
|
||||
- Before committing schemas to version control
|
||||
- During code review to ensure quality
|
||||
- When creating new schemas from examples
|
||||
- To understand why a schema fails validation
|
||||
|
||||
### When to Use schema-refine
|
||||
|
||||
- After auto-generating schemas from documents
|
||||
- When inheriting legacy schemas
|
||||
- To quickly fix common rigidity issues
|
||||
- Before publishing schemas for reuse
|
||||
|
||||
### When to Use --interactive
|
||||
|
||||
- When you need fine-grained control
|
||||
- For schemas with domain-specific requirements
|
||||
- When learning about schema design
|
||||
- To review fixes before applying
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
For most use cases:
|
||||
```bash
|
||||
# Balanced refinement (default)
|
||||
markitect schema-refine schema.json
|
||||
|
||||
# Conservative (preserve more constraints)
|
||||
markitect schema-refine schema.json --no-round-numbers
|
||||
|
||||
# Aggressive (maximum flexibility)
|
||||
markitect schema-refine schema.json --loosen-counts --round-numbers
|
||||
```
|
||||
|
||||
## Understanding Rigidity Scores
|
||||
|
||||
The rigidity score is calculated by weighting detected issues:
|
||||
|
||||
| Issue Type | Weight |
|
||||
|------------|--------|
|
||||
| Exact Count | 15 |
|
||||
| Overly Specific | 10 |
|
||||
| No Flexibility | 8 |
|
||||
| Missing Classifications | 5 |
|
||||
| Deprecated Extensions | 5 |
|
||||
| Missing Content Control | 3 |
|
||||
|
||||
**Score Interpretation**:
|
||||
- **0-20**: Excellent - Well-designed, flexible schema
|
||||
- **21-40**: Good - Minor improvements possible
|
||||
- **41-60**: Fair - Moderate rigidity, refinement recommended
|
||||
- **61-80**: Poor - Significant rigidity, refinement needed
|
||||
- **81-100**: Very Poor - Highly rigid, manual review recommended
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Git Pre-commit Hook
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .git/hooks/pre-commit
|
||||
|
||||
SCHEMAS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.json$')
|
||||
|
||||
for schema in $SCHEMAS; do
|
||||
if markitect schema-analyze "$schema" 2>&1 | grep -q "RIGID"; then
|
||||
echo "Error: $schema is too rigid"
|
||||
echo "Run: markitect schema-refine $schema"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Makefile Target
|
||||
|
||||
```makefile
|
||||
.PHONY: check-schemas
|
||||
check-schemas:
|
||||
@for schema in schemas/*.json; do \
|
||||
echo "Checking $$schema..."; \
|
||||
markitect schema-analyze $$schema || exit 1; \
|
||||
done
|
||||
|
||||
.PHONY: refine-schemas
|
||||
refine-schemas:
|
||||
@for schema in schemas/*.json; do \
|
||||
echo "Refining $$schema..."; \
|
||||
markitect schema-refine $$schema; \
|
||||
done
|
||||
```
|
||||
|
||||
### Python Integration
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def analyze_schema(schema_path):
|
||||
"""Analyze a schema and return rigidity score."""
|
||||
result = subprocess.run(
|
||||
["markitect", "schema-analyze", schema_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Parse output for score
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Rigidity Score:' in line:
|
||||
score = int(line.split(':')[1].split('/')[0].strip())
|
||||
return score
|
||||
return None
|
||||
|
||||
def refine_schema(schema_path, output_path):
|
||||
"""Refine a schema and save to output path."""
|
||||
result = subprocess.run(
|
||||
["markitect", "schema-refine", schema_path, "-o", output_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
# Usage
|
||||
score = analyze_schema("schema.json")
|
||||
if score > 50:
|
||||
print(f"Schema is rigid (score: {score})")
|
||||
refine_schema("schema.json", "schema-refined.json")
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Schema Not Found
|
||||
|
||||
**Error**: `Error: Schema file not found: schema.json`
|
||||
|
||||
**Solution**: Check file path and ensure file exists.
|
||||
|
||||
### Invalid JSON
|
||||
|
||||
**Error**: `Error: Invalid JSON in schema file`
|
||||
|
||||
**Solution**: Validate JSON syntax using `jsonlint` or similar tool.
|
||||
|
||||
### No Changes Applied
|
||||
|
||||
**Output**: `No refinements needed - schema is already flexible`
|
||||
|
||||
**Reason**: Schema doesn't have any detectable rigidity issues or has rigidity score < 50.
|
||||
|
||||
**Action**: Use `--verbose` to see all issues including INFO level.
|
||||
|
||||
### Refinement Broke Schema
|
||||
|
||||
**Problem**: Refined schema is too permissive.
|
||||
|
||||
**Solution**:
|
||||
1. Use `--interactive` to selectively apply fixes
|
||||
2. Use `--no-loosen-counts` or `--no-round-numbers` to preserve constraints
|
||||
3. Manually adjust ranges after refinement
|
||||
|
||||
## See Also
|
||||
|
||||
- [Schema Extensions Specification](../specifications/schema-extensions-spec.md) - Complete Phase 1 specification
|
||||
- [Schema Evolution Workplan](../../examples/manpages/SCHEMA_EVOLUTION_WORKPLAN.md) - Roadmap for schema features
|
||||
- [Manpage Example](../../examples/manpages/README.md) - Complete example demonstrating schema validation
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or feature requests:
|
||||
- GitHub Issues: https://github.com/anthropics/markitect/issues
|
||||
- Documentation: https://github.com/anthropics/markitect/docs
|
||||
@@ -3,6 +3,152 @@
|
||||
"type": "object",
|
||||
"title": "Markdown Manpage Schema",
|
||||
"description": "JSON schema defining the structure of Unix-style manual pages written in Markdown. Compatible with man(1) section format and conventions.",
|
||||
"x-markitect-sections": {
|
||||
"SYNOPSIS": {
|
||||
"classification": "required",
|
||||
"heading_level": 2,
|
||||
"position": "after_title",
|
||||
"content_instruction": "Brief command syntax showing options and arguments in standard Unix format",
|
||||
"min_paragraphs": 1,
|
||||
"max_paragraphs": 5,
|
||||
"error_message": "SYNOPSIS section is mandatory for all Unix manual pages"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"classification": "required",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Detailed explanation of the command's purpose and functionality",
|
||||
"min_paragraphs": 2,
|
||||
"error_message": "DESCRIPTION section is mandatory for all Unix manual pages"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"classification": "recommended",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Command-line options and flags with descriptions",
|
||||
"alternatives": ["GLOBAL OPTIONS", "COMMAND OPTIONS", "FLAGS"],
|
||||
"warning_if_missing": "Documenting command options improves usability"
|
||||
},
|
||||
"EXAMPLES": {
|
||||
"classification": "recommended",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Practical usage examples demonstrating common use cases",
|
||||
"min_code_blocks": 2,
|
||||
"warning_if_missing": "Examples significantly improve manpage usability and comprehension"
|
||||
},
|
||||
"SEE ALSO": {
|
||||
"classification": "recommended",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Related commands, configuration files, and documentation references",
|
||||
"warning_if_missing": "Cross-references help users discover related functionality"
|
||||
},
|
||||
"COPYRIGHT": {
|
||||
"classification": "recommended",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Copyright statement and license information",
|
||||
"warning_if_missing": "License information should be documented for clarity"
|
||||
},
|
||||
"COMMANDS": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Subcommands and their brief descriptions"
|
||||
},
|
||||
"CONFIGURATION": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Configuration file format and options"
|
||||
},
|
||||
"FILES": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Important files used by the command with their purposes"
|
||||
},
|
||||
"EXIT STATUS": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Exit codes and their meanings"
|
||||
},
|
||||
"ENVIRONMENT": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Environment variables used or set by the command"
|
||||
},
|
||||
"BUGS": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Known issues and bug reporting instructions"
|
||||
},
|
||||
"AUTHORS": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "List of contributors and maintainers"
|
||||
}
|
||||
},
|
||||
"x-markitect-content-control": {
|
||||
"synopsis": {
|
||||
"required_patterns": [
|
||||
"\\*\\*[a-z][a-z0-9-]*\\*\\*",
|
||||
"\\[.*\\]"
|
||||
],
|
||||
"discouraged_patterns": [
|
||||
"TODO",
|
||||
"FIXME"
|
||||
],
|
||||
"content_quality": {
|
||||
"min_words": 5,
|
||||
"max_words": 150,
|
||||
"readability_target": "technical"
|
||||
},
|
||||
"content_instructions": [
|
||||
"Show command name in bold: **command**",
|
||||
"Use brackets [] for optional arguments",
|
||||
"Use italic *ARG* for required arguments",
|
||||
"Keep synopsis concise (1-5 lines)",
|
||||
"Follow man(1) synopsis conventions"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"discouraged_patterns": [
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"\\bWIP\\b",
|
||||
"TBD"
|
||||
],
|
||||
"forbidden_patterns": [
|
||||
"password\\s*=\\s*[\"'].*[\"']",
|
||||
"api[_-]?key\\s*=\\s*[\"'].*[\"']"
|
||||
],
|
||||
"content_quality": {
|
||||
"min_words": 50,
|
||||
"max_words": 1000,
|
||||
"readability_target": "technical",
|
||||
"min_sentences": 3
|
||||
},
|
||||
"content_instructions": [
|
||||
"Explain what the command does",
|
||||
"Describe the primary purpose",
|
||||
"Mention key features and capabilities",
|
||||
"Note any prerequisites or dependencies",
|
||||
"Keep language clear and technical"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"required_patterns": [
|
||||
"```",
|
||||
"#"
|
||||
],
|
||||
"content_quality": {
|
||||
"min_words": 50,
|
||||
"max_words": 2000,
|
||||
"readability_target": "general"
|
||||
},
|
||||
"content_instructions": [
|
||||
"Use bash code blocks with syntax highlighting",
|
||||
"Include comments explaining each example",
|
||||
"Start with simple examples, progress to complex",
|
||||
"Show actual output when helpful",
|
||||
"Cover the most common use cases"
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
@@ -96,31 +242,5 @@
|
||||
"maxItems": 500
|
||||
}
|
||||
},
|
||||
"required": ["headings", "paragraphs", "code_blocks", "emphasis"],
|
||||
"x-markitect-required-sections": [
|
||||
"SYNOPSIS",
|
||||
"DESCRIPTION"
|
||||
],
|
||||
"x-markitect-recommended-sections": [
|
||||
"OPTIONS",
|
||||
"EXAMPLES",
|
||||
"SEE ALSO",
|
||||
"COPYRIGHT"
|
||||
],
|
||||
"x-markitect-optional-sections": [
|
||||
"COMMANDS",
|
||||
"CONFIGURATION",
|
||||
"FILES",
|
||||
"EXIT STATUS",
|
||||
"ENVIRONMENT",
|
||||
"BUGS",
|
||||
"AUTHORS"
|
||||
],
|
||||
"x-markitect-conventions": {
|
||||
"heading_case": "UPPERCASE for H2 sections",
|
||||
"command_format": "Bold with **command** for commands and options",
|
||||
"argument_format": "Italic with *ARG* for arguments and placeholders",
|
||||
"example_language": "bash for code blocks",
|
||||
"definition_lists": "Use bold followed by colon for FILES, EXIT STATUS, ENVIRONMENT sections"
|
||||
}
|
||||
"required": ["headings", "paragraphs", "code_blocks", "emphasis"]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,106 @@ MarkiTect parses markdown files into an AST representation, then validates the A
|
||||
|
||||
Schemas validate structure, not semantics. A document can pass validation while containing incorrect content, as long as the structure matches the schema.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
### Validation Options
|
||||
|
||||
**--schema** *PATH*, **-s** *PATH*
|
||||
: Path to JSON schema file for validation
|
||||
: Used with **validate** command to specify schema location
|
||||
|
||||
**--schema-json** *TEXT*
|
||||
: JSON schema provided as inline string
|
||||
: Alternative to --schema for programmatic use
|
||||
: Useful for testing or dynamic schema generation
|
||||
|
||||
**--detailed-errors**, **--errors**
|
||||
: Show detailed validation errors with line numbers
|
||||
: Provides specific locations and descriptions of failures
|
||||
: Essential for debugging complex schema validation issues
|
||||
|
||||
**--error-format** *FORMAT*
|
||||
: Format for error output: **text**, **json**, or **markdown**
|
||||
: Default: **text**
|
||||
: JSON format useful for CI/CD pipeline integration
|
||||
: Markdown format for inclusion in documentation
|
||||
|
||||
**--quiet**, **-q**
|
||||
: Only output validation result (true/false)
|
||||
: Suppresses all other output for scripting
|
||||
: Exit code indicates success (0) or failure (non-zero)
|
||||
|
||||
### Schema Generation Options
|
||||
|
||||
**--output** *PATH*, **-o** *PATH*
|
||||
: Output file path for generated schema or document
|
||||
: Used with **schema-generate** and **generate-stub** commands
|
||||
: If omitted, outputs to stdout
|
||||
|
||||
**--style** *STYLE*
|
||||
: Placeholder content style for **generate-stub** command
|
||||
: Options: **default**, **custom**, **detailed**
|
||||
: Affects the verbosity of generated stub content
|
||||
|
||||
**--title** *TEXT*
|
||||
: Custom document title for generated stubs
|
||||
: Overrides default title derived from schema
|
||||
: Useful for creating multiple documents from one schema
|
||||
|
||||
### Schema Management Options
|
||||
|
||||
**--schema-list**
|
||||
: List all available schemas in the library
|
||||
: Shows schema names and descriptions
|
||||
: Helps discover reusable schema patterns
|
||||
|
||||
**--schema-info** *SCHEMA_NAME*
|
||||
: Display detailed information about a specific schema
|
||||
: Shows schema structure, requirements, and metadata
|
||||
: Useful for understanding schema capabilities before use
|
||||
|
||||
**--schema-delete** *SCHEMA_NAME*
|
||||
: Remove a schema from the library
|
||||
: Requires confirmation unless **--confirm** flag is used
|
||||
: Irreversible operation - use with caution
|
||||
|
||||
**--confirm**
|
||||
: Skip confirmation prompts for destructive operations
|
||||
: Used with **schema-delete** and similar commands
|
||||
: Useful for automation scripts
|
||||
|
||||
### Phase 2 Schema Refinement Options
|
||||
|
||||
**--verbose**, **-v**
|
||||
: Show detailed analysis with current and suggested values
|
||||
: Used with **schema-analyze** command
|
||||
: Provides comprehensive rigidity assessment
|
||||
|
||||
**--dry-run**
|
||||
: Preview refinement changes without applying them
|
||||
: Used with **schema-refine** command
|
||||
: Allows review before modifying schemas
|
||||
|
||||
**--interactive**, **-i**
|
||||
: Prompt for each refinement interactively
|
||||
: Used with **schema-refine** command
|
||||
: Provides fine-grained control over applied fixes
|
||||
|
||||
**--loosen-counts**
|
||||
: Convert exact counts to flexible ranges (default: enabled)
|
||||
: Part of schema refinement process
|
||||
: Can be disabled with **--no-loosen-counts**
|
||||
|
||||
**--round-numbers**
|
||||
: Round overly specific numbers (default: enabled)
|
||||
: Improves schema reusability
|
||||
: Can be disabled with **--no-round-numbers**
|
||||
|
||||
**--migrate-deprecated**
|
||||
: Document deprecated extension usage
|
||||
: Helps identify schemas needing manual migration
|
||||
: Does not automatically migrate (too risky)
|
||||
|
||||
## SCHEMA STRUCTURE
|
||||
|
||||
### JSON Schema Format
|
||||
@@ -59,13 +159,21 @@ MarkiTect schemas are standard JSON Schema (draft-07) documents with custom exte
|
||||
|
||||
MarkiTect extends JSON Schema with custom properties prefixed with **x-markitect-**:
|
||||
|
||||
**x-markitect-required-sections**
|
||||
: Array of required H2 section names
|
||||
: Example: ["SYNOPSIS", "DESCRIPTION", "EXAMPLES"]
|
||||
**x-markitect-sections**
|
||||
: Section classification and content control system
|
||||
: Defines sections with five classification levels:
|
||||
: - **required**: Must be present (validation fails if missing)
|
||||
: - **recommended**: Should be present (warning if missing)
|
||||
: - **optional**: May be present (no validation impact)
|
||||
: - **discouraged**: Should not be present (warning if present)
|
||||
: - **improper**: Must not be present (validation fails if present)
|
||||
: Each section can specify content instructions, constraints, and custom messages
|
||||
|
||||
**x-markitect-recommended-sections**
|
||||
: Array of recommended but optional section names
|
||||
: Generates warnings when missing
|
||||
**x-markitect-content-control**
|
||||
: Content validation rules for section content
|
||||
: Defines required/discouraged/forbidden patterns
|
||||
: Specifies content quality metrics (word count, readability)
|
||||
: Provides content instructions for authors
|
||||
|
||||
**x-markitect-outline-mode**
|
||||
: Boolean enabling outline-only validation
|
||||
@@ -236,6 +344,139 @@ The **metadata** property validates overall document characteristics:
|
||||
|
||||
Use **const** for exact matches or ranges for flexibility.
|
||||
|
||||
### Section Classification System
|
||||
|
||||
MarkiTect provides a five-level classification system for document sections through **x-markitect-sections**:
|
||||
|
||||
#### Required Sections
|
||||
|
||||
Sections marked as **required** must be present in the document. Validation fails with an error if missing.
|
||||
|
||||
```json
|
||||
"SYNOPSIS": {
|
||||
"classification": "required",
|
||||
"error_message": "SYNOPSIS section is mandatory for all manpages"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Behavior**:
|
||||
- Missing → ERROR → validation fails
|
||||
- Present → Continue validation
|
||||
|
||||
#### Recommended Sections
|
||||
|
||||
Sections marked as **recommended** should be present. A warning is generated if missing, but validation succeeds.
|
||||
|
||||
```json
|
||||
"EXAMPLES": {
|
||||
"classification": "recommended",
|
||||
"warning_if_missing": "Examples improve documentation usability"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Behavior**:
|
||||
- Missing → WARNING → validation succeeds with warnings
|
||||
- Present → Continue validation
|
||||
|
||||
#### Optional Sections
|
||||
|
||||
Sections marked as **optional** may or may not be present with no validation impact.
|
||||
|
||||
```json
|
||||
"BUGS": {
|
||||
"classification": "optional",
|
||||
"content_instruction": "Known issues and bug reporting"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Behavior**:
|
||||
- Missing → No impact
|
||||
- Present → Continue validation
|
||||
|
||||
#### Discouraged Sections
|
||||
|
||||
Sections marked as **discouraged** should not be present. A warning is generated if found, but validation succeeds.
|
||||
|
||||
```json
|
||||
"DEPRECATED": {
|
||||
"classification": "discouraged",
|
||||
"warning_if_missing": "Move deprecated content to HISTORY section"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Behavior**:
|
||||
- Missing → No impact
|
||||
- Present → WARNING → validation succeeds with warnings
|
||||
|
||||
#### Improper Sections
|
||||
|
||||
Sections marked as **improper** must not be present. Validation fails with an error if found.
|
||||
|
||||
```json
|
||||
"TODO": {
|
||||
"classification": "improper",
|
||||
"error_message": "TODO sections must be removed before publication"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Behavior**:
|
||||
- Missing → No impact
|
||||
- Present → ERROR → validation fails
|
||||
|
||||
### Content Control
|
||||
|
||||
The **x-markitect-content-control** extension enables content-level validation:
|
||||
|
||||
#### Pattern Validation
|
||||
|
||||
**required_patterns** - Array of regex patterns that must appear in content:
|
||||
```json
|
||||
"required_patterns": ["\\*\\*command\\*\\*", "\\[.*\\]"]
|
||||
```
|
||||
|
||||
**discouraged_patterns** - Patterns that should not appear (generates warnings):
|
||||
```json
|
||||
"discouraged_patterns": ["TODO", "FIXME", "\\bWIP\\b"]
|
||||
```
|
||||
|
||||
**forbidden_patterns** - Patterns that must not appear (validation fails):
|
||||
```json
|
||||
"forbidden_patterns": ["password\\s*=", "api[_-]?key\\s*="]
|
||||
```
|
||||
|
||||
#### Content Quality Metrics
|
||||
|
||||
Validate content length and readability:
|
||||
|
||||
```json
|
||||
"content_quality": {
|
||||
"min_words": 50,
|
||||
"max_words": 1000,
|
||||
"readability_target": "technical",
|
||||
"min_sentences": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Readability Targets**:
|
||||
- **simple** - Elementary school level
|
||||
- **general** - General audience
|
||||
- **technical** - Technical audience (default for documentation)
|
||||
- **advanced** - Expert/academic level
|
||||
|
||||
#### Content Instructions
|
||||
|
||||
Provide guidance for content authors:
|
||||
|
||||
```json
|
||||
"content_instructions": [
|
||||
"Show command name in bold",
|
||||
"Use brackets [] for optional arguments",
|
||||
"Keep synopsis concise (1-5 lines)"
|
||||
]
|
||||
```
|
||||
|
||||
These instructions appear in validation reports and generated templates.
|
||||
|
||||
## ERROR HANDLING
|
||||
|
||||
### Common Validation Errors
|
||||
@@ -303,11 +544,33 @@ Allow flexibility with minItems/maxItems ranges:
|
||||
|
||||
Avoid exact counts (**const**) unless structure is truly rigid.
|
||||
|
||||
**Required vs Optional Sections**
|
||||
**Section Classification**
|
||||
|
||||
Use **x-markitect-required-sections** for essential sections like SYNOPSIS and DESCRIPTION.
|
||||
Use the five-level classification system to define section requirements:
|
||||
|
||||
Use **x-markitect-recommended-sections** for important but optional sections like EXAMPLES.
|
||||
```json
|
||||
"x-markitect-sections": {
|
||||
"SYNOPSIS": {
|
||||
"classification": "required",
|
||||
"content_instruction": "Brief command syntax",
|
||||
"error_message": "SYNOPSIS is mandatory"
|
||||
},
|
||||
"EXAMPLES": {
|
||||
"classification": "recommended",
|
||||
"warning_if_missing": "Examples improve usability"
|
||||
},
|
||||
"BUGS": {
|
||||
"classification": "optional"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Choose classifications based on importance:
|
||||
- **required** for essential sections (SYNOPSIS, DESCRIPTION)
|
||||
- **recommended** for important sections (EXAMPLES, SEE ALSO)
|
||||
- **optional** for nice-to-have sections (BUGS, AUTHORS)
|
||||
- **discouraged** for sections that should be elsewhere (DEPRECATED)
|
||||
- **improper** for sections that must not appear (TODO, INTERNAL_NOTES)
|
||||
|
||||
**Heading Patterns**
|
||||
|
||||
@@ -463,6 +726,78 @@ markitect schema-generate example.md --max-depth 2 --output v2-schema.json
|
||||
markitect validate test-doc.md v2-schema.json
|
||||
```
|
||||
|
||||
### Schema with Classification System
|
||||
|
||||
Create a schema with section classifications and content control:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Technical Documentation Schema",
|
||||
"x-markitect-sections": {
|
||||
"OVERVIEW": {
|
||||
"classification": "required",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "High-level description of the system",
|
||||
"min_paragraphs": 2,
|
||||
"error_message": "OVERVIEW section is required"
|
||||
},
|
||||
"EXAMPLES": {
|
||||
"classification": "recommended",
|
||||
"heading_level": 2,
|
||||
"min_code_blocks": 2,
|
||||
"warning_if_missing": "Examples help users understand usage"
|
||||
},
|
||||
"REFERENCES": {
|
||||
"classification": "optional",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "External documentation and resources"
|
||||
},
|
||||
"TODO": {
|
||||
"classification": "improper",
|
||||
"error_message": "Remove TODO sections before publishing"
|
||||
}
|
||||
},
|
||||
"x-markitect-content-control": {
|
||||
"overview": {
|
||||
"discouraged_patterns": ["TODO", "FIXME"],
|
||||
"forbidden_patterns": ["password", "secret"],
|
||||
"content_quality": {
|
||||
"min_words": 100,
|
||||
"max_words": 500,
|
||||
"readability_target": "technical"
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"headings": {
|
||||
"properties": {
|
||||
"level_1": {"minItems": 1, "maxItems": 1},
|
||||
"level_2": {"minItems": 2, "maxItems": 20}
|
||||
}
|
||||
},
|
||||
"paragraphs": {"minItems": 10, "maxItems": 200},
|
||||
"code_blocks": {"minItems": 1}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Validate documents against this schema:
|
||||
|
||||
```bash
|
||||
# Missing required section = ERROR
|
||||
markitect validate doc-without-overview.md tech-schema.json
|
||||
# Result: INVALID - missing required section OVERVIEW
|
||||
|
||||
# Missing recommended section = WARNING
|
||||
markitect validate doc-without-examples.md tech-schema.json
|
||||
# Result: VALID (with warnings) - missing recommended section EXAMPLES
|
||||
|
||||
# Improper section present = ERROR
|
||||
markitect validate doc-with-todo.md tech-schema.json
|
||||
# Result: INVALID - improper section TODO must not be present
|
||||
```
|
||||
|
||||
## FILES
|
||||
|
||||
**\*.json**
|
||||
|
||||
@@ -1872,6 +1872,95 @@ def schema_delete(config, schema_name, confirm):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command('schema-analyze')
|
||||
@click.argument('schema_file', type=click.Path(exists=True))
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Show detailed analysis')
|
||||
@pass_config
|
||||
def schema_analyze_cmd(config, schema_file, verbose):
|
||||
"""
|
||||
Analyze a schema for rigidity issues and suggest improvements.
|
||||
|
||||
Examines JSON schemas to detect:
|
||||
- Exact counts that should be ranges
|
||||
- Missing classification system
|
||||
- Deprecated extensions
|
||||
- Overly specific constraints
|
||||
|
||||
Returns exit code 0 for flexible schemas, 1 for rigid schemas, 2 for errors.
|
||||
|
||||
Examples:
|
||||
markitect schema-analyze schema.json
|
||||
markitect schema-analyze schema.json --verbose
|
||||
"""
|
||||
from .schema_analyzer import analyze_schema_cli
|
||||
sys.exit(analyze_schema_cli(schema_file, verbose=verbose))
|
||||
|
||||
|
||||
@cli.command('schema-refine')
|
||||
@click.argument('schema_file', type=click.Path(exists=True))
|
||||
@click.option('--output', '-o', type=click.Path(),
|
||||
help='Output file (default: overwrite input file)')
|
||||
@click.option('--loosen-counts', is_flag=True, default=True,
|
||||
help='Convert exact counts to flexible ranges (default: enabled)')
|
||||
@click.option('--no-loosen-counts', is_flag=True,
|
||||
help='Disable count loosening')
|
||||
@click.option('--round-numbers', is_flag=True, default=True,
|
||||
help='Round overly specific numbers (default: enabled)')
|
||||
@click.option('--no-round-numbers', is_flag=True,
|
||||
help='Disable number rounding')
|
||||
@click.option('--migrate-deprecated', is_flag=True, default=False,
|
||||
help='Migrate deprecated extensions (requires manual review)')
|
||||
@click.option('--dry-run', is_flag=True,
|
||||
help='Show changes without applying them')
|
||||
@click.option('--interactive', '-i', is_flag=True,
|
||||
help='Prompt for each refinement interactively')
|
||||
@pass_config
|
||||
def schema_refine_cmd(config, schema_file, output, loosen_counts, no_loosen_counts,
|
||||
round_numbers, no_round_numbers, migrate_deprecated, dry_run, interactive):
|
||||
"""
|
||||
Refine a schema by automatically applying fixes for rigidity issues.
|
||||
|
||||
This command analyzes the schema and applies automatic fixes:
|
||||
- Converts exact counts to flexible ranges
|
||||
- Rounds overly specific numbers
|
||||
- Widens narrow integer constraints
|
||||
- Documents deprecated extension usage
|
||||
|
||||
By default, the input file is overwritten. Use --output to save to a different file.
|
||||
|
||||
Examples:
|
||||
# Refine schema in place
|
||||
markitect schema-refine schema.json
|
||||
|
||||
# Preview changes without applying
|
||||
markitect schema-refine schema.json --dry-run
|
||||
|
||||
# Review each fix interactively
|
||||
markitect schema-refine schema.json --interactive
|
||||
|
||||
# Save refined schema to new file
|
||||
markitect schema-refine schema.json --output refined-schema.json
|
||||
|
||||
# Disable specific refinements
|
||||
markitect schema-refine schema.json --no-loosen-counts
|
||||
"""
|
||||
from .schema_refiner import refine_schema_cli
|
||||
|
||||
# Handle flag conflicts
|
||||
loosen = loosen_counts and not no_loosen_counts
|
||||
round_nums = round_numbers and not no_round_numbers
|
||||
|
||||
sys.exit(refine_schema_cli(
|
||||
schema_file,
|
||||
output=output,
|
||||
loosen_counts=loosen,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_nums,
|
||||
dry_run=dry_run,
|
||||
interactive=interactive
|
||||
))
|
||||
|
||||
|
||||
@cli.command('generate-stub')
|
||||
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||
@click.option('--output', '-o', type=click.Path(path_type=Path),
|
||||
|
||||
352
markitect/schema_analyzer.py
Normal file
352
markitect/schema_analyzer.py
Normal file
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Schema Analyzer for Phase 2: Schema Refinement Tools
|
||||
|
||||
Analyzes JSON schemas to detect rigidity issues and provide suggestions
|
||||
for improvement using the Phase 1 classification system.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IssueType(Enum):
|
||||
"""Types of schema rigidity issues."""
|
||||
EXACT_COUNT = "exact_count"
|
||||
MISSING_CLASSIFICATIONS = "missing_classifications"
|
||||
MISSING_CONTENT_INSTRUCTIONS = "missing_content_instructions"
|
||||
OVERLY_SPECIFIC = "overly_specific"
|
||||
NO_FLEXIBILITY = "no_flexibility"
|
||||
DEPRECATED_EXTENSIONS = "deprecated_extensions"
|
||||
|
||||
|
||||
class IssueSeverity(Enum):
|
||||
"""Severity levels for schema issues."""
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaIssue:
|
||||
"""Represents a detected schema issue."""
|
||||
issue_type: IssueType
|
||||
severity: IssueSeverity
|
||||
path: str
|
||||
message: str
|
||||
suggestion: str
|
||||
current_value: Any = None
|
||||
suggested_value: Any = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaAnalysisResult:
|
||||
"""Results of schema analysis."""
|
||||
is_rigid: bool
|
||||
rigidity_score: int # 0-100, higher = more rigid
|
||||
issues: List[SchemaIssue] = field(default_factory=list)
|
||||
has_classifications: bool = False
|
||||
has_content_control: bool = False
|
||||
uses_deprecated_extensions: bool = False
|
||||
|
||||
@property
|
||||
def issue_count_by_severity(self) -> Dict[IssueSeverity, int]:
|
||||
"""Count issues by severity."""
|
||||
counts = {severity: 0 for severity in IssueSeverity}
|
||||
for issue in self.issues:
|
||||
counts[issue.severity] += 1
|
||||
return counts
|
||||
|
||||
|
||||
class SchemaAnalyzer:
|
||||
"""Analyzes schemas for rigidity and suggests improvements."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the schema analyzer."""
|
||||
self.deprecated_extensions = [
|
||||
"x-markitect-required-sections",
|
||||
"x-markitect-recommended-sections",
|
||||
"x-markitect-optional-sections"
|
||||
]
|
||||
|
||||
def analyze_schema(self, schema: Dict[str, Any]) -> SchemaAnalysisResult:
|
||||
"""
|
||||
Analyze a schema for rigidity issues.
|
||||
|
||||
Args:
|
||||
schema: The JSON schema to analyze
|
||||
|
||||
Returns:
|
||||
SchemaAnalysisResult with detected issues and suggestions
|
||||
"""
|
||||
result = SchemaAnalysisResult(is_rigid=False, rigidity_score=0)
|
||||
|
||||
# Check for Phase 1 features
|
||||
result.has_classifications = "x-markitect-sections" in schema
|
||||
result.has_content_control = "x-markitect-content-control" in schema
|
||||
|
||||
# Check for deprecated extensions
|
||||
for deprecated in self.deprecated_extensions:
|
||||
if deprecated in schema:
|
||||
result.uses_deprecated_extensions = True
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.DEPRECATED_EXTENSIONS,
|
||||
severity=IssueSeverity.WARNING,
|
||||
path=deprecated,
|
||||
message=f"Using deprecated extension '{deprecated}'",
|
||||
suggestion=f"Migrate to 'x-markitect-sections' with classification system"
|
||||
))
|
||||
|
||||
# Analyze properties for rigidity
|
||||
if "properties" in schema:
|
||||
self._analyze_properties(schema["properties"], result, "properties")
|
||||
|
||||
# Check for missing classifications
|
||||
if not result.has_classifications:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.MISSING_CLASSIFICATIONS,
|
||||
severity=IssueSeverity.INFO,
|
||||
path="root",
|
||||
message="Schema does not use section classification system",
|
||||
suggestion="Add 'x-markitect-sections' to classify sections as required/recommended/optional/discouraged/improper"
|
||||
))
|
||||
|
||||
# Check for missing content control
|
||||
if not result.has_content_control:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.MISSING_CONTENT_INSTRUCTIONS,
|
||||
severity=IssueSeverity.INFO,
|
||||
path="root",
|
||||
message="Schema does not provide content control",
|
||||
suggestion="Add 'x-markitect-content-control' for pattern validation and quality metrics"
|
||||
))
|
||||
|
||||
# Calculate rigidity score
|
||||
result.rigidity_score = self._calculate_rigidity_score(result)
|
||||
result.is_rigid = result.rigidity_score > 50
|
||||
|
||||
return result
|
||||
|
||||
def _analyze_properties(self, properties: Dict[str, Any], result: SchemaAnalysisResult, path: str):
|
||||
"""Analyze schema properties for rigidity issues."""
|
||||
for prop_name, prop_def in properties.items():
|
||||
prop_path = f"{path}.{prop_name}"
|
||||
|
||||
if not isinstance(prop_def, dict):
|
||||
continue
|
||||
|
||||
# Check for exact counts (const)
|
||||
if "const" in prop_def:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.EXACT_COUNT,
|
||||
severity=IssueSeverity.WARNING,
|
||||
path=prop_path,
|
||||
message=f"Property '{prop_name}' requires exact value",
|
||||
suggestion=f"Consider using a range or removing constraint for flexibility",
|
||||
current_value=prop_def["const"]
|
||||
))
|
||||
|
||||
# Check for arrays with exact counts
|
||||
if prop_def.get("type") == "array":
|
||||
min_items = prop_def.get("minItems")
|
||||
max_items = prop_def.get("maxItems")
|
||||
|
||||
if min_items is not None and max_items is not None and min_items == max_items:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.EXACT_COUNT,
|
||||
severity=IssueSeverity.WARNING,
|
||||
path=prop_path,
|
||||
message=f"Array '{prop_name}' requires exactly {min_items} items",
|
||||
suggestion=f"Use a range like minItems: {max(0, min_items - 2)}, maxItems: {min_items + 5}",
|
||||
current_value={"minItems": min_items, "maxItems": max_items},
|
||||
suggested_value={
|
||||
"minItems": max(0, min_items - 2),
|
||||
"maxItems": min_items + 5
|
||||
}
|
||||
))
|
||||
|
||||
# Check for overly specific counts (large numbers)
|
||||
if min_items is not None and min_items > 50:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.OVERLY_SPECIFIC,
|
||||
severity=IssueSeverity.INFO,
|
||||
path=prop_path,
|
||||
message=f"Array '{prop_name}' has very specific minItems: {min_items}",
|
||||
suggestion=f"Consider rounding to {(min_items // 10) * 10} for flexibility",
|
||||
current_value=min_items,
|
||||
suggested_value=(min_items // 10) * 10
|
||||
))
|
||||
|
||||
# Check for overly specific integer constraints
|
||||
if prop_def.get("type") == "integer":
|
||||
if "minimum" in prop_def and "maximum" in prop_def:
|
||||
min_val = prop_def["minimum"]
|
||||
max_val = prop_def["maximum"]
|
||||
range_size = max_val - min_val
|
||||
|
||||
if range_size < 3:
|
||||
result.issues.append(SchemaIssue(
|
||||
issue_type=IssueType.NO_FLEXIBILITY,
|
||||
severity=IssueSeverity.INFO,
|
||||
path=prop_path,
|
||||
message=f"Integer '{prop_name}' has very narrow range: {min_val}-{max_val}",
|
||||
suggestion=f"Consider widening range for flexibility",
|
||||
current_value={"minimum": min_val, "maximum": max_val}
|
||||
))
|
||||
|
||||
# Recursively check nested properties
|
||||
if "properties" in prop_def:
|
||||
self._analyze_properties(prop_def["properties"], result, prop_path)
|
||||
|
||||
# Check items schema for arrays
|
||||
if "items" in prop_def and isinstance(prop_def["items"], dict):
|
||||
if "properties" in prop_def["items"]:
|
||||
self._analyze_properties(
|
||||
prop_def["items"]["properties"],
|
||||
result,
|
||||
f"{prop_path}.items"
|
||||
)
|
||||
|
||||
def _calculate_rigidity_score(self, result: SchemaAnalysisResult) -> int:
|
||||
"""
|
||||
Calculate overall rigidity score (0-100).
|
||||
|
||||
Higher score = more rigid schema.
|
||||
"""
|
||||
score = 0
|
||||
|
||||
# Count issues by type with weighted scores
|
||||
weights = {
|
||||
IssueType.EXACT_COUNT: 15,
|
||||
IssueType.OVERLY_SPECIFIC: 10,
|
||||
IssueType.NO_FLEXIBILITY: 8,
|
||||
IssueType.MISSING_CLASSIFICATIONS: 5,
|
||||
IssueType.MISSING_CONTENT_INSTRUCTIONS: 3,
|
||||
IssueType.DEPRECATED_EXTENSIONS: 5
|
||||
}
|
||||
|
||||
for issue in result.issues:
|
||||
score += weights.get(issue.issue_type, 5)
|
||||
|
||||
# Cap at 100
|
||||
return min(100, score)
|
||||
|
||||
def analyze_schema_file(self, schema_path: Path) -> SchemaAnalysisResult:
|
||||
"""
|
||||
Analyze a schema file.
|
||||
|
||||
Args:
|
||||
schema_path: Path to JSON schema file
|
||||
|
||||
Returns:
|
||||
SchemaAnalysisResult
|
||||
"""
|
||||
with open(schema_path) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
return self.analyze_schema(schema)
|
||||
|
||||
def format_analysis_report(self, result: SchemaAnalysisResult, verbose: bool = False) -> str:
|
||||
"""
|
||||
Format analysis results as a human-readable report.
|
||||
|
||||
Args:
|
||||
result: Analysis results
|
||||
verbose: Include detailed information
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("=" * 70)
|
||||
lines.append("Schema Analysis Report")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# Overall assessment
|
||||
rigidity_level = "HIGH" if result.rigidity_score > 70 else "MEDIUM" if result.rigidity_score > 40 else "LOW"
|
||||
lines.append(f"Rigidity Score: {result.rigidity_score}/100 ({rigidity_level})")
|
||||
lines.append(f"Status: {'RIGID - Needs refinement' if result.is_rigid else 'FLEXIBLE - Good'}")
|
||||
lines.append("")
|
||||
|
||||
# Features check
|
||||
lines.append("Phase 1 Features:")
|
||||
lines.append(f" ✓ Classifications: {'Yes' if result.has_classifications else 'No'}")
|
||||
lines.append(f" ✓ Content Control: {'Yes' if result.has_content_control else 'No'}")
|
||||
if result.uses_deprecated_extensions:
|
||||
lines.append(f" ⚠ Deprecated Extensions: Yes (needs migration)")
|
||||
lines.append("")
|
||||
|
||||
# Issue summary
|
||||
counts = result.issue_count_by_severity
|
||||
lines.append(f"Issues Found: {len(result.issues)} total")
|
||||
lines.append(f" - Errors: {counts[IssueSeverity.ERROR]}")
|
||||
lines.append(f" - Warnings: {counts[IssueSeverity.WARNING]}")
|
||||
lines.append(f" - Info: {counts[IssueSeverity.INFO]}")
|
||||
lines.append("")
|
||||
|
||||
# List issues
|
||||
if result.issues:
|
||||
lines.append("Detected Issues:")
|
||||
lines.append("-" * 70)
|
||||
|
||||
for i, issue in enumerate(result.issues, 1):
|
||||
severity_icon = "❌" if issue.severity == IssueSeverity.ERROR else "⚠️ " if issue.severity == IssueSeverity.WARNING else "ℹ️ "
|
||||
lines.append(f"{i}. {severity_icon} {issue.message}")
|
||||
lines.append(f" Path: {issue.path}")
|
||||
lines.append(f" Suggestion: {issue.suggestion}")
|
||||
|
||||
if verbose and issue.current_value is not None:
|
||||
lines.append(f" Current: {json.dumps(issue.current_value)}")
|
||||
if verbose and issue.suggested_value is not None:
|
||||
lines.append(f" Suggested: {json.dumps(issue.suggested_value)}")
|
||||
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append("✅ No issues found - schema is well-designed!")
|
||||
lines.append("")
|
||||
|
||||
# Recommendations
|
||||
if result.is_rigid:
|
||||
lines.append("Recommendations:")
|
||||
lines.append("-" * 70)
|
||||
lines.append("Run: markitect schema-refine <schema-file> --loosen-counts")
|
||||
lines.append(" to automatically apply suggested improvements")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def analyze_schema_cli(schema_path: str, verbose: bool = False) -> int:
|
||||
"""
|
||||
CLI entry point for schema analysis.
|
||||
|
||||
Args:
|
||||
schema_path: Path to schema file
|
||||
verbose: Show detailed information
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success, 1 = rigid schema found)
|
||||
"""
|
||||
analyzer = SchemaAnalyzer()
|
||||
|
||||
try:
|
||||
result = analyzer.analyze_schema_file(Path(schema_path))
|
||||
report = analyzer.format_analysis_report(result, verbose=verbose)
|
||||
print(report)
|
||||
|
||||
return 1 if result.is_rigid else 0
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Schema file not found: {schema_path}")
|
||||
return 2
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON in schema file: {e}")
|
||||
return 2
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 2
|
||||
530
markitect/schema_refiner.py
Normal file
530
markitect/schema_refiner.py
Normal file
@@ -0,0 +1,530 @@
|
||||
"""
|
||||
Schema Refiner for Phase 2: Schema Refinement Tools
|
||||
|
||||
Automatically refines rigid schemas by applying loosening rules and fixes.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
import json
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .schema_analyzer import SchemaAnalyzer, SchemaIssue, IssueType, IssueSeverity
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefinementAction:
|
||||
"""Represents a refinement action taken on the schema."""
|
||||
issue_type: IssueType
|
||||
path: str
|
||||
description: str
|
||||
old_value: Any = None
|
||||
new_value: Any = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefinementResult:
|
||||
"""Results of schema refinement."""
|
||||
success: bool
|
||||
actions_taken: List[RefinementAction] = field(default_factory=list)
|
||||
refined_schema: Optional[Dict[str, Any]] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class SchemaRefiner:
|
||||
"""Refines rigid schemas by applying loosening rules."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the schema refiner."""
|
||||
self.analyzer = SchemaAnalyzer()
|
||||
|
||||
def _navigate_to_path(self, schema: Dict[str, Any], path: str) -> Optional[Tuple[Dict[str, Any], str]]:
|
||||
"""
|
||||
Navigate to a path in the schema, handling nested 'properties' objects.
|
||||
|
||||
Returns (parent_object, property_name) or None if path doesn't exist.
|
||||
"""
|
||||
path_parts = path.split('.')
|
||||
obj = schema
|
||||
|
||||
# Navigate through all but the last part
|
||||
for i, part in enumerate(path_parts[:-1]):
|
||||
# Try direct access first
|
||||
if part in obj:
|
||||
obj = obj[part]
|
||||
# If not found and obj has 'properties', try there
|
||||
elif isinstance(obj, dict) and "properties" in obj and part in obj["properties"]:
|
||||
obj = obj["properties"][part]
|
||||
else:
|
||||
return None
|
||||
|
||||
# For the final part, check if we need to descend into 'properties'
|
||||
prop_name = path_parts[-1]
|
||||
if prop_name in obj:
|
||||
return (obj, prop_name)
|
||||
elif isinstance(obj, dict) and "properties" in obj and prop_name in obj["properties"]:
|
||||
return (obj["properties"], prop_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
def refine_schema_interactive(
|
||||
self,
|
||||
schema: Dict[str, Any],
|
||||
loosen_counts: bool = True,
|
||||
migrate_deprecated: bool = False,
|
||||
round_numbers: bool = True
|
||||
) -> RefinementResult:
|
||||
"""
|
||||
Refine a schema interactively, prompting for each fix.
|
||||
|
||||
Args:
|
||||
schema: The JSON schema to refine
|
||||
loosen_counts: Enable fixes for exact counts
|
||||
migrate_deprecated: Enable migration of deprecated extensions
|
||||
round_numbers: Enable rounding of overly specific numbers
|
||||
|
||||
Returns:
|
||||
RefinementResult with actions taken and refined schema
|
||||
"""
|
||||
result = RefinementResult(success=False)
|
||||
|
||||
try:
|
||||
# Analyze the schema first
|
||||
analysis = self.analyzer.analyze_schema(schema)
|
||||
|
||||
print(f"\nFound {len(analysis.issues)} issue(s) to review\n")
|
||||
|
||||
# Deep copy to avoid modifying original
|
||||
refined = copy.deepcopy(schema)
|
||||
|
||||
# Process each issue interactively
|
||||
for i, issue in enumerate(analysis.issues, 1):
|
||||
print(f"Issue {i}/{len(analysis.issues)}")
|
||||
print(f" Type: {issue.issue_type.value}")
|
||||
print(f" Path: {issue.path}")
|
||||
print(f" {issue.message}")
|
||||
print(f" Suggestion: {issue.suggestion}")
|
||||
|
||||
if issue.current_value is not None:
|
||||
print(f" Current: {json.dumps(issue.current_value)}")
|
||||
if issue.suggested_value is not None:
|
||||
print(f" Suggested: {json.dumps(issue.suggested_value)}")
|
||||
|
||||
# Ask user if they want to apply the fix
|
||||
response = input("\nApply this fix? [y/N/q]: ").strip().lower()
|
||||
|
||||
if response == 'q':
|
||||
print("Refinement cancelled by user")
|
||||
result.success = False
|
||||
return result
|
||||
elif response == 'y':
|
||||
action = None
|
||||
|
||||
if loosen_counts and issue.issue_type == IssueType.EXACT_COUNT:
|
||||
action = self._fix_exact_count(refined, issue)
|
||||
|
||||
elif round_numbers and issue.issue_type == IssueType.OVERLY_SPECIFIC:
|
||||
action = self._fix_overly_specific(refined, issue)
|
||||
|
||||
elif loosen_counts and issue.issue_type == IssueType.NO_FLEXIBILITY:
|
||||
action = self._fix_no_flexibility(refined, issue)
|
||||
|
||||
elif migrate_deprecated and issue.issue_type == IssueType.DEPRECATED_EXTENSIONS:
|
||||
action = self._fix_deprecated_extension(refined, issue)
|
||||
|
||||
if action:
|
||||
result.actions_taken.append(action)
|
||||
print(f" ✓ Applied")
|
||||
else:
|
||||
print(f" ✗ Could not apply fix")
|
||||
else:
|
||||
print(f" - Skipped")
|
||||
|
||||
print()
|
||||
|
||||
result.refined_schema = refined
|
||||
result.success = True
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def refine_schema(
|
||||
self,
|
||||
schema: Dict[str, Any],
|
||||
loosen_counts: bool = True,
|
||||
migrate_deprecated: bool = False,
|
||||
round_numbers: bool = True
|
||||
) -> RefinementResult:
|
||||
"""
|
||||
Refine a schema by applying fixes for detected issues.
|
||||
|
||||
Args:
|
||||
schema: The JSON schema to refine
|
||||
loosen_counts: Apply fixes for exact counts
|
||||
migrate_deprecated: Migrate deprecated extensions
|
||||
round_numbers: Round overly specific numbers
|
||||
|
||||
Returns:
|
||||
RefinementResult with actions taken and refined schema
|
||||
"""
|
||||
result = RefinementResult(success=False)
|
||||
|
||||
try:
|
||||
# Analyze the schema first
|
||||
analysis = self.analyzer.analyze_schema(schema)
|
||||
|
||||
# Deep copy to avoid modifying original
|
||||
refined = copy.deepcopy(schema)
|
||||
|
||||
# Apply fixes based on issues found
|
||||
for issue in analysis.issues:
|
||||
action = None
|
||||
|
||||
if loosen_counts and issue.issue_type == IssueType.EXACT_COUNT:
|
||||
action = self._fix_exact_count(refined, issue)
|
||||
|
||||
elif round_numbers and issue.issue_type == IssueType.OVERLY_SPECIFIC:
|
||||
action = self._fix_overly_specific(refined, issue)
|
||||
|
||||
elif loosen_counts and issue.issue_type == IssueType.NO_FLEXIBILITY:
|
||||
action = self._fix_no_flexibility(refined, issue)
|
||||
|
||||
elif migrate_deprecated and issue.issue_type == IssueType.DEPRECATED_EXTENSIONS:
|
||||
action = self._fix_deprecated_extension(refined, issue)
|
||||
|
||||
if action:
|
||||
result.actions_taken.append(action)
|
||||
|
||||
result.refined_schema = refined
|
||||
result.success = True
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def _fix_exact_count(self, schema: Dict[str, Any], issue: SchemaIssue) -> Optional[RefinementAction]:
|
||||
"""Fix exact count constraints by converting to ranges."""
|
||||
nav_result = self._navigate_to_path(schema, issue.path)
|
||||
if not nav_result:
|
||||
return None
|
||||
|
||||
obj, prop_name = nav_result
|
||||
prop_def = obj[prop_name]
|
||||
old_value = copy.deepcopy(prop_def)
|
||||
|
||||
# Check if it's an array with exact minItems/maxItems
|
||||
if isinstance(prop_def, dict) and prop_def.get("type") == "array":
|
||||
min_items = prop_def.get("minItems")
|
||||
max_items = prop_def.get("maxItems")
|
||||
|
||||
if min_items is not None and max_items is not None and min_items == max_items:
|
||||
# Apply suggested loosening
|
||||
new_min = max(0, min_items - 2)
|
||||
new_max = min_items + 5
|
||||
|
||||
prop_def["minItems"] = new_min
|
||||
prop_def["maxItems"] = new_max
|
||||
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.EXACT_COUNT,
|
||||
path=issue.path,
|
||||
description=f"Loosened array count from exactly {min_items} to range {new_min}-{new_max}",
|
||||
old_value={"minItems": min_items, "maxItems": max_items},
|
||||
new_value={"minItems": new_min, "maxItems": new_max}
|
||||
)
|
||||
|
||||
# Check if it's a const value
|
||||
if isinstance(prop_def, dict) and "const" in prop_def:
|
||||
const_value = prop_def["const"]
|
||||
del prop_def["const"]
|
||||
|
||||
# If it's a number, convert to a range
|
||||
if isinstance(const_value, int):
|
||||
prop_def["minimum"] = const_value - 1
|
||||
prop_def["maximum"] = const_value + 1
|
||||
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.EXACT_COUNT,
|
||||
path=issue.path,
|
||||
description=f"Converted const {const_value} to range {const_value-1}-{const_value+1}",
|
||||
old_value=const_value,
|
||||
new_value={"minimum": const_value - 1, "maximum": const_value + 1}
|
||||
)
|
||||
else:
|
||||
# For non-numeric constants, just remove the constraint
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.EXACT_COUNT,
|
||||
path=issue.path,
|
||||
description=f"Removed const constraint: {const_value}",
|
||||
old_value=const_value,
|
||||
new_value=None
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _fix_overly_specific(self, schema: Dict[str, Any], issue: SchemaIssue) -> Optional[RefinementAction]:
|
||||
"""Fix overly specific number constraints by rounding."""
|
||||
if issue.suggested_value is None:
|
||||
return None
|
||||
|
||||
nav_result = self._navigate_to_path(schema, issue.path)
|
||||
if not nav_result:
|
||||
return None
|
||||
|
||||
obj, prop_name = nav_result
|
||||
prop_def = obj[prop_name]
|
||||
|
||||
# Round the minItems value
|
||||
if isinstance(prop_def, dict) and "minItems" in prop_def:
|
||||
old_value = prop_def["minItems"]
|
||||
new_value = issue.suggested_value
|
||||
prop_def["minItems"] = new_value
|
||||
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.OVERLY_SPECIFIC,
|
||||
path=issue.path,
|
||||
description=f"Rounded minItems from {old_value} to {new_value}",
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _fix_no_flexibility(self, schema: Dict[str, Any], issue: SchemaIssue) -> Optional[RefinementAction]:
|
||||
"""Fix narrow ranges by widening them."""
|
||||
nav_result = self._navigate_to_path(schema, issue.path)
|
||||
if not nav_result:
|
||||
return None
|
||||
|
||||
obj, prop_name = nav_result
|
||||
prop_def = obj[prop_name]
|
||||
|
||||
if isinstance(prop_def, dict) and "minimum" in prop_def and "maximum" in prop_def:
|
||||
old_min = prop_def["minimum"]
|
||||
old_max = prop_def["maximum"]
|
||||
range_size = old_max - old_min
|
||||
|
||||
# Widen the range
|
||||
new_min = old_min - 5
|
||||
new_max = old_max + 5
|
||||
|
||||
prop_def["minimum"] = new_min
|
||||
prop_def["maximum"] = new_max
|
||||
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.NO_FLEXIBILITY,
|
||||
path=issue.path,
|
||||
description=f"Widened range from {old_min}-{old_max} to {new_min}-{new_max}",
|
||||
old_value={"minimum": old_min, "maximum": old_max},
|
||||
new_value={"minimum": new_min, "maximum": new_max}
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _fix_deprecated_extension(self, schema: Dict[str, Any], issue: SchemaIssue) -> Optional[RefinementAction]:
|
||||
"""Remove deprecated extension (migration requires manual work)."""
|
||||
# For now, just document that manual migration is needed
|
||||
# Full migration would require understanding the old format
|
||||
|
||||
deprecated_key = issue.path
|
||||
if deprecated_key in schema:
|
||||
old_value = schema[deprecated_key]
|
||||
# Don't actually remove it automatically - too risky
|
||||
return RefinementAction(
|
||||
issue_type=IssueType.DEPRECATED_EXTENSIONS,
|
||||
path=issue.path,
|
||||
description=f"Detected deprecated extension (manual migration recommended)",
|
||||
old_value=old_value,
|
||||
new_value=None
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def refine_schema_file(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_path: Optional[Path] = None,
|
||||
loosen_counts: bool = True,
|
||||
migrate_deprecated: bool = False,
|
||||
round_numbers: bool = True
|
||||
) -> RefinementResult:
|
||||
"""
|
||||
Refine a schema file.
|
||||
|
||||
Args:
|
||||
input_path: Path to input schema file
|
||||
output_path: Path to output file (if None, overwrites input)
|
||||
loosen_counts: Apply fixes for exact counts
|
||||
migrate_deprecated: Migrate deprecated extensions
|
||||
round_numbers: Round overly specific numbers
|
||||
|
||||
Returns:
|
||||
RefinementResult
|
||||
"""
|
||||
with open(input_path) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
result = self.refine_schema(
|
||||
schema,
|
||||
loosen_counts=loosen_counts,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_numbers
|
||||
)
|
||||
|
||||
if result.success and result.refined_schema:
|
||||
output = output_path or input_path
|
||||
with open(output, 'w') as f:
|
||||
json.dump(result.refined_schema, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
def format_refinement_report(self, result: RefinementResult) -> str:
|
||||
"""
|
||||
Format refinement results as a human-readable report.
|
||||
|
||||
Args:
|
||||
result: Refinement results
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("=" * 70)
|
||||
lines.append("Schema Refinement Report")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
if not result.success:
|
||||
lines.append(f"❌ Refinement failed: {result.error_message}")
|
||||
return "\n".join(lines)
|
||||
|
||||
# Summary
|
||||
action_count = len(result.actions_taken)
|
||||
if action_count == 0:
|
||||
lines.append("✅ No refinements needed - schema is already flexible")
|
||||
else:
|
||||
lines.append(f"✅ Applied {action_count} refinement(s)")
|
||||
lines.append("")
|
||||
|
||||
# List actions
|
||||
if result.actions_taken:
|
||||
lines.append("Actions Taken:")
|
||||
lines.append("-" * 70)
|
||||
|
||||
for i, action in enumerate(result.actions_taken, 1):
|
||||
lines.append(f"{i}. {action.description}")
|
||||
lines.append(f" Path: {action.path}")
|
||||
|
||||
if action.old_value is not None:
|
||||
lines.append(f" Before: {json.dumps(action.old_value)}")
|
||||
if action.new_value is not None:
|
||||
lines.append(f" After: {json.dumps(action.new_value)}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def refine_schema_cli(
|
||||
schema_path: str,
|
||||
output: Optional[str] = None,
|
||||
loosen_counts: bool = True,
|
||||
migrate_deprecated: bool = False,
|
||||
round_numbers: bool = True,
|
||||
dry_run: bool = False,
|
||||
interactive: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
CLI entry point for schema refinement.
|
||||
|
||||
Args:
|
||||
schema_path: Path to schema file
|
||||
output: Output path (None = overwrite input)
|
||||
loosen_counts: Apply count loosening fixes
|
||||
migrate_deprecated: Migrate deprecated extensions
|
||||
round_numbers: Round overly specific numbers
|
||||
dry_run: Show changes without applying
|
||||
interactive: Prompt for each fix
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success, 1 = no changes needed, 2 = error)
|
||||
"""
|
||||
refiner = SchemaRefiner()
|
||||
|
||||
try:
|
||||
input_path = Path(schema_path)
|
||||
output_path = Path(output) if output else None
|
||||
|
||||
# Load schema
|
||||
with open(input_path) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
if interactive:
|
||||
# Interactive mode - prompt for each fix
|
||||
print(f"Refining schema: {schema_path}")
|
||||
result = refiner.refine_schema_interactive(
|
||||
schema,
|
||||
loosen_counts=loosen_counts,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_numbers
|
||||
)
|
||||
|
||||
if result.success and result.refined_schema and not dry_run:
|
||||
# Write the refined schema
|
||||
output = output_path or input_path
|
||||
with open(output, 'w') as f:
|
||||
json.dump(result.refined_schema, f, indent=2)
|
||||
print(f"\nRefined schema written to: {output}")
|
||||
|
||||
elif dry_run:
|
||||
# Just analyze and show what would be done
|
||||
result = refiner.refine_schema(
|
||||
schema,
|
||||
loosen_counts=loosen_counts,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_numbers
|
||||
)
|
||||
|
||||
print("DRY RUN - No changes will be made")
|
||||
print()
|
||||
else:
|
||||
result = refiner.refine_schema_file(
|
||||
input_path,
|
||||
output_path,
|
||||
loosen_counts=loosen_counts,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_numbers
|
||||
)
|
||||
|
||||
# Only print full report if not in interactive mode (user already saw changes)
|
||||
if not interactive:
|
||||
report = refiner.format_refinement_report(result)
|
||||
print(report)
|
||||
elif result.success:
|
||||
# Just print summary for interactive mode
|
||||
print(f"\n{'='*70}")
|
||||
print(f"Refinement complete: {len(result.actions_taken)} change(s) applied")
|
||||
print(f"{'='*70}")
|
||||
|
||||
if result.success and len(result.actions_taken) > 0:
|
||||
return 0 # Success with changes
|
||||
elif result.success:
|
||||
return 1 # Success but no changes needed
|
||||
else:
|
||||
return 2 # Error
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Schema file not found: {schema_path}")
|
||||
return 2
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON in schema file: {e}")
|
||||
return 2
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 2
|
||||
381
tests/test_schema_analyzer.py
Normal file
381
tests/test_schema_analyzer.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
Unit tests for schema_analyzer module (Phase 2 schema refinement).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from markitect.schema_analyzer import (
|
||||
SchemaAnalyzer,
|
||||
IssueType,
|
||||
IssueSeverity,
|
||||
SchemaAnalysisResult
|
||||
)
|
||||
|
||||
|
||||
class TestSchemaAnalyzer:
|
||||
"""Tests for SchemaAnalyzer class."""
|
||||
|
||||
def test_analyze_flexible_schema(self):
|
||||
"""Test analysis of a well-designed flexible schema."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-sections": {
|
||||
"INTRO": {
|
||||
"classification": "required",
|
||||
"heading_level": 2
|
||||
}
|
||||
},
|
||||
"x-markitect-content-control": {
|
||||
"intro": {
|
||||
"content_quality": {
|
||||
"min_words": 50,
|
||||
"max_words": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
assert isinstance(result, SchemaAnalysisResult)
|
||||
assert result.has_classifications
|
||||
assert result.has_content_control
|
||||
assert result.rigidity_score < 50
|
||||
assert not result.is_rigid
|
||||
|
||||
def test_analyze_rigid_schema_exact_counts(self):
|
||||
"""Test detection of exact count constraints."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paragraphs": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5 # Exact count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
assert result.rigidity_score > 0
|
||||
exact_count_issues = [i for i in result.issues if i.issue_type == IssueType.EXACT_COUNT]
|
||||
assert len(exact_count_issues) > 0
|
||||
assert exact_count_issues[0].severity == IssueSeverity.WARNING
|
||||
|
||||
def test_analyze_const_values(self):
|
||||
"""Test detection of const constraints."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
const_issues = [i for i in result.issues if i.issue_type == IssueType.EXACT_COUNT]
|
||||
assert len(const_issues) > 0
|
||||
assert const_issues[0].current_value == 1
|
||||
|
||||
def test_analyze_overly_specific_numbers(self):
|
||||
"""Test detection of overly specific numbers."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 73 # Overly specific
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
specific_issues = [i for i in result.issues if i.issue_type == IssueType.OVERLY_SPECIFIC]
|
||||
assert len(specific_issues) > 0
|
||||
assert specific_issues[0].current_value == 73
|
||||
assert specific_issues[0].suggested_value == 70 # Should be rounded
|
||||
|
||||
def test_analyze_narrow_range(self):
|
||||
"""Test detection of narrow integer ranges."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer",
|
||||
"minimum": 5,
|
||||
"maximum": 6 # Very narrow range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
narrow_issues = [i for i in result.issues if i.issue_type == IssueType.NO_FLEXIBILITY]
|
||||
assert len(narrow_issues) > 0
|
||||
|
||||
def test_analyze_deprecated_extensions(self):
|
||||
"""Test detection of deprecated extensions."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-required-sections": ["INTRO", "CONCLUSION"]
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
assert result.uses_deprecated_extensions
|
||||
deprecated_issues = [i for i in result.issues if i.issue_type == IssueType.DEPRECATED_EXTENSIONS]
|
||||
assert len(deprecated_issues) > 0
|
||||
assert deprecated_issues[0].severity == IssueSeverity.WARNING
|
||||
|
||||
def test_analyze_missing_classifications(self):
|
||||
"""Test detection of missing classification system."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
assert not result.has_classifications
|
||||
classification_issues = [i for i in result.issues if i.issue_type == IssueType.MISSING_CLASSIFICATIONS]
|
||||
assert len(classification_issues) > 0
|
||||
assert classification_issues[0].severity == IssueSeverity.INFO
|
||||
|
||||
def test_analyze_missing_content_control(self):
|
||||
"""Test detection of missing content control."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-sections": {
|
||||
"INTRO": {"classification": "required"}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
assert result.has_classifications
|
||||
assert not result.has_content_control
|
||||
content_issues = [i for i in result.issues if i.issue_type == IssueType.MISSING_CONTENT_INSTRUCTIONS]
|
||||
assert len(content_issues) > 0
|
||||
|
||||
def test_rigidity_score_calculation(self):
|
||||
"""Test rigidity score calculation with multiple issues."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"array1": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
},
|
||||
"array2": {
|
||||
"type": "array",
|
||||
"minItems": 73
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"const": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
# Should have moderate rigidity with multiple issues
|
||||
assert result.rigidity_score > 30
|
||||
assert result.rigidity_score < 60 # Moderate range
|
||||
|
||||
def test_issue_count_by_severity(self):
|
||||
"""Test counting issues by severity."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
counts = result.issue_count_by_severity
|
||||
assert IssueSeverity.WARNING in counts
|
||||
assert IssueSeverity.ERROR in counts
|
||||
assert IssueSeverity.INFO in counts
|
||||
|
||||
def test_nested_properties_analysis(self):
|
||||
"""Test analysis of nested property structures."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inner": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
# Should detect exact count in nested property
|
||||
exact_count_issues = [i for i in result.issues if i.issue_type == IssueType.EXACT_COUNT]
|
||||
assert len(exact_count_issues) > 0
|
||||
assert "properties.outer.inner" in exact_count_issues[0].path
|
||||
|
||||
def test_format_analysis_report(self):
|
||||
"""Test report formatting."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
report = analyzer.format_analysis_report(result, verbose=False)
|
||||
|
||||
assert "Schema Analysis Report" in report
|
||||
assert "Rigidity Score" in report
|
||||
assert "Issues Found" in report
|
||||
|
||||
def test_format_analysis_report_verbose(self):
|
||||
"""Test verbose report formatting."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
report = analyzer.format_analysis_report(result, verbose=True)
|
||||
|
||||
assert "Current:" in report
|
||||
assert "Suggested:" in report
|
||||
|
||||
def test_analyze_array_items_with_properties(self):
|
||||
"""Test analysis of array items that have nested properties."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
# Should detect const in nested items
|
||||
const_issues = [i for i in result.issues if i.issue_type == IssueType.EXACT_COUNT]
|
||||
assert len(const_issues) > 0
|
||||
assert "items" in const_issues[0].path
|
||||
|
||||
def test_empty_schema(self):
|
||||
"""Test analysis of minimal/empty schema."""
|
||||
schema = {
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
|
||||
# Should detect missing features but not crash
|
||||
assert not result.has_classifications
|
||||
assert not result.has_content_control
|
||||
assert result.rigidity_score < 50 # Not rigid, just minimal
|
||||
|
||||
def test_no_issues_schema(self):
|
||||
"""Test schema with perfect design (no issues)."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-sections": {
|
||||
"INTRO": {
|
||||
"classification": "required",
|
||||
"heading_level": 2,
|
||||
"content_instruction": "Introduction section"
|
||||
}
|
||||
},
|
||||
"x-markitect-content-control": {
|
||||
"intro": {
|
||||
"content_quality": {
|
||||
"min_words": 50,
|
||||
"max_words": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"paragraphs": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 50 # Good range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzer = SchemaAnalyzer()
|
||||
result = analyzer.analyze_schema(schema)
|
||||
report = analyzer.format_analysis_report(result)
|
||||
|
||||
assert result.rigidity_score < 20
|
||||
assert not result.is_rigid
|
||||
assert "No issues found" in report or result.issue_count_by_severity[IssueSeverity.WARNING] == 0
|
||||
462
tests/test_schema_refiner.py
Normal file
462
tests/test_schema_refiner.py
Normal file
@@ -0,0 +1,462 @@
|
||||
"""
|
||||
Unit tests for schema_refiner module (Phase 2 schema refinement).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import copy
|
||||
from markitect.schema_refiner import (
|
||||
SchemaRefiner,
|
||||
RefinementResult,
|
||||
RefinementAction
|
||||
)
|
||||
from markitect.schema_analyzer import IssueType
|
||||
|
||||
|
||||
class TestSchemaRefiner:
|
||||
"""Tests for SchemaRefiner class."""
|
||||
|
||||
def test_refine_exact_count_array(self):
|
||||
"""Test refinement of exact array counts."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) > 0
|
||||
|
||||
# Check that the array range was loosened
|
||||
refined_items = result.refined_schema["properties"]["items"]
|
||||
assert refined_items["minItems"] < 5
|
||||
assert refined_items["maxItems"] > 5
|
||||
|
||||
def test_refine_const_value(self):
|
||||
"""Test refinement of const constraints."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) > 0
|
||||
|
||||
# const should be removed and replaced with a range
|
||||
refined_level = result.refined_schema["properties"]["level"]
|
||||
assert "const" not in refined_level
|
||||
assert "minimum" in refined_level
|
||||
assert "maximum" in refined_level
|
||||
|
||||
def test_refine_overly_specific_number(self):
|
||||
"""Test rounding of overly specific numbers."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 73
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, round_numbers=True)
|
||||
|
||||
assert result.success
|
||||
|
||||
# Should round to 70
|
||||
if len(result.actions_taken) > 0:
|
||||
refined_items = result.refined_schema["properties"]["items"]
|
||||
assert refined_items["minItems"] == 70
|
||||
|
||||
def test_refine_narrow_range(self):
|
||||
"""Test widening of narrow integer ranges."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "integer",
|
||||
"minimum": 5,
|
||||
"maximum": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
|
||||
# Range should be widened
|
||||
if len(result.actions_taken) > 0:
|
||||
refined_score = result.refined_schema["properties"]["score"]
|
||||
range_size = refined_score["maximum"] - refined_score["minimum"]
|
||||
assert range_size > 1
|
||||
|
||||
def test_refine_nested_properties(self):
|
||||
"""Test refinement of nested property structures."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inner": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) > 0
|
||||
|
||||
# Check nested property was refined
|
||||
refined_inner = result.refined_schema["properties"]["outer"]["properties"]["inner"]
|
||||
assert refined_inner["minItems"] < 3
|
||||
assert refined_inner["maxItems"] > 3
|
||||
|
||||
def test_refine_array_items_with_const(self):
|
||||
"""Test refinement of array items with const properties."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) > 0
|
||||
|
||||
# const in items should be refined
|
||||
refined_level = result.refined_schema["properties"]["headings"]["items"]["properties"]["level"]
|
||||
assert "const" not in refined_level
|
||||
|
||||
def test_refine_no_changes_needed(self):
|
||||
"""Test refinement of already flexible schema."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-sections": {
|
||||
"INTRO": {"classification": "required"}
|
||||
},
|
||||
"x-markitect-content-control": {
|
||||
"intro": {"content_quality": {"min_words": 50}}
|
||||
},
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 50 # Good range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
# May have some minor improvements but should be mostly unchanged
|
||||
assert len(result.actions_taken) < 3
|
||||
|
||||
def test_refine_with_disabled_options(self):
|
||||
"""Test refinement with options disabled."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"const": 73
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(
|
||||
schema,
|
||||
loosen_counts=False, # Disabled
|
||||
round_numbers=False
|
||||
)
|
||||
|
||||
assert result.success
|
||||
# No changes should be made since options are disabled
|
||||
assert len(result.actions_taken) == 0
|
||||
|
||||
def test_refinement_action_details(self):
|
||||
"""Test that refinement actions contain proper details."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert len(result.actions_taken) > 0
|
||||
action = result.actions_taken[0]
|
||||
|
||||
assert isinstance(action, RefinementAction)
|
||||
assert action.issue_type == IssueType.EXACT_COUNT
|
||||
assert "properties.items" in action.path
|
||||
assert action.old_value is not None
|
||||
assert action.new_value is not None
|
||||
assert "loosened" in action.description.lower() or "converted" in action.description.lower()
|
||||
|
||||
def test_original_schema_unchanged(self):
|
||||
"""Test that original schema is not modified."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
original_schema = copy.deepcopy(schema)
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
# Original should be unchanged
|
||||
assert schema == original_schema
|
||||
|
||||
# But refined should be different
|
||||
assert result.refined_schema != original_schema
|
||||
|
||||
def test_format_refinement_report(self):
|
||||
"""Test refinement report formatting."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
report = refiner.format_refinement_report(result)
|
||||
|
||||
assert "Schema Refinement Report" in report
|
||||
assert "Actions Taken" in report or "No refinements needed" in report
|
||||
|
||||
def test_refinement_with_multiple_issues(self):
|
||||
"""Test refinement of schema with multiple issues."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"array1": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
},
|
||||
"array2": {
|
||||
"type": "array",
|
||||
"minItems": 73
|
||||
},
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(
|
||||
schema,
|
||||
loosen_counts=True,
|
||||
round_numbers=True
|
||||
)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) >= 2 # Should fix multiple issues
|
||||
|
||||
def test_navigation_to_deeply_nested_path(self):
|
||||
"""Test path navigation for deeply nested schemas."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level2": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level3": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
# Should successfully navigate and refine deep path
|
||||
refined_level3 = result.refined_schema["properties"]["level1"]["properties"]["level2"]["properties"]["level3"]
|
||||
assert refined_level3["minItems"] < 1 or refined_level3["maxItems"] > 1
|
||||
|
||||
def test_deprecated_extension_detection(self):
|
||||
"""Test detection (but not automatic migration) of deprecated extensions."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"x-markitect-required-sections": ["INTRO"]
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, migrate_deprecated=True)
|
||||
|
||||
assert result.success
|
||||
# Should document deprecated extension but not remove it automatically
|
||||
deprecated_actions = [a for a in result.actions_taken
|
||||
if a.issue_type == IssueType.DEPRECATED_EXTENSIONS]
|
||||
# Migration is detected but not fully automated (too risky)
|
||||
assert len(deprecated_actions) >= 0
|
||||
|
||||
def test_refine_empty_schema(self):
|
||||
"""Test refinement of minimal schema."""
|
||||
schema = {
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema)
|
||||
|
||||
assert result.success
|
||||
# Minimal schema shouldn't crash the refiner
|
||||
assert result.refined_schema is not None
|
||||
|
||||
def test_refine_schema_with_string_const(self):
|
||||
"""Test refinement of non-numeric const values."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"const": "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
# String const should be removed (can't be converted to range)
|
||||
if len(result.actions_taken) > 0:
|
||||
refined_status = result.refined_schema["properties"]["status"]
|
||||
assert "const" not in refined_status
|
||||
|
||||
def test_complex_manpage_schema(self):
|
||||
"""Test refinement of a realistic manpage schema."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level_1": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"level_2": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 30,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"const": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refiner = SchemaRefiner()
|
||||
result = refiner.refine_schema(schema, loosen_counts=True)
|
||||
|
||||
assert result.success
|
||||
assert len(result.actions_taken) >= 2 # Should fix at least the exact counts
|
||||
|
||||
# level_1 should be loosened
|
||||
refined_level_1 = result.refined_schema["properties"]["headings"]["properties"]["level_1"]
|
||||
assert refined_level_1["minItems"] < 1 or refined_level_1["maxItems"] > 1
|
||||
|
||||
# const values in items should be loosened
|
||||
items_level_1 = refined_level_1["items"]["properties"]["level"]
|
||||
assert "const" not in items_level_1
|
||||
Reference in New Issue
Block a user