Compare commits
31 Commits
testdrive-
...
f3aaec99bb
| Author | SHA1 | Date | |
|---|---|---|---|
| f3aaec99bb | |||
| b81ce5631d | |||
| 14108533fb | |||
| b6f95066a3 | |||
| 6df9b5df05 | |||
| 82c1a3ab65 | |||
| da34303057 | |||
| d2cd2d22fd | |||
| 48e0b60be5 | |||
| 2b35fcde62 | |||
| c46d9f7a0b | |||
| 2b687a4ca8 | |||
| d68e762612 | |||
| b51999582e | |||
| b4157da3dd | |||
| 916c09a22b | |||
| 4d899d0690 | |||
| dcb51b7e3a | |||
| d0432dbe0d | |||
| 45e4c7a6e9 | |||
| 01e5c811ab | |||
| 9fe2960842 | |||
| 7be37df3e4 | |||
| 21189f7664 | |||
| ddd8189576 | |||
| 2e6f292e48 | |||
| a1476a98b5 | |||
| 304959b3ee | |||
| 83086b3773 | |||
| 82eef76366 | |||
| 2838135450 |
323
.claude/capabilities/issue-facade.md
Normal file
323
.claude/capabilities/issue-facade.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
# Issue Facade - Agent Integration Context
|
||||||
|
|
||||||
|
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
||||||
|
|
||||||
|
## Critical: DO NOT Bypass This Capability
|
||||||
|
|
||||||
|
⚠️ **IMPORTANT:** If you need to work with issues, **you MUST use this capability**. Do NOT:
|
||||||
|
- ❌ Make direct API calls to Gitea/GitHub/GitLab
|
||||||
|
- ❌ Use platform CLIs (gh, glab, etc.)
|
||||||
|
- ❌ Import platform libraries (PyGithub, python-gitlab, etc.)
|
||||||
|
- ❌ Parse HTML/scrape issue tracker web UIs
|
||||||
|
|
||||||
|
**Why?** Bypassing this capability causes:
|
||||||
|
- Credential management chaos (tokens scattered everywhere)
|
||||||
|
- Inconsistent issue state across agents
|
||||||
|
- Massive token waste (redundant API calls)
|
||||||
|
- Platform lock-in (can't switch Gitea → GitHub easily)
|
||||||
|
- Race conditions in multi-agent scenarios
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Check if Capability is Available
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify installation
|
||||||
|
issue --version
|
||||||
|
# or
|
||||||
|
python -c "from issue_tracker.backends.gitea import GiteaBackend; print('OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Usage (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from issue_tracker.backends.gitea import GiteaBackend
|
||||||
|
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
|
||||||
|
from issue_tracker.core.interfaces import IssueFilter
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Connect (assumes backend is configured)
|
||||||
|
backend = GiteaBackend()
|
||||||
|
backend.connect({
|
||||||
|
'base_url': os.environ['GITEA_URL'],
|
||||||
|
'token': os.environ['GITEA_API_TOKEN'],
|
||||||
|
'owner': os.environ['GITEA_OWNER'],
|
||||||
|
'repo': os.environ['GITEA_REPO']
|
||||||
|
})
|
||||||
|
|
||||||
|
# List issues for me
|
||||||
|
my_issues = backend.list_issues(IssueFilter(
|
||||||
|
state='open',
|
||||||
|
assignee='my-agent-id',
|
||||||
|
labels=['needs-implementation']
|
||||||
|
))
|
||||||
|
|
||||||
|
# Create issue
|
||||||
|
new_issue = Issue(
|
||||||
|
id=None, number=0,
|
||||||
|
title="Implement feature X",
|
||||||
|
description="Details...",
|
||||||
|
state=IssueState.OPEN,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
updated_at=datetime.now(timezone.utc),
|
||||||
|
labels=[Label(name="feature"), Label(name="priority:high")]
|
||||||
|
)
|
||||||
|
created = backend.create_issue(new_issue)
|
||||||
|
|
||||||
|
# Update issue
|
||||||
|
created.state = IssueState.IN_PROGRESS
|
||||||
|
created.assignees = [User(id="agent-id", username="agent-id")]
|
||||||
|
backend.update_issue(created)
|
||||||
|
|
||||||
|
# Add comment
|
||||||
|
comment = Comment(
|
||||||
|
id=None,
|
||||||
|
body="Implementation started. Working on database schema.",
|
||||||
|
author=User(id="agent-id", username="agent-id"),
|
||||||
|
created_at=datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
backend.add_comment(created.id, comment)
|
||||||
|
|
||||||
|
# Close when done
|
||||||
|
created.state = IssueState.CLOSED
|
||||||
|
created.closed_at = datetime.now(timezone.utc)
|
||||||
|
backend.update_issue(created)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Usage (CLI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List my open issues
|
||||||
|
issue list --state=open --assignee=agent-id --format=json
|
||||||
|
|
||||||
|
# Create issue
|
||||||
|
issue create "Implement feature X" \
|
||||||
|
--label=feature \
|
||||||
|
--label=priority:high \
|
||||||
|
--description="Details here"
|
||||||
|
|
||||||
|
# Update state
|
||||||
|
issue edit 42 --state=in_progress --assignee=agent-id
|
||||||
|
|
||||||
|
# Add comment
|
||||||
|
issue comment 42 "Implementation started"
|
||||||
|
|
||||||
|
# Close
|
||||||
|
issue close 42 --comment="Completed successfully"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Find Work
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get next available task
|
||||||
|
available_tasks = backend.list_issues(IssueFilter(
|
||||||
|
state='open',
|
||||||
|
labels=['ready', 'needs-implementation']
|
||||||
|
))
|
||||||
|
|
||||||
|
# Filter to unassigned
|
||||||
|
unassigned = [t for t in available_tasks if not t.assignees]
|
||||||
|
|
||||||
|
if unassigned:
|
||||||
|
task = unassigned[0]
|
||||||
|
# Claim it...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Claim Issue (Prevent Race Conditions)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def claim_issue(issue: Issue, agent_id: str) -> bool:
|
||||||
|
"""Claim an issue safely."""
|
||||||
|
# Check if already claimed
|
||||||
|
if issue.assignees:
|
||||||
|
return False # Already taken
|
||||||
|
|
||||||
|
# Claim it
|
||||||
|
issue.state = IssueState.IN_PROGRESS
|
||||||
|
issue.assignees = [User(id=agent_id, username=agent_id)]
|
||||||
|
backend.update_issue(issue)
|
||||||
|
|
||||||
|
# Announce claim
|
||||||
|
backend.add_comment(issue.id, Comment(
|
||||||
|
id=None,
|
||||||
|
body=f"🤖 Claimed by {agent_id}",
|
||||||
|
author=User(id=agent_id, username=agent_id),
|
||||||
|
created_at=datetime.now(timezone.utc)
|
||||||
|
))
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Progress Updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
def report_progress(issue: Issue, message: str, agent_id: str):
|
||||||
|
"""Report progress on an issue."""
|
||||||
|
backend.add_comment(issue.id, Comment(
|
||||||
|
id=None,
|
||||||
|
body=f"**Progress Update:**\n\n{message}",
|
||||||
|
author=User(id=agent_id, username=agent_id),
|
||||||
|
created_at=datetime.now(timezone.utc)
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Agent-to-Agent Communication
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
def post_agent_message(issue_id: str, msg_type: str, data: dict, agent_id: str):
|
||||||
|
"""Post structured message for other agents."""
|
||||||
|
message = {
|
||||||
|
'type': msg_type,
|
||||||
|
'agent': agent_id,
|
||||||
|
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
backend.add_comment(issue_id, Comment(
|
||||||
|
id=None,
|
||||||
|
body=f"```agent-message\n{json.dumps(message, indent=2)}\n```",
|
||||||
|
author=User(id=agent_id, username=agent_id),
|
||||||
|
created_at=datetime.now(timezone.utc)
|
||||||
|
))
|
||||||
|
|
||||||
|
def read_agent_messages(issue_id: str, msg_type: str = None):
|
||||||
|
"""Read messages from other agents."""
|
||||||
|
comments = backend.get_comments(issue_id)
|
||||||
|
messages = []
|
||||||
|
for comment in comments:
|
||||||
|
if '```agent-message' in comment.body:
|
||||||
|
try:
|
||||||
|
json_str = comment.body.split('```agent-message\n')[1].split('\n```')[0]
|
||||||
|
msg = json.loads(json_str)
|
||||||
|
if msg_type is None or msg['type'] == msg_type:
|
||||||
|
messages.append(msg)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
return messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Check
|
||||||
|
|
||||||
|
Before using issue tracking, verify configuration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def verify_issue_backend() -> bool:
|
||||||
|
"""Verify issue backend is configured."""
|
||||||
|
try:
|
||||||
|
backend = GiteaBackend()
|
||||||
|
backend.connect({
|
||||||
|
'base_url': os.environ['GITEA_URL'],
|
||||||
|
'token': os.environ['GITEA_API_TOKEN'],
|
||||||
|
'owner': os.environ['GITEA_OWNER'],
|
||||||
|
'repo': os.environ['GITEA_REPO']
|
||||||
|
})
|
||||||
|
return backend.test_connection()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Issue backend not configured: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Use it
|
||||||
|
if not verify_issue_backend():
|
||||||
|
print("ERROR: Issue tracking not available. Check configuration.")
|
||||||
|
sys.exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
from issue_tracker.backends.gitea.backend import GiteaAPIError
|
||||||
|
|
||||||
|
try:
|
||||||
|
issue = backend.get_issue_by_number(42)
|
||||||
|
except GiteaAPIError as e:
|
||||||
|
if e.status_code == 404:
|
||||||
|
print("Issue not found")
|
||||||
|
elif e.status_code == 401:
|
||||||
|
print("Authentication failed - check GITEA_API_TOKEN")
|
||||||
|
elif e.status_code == 429:
|
||||||
|
print("Rate limited - wait and retry")
|
||||||
|
else:
|
||||||
|
print(f"API error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Use filters** instead of fetching all issues:
|
||||||
|
```python
|
||||||
|
# BAD: Get all, filter in Python
|
||||||
|
all_issues = backend.list_issues()
|
||||||
|
my_issues = [i for i in all_issues if i.assignees and i.assignees[0].username == 'me']
|
||||||
|
|
||||||
|
# GOOD: Filter at backend
|
||||||
|
my_issues = backend.list_issues(IssueFilter(assignee='me'))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use JSON output** for CLI parsing:
|
||||||
|
```bash
|
||||||
|
issue list --format=json | jq '.[] | select(.state == "open")'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Batch comments** instead of rapid-fire updates
|
||||||
|
|
||||||
|
4. **Check local cache** before querying (if available)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Backend not configured"
|
||||||
|
```bash
|
||||||
|
# Check config
|
||||||
|
issue backend list
|
||||||
|
|
||||||
|
# If empty, configure
|
||||||
|
export GITEA_API_TOKEN="your-token"
|
||||||
|
issue backend add myproject gitea
|
||||||
|
issue backend set-default myproject
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Authentication failed"
|
||||||
|
```bash
|
||||||
|
# Verify token
|
||||||
|
curl -H "Authorization: token $GITEA_API_TOKEN" $GITEA_URL/api/v1/user
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Issue not found"
|
||||||
|
```python
|
||||||
|
# Use get_issue_by_number, not get_issue
|
||||||
|
issue = backend.get_issue_by_number(42) # Correct
|
||||||
|
# issue = backend.get_issue("42") # Wrong - needs backend_id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Documentation
|
||||||
|
|
||||||
|
- **Integration Guide:** `AGENT_INTEGRATION.md` (comprehensive patterns and strategies)
|
||||||
|
- **API Reference:** `CLAUDE.md` (for developers extending the capability)
|
||||||
|
- **Examples:** `examples/agents/` (working agent implementations)
|
||||||
|
- **Roadmap:** `ROADMAP.md` (upcoming features)
|
||||||
|
|
||||||
|
## Current Limitations (v1.0)
|
||||||
|
|
||||||
|
Be aware of these limitations:
|
||||||
|
|
||||||
|
1. **Manual Configuration:** Backend must be configured before use (auto-detect in v1.1)
|
||||||
|
2. **User Context:** Uses hardcoded user for CLI operations (agent identity in v1.2)
|
||||||
|
3. **No Built-in Locking:** Use assignee + comment workaround for claiming (native in v1.2)
|
||||||
|
4. **Basic Conflicts:** Manual resolution required for complex sync conflicts (advanced in v2.0)
|
||||||
|
|
||||||
|
Workarounds are documented in `AGENT_INTEGRATION.md`.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you're unsure whether to use this capability for something:
|
||||||
|
|
||||||
|
**ASK:** "Does this involve creating, reading, updating, or searching issues?"
|
||||||
|
- **YES** → Use this capability
|
||||||
|
- **NO** → You can use other methods
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
- "Create an issue for the bug I found" → **Use issue-facade**
|
||||||
|
- "Read the project README" → Don't need issue-facade
|
||||||
|
- "Check if issue #42 exists" → **Use issue-facade**
|
||||||
|
- "Clone the repository" → Don't need issue-facade
|
||||||
6
.claude/commands/use-issues.md
Normal file
6
.claude/commands/use-issues.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Use the issue-facade capability for all issue tracking operations.
|
||||||
|
|
||||||
|
**Quick reference:** See `.claude/capabilities/issue-facade.md`
|
||||||
|
**Examples:** See `capabilities/issue-facade/examples/agents/`
|
||||||
|
|
||||||
|
**DO NOT use direct API calls or platform CLIs!**
|
||||||
8
.claude/context/capabilities.md
Normal file
8
.claude/context/capabilities.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Available Capabilities
|
||||||
|
|
||||||
|
## Issue Tracking: issue-facade
|
||||||
|
|
||||||
|
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
||||||
|
|
||||||
|
**Docs:** `.claude/capabilities/issue-facade.md`
|
||||||
|
**Usage:** `/use-issues`
|
||||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -2,12 +2,13 @@
|
|||||||
path = wiki
|
path = wiki
|
||||||
url = http://92.205.130.254:32166/coulomb/markitect_project.wiki.git
|
url = http://92.205.130.254:32166/coulomb/markitect_project.wiki.git
|
||||||
branch = main
|
branch = main
|
||||||
[submodule "capabilities/issue-facade"]
|
|
||||||
path = capabilities/issue-facade
|
|
||||||
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
|
||||||
[submodule "capabilities/kaizen-agentic"]
|
[submodule "capabilities/kaizen-agentic"]
|
||||||
path = capabilities/kaizen-agentic
|
path = capabilities/kaizen-agentic
|
||||||
url = http://92.205.130.254:32166/coulomb/kaizen-agentic.git
|
url = http://92.205.130.254:32166/coulomb/kaizen-agentic.git
|
||||||
[submodule "capabilities/testdrive-jsui"]
|
[submodule "capabilities/testdrive-jsui"]
|
||||||
path = capabilities/testdrive-jsui
|
path = capabilities/testdrive-jsui
|
||||||
url = http://92.205.130.254:32166/coulomb/testdrive-jsui.git
|
url = http://92.205.130.254:32166/coulomb/testdrive-jsui.git
|
||||||
|
[submodule "_issue-tracking/issue-facade"]
|
||||||
|
path = _issue-tracking/issue-facade
|
||||||
|
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
||||||
|
branch = main
|
||||||
|
|||||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -8,9 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **Schema Management System**: Comprehensive schema management infrastructure with naming conventions and versioning
|
||||||
|
- Naming convention: `{domain}-schema-v{major}.{minor}.md` for all schemas
|
||||||
|
- Markdown-first schema format with embedded JSON (documentation + schema in one file)
|
||||||
|
- Schema catalog (`markitect/schemas/schema-catalog.yaml`) for metadata and discovery
|
||||||
|
- Terminology validation example (`examples/terminology/`) demonstrating schema usage beyond manpages
|
||||||
|
- Schema-for-schemas workplan in `roadmap/schema-of-schemas/` directory
|
||||||
|
- **Enhanced schema-list Command**: Now displays creation timestamps in all output formats
|
||||||
|
- Default format shows timestamps inline: `schema-name.json (added: 2026-01-04T23:01:19)`
|
||||||
|
- Table format includes Created/Updated columns
|
||||||
|
- Cleaner timestamp formatting (removed microseconds)
|
||||||
- Enhanced control panel UI with better resize handle positioning for improved user interaction
|
- Enhanced control panel UI with better resize handle positioning for improved user interaction
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Directory Reorganization**: Renamed `todo/` → `roadmap/` for better organization of planning documents
|
||||||
|
- Created `roadmap/schema-of-schemas/` subdirectory for schema management planning artifacts
|
||||||
|
- Moved schema management proposals and workplan to dedicated directory
|
||||||
- Refactored contents control architecture to use base class pattern properly for better code organization
|
- Refactored contents control architecture to use base class pattern properly for better code organization
|
||||||
- Updated all file references and paths to point to single source of truth in capabilities/testdrive-jsui/js/controls/ directory
|
- Updated all file references and paths to point to single source of truth in capabilities/testdrive-jsui/js/controls/ directory
|
||||||
|
|
||||||
@@ -21,6 +34,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Removed
|
### Removed
|
||||||
- **BREAKING**: Legacy DocumentControls component from TestDrive JSUI plugin system - all control panel functionality now provided by enhanced control panels (ContentsControl, StatusControl, DebugControl, EditControl) with Reset All button functionality moved to EditControl for better maintainability and elimination of code duplication
|
- **BREAKING**: Legacy DocumentControls component from TestDrive JSUI plugin system - all control panel functionality now provided by enhanced control panels (ContentsControl, StatusControl, DebugControl, EditControl) with Reset All button functionality moved to EditControl for better maintainability and elimination of code duplication
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- **Schema-of-Schemas Implementation** (Phase 3 of 6 - Completed ✅)
|
||||||
|
- ✅ Phase 1: Filename validation for schema naming convention (`markitect/schema_naming.py`, 50 tests)
|
||||||
|
- ✅ Phase 2: Markdown schema loader to parse `.md` schema files (`markitect/schema_loader.py`, 35 tests)
|
||||||
|
- ✅ Phase 3: Schema-for-schemas metaschema for schema validation (`schema-schema-v1.0.md`, 12 tests)
|
||||||
|
- ⏳ Phase 4: Migration of 5 existing schemas to new format (will remove 2 duplicates)
|
||||||
|
- ⏳ Phase 5: CLI updates and documentation
|
||||||
|
- ⏳ Phase 6: Integration testing and validation
|
||||||
|
|
||||||
## [0.9.0] - 2025-11-14
|
## [0.9.0] - 2025-11-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
187
TODO.md
187
TODO.md
@@ -12,10 +12,193 @@ The structure organizes **future tasks** by their impact, just as a changelog or
|
|||||||
|
|
||||||
This section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.
|
This section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.
|
||||||
|
|
||||||
*No active tasks at this time.*
|
### Schema-of-Schemas Implementation (Active - Phase 3)
|
||||||
|
|
||||||
|
**Status:** Phase 3 - Schema-for-Schemas Metaschema (Completed ✅)
|
||||||
|
**Workplan:** See `roadmap/schema-of-schemas/WORKPLAN.md`
|
||||||
|
|
||||||
|
**Current Goals:**
|
||||||
|
1. ✅ Establish naming convention: `{domain}-schema-v{major}.{minor}.md`
|
||||||
|
2. ✅ Implement filename validation logic
|
||||||
|
3. ✅ Create markdown schema loader
|
||||||
|
4. ✅ Create example markdown schema
|
||||||
|
5. ✅ Build schema-for-schemas metaschema
|
||||||
|
6. ⏳ Migrate existing schemas to new format (Next: Phase 4)
|
||||||
|
|
||||||
|
**Phase 1 Tasks (Completed ✅):**
|
||||||
|
- [x] Write `markitect/schema_naming.py` with validation logic
|
||||||
|
- [x] Add unit tests for filename validation (50 tests, 100% passing)
|
||||||
|
- [x] Create SCHEMA_NAMING_SPEC.md documentation
|
||||||
|
|
||||||
|
**Phase 2 Tasks (Completed ✅):**
|
||||||
|
- [x] Implement MarkdownSchemaLoader class (markitect/schema_loader.py, 515 lines)
|
||||||
|
- [x] Add frontmatter extraction (YAML)
|
||||||
|
- [x] Add JSON code block extraction with section preference
|
||||||
|
- [x] Add metadata merging with x-markitect-source tracking
|
||||||
|
- [x] Write comprehensive unit tests (35 tests, 100% passing)
|
||||||
|
- [x] Create example markdown schema (manpage-schema-v1.0.md)
|
||||||
|
- [x] Create SCHEMA_LOADER_GUIDE.md documentation
|
||||||
|
|
||||||
|
**Phase 3 Tasks (Completed ✅):**
|
||||||
|
- [x] Design schema-for-schemas metaschema (schema-schema-v1.0.md)
|
||||||
|
- [x] Implement metaschema with validation rules for MarkiTect conventions
|
||||||
|
- [x] Add schema-validate CLI command with detailed error reporting
|
||||||
|
- [x] Write comprehensive unit tests (12 tests, 100% passing)
|
||||||
|
- [x] Test metaschema self-validation
|
||||||
|
- [x] Validate existing schemas against metaschema
|
||||||
|
|
||||||
|
**Next Phases:**
|
||||||
|
- Phase 4: Schema Migration (1-2 days)
|
||||||
|
- Phase 5: CLI & Documentation Updates (1 day)
|
||||||
|
- Phase 6: Testing & Validation (1 day)
|
||||||
|
|
||||||
|
**Expected Completion:** 4-5 days remaining
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Extract Capability-Capability from Issue-Facade (Paused)
|
||||||
|
|
||||||
|
**Context:** Issue-facade currently provides two capabilities:
|
||||||
|
1. **issue-tracking** (explicit in CAPABILITY-issue-tracking.yaml) - Issue management across platforms
|
||||||
|
2. **capability-capability** (implicit) - Patterns and tools for creating/managing capabilities
|
||||||
|
|
||||||
|
The **capability-capability** includes:
|
||||||
|
- Feedback pattern (feedback/ directory, .capability/feedback CLI tool, documentation)
|
||||||
|
- Detachment facility (.capability/detach script for clean capability removal)
|
||||||
|
- Integration pattern (.capability/integrate.sh for project integration)
|
||||||
|
- CAPABILITY-*.yaml specification format
|
||||||
|
- ReusableCapabilitiesArchitecture.md (complete specification)
|
||||||
|
- Directory conventions (_family/implementation, visible/hidden patterns)
|
||||||
|
|
||||||
|
**Goal:** Extract capability-capability to separate `reusable-capability` repository so it can be used by any capability in the markitect ecosystem.
|
||||||
|
|
||||||
|
**Approach:** Step-by-step extraction, starting with specification.
|
||||||
|
|
||||||
|
#### Phase 1: Specification & Planning (Current)
|
||||||
|
|
||||||
|
- [ ] Create CAPABILITY-capability.yaml in issue-facade to explicitly declare the implicit capability
|
||||||
|
- [ ] Define what belongs to capability-capability family vs issue-tracking family
|
||||||
|
- [ ] Document the capability-capability API surface (what tools/patterns it provides)
|
||||||
|
- [ ] Identify all files/directories to extract
|
||||||
|
- [ ] Plan extraction strategy (copy vs move, how to maintain during transition)
|
||||||
|
|
||||||
|
#### Phase 2: Repository Creation
|
||||||
|
|
||||||
|
- [ ] Create reusable-capability repository structure
|
||||||
|
- [ ] Extract ReusableCapabilitiesArchitecture.md to new repo
|
||||||
|
- [ ] Extract feedback pattern (directory structure, CLI tool, README)
|
||||||
|
- [ ] Extract detachment facility (.capability/detach)
|
||||||
|
- [ ] Extract integration scripts (.capability/integrate.sh, integration-checklist.md)
|
||||||
|
- [ ] Create CAPABILITY-capability.yaml in new repo (canonical version)
|
||||||
|
- [ ] Add README.md for reusable-capability repo
|
||||||
|
|
||||||
|
#### Phase 3: Integration & Testing
|
||||||
|
|
||||||
|
- [ ] Update issue-facade to depend on reusable-capability (as integrated capability)
|
||||||
|
- [ ] Integrate reusable-capability into issue-facade using _capability/reusable-capability pattern
|
||||||
|
- [ ] Test that issue-facade still works with extracted capability
|
||||||
|
- [ ] Update issue-facade documentation to reference both capabilities it provides/uses
|
||||||
|
- [ ] Verify feedback system still works
|
||||||
|
- [ ] Verify detachment still works
|
||||||
|
|
||||||
|
#### Phase 4: Dogfooding & Validation
|
||||||
|
|
||||||
|
- [ ] Choose another markitect capability for dogfooding
|
||||||
|
- [ ] Integrate reusable-capability into that capability
|
||||||
|
- [ ] Add feedback system to new capability
|
||||||
|
- [ ] Add detachment facility to new capability
|
||||||
|
- [ ] Document learnings and refine reusable-capability based on real-world usage
|
||||||
|
- [ ] Update ReusableCapabilitiesArchitecture.md with insights
|
||||||
|
|
||||||
|
**Current Step:** Phase 1, Task 1 - Create CAPABILITY-capability.yaml
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Completed Tasks
|
## Completed Tasks
|
||||||
|
|
||||||
*Recent completed tasks have been documented in CHANGELOG.md following Keep a Changelog format.*
|
*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 & Terminology Example
|
||||||
|
- ✅ 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
|
||||||
|
- ✅ Moved SCHEMA_EVOLUTION_WORKPLAN.md to todo/ directory
|
||||||
|
- ✅ Created terminology validation example (examples/terminology/)
|
||||||
|
|
||||||
|
**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)
|
||||||
|
|
||||||
|
**Terminology Example:**
|
||||||
|
- Complete terminology document structure (terminology-example.md)
|
||||||
|
- JSON schema with MarkiTect extensions (terminology-schema.json)
|
||||||
|
- Demonstrates schema usage for non-manpage documents
|
||||||
|
- Validates term definitions, synonyms, related terms, examples
|
||||||
|
- Includes content control and validation rules
|
||||||
|
- Full documentation and usage examples (README.md)
|
||||||
|
|
||||||
|
### 2026-01-04 - Phase 2: Markdown Schema Loader
|
||||||
|
- ✅ Implemented MarkdownSchemaLoader class (markitect/schema_loader.py, 515 lines)
|
||||||
|
- ✅ YAML frontmatter extraction with validation
|
||||||
|
- ✅ JSON code block extraction with "Schema Definition" section preference
|
||||||
|
- ✅ Metadata merging with x-markitect-source tracking
|
||||||
|
- ✅ Schema saving with template support and round-trip capability
|
||||||
|
- ✅ Comprehensive test suite (35 unit tests, 100% passing)
|
||||||
|
- ✅ Created example markdown schema (manpage-schema-v1.0.md)
|
||||||
|
- ✅ Created SCHEMA_LOADER_GUIDE.md with complete usage documentation
|
||||||
|
|
||||||
|
**Key Features Delivered:**
|
||||||
|
- Markdown-first schema format with embedded JSON
|
||||||
|
- Frontmatter metadata merges into schema ($id, version, status)
|
||||||
|
- Automatic detection of multiple JSON blocks
|
||||||
|
- Schema structure validation helper
|
||||||
|
- Error handling for binary files and invalid formats
|
||||||
|
- List JSON blocks helper for debugging
|
||||||
|
- Full round-trip save/load capability
|
||||||
|
|
||||||
|
**Example Markdown Schema:**
|
||||||
|
- manpage-schema-v1.0.md demonstrating complete format
|
||||||
|
- Includes frontmatter, documentation, and JSON schema
|
||||||
|
- Shows section classification and content control
|
||||||
|
- Follows naming convention: {domain}-schema-v{major}.{minor}.md
|
||||||
|
|
||||||
|
### 2026-01-04 - Phase 3: Schema-for-Schemas Metaschema
|
||||||
|
- ✅ Created schema-schema-v1.0.md metaschema (650+ lines)
|
||||||
|
- ✅ Validates core JSON Schema fields ($schema, $id, title, description)
|
||||||
|
- ✅ Validates MarkiTect version field (SemVer: major.minor.patch)
|
||||||
|
- ✅ Validates $id URL format (HTTPS with version)
|
||||||
|
- ✅ Validates MarkiTect extensions (x-markitect-sections, x-markitect-content-control, x-markitect-metadata)
|
||||||
|
- ✅ Implemented schema-validate CLI command with detailed error reporting
|
||||||
|
- ✅ Comprehensive test suite (12 unit tests, 100% passing)
|
||||||
|
- ✅ Metaschema self-validation successful
|
||||||
|
|
||||||
|
**Key Features Delivered:**
|
||||||
|
- Complete metaschema for validating all MarkiTect schemas
|
||||||
|
- Section classification validation (required, recommended, optional, discouraged, improper)
|
||||||
|
- Content control pattern validation
|
||||||
|
- Version format enforcement (SemVer)
|
||||||
|
- $id URL format enforcement (HTTPS with version)
|
||||||
|
- CLI command for easy schema validation
|
||||||
|
- Detailed error messages with schema paths
|
||||||
|
|
||||||
|
**Validation Results:**
|
||||||
|
- ✅ Metaschema validates itself
|
||||||
|
- ✅ Manpage schema validates successfully
|
||||||
|
- ⚠️ Terminology schema needs migration (missing version field, incorrect $id format)
|
||||||
|
|
||||||
|
### 2025-12-17 - Architecture Refactoring
|
||||||
|
- ✅ Implemented ReusableCapabilitiesArchitecture v0.1
|
||||||
|
- ✅ Added feedback capability to issue-facade
|
||||||
|
- ✅ Created detachment facility
|
||||||
|
- ✅ Refactored to family-based directory structure (_issue-tracking/issue-facade)
|
||||||
|
- ✅ Made feedback directory visible (feedback/ not .feedback/)
|
||||||
|
- ✅ Renamed to explicit family declaration (CAPABILITY-issue-tracking.yaml)
|
||||||
|
- ✅ Created CHANGELOG.md documenting v1.0.0
|
||||||
|
|||||||
1
_issue-tracking/issue-facade
Submodule
1
_issue-tracking/issue-facade
Submodule
Submodule _issue-tracking/issue-facade added at 70d7ec0cdc
51
capabilities/DETACHED-issue-facade.yaml
Normal file
51
capabilities/DETACHED-issue-facade.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Detachment Manifest
|
||||||
|
# This file records the removal of the issue-facade capability
|
||||||
|
# Use this information to re-integrate with updated architecture
|
||||||
|
|
||||||
|
detachment:
|
||||||
|
timestamp: 2025-12-17T21:23:14Z
|
||||||
|
capability_name: issue-facade
|
||||||
|
capability_family: issue-tracking
|
||||||
|
integration_pattern: capabilities-directory
|
||||||
|
original_location: /home/worsch/markitect_project/capabilities/issue-facade
|
||||||
|
|
||||||
|
capability_metadata:
|
||||||
|
spec_file: CAPABILITY-issue-tracking.yaml
|
||||||
|
version: unknown
|
||||||
|
implementation: unknown
|
||||||
|
maturity: unknown
|
||||||
|
|
||||||
|
integration_details:
|
||||||
|
parent_project: capabilities
|
||||||
|
parent_path: /home/worsch/markitect_project/capabilities
|
||||||
|
|
||||||
|
re_integration_guide: |
|
||||||
|
To re-integrate this capability using the new architecture:
|
||||||
|
|
||||||
|
# Option 1: Git submodule (recommended)
|
||||||
|
cd /home/worsch/markitect_project/capabilities
|
||||||
|
git submodule add <repo-url> _issue-facade
|
||||||
|
pip install -e _issue-facade/
|
||||||
|
|
||||||
|
# Option 2: Clone directly
|
||||||
|
cd /home/worsch/markitect_project/capabilities
|
||||||
|
git clone <repo-url> _issue-facade
|
||||||
|
pip install -e _issue-facade/
|
||||||
|
|
||||||
|
# Option 3: Copy into project
|
||||||
|
cd /home/worsch/markitect_project/capabilities
|
||||||
|
cp -r /path/to/issue-facade _issue-facade
|
||||||
|
pip install -e _issue-facade/
|
||||||
|
|
||||||
|
Note: Use underscore prefix (_issue-facade) per ReusableCapabilitiesArchitecture
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- The original integration used pattern: capabilities-directory
|
||||||
|
- New architecture recommends: underscore-prefix at repo root
|
||||||
|
- See ReusableCapabilitiesArchitecture.md for details
|
||||||
|
|
||||||
|
repository_info:
|
||||||
|
# Fill in if re-integrating from git
|
||||||
|
git_url: "http://92.205.130.254:32166/coulomb/issue-facade.git" # e.g., https://github.com/markitect/issue-facade
|
||||||
|
git_branch: "main" # e.g., main
|
||||||
|
git_commit: "35daa514e59788250847cd706c43ea78f24c5c1d" # Optional: specific commit to use
|
||||||
Submodule capabilities/issue-facade deleted from 34a8bc7d4c
Submodule capabilities/testdrive-jsui updated: f5ce02cf8d...b8f13b4ae5
662
docs/specifications/schema-extensions-spec.md
Normal file
662
docs/specifications/schema-extensions-spec.md
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
# MarkiTect Schema Extensions Specification v1.0
|
||||||
|
|
||||||
|
## Status: Draft - Phase 1 Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This specification defines MarkiTect-specific extensions to JSON Schema (draft-07) for markdown document validation with content control, section classification, and flexible structural constraints.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Backward Compatibility**: Existing schemas without extensions continue to work
|
||||||
|
2. **Namespace Isolation**: All extensions prefixed with `x-markitect-`
|
||||||
|
3. **Progressive Enhancement**: Extensions add capabilities without breaking standard JSON Schema
|
||||||
|
4. **Clear Semantics**: Each extension has well-defined validation behavior
|
||||||
|
5. **Metaschema Validation**: All extensions validated by MarkiTect metaschema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extension: `x-markitect-sections`
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Define document sections with classification levels (required, recommended, optional, discouraged, improper) and content control specifications.
|
||||||
|
|
||||||
|
### Schema Location
|
||||||
|
|
||||||
|
Applied at the **root level** of the schema or within **properties** that represent document sections.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SECTION_NAME": {
|
||||||
|
"classification": "required|recommended|optional|discouraged|improper",
|
||||||
|
"heading_level": 1|2|3|4|5|6,
|
||||||
|
"position": "after_title|before_section_name|after_section_name|anywhere",
|
||||||
|
"content_instruction": "string",
|
||||||
|
"min_paragraphs": integer,
|
||||||
|
"max_paragraphs": integer,
|
||||||
|
"min_code_blocks": integer,
|
||||||
|
"max_code_blocks": integer,
|
||||||
|
"min_lists": integer,
|
||||||
|
"max_lists": integer,
|
||||||
|
"warning_if_missing": "string",
|
||||||
|
"error_message": "string",
|
||||||
|
"alternatives": ["SECTION_NAME_1", "SECTION_NAME_2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Property Definitions
|
||||||
|
|
||||||
|
#### `classification` (required)
|
||||||
|
|
||||||
|
Classification level determining validation behavior:
|
||||||
|
|
||||||
|
- **`required`**: Section MUST be present. Validation fails if missing.
|
||||||
|
- **`recommended`**: Section SHOULD be present. Warning if missing, but validation succeeds.
|
||||||
|
- **`optional`**: Section MAY be present. No validation impact either way.
|
||||||
|
- **`discouraged`**: Section SHOULD NOT be present. Warning if present, but validation succeeds.
|
||||||
|
- **`improper`**: Section MUST NOT be present. Validation fails if present.
|
||||||
|
|
||||||
|
**Type**: String enum
|
||||||
|
**Required**: Yes
|
||||||
|
**Values**: `["required", "recommended", "optional", "discouraged", "improper"]`
|
||||||
|
|
||||||
|
#### `heading_level` (optional)
|
||||||
|
|
||||||
|
The heading level (H1-H6) for this section.
|
||||||
|
|
||||||
|
**Type**: Integer
|
||||||
|
**Range**: 1-6
|
||||||
|
**Default**: 2 (for standard sections)
|
||||||
|
|
||||||
|
#### `position` (optional)
|
||||||
|
|
||||||
|
Where this section should appear relative to other sections.
|
||||||
|
|
||||||
|
**Type**: String enum
|
||||||
|
**Values**:
|
||||||
|
- `"after_title"` - Immediately after document title (H1)
|
||||||
|
- `"before_section_name"` - Before another named section
|
||||||
|
- `"after_section_name"` - After another named section
|
||||||
|
- `"anywhere"` - No position constraint (default)
|
||||||
|
|
||||||
|
**Default**: `"anywhere"`
|
||||||
|
|
||||||
|
#### `content_instruction` (optional)
|
||||||
|
|
||||||
|
Human-readable instruction describing what content belongs in this section.
|
||||||
|
|
||||||
|
**Type**: String
|
||||||
|
**Usage**: Displayed in validation warnings, generated templates, and documentation
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"content_instruction": "Brief command syntax showing all options and arguments"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Content Constraints (optional)
|
||||||
|
|
||||||
|
Minimum and maximum counts for content elements within the section:
|
||||||
|
|
||||||
|
- **`min_paragraphs`**: Minimum paragraph count (integer ≥ 0)
|
||||||
|
- **`max_paragraphs`**: Maximum paragraph count (integer ≥ min_paragraphs)
|
||||||
|
- **`min_code_blocks`**: Minimum code block count (integer ≥ 0)
|
||||||
|
- **`max_code_blocks`**: Maximum code block count (integer ≥ min_code_blocks)
|
||||||
|
- **`min_lists`**: Minimum list count (integer ≥ 0)
|
||||||
|
- **`max_lists`**: Maximum list count (integer ≥ max_lists)
|
||||||
|
|
||||||
|
**Type**: Integer
|
||||||
|
**Default**: No constraint if omitted
|
||||||
|
|
||||||
|
#### `warning_if_missing` (optional)
|
||||||
|
|
||||||
|
Custom warning message when a recommended section is missing.
|
||||||
|
|
||||||
|
**Type**: String
|
||||||
|
**Applies to**: `classification: "recommended"` only
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"warning_if_missing": "Examples greatly improve documentation usability"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `error_message` (optional)
|
||||||
|
|
||||||
|
Custom error message when validation fails.
|
||||||
|
|
||||||
|
**Type**: String
|
||||||
|
**Applies to**: `classification: "required"` or `"improper"`
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"error_message": "Internal notes must not appear in published documentation"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `alternatives` (optional)
|
||||||
|
|
||||||
|
Array of alternative section names that satisfy the requirement.
|
||||||
|
|
||||||
|
**Type**: Array of strings
|
||||||
|
**Usage**: If any alternative is present, requirement is satisfied
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"classification": "required",
|
||||||
|
"alternatives": ["EXAMPLES", "USAGE", "TUTORIAL"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Manpage Schema with Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Unix Manpage Schema",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_title",
|
||||||
|
"content_instruction": "Brief command syntax with options and arguments",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 5,
|
||||||
|
"min_code_blocks": 0,
|
||||||
|
"max_code_blocks": 3,
|
||||||
|
"error_message": "SYNOPSIS section is mandatory for all manpages"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_section_name",
|
||||||
|
"content_instruction": "Detailed explanation of what the command does",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"error_message": "DESCRIPTION section is mandatory for all manpages"
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Practical usage examples with explanations",
|
||||||
|
"min_code_blocks": 3,
|
||||||
|
"warning_if_missing": "Examples greatly improve manpage usability"
|
||||||
|
},
|
||||||
|
"SEE ALSO": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Related commands and documentation references",
|
||||||
|
"warning_if_missing": "Cross-references help users discover related functionality"
|
||||||
|
},
|
||||||
|
"BUGS": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Known issues and bug reporting information"
|
||||||
|
},
|
||||||
|
"DEPRECATED": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Consider moving deprecated content to historical documentation"
|
||||||
|
},
|
||||||
|
"INTERNAL_NOTES": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Internal notes must not appear in published manpages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Behavior
|
||||||
|
|
||||||
|
#### Required Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
"SYNOPSIS": {"classification": "required"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- Section missing → **ERROR** → `is_valid = False`
|
||||||
|
- Section present → Continue validation
|
||||||
|
- Custom `error_message` used if provided
|
||||||
|
|
||||||
|
#### Recommended Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
"EXAMPLES": {"classification": "recommended"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- Section missing → **WARNING** → `is_valid = True` (with warnings)
|
||||||
|
- Section present → Continue validation
|
||||||
|
- Custom `warning_if_missing` used if provided
|
||||||
|
|
||||||
|
#### Optional Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
"BUGS": {"classification": "optional"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- Section missing → No impact
|
||||||
|
- Section present → Continue validation
|
||||||
|
- No messages generated
|
||||||
|
|
||||||
|
#### Discouraged Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
"DEPRECATED": {"classification": "discouraged"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- Section missing → No impact
|
||||||
|
- Section present → **WARNING** → `is_valid = True` (with warnings)
|
||||||
|
- Custom warning message used if provided
|
||||||
|
|
||||||
|
#### Improper Sections
|
||||||
|
|
||||||
|
```json
|
||||||
|
"INTERNAL_NOTES": {"classification": "improper"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation**:
|
||||||
|
- Section missing → No impact
|
||||||
|
- Section present → **ERROR** → `is_valid = False`
|
||||||
|
- Custom `error_message` used if provided
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extension: `x-markitect-content-control`
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Define content validation rules for document sections including pattern matching, quality metrics, and semantic constraints.
|
||||||
|
|
||||||
|
### Schema Location
|
||||||
|
|
||||||
|
Applied at **root level** or within specific **section properties**.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"section_name": {
|
||||||
|
"required_patterns": ["regex_pattern_1", "regex_pattern_2"],
|
||||||
|
"discouraged_patterns": ["regex_pattern_1"],
|
||||||
|
"forbidden_patterns": ["regex_pattern_1"],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": integer,
|
||||||
|
"max_words": integer,
|
||||||
|
"readability_target": "technical|general|simple|advanced",
|
||||||
|
"min_sentences": integer,
|
||||||
|
"max_sentences": integer
|
||||||
|
},
|
||||||
|
"content_instructions": ["instruction_1", "instruction_2"],
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": boolean,
|
||||||
|
"check_external": boolean,
|
||||||
|
"allow_fragments": boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Property Definitions
|
||||||
|
|
||||||
|
#### `required_patterns` (optional)
|
||||||
|
|
||||||
|
Array of regex patterns that MUST appear in section content.
|
||||||
|
|
||||||
|
**Type**: Array of strings (valid regex patterns)
|
||||||
|
**Validation**: ERROR if any pattern missing
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z-]+\\*\\*", // Bold command name
|
||||||
|
"\\[.*\\]" // Options in brackets
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `discouraged_patterns` (optional)
|
||||||
|
|
||||||
|
Array of regex patterns that SHOULD NOT appear in content.
|
||||||
|
|
||||||
|
**Type**: Array of strings (valid regex patterns)
|
||||||
|
**Validation**: WARNING if any pattern found
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"\\bWIP\\b"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `forbidden_patterns` (optional)
|
||||||
|
|
||||||
|
Array of regex patterns that MUST NOT appear in content.
|
||||||
|
|
||||||
|
**Type**: Array of strings (valid regex patterns)
|
||||||
|
**Validation**: ERROR if any pattern found
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"forbidden_patterns": [
|
||||||
|
"password\\s*=\\s*[\"'].*[\"']", // Hard-coded passwords
|
||||||
|
"api[_-]?key\\s*=\\s*[\"'].*[\"']" // Hard-coded API keys
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `content_quality` (optional)
|
||||||
|
|
||||||
|
Quality metrics for section content:
|
||||||
|
|
||||||
|
**Sub-properties**:
|
||||||
|
- **`min_words`**: Minimum word count (integer ≥ 0)
|
||||||
|
- **`max_words`**: Maximum word count (integer ≥ min_words)
|
||||||
|
- **`readability_target`**: Target readability level (enum)
|
||||||
|
- `"simple"` - Elementary school level
|
||||||
|
- `"general"` - General audience
|
||||||
|
- `"technical"` - Technical audience
|
||||||
|
- `"advanced"` - Expert/academic level
|
||||||
|
- **`min_sentences`**: Minimum sentence count (integer ≥ 0)
|
||||||
|
- **`max_sentences`**: Maximum sentence count (integer ≥ min_sentences)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 300,
|
||||||
|
"readability_target": "technical",
|
||||||
|
"min_sentences": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `content_instructions` (optional)
|
||||||
|
|
||||||
|
Array of human-readable instructions for content creation.
|
||||||
|
|
||||||
|
**Type**: Array of strings
|
||||||
|
**Usage**: Displayed in templates, validation reports, and documentation
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"content_instructions": [
|
||||||
|
"Show command name in bold",
|
||||||
|
"Include all major options",
|
||||||
|
"Use italic for arguments and placeholders",
|
||||||
|
"Keep syntax examples concise (1-3 lines)"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `link_validation` (optional)
|
||||||
|
|
||||||
|
Link checking configuration:
|
||||||
|
|
||||||
|
**Sub-properties**:
|
||||||
|
- **`check_internal`**: Validate internal document links (boolean)
|
||||||
|
- **`check_external`**: Validate external URLs (boolean)
|
||||||
|
- **`allow_fragments`**: Allow fragment-only links like `#section` (boolean)
|
||||||
|
|
||||||
|
**Default**: All false (no link validation)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```json
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": false,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Content Control for API Documentation
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "API Documentation Schema",
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"synopsis": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[A-Z]+\\*\\*", // HTTP method in bold
|
||||||
|
"`/api/.*`" // Endpoint path in code
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 10,
|
||||||
|
"max_words": 100,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Start with HTTP method in bold (e.g., **GET**)",
|
||||||
|
"Show endpoint path in code format",
|
||||||
|
"Include brief one-line description"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"request_parameters": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z_]+\\*\\*.*\\*[A-Za-z]+\\*" // Bold param name with italic type
|
||||||
|
],
|
||||||
|
"content_instructions": [
|
||||||
|
"Use bold for parameter names",
|
||||||
|
"Use italic for parameter types",
|
||||||
|
"Include description for each parameter",
|
||||||
|
"Mark required parameters clearly"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"TBD"
|
||||||
|
],
|
||||||
|
"forbidden_patterns": [
|
||||||
|
"password\\s*=",
|
||||||
|
"secret\\s*=",
|
||||||
|
"token\\s*="
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 500,
|
||||||
|
"readability_target": "technical",
|
||||||
|
"min_sentences": 3
|
||||||
|
},
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": true,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Result Structure
|
||||||
|
|
||||||
|
### Enhanced ValidationResult Class
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ValidationResult:
|
||||||
|
"""Result of schema validation with classification support."""
|
||||||
|
|
||||||
|
status: Literal["valid", "valid_with_warnings", "invalid"]
|
||||||
|
errors: List[ValidationError] # Required/improper violations
|
||||||
|
warnings: List[ValidationWarning] # Recommended/discouraged violations
|
||||||
|
suggestions: List[str] # Optional improvements
|
||||||
|
quality_metrics: Dict[str, Any] # Content quality scores
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Status Values
|
||||||
|
|
||||||
|
- **`"valid"`**: No errors, no warnings. Document fully conforms.
|
||||||
|
- **`"valid_with_warnings"`**: No errors, but has warnings. Document acceptable but improvable.
|
||||||
|
- **`"invalid"`**: Has errors. Document does not conform to schema.
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ValidationErrorType(Enum):
|
||||||
|
MISSING_REQUIRED_SECTION = "missing_required_section"
|
||||||
|
IMPROPER_SECTION_PRESENT = "improper_section_present"
|
||||||
|
CONTENT_PATTERN_MISSING = "content_pattern_missing"
|
||||||
|
CONTENT_PATTERN_FORBIDDEN = "content_pattern_forbidden"
|
||||||
|
CONTENT_TOO_SHORT = "content_too_short"
|
||||||
|
CONTENT_TOO_LONG = "content_too_long"
|
||||||
|
INVALID_LINK = "invalid_link"
|
||||||
|
STRUCTURE_MISMATCH = "structure_mismatch"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warning Types
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ValidationWarningType(Enum):
|
||||||
|
MISSING_RECOMMENDED_SECTION = "missing_recommended_section"
|
||||||
|
DISCOURAGED_SECTION_PRESENT = "discouraged_section_present"
|
||||||
|
CONTENT_PATTERN_DISCOURAGED = "content_pattern_discouraged"
|
||||||
|
CONTENT_QUALITY_BELOW_TARGET = "content_quality_below_target"
|
||||||
|
READABILITY_MISMATCH = "readability_mismatch"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metaschema Validation
|
||||||
|
|
||||||
|
### Extension Validation Rules
|
||||||
|
|
||||||
|
The MarkiTect metaschema validates these extensions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Z][A-Z0-9_ ]*$": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"classification": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["required", "recommended", "optional", "discouraged", "improper"]
|
||||||
|
},
|
||||||
|
"heading_level": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 6
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["after_title", "before_section_name", "after_section_name", "anywhere"]
|
||||||
|
},
|
||||||
|
"content_instruction": {"type": "string"},
|
||||||
|
"min_paragraphs": {"type": "integer", "minimum": 0},
|
||||||
|
"max_paragraphs": {"type": "integer", "minimum": 0},
|
||||||
|
"min_code_blocks": {"type": "integer", "minimum": 0},
|
||||||
|
"max_code_blocks": {"type": "integer", "minimum": 0},
|
||||||
|
"min_lists": {"type": "integer", "minimum": 0},
|
||||||
|
"max_lists": {"type": "integer", "minimum": 0},
|
||||||
|
"warning_if_missing": {"type": "string"},
|
||||||
|
"error_message": {"type": "string"},
|
||||||
|
"alternatives": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["classification"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-z][a-z0-9_]*$": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"required_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "format": "regex"}
|
||||||
|
},
|
||||||
|
"discouraged_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "format": "regex"}
|
||||||
|
},
|
||||||
|
"forbidden_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "format": "regex"}
|
||||||
|
},
|
||||||
|
"content_quality": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"min_words": {"type": "integer", "minimum": 0},
|
||||||
|
"max_words": {"type": "integer", "minimum": 0},
|
||||||
|
"readability_target": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["simple", "general", "technical", "advanced"]
|
||||||
|
},
|
||||||
|
"min_sentences": {"type": "integer", "minimum": 0},
|
||||||
|
"max_sentences": {"type": "integer", "minimum": 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content_instructions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"}
|
||||||
|
},
|
||||||
|
"link_validation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"check_internal": {"type": "boolean"},
|
||||||
|
"check_external": {"type": "boolean"},
|
||||||
|
"allow_fragments": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Phase 1 Scope
|
||||||
|
|
||||||
|
1. Define and document extension formats ✓
|
||||||
|
2. Update metaschema to validate extensions
|
||||||
|
3. Implement basic classification validation (required/recommended/optional/discouraged/improper)
|
||||||
|
4. Create example schemas demonstrating all features
|
||||||
|
5. Update CLI to report errors vs warnings separately
|
||||||
|
|
||||||
|
### Future Enhancements (Phase 2+)
|
||||||
|
|
||||||
|
- Content pattern matching implementation
|
||||||
|
- Quality metrics calculation
|
||||||
|
- Link validation
|
||||||
|
- Readability scoring
|
||||||
|
- Position constraints enforcement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.0 (Draft)** - Initial specification for Phase 1 implementation
|
||||||
|
- `x-markitect-sections` extension defined
|
||||||
|
- `x-markitect-content-control` extension defined
|
||||||
|
- Validation result structure defined
|
||||||
|
- Metaschema validation rules defined
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- JSON Schema Draft-07: https://json-schema.org/draft-07/schema
|
||||||
|
- MarkiTect Schema Evolution Workplan: `examples/manpages/SCHEMA_EVOLUTION_WORKPLAN.md`
|
||||||
|
- Existing Metaschema: `markitect/schemas/markitect-metaschema.json`
|
||||||
|
- Metaschema Validator: `markitect/metaschema.py`
|
||||||
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
|
||||||
158
examples/design-patterns/CopyFirstMigration.md
Normal file
158
examples/design-patterns/CopyFirstMigration.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Design Principle: Copy First Migration
|
||||||
|
|
||||||
|
## Meta
|
||||||
|
- **Name:** Copy First Migration
|
||||||
|
- **ShortName:** CopyFirst
|
||||||
|
- **Version:** 0.1
|
||||||
|
- **Status:** Draft
|
||||||
|
- **Tags:** refactoring, migration, safety, testing, legacy
|
||||||
|
- **RelatedPrinciples:** Don’t Repeat Yourself, Safe Refactoring, Test Pyramid, Capability-Based Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
Enable safe refactoring and structural migration of codebases by preserving
|
||||||
|
existing, working functionality until the new implementation is fully verified.
|
||||||
|
|
||||||
|
This principle prioritizes **reversibility, confidence, and continuity** over
|
||||||
|
speed or elegance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CoreStatement
|
||||||
|
Never move code directly; always copy first and delete only after verified
|
||||||
|
behavioral equivalence is established.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### InScope
|
||||||
|
- Large-scale refactors or directory restructurings
|
||||||
|
- Technology or language migrations (e.g. JS → new JS layout, JS → Python integration)
|
||||||
|
- Legacy code stabilization
|
||||||
|
- Safety-critical or business-critical systems
|
||||||
|
- Situations with incomplete test coverage
|
||||||
|
|
||||||
|
### OutOfScope
|
||||||
|
- Greenfield development
|
||||||
|
- Trivial refactors with full and trusted test coverage
|
||||||
|
- One-off throwaway scripts
|
||||||
|
- Performance-driven rewrites where duplication is unacceptable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## InterpretationGuidelines
|
||||||
|
|
||||||
|
### What “Copy First” Means
|
||||||
|
- The original code remains untouched and functional
|
||||||
|
- The new version is treated as **experimental until proven**
|
||||||
|
- Deletion is a **final, explicit act**, not an implicit side effect
|
||||||
|
|
||||||
|
### Common Misinterpretations
|
||||||
|
- “This is inefficient because it duplicates code”
|
||||||
|
→ Duplication is intentional and temporary
|
||||||
|
- “Moving files is faster”
|
||||||
|
→ Speed is not the optimization target here
|
||||||
|
- “Tests alone are enough”
|
||||||
|
→ Tests are necessary but not sufficient without behavioral comparison
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DetectionHeuristics
|
||||||
|
|
||||||
|
### Structural Signals
|
||||||
|
- Files or modules being relocated across directories or packages
|
||||||
|
- Parallel implementations during migration
|
||||||
|
- Introduction of a new architectural boundary
|
||||||
|
|
||||||
|
### Semantic Signals
|
||||||
|
- Code paths that must remain behaviorally identical
|
||||||
|
- Business rules with high regression risk
|
||||||
|
- Legacy logic that is poorly documented but relied upon
|
||||||
|
|
||||||
|
### Change-Cost Signals
|
||||||
|
- Rollbacks are expensive or disruptive
|
||||||
|
- Failures would impact production or customers
|
||||||
|
- Migration spans multiple commits or teams
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DiagnosticQuestions
|
||||||
|
1. What breaks if this migration is wrong?
|
||||||
|
2. Do we have a known-good reference implementation?
|
||||||
|
3. Can both old and new code paths run in parallel?
|
||||||
|
4. How quickly can we revert if a defect is found?
|
||||||
|
5. What is the minimal proof of behavioral equivalence?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RecommendedActions
|
||||||
|
|
||||||
|
### Low-Risk Actions
|
||||||
|
- Copy files to the new location instead of moving
|
||||||
|
- Preserve original imports and entry points
|
||||||
|
- Add logging or tracing for comparison
|
||||||
|
|
||||||
|
### Medium-Risk Actions
|
||||||
|
- Introduce dual-track execution (old + new)
|
||||||
|
- Add integration tests targeting both implementations
|
||||||
|
- Compare outputs, side effects, and error behavior
|
||||||
|
|
||||||
|
### High-Risk Actions
|
||||||
|
- Switch production usage to the new implementation
|
||||||
|
- Remove old code only after full verification
|
||||||
|
- Collapse duplicated paths once confidence is established
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AcceptanceCriteria
|
||||||
|
- Original code remains functional until final removal
|
||||||
|
- New code passes all existing tests
|
||||||
|
- New integration tests validate identical behavior
|
||||||
|
- Dual-track comparisons show no regressions
|
||||||
|
- Deletion of old code is deliberate and reversible up to the final step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AntiPatterns
|
||||||
|
- Moving files directly without a fallback
|
||||||
|
- Refactoring and migration in a single irreversible step
|
||||||
|
- Deleting “unused” code before equivalence is proven
|
||||||
|
- Assuming test parity guarantees behavioral parity
|
||||||
|
- Big-bang migrations without rollback paths
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tradeoffs
|
||||||
|
Applying Copy First Migration intentionally:
|
||||||
|
- Introduces temporary duplication
|
||||||
|
- Increases short-term codebase size
|
||||||
|
- Slows perceived progress
|
||||||
|
|
||||||
|
These costs are justified by dramatically reduced risk and higher confidence
|
||||||
|
during complex migrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AgentUsage
|
||||||
|
|
||||||
|
### When to Apply This Lens
|
||||||
|
- During directory, module, or architecture migrations
|
||||||
|
- When refactoring legacy or poorly understood code
|
||||||
|
- When safety and uptime matter more than speed
|
||||||
|
- When rollback must remain possible at all times
|
||||||
|
|
||||||
|
### When to Suspend This Lens
|
||||||
|
- In greenfield projects
|
||||||
|
- When full test coverage and confidence already exist
|
||||||
|
- For trivial mechanical refactors
|
||||||
|
|
||||||
|
### Expected Agent Output
|
||||||
|
- Identification of migration boundaries
|
||||||
|
- Copy-first migration plan with explicit stages
|
||||||
|
- Test strategy (unit, integration, dual-track)
|
||||||
|
- Rollback points and deletion criteria
|
||||||
|
- Clear signal for when old code may be removed
|
||||||
|
|
||||||
|
xxx
|
||||||
135
examples/design-patterns/DesignPrincipleSchema.json
Normal file
135
examples/design-patterns/DesignPrincipleSchema.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"title": "Schema for DesignPrinciples",
|
||||||
|
"description": "JSON schema describing the markdown structure of OperationalKnowledge DesignPrinciples",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Document heading structure",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Headings at level 1",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"content",
|
||||||
|
"level"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Headings at level 2",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"content",
|
||||||
|
"level"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minItems": 4,
|
||||||
|
"maxItems": 12
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Headings at level 3",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"content",
|
||||||
|
"level"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Text paragraphs",
|
||||||
|
"minItems": 8,
|
||||||
|
"maxItems": 120
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Lists (ordered and unordered)",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 20
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Text emphasis (bold, italic)",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 120
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Document structure metadata",
|
||||||
|
"properties": {
|
||||||
|
"total_elements": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 115
|
||||||
|
},
|
||||||
|
"structure_types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "All structural element types found",
|
||||||
|
"const": [
|
||||||
|
"paragraph_close",
|
||||||
|
"heading_close",
|
||||||
|
"hr",
|
||||||
|
"bullet_list_open",
|
||||||
|
"paragraph_open",
|
||||||
|
"heading_open",
|
||||||
|
"ordered_list_open",
|
||||||
|
"ordered_list_close",
|
||||||
|
"inline",
|
||||||
|
"list_item_close",
|
||||||
|
"list_item_open",
|
||||||
|
"bullet_list_close"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
examples/design-patterns/DontRepeatYourself.md
Normal file
160
examples/design-patterns/DontRepeatYourself.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Design Principle: Don’t Repeat Yourself (DRY)
|
||||||
|
|
||||||
|
## Meta
|
||||||
|
- **Name:** Don’t Repeat Yourself
|
||||||
|
- **ShortName:** DRY
|
||||||
|
- **Version:** 0.1
|
||||||
|
- **Status:** Stable
|
||||||
|
- **Tags:** maintainability, refactoring, architecture, quality
|
||||||
|
- **RelatedPrinciples:** Single Responsibility, YAGNI, Separation of Concerns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
Reduce maintenance cost and behavioral drift by ensuring that each piece of
|
||||||
|
knowledge, rule, or decision logic has a single authoritative representation
|
||||||
|
in the codebase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CoreStatement
|
||||||
|
A codebase violates DRY when the same knowledge is expressed in multiple places
|
||||||
|
such that a change would require edits in more than one location or risks
|
||||||
|
inconsistent behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### InScope
|
||||||
|
- Business rules and decision logic
|
||||||
|
- Algorithms and validation logic
|
||||||
|
- Data schemas, DTOs, and field definitions
|
||||||
|
- Configuration values and feature flags
|
||||||
|
- Repeated workflows or orchestration logic
|
||||||
|
- Test setup and invariant test scenarios
|
||||||
|
|
||||||
|
### OutOfScope
|
||||||
|
- Superficial textual similarity without shared meaning
|
||||||
|
- Intentional duplication for isolation or clarity
|
||||||
|
- Early-stage exploratory code where abstractions are not yet clear
|
||||||
|
- Performance-driven duplication with explicit justification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## InterpretationGuidelines
|
||||||
|
|
||||||
|
### What “Repeat” Means
|
||||||
|
DRY is about **duplication of knowledge**, not duplication of text.
|
||||||
|
|
||||||
|
Examples of knowledge duplication:
|
||||||
|
- The same validation rule implemented in multiple services
|
||||||
|
- Identical conditional logic controlling the same behavior
|
||||||
|
- The same data structure defined independently in multiple modules
|
||||||
|
|
||||||
|
### Common Misinterpretations
|
||||||
|
- “Any repeated code is bad” (false)
|
||||||
|
- “DRY means maximum abstraction” (false)
|
||||||
|
- “Utility modules automatically improve DRY” (often false)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DetectionHeuristics
|
||||||
|
|
||||||
|
### Structural Signals
|
||||||
|
- Functions with highly similar bodies and signatures
|
||||||
|
- Repeated constants, strings, regexes, or SQL fragments
|
||||||
|
- Parallel modules with mirrored internal structure
|
||||||
|
|
||||||
|
### Semantic Signals
|
||||||
|
- Identical error messages or validation rules in different layers
|
||||||
|
- Repeated mapping logic between the same concepts
|
||||||
|
- Copy-paste variations differing only in naming
|
||||||
|
|
||||||
|
### Change-Cost Signals
|
||||||
|
- A requirement change touches multiple files for the same reason
|
||||||
|
- Fixes applied in one location but missing in others
|
||||||
|
- Tests failing inconsistently after partial updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DiagnosticQuestions
|
||||||
|
1. Is this duplication representing the same rule or policy?
|
||||||
|
2. If this rule changes, how many places must be updated?
|
||||||
|
3. Is the duplicated logic stable or likely to evolve?
|
||||||
|
4. Are the differences intentional or accidental?
|
||||||
|
5. Where is the natural “source of truth” for this knowledge?
|
||||||
|
6. Would abstraction reduce or increase cognitive load?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RecommendedActions
|
||||||
|
|
||||||
|
### Low-Risk Refactors
|
||||||
|
- Extract constants or configuration values
|
||||||
|
- Centralize literals and error messages
|
||||||
|
- Introduce shared test fixtures or helpers
|
||||||
|
|
||||||
|
### Medium-Risk Refactors
|
||||||
|
- Extract pure helper functions
|
||||||
|
- Introduce shared domain services or modules
|
||||||
|
- Unify schema/type definitions
|
||||||
|
|
||||||
|
### High-Risk Refactors
|
||||||
|
- Introduce strategy/template patterns
|
||||||
|
- Merge parallel subsystems
|
||||||
|
- Redesign domain boundaries to align ownership of rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AcceptanceCriteria
|
||||||
|
- Each rule or behavior has a single authoritative implementation
|
||||||
|
- Required changes affect fewer locations than before
|
||||||
|
- Naming reflects domain meaning, not technical convenience
|
||||||
|
- Tests pass without behavior regression
|
||||||
|
- Coupling does not increase unintentionally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AntiPatterns
|
||||||
|
- “God” utility modules with unrelated helpers
|
||||||
|
- Over-generalized abstractions with many parameters
|
||||||
|
- Shared code across domains that should evolve independently
|
||||||
|
- Premature abstraction of coincidental similarities
|
||||||
|
- Hiding meaningful differences behind generic interfaces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tradeoffs
|
||||||
|
Applying DRY may:
|
||||||
|
- Increase indirection
|
||||||
|
- Reduce local readability
|
||||||
|
- Introduce coupling between modules
|
||||||
|
|
||||||
|
These costs are acceptable only when outweighed by reduced change cost
|
||||||
|
and increased behavioral consistency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AgentUsage
|
||||||
|
|
||||||
|
### When to Apply This Lens
|
||||||
|
- During refactoring or maintenance work
|
||||||
|
- When change requests repeatedly touch similar code
|
||||||
|
- When bugs recur due to partial updates
|
||||||
|
- During architectural consolidation
|
||||||
|
|
||||||
|
### When to Suspend This Lens
|
||||||
|
- During early exploration or prototyping
|
||||||
|
- When future variability is unclear
|
||||||
|
- When isolation is more valuable than reuse
|
||||||
|
|
||||||
|
### Expected Agent Output
|
||||||
|
- Identified DRY violations with locations
|
||||||
|
- Rationale for why duplication matters
|
||||||
|
- Volatility assessment (stable vs evolving)
|
||||||
|
- Recommended refactor type and target
|
||||||
|
- Risk notes and minimal patch sequence
|
||||||
|
|
||||||
|
xxx
|
||||||
|
|
||||||
388
examples/manpages/README.md
Normal file
388
examples/manpages/README.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# Unix Manpage Schema Validation Example
|
||||||
|
|
||||||
|
This example demonstrates MarkiTect's schema validation system by creating a self-validating documentation set: a schema that defines Unix manpage structure and a comprehensive manual about schema validation that validates against its own schema definition.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This example showcases the "dogfooding" principle - using MarkiTect's schema validation to document schema validation itself. It demonstrates:
|
||||||
|
|
||||||
|
- **Schema-driven documentation** - Defining document structure with JSON Schema
|
||||||
|
- **Self-validation** - The manual validates against the manpage schema it demonstrates
|
||||||
|
- **Reusable patterns** - The manpage schema can validate any Unix-style manual page
|
||||||
|
- **Complete workflow** - From schema creation through validation and refinement
|
||||||
|
|
||||||
|
## Files in This Example
|
||||||
|
|
||||||
|
### `markdown-manpage-schema.json`
|
||||||
|
|
||||||
|
A JSON Schema defining the structure of Unix-style manual pages written in Markdown.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Validates H1 title format: `command(section) - description`
|
||||||
|
- Requires SYNOPSIS and DESCRIPTION sections
|
||||||
|
- Validates heading hierarchy (H1, H2, H3, H4)
|
||||||
|
- Ensures presence of code examples, paragraphs, and emphasis
|
||||||
|
- Includes custom `x-markitect-*` extensions for manpage conventions
|
||||||
|
|
||||||
|
**Schema Requirements:**
|
||||||
|
- Exactly 1 H1 heading (document title)
|
||||||
|
- 3-30 H2 headings (major sections)
|
||||||
|
- 0-50 H3 headings (subsections)
|
||||||
|
- 5-500 paragraphs (content)
|
||||||
|
- 1-50 code blocks (examples)
|
||||||
|
- 10-500 emphasis elements (commands/arguments)
|
||||||
|
|
||||||
|
### `markdown-schema-validation.1.md`
|
||||||
|
|
||||||
|
A comprehensive manual page (section 7) documenting MarkiTect's markdown schema validation system.
|
||||||
|
|
||||||
|
**Sections Include:**
|
||||||
|
- SYNOPSIS - Command syntax reference
|
||||||
|
- DESCRIPTION - How schema validation works
|
||||||
|
- SCHEMA STRUCTURE - JSON Schema format details
|
||||||
|
- COMMANDS - Schema management and validation commands
|
||||||
|
- WORKFLOW - Step-by-step validation workflows
|
||||||
|
- VALIDATION RULES - What schemas validate
|
||||||
|
- ERROR HANDLING - Understanding validation errors
|
||||||
|
- SCHEMA DESIGN - Best practices and anti-patterns
|
||||||
|
- INTEGRATION - CI/CD, git hooks, build systems
|
||||||
|
- EXAMPLES - Practical usage demonstrations
|
||||||
|
- Plus standard manpage sections: FILES, EXIT STATUS, ENVIRONMENT, SEE ALSO, etc.
|
||||||
|
|
||||||
|
**Statistics:**
|
||||||
|
- 19 H2 sections
|
||||||
|
- 24 H3 subsections
|
||||||
|
- 147 paragraphs
|
||||||
|
- 23 code examples
|
||||||
|
- 105 emphasis markers
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
### 1. Validate the Manual Against the Schema
|
||||||
|
|
||||||
|
Verify that the manual conforms to the manpage schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/manpages
|
||||||
|
|
||||||
|
markitect validate markdown-schema-validation.1.md \
|
||||||
|
--schema markdown-manpage-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: ✅ **VALID** - Document structure matches schema requirements
|
||||||
|
|
||||||
|
### 2. Show Detailed Validation
|
||||||
|
|
||||||
|
See detailed validation information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate markdown-schema-validation.1.md \
|
||||||
|
--schema markdown-manpage-schema.json \
|
||||||
|
--detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generate Schema from the Manual
|
||||||
|
|
||||||
|
Analyze the manual's actual structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-generate markdown-schema-validation.1.md \
|
||||||
|
--output actual-structure-schema.json
|
||||||
|
|
||||||
|
cat actual-structure-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Compare the generated schema with the manpage schema to see how the manual conforms.
|
||||||
|
|
||||||
|
### 4. Examine AST Structure
|
||||||
|
|
||||||
|
View the parsed structure of the manual:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect ast-show markdown-schema-validation.1.md --format tree
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in compact format:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect ast-show markdown-schema-validation.1.md --format compact | head -50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Store Schema for Reuse
|
||||||
|
|
||||||
|
Add the manpage schema to MarkiTect's database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-ingest markdown-manpage-schema.json
|
||||||
|
markitect schema-list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Validate Other Manpages
|
||||||
|
|
||||||
|
Use the schema to validate other manual pages in the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate ../../docs/manuals/markitect.1.md \
|
||||||
|
--schema markdown-manpage-schema.json
|
||||||
|
|
||||||
|
markitect validate ../../docs/manuals/issue.1.md \
|
||||||
|
--schema markdown-manpage-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Generate Manpage Template
|
||||||
|
|
||||||
|
Create a template for new manpages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect generate-stub markdown-manpage-schema.json \
|
||||||
|
--output new-manpage-template.md
|
||||||
|
|
||||||
|
cat new-manpage-template.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## What This Example Demonstrates
|
||||||
|
|
||||||
|
### 1. Schema-Driven Documentation
|
||||||
|
|
||||||
|
The manpage schema defines what a valid Unix manual page looks like:
|
||||||
|
|
||||||
|
- Required structural elements (title, synopsis, description)
|
||||||
|
- Heading hierarchy constraints
|
||||||
|
- Content density requirements (minimum paragraphs, code examples)
|
||||||
|
- Formatting conventions (bold commands, italic arguments)
|
||||||
|
|
||||||
|
### 2. Self-Validating System
|
||||||
|
|
||||||
|
The schema validation manual validates against the manpage schema, proving:
|
||||||
|
|
||||||
|
- The schema is practical and usable
|
||||||
|
- The manual follows manpage conventions
|
||||||
|
- Schema validation works as documented
|
||||||
|
- The system is reliable enough to document itself
|
||||||
|
|
||||||
|
### 3. Structural vs Semantic Validation
|
||||||
|
|
||||||
|
The schema validates **structure**, not **content**:
|
||||||
|
|
||||||
|
- ✅ Validates: Correct number of sections, heading levels, code examples present
|
||||||
|
- ❌ Does not validate: Grammar, code correctness, factual accuracy, logical flow
|
||||||
|
|
||||||
|
This distinction is crucial for understanding what schemas can and cannot do.
|
||||||
|
|
||||||
|
### 4. Reusable Patterns
|
||||||
|
|
||||||
|
The manpage schema is a reusable pattern that can:
|
||||||
|
|
||||||
|
- Validate any Unix-style manual page
|
||||||
|
- Enforce documentation consistency across a project
|
||||||
|
- Generate templates for new documentation
|
||||||
|
- Integrate into CI/CD pipelines for quality checks
|
||||||
|
|
||||||
|
### 5. Custom Schema Extensions
|
||||||
|
|
||||||
|
The schema demonstrates MarkiTect's custom extensions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"x-markitect-required-sections": [
|
||||||
|
"SYNOPSIS",
|
||||||
|
"DESCRIPTION"
|
||||||
|
],
|
||||||
|
"x-markitect-recommended-sections": [
|
||||||
|
"OPTIONS",
|
||||||
|
"EXAMPLES",
|
||||||
|
"SEE ALSO"
|
||||||
|
],
|
||||||
|
"x-markitect-conventions": {
|
||||||
|
"heading_case": "UPPERCASE for H2 sections",
|
||||||
|
"command_format": "Bold with **command**",
|
||||||
|
"argument_format": "Italic with *ARG*"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These extensions provide metadata about schema intent and conventions beyond structural validation.
|
||||||
|
|
||||||
|
## Validation Workflow Demonstrated
|
||||||
|
|
||||||
|
This example shows the complete schema validation workflow:
|
||||||
|
|
||||||
|
### Step 1: Schema Creation
|
||||||
|
- Analyze existing manpages (markitect.1.md, issue.1.md)
|
||||||
|
- Identify common structural patterns
|
||||||
|
- Generate base schema from example document
|
||||||
|
- Refine schema to be flexible yet meaningful
|
||||||
|
|
||||||
|
### Step 2: Schema Refinement
|
||||||
|
- Adjust minItems/maxItems for appropriate ranges
|
||||||
|
- Add custom MarkiTect extensions
|
||||||
|
- Include heading patterns and conventions
|
||||||
|
- Balance strictness with flexibility
|
||||||
|
|
||||||
|
### Step 3: Document Creation
|
||||||
|
- Write document following schema structure
|
||||||
|
- Use template generated from schema as starting point
|
||||||
|
- Ensure all required sections present
|
||||||
|
- Include appropriate code examples and formatting
|
||||||
|
|
||||||
|
### Step 4: Validation
|
||||||
|
- Validate document against schema
|
||||||
|
- Review validation errors if any
|
||||||
|
- Fix structural issues
|
||||||
|
- Re-validate until passing
|
||||||
|
|
||||||
|
### Step 5: Iteration
|
||||||
|
- Refine schema based on validation experience
|
||||||
|
- Adjust constraints for real-world use cases
|
||||||
|
- Document lessons learned
|
||||||
|
- Share schema for reuse
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
|
||||||
|
Add to `.github/workflows/docs.yml` or similar:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Validate Manpages
|
||||||
|
run: |
|
||||||
|
for manpage in docs/manuals/*.md; do
|
||||||
|
markitect validate "$manpage" \
|
||||||
|
--schema examples/manpages/markdown-manpage-schema.json \
|
||||||
|
|| exit 1
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
|
||||||
|
Add to `.git/hooks/pre-commit`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
changed_manpages=$(git diff --cached --name-only --diff-filter=ACM | grep 'docs/manuals/.*\.md$')
|
||||||
|
|
||||||
|
for manpage in $changed_manpages; do
|
||||||
|
markitect validate "$manpage" \
|
||||||
|
--schema examples/manpages/markdown-manpage-schema.json \
|
||||||
|
--quiet || {
|
||||||
|
echo "Manpage validation failed: $manpage"
|
||||||
|
markitect validate "$manpage" \
|
||||||
|
--schema examples/manpages/markdown-manpage-schema.json \
|
||||||
|
--detailed-errors
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile Integration
|
||||||
|
|
||||||
|
Add to project `Makefile`:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
.PHONY: validate-manpages
|
||||||
|
validate-manpages:
|
||||||
|
@echo "Validating manual pages..."
|
||||||
|
@for manpage in docs/manuals/*.md; do \
|
||||||
|
markitect validate "$$manpage" \
|
||||||
|
--schema examples/manpages/markdown-manpage-schema.json \
|
||||||
|
|| exit 1; \
|
||||||
|
done
|
||||||
|
@echo "✅ All manpages valid"
|
||||||
|
|
||||||
|
.PHONY: docs
|
||||||
|
docs: validate-manpages
|
||||||
|
# Continue with doc generation...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Lessons from This Example
|
||||||
|
|
||||||
|
### 1. Start with Real Documents
|
||||||
|
|
||||||
|
The manpage schema was created by analyzing existing manpages (markitect.1.md, issue.1.md), not designed in isolation. This ensures the schema reflects real-world usage.
|
||||||
|
|
||||||
|
### 2. Use Ranges, Not Exact Counts
|
||||||
|
|
||||||
|
The schema uses ranges like `5-500 paragraphs` instead of exact counts. This provides flexibility while still enforcing quality standards.
|
||||||
|
|
||||||
|
### 3. Required vs Recommended
|
||||||
|
|
||||||
|
The schema distinguishes between required sections (SYNOPSIS, DESCRIPTION) and recommended sections (EXAMPLES, SEE ALSO), allowing flexibility where appropriate.
|
||||||
|
|
||||||
|
### 4. Validate Structure, Not Semantics
|
||||||
|
|
||||||
|
Schemas validate document structure, not content quality. Grammar checking, code correctness, and factual accuracy require other tools.
|
||||||
|
|
||||||
|
### 5. Progressive Refinement
|
||||||
|
|
||||||
|
Schemas should evolve based on validation experience. Start loose, tighten based on actual needs, never over-specify.
|
||||||
|
|
||||||
|
### 6. Documentation is Essential
|
||||||
|
|
||||||
|
The schema includes extensive metadata about conventions and intent through custom extensions, making it self-documenting.
|
||||||
|
|
||||||
|
## Extending This Example
|
||||||
|
|
||||||
|
### Create Schema Variants
|
||||||
|
|
||||||
|
Create specialized schemas for different manpage types:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For command manpages (section 1)
|
||||||
|
cp markdown-manpage-schema.json command-manpage-schema.json
|
||||||
|
# Edit to require COMMANDS section
|
||||||
|
|
||||||
|
# For format manpages (section 5)
|
||||||
|
cp markdown-manpage-schema.json format-manpage-schema.json
|
||||||
|
# Edit to require FORMAT section
|
||||||
|
|
||||||
|
# For convention manpages (section 7)
|
||||||
|
cp markdown-manpage-schema.json convention-manpage-schema.json
|
||||||
|
# Edit to be more flexible
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Your Own Documentation
|
||||||
|
|
||||||
|
Apply the manpage schema to your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate README
|
||||||
|
markitect validate README.md \
|
||||||
|
--schema markdown-manpage-schema.json
|
||||||
|
|
||||||
|
# May need adjustments for non-manpage docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Schema Family
|
||||||
|
|
||||||
|
Create schemas for related document types:
|
||||||
|
|
||||||
|
- API documentation schema
|
||||||
|
- Tutorial schema
|
||||||
|
- RFC/specification schema
|
||||||
|
- Architecture decision record (ADR) schema
|
||||||
|
|
||||||
|
Each can follow similar validation principles while enforcing type-specific structure.
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- **markdown-schema-validation.1.md** - Complete reference for schema validation
|
||||||
|
- **../../docs/manuals/markitect.1.md** - MarkiTect command reference
|
||||||
|
- **JSON Schema Specification** - https://json-schema.org/
|
||||||
|
- **Unix Manual Page Conventions** - `man 7 man-pages` on Unix systems
|
||||||
|
|
||||||
|
## Validation Results
|
||||||
|
|
||||||
|
This example has been validated to confirm:
|
||||||
|
|
||||||
|
✅ Manual validates against manpage schema
|
||||||
|
✅ Schema is well-formed JSON Schema draft-07
|
||||||
|
✅ All required sections present in manual
|
||||||
|
✅ Heading hierarchy follows Unix conventions
|
||||||
|
✅ Code examples demonstrate actual usage
|
||||||
|
✅ Structure matches defined constraints
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the MarkiTect project. Licensed under MIT License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This example represents a complete, production-ready use case of MarkiTect's schema validation system. The files can be used as-is or adapted for your own documentation requirements.
|
||||||
230
examples/manpages/api-documentation-schema.json
Normal file
230
examples/manpages/api-documentation-schema.json
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "API Endpoint Documentation Schema",
|
||||||
|
"description": "Schema for API endpoint documentation with classification and content control",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"ENDPOINT": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_title",
|
||||||
|
"content_instruction": "HTTP method and endpoint path (e.g., GET /api/v1/users)",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 3,
|
||||||
|
"error_message": "ENDPOINT section must specify the HTTP method and path"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "What this endpoint does and when to use it",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"error_message": "DESCRIPTION is required to explain endpoint functionality"
|
||||||
|
},
|
||||||
|
"AUTHENTICATION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Authentication requirements (API key, OAuth, etc.)",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"error_message": "AUTHENTICATION requirements must be documented"
|
||||||
|
},
|
||||||
|
"REQUEST PARAMETERS": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "List all request parameters with types and descriptions",
|
||||||
|
"alternatives": ["PARAMETERS", "REQUEST", "INPUT"],
|
||||||
|
"warning_if_missing": "Documenting request parameters helps API consumers use the endpoint correctly"
|
||||||
|
},
|
||||||
|
"RESPONSE": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Response format, status codes, and example responses",
|
||||||
|
"min_code_blocks": 1,
|
||||||
|
"warning_if_missing": "Response documentation with examples improves API usability"
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Complete request/response examples",
|
||||||
|
"min_code_blocks": 2,
|
||||||
|
"warning_if_missing": "Examples make API documentation significantly more useful"
|
||||||
|
},
|
||||||
|
"ERROR CODES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Possible error responses and how to handle them",
|
||||||
|
"alternatives": ["ERRORS", "ERROR HANDLING"],
|
||||||
|
"warning_if_missing": "Error documentation helps developers handle failures gracefully"
|
||||||
|
},
|
||||||
|
"RATE LIMITING": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Rate limit information for this endpoint"
|
||||||
|
},
|
||||||
|
"CHANGELOG": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Version history and changes to this endpoint"
|
||||||
|
},
|
||||||
|
"SEE ALSO": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Related endpoints and documentation"
|
||||||
|
},
|
||||||
|
"IMPLEMENTATION NOTES": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Implementation details should be in developer documentation, not API docs"
|
||||||
|
},
|
||||||
|
"INTERNAL API": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Internal API endpoints must not be in public documentation"
|
||||||
|
},
|
||||||
|
"EXPERIMENTAL": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Experimental features must not be in stable API documentation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"endpoint": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[A-Z]+\\*\\*",
|
||||||
|
"`/api/",
|
||||||
|
"\\*\\*[A-Z]+\\*\\*\\s+`/[^`]+`"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 5,
|
||||||
|
"max_words": 50,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Format: **METHOD** `endpoint_path`",
|
||||||
|
"Example: **GET** `/api/v1/users/{id}`",
|
||||||
|
"Use bold for HTTP method",
|
||||||
|
"Use code formatting for path",
|
||||||
|
"Include path parameters in curly braces"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"TBD",
|
||||||
|
"Coming soon"
|
||||||
|
],
|
||||||
|
"forbidden_patterns": [
|
||||||
|
"password",
|
||||||
|
"secret",
|
||||||
|
"api[_-]?key\\s*=",
|
||||||
|
"token\\s*="
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 30,
|
||||||
|
"max_words": 500,
|
||||||
|
"readability_target": "technical",
|
||||||
|
"min_sentences": 2
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Explain what the endpoint does",
|
||||||
|
"Describe the main use case",
|
||||||
|
"Mention any prerequisites",
|
||||||
|
"Note any side effects",
|
||||||
|
"Keep concise but complete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"request_parameters": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z_]+\\*\\*",
|
||||||
|
"\\*[A-Za-z]+\\*"
|
||||||
|
],
|
||||||
|
"content_instructions": [
|
||||||
|
"Use bold for parameter names",
|
||||||
|
"Use italic for parameter types",
|
||||||
|
"Include: name, type, required/optional, description",
|
||||||
|
"Use definition list format",
|
||||||
|
"Specify default values where applicable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"required_patterns": [
|
||||||
|
"```json",
|
||||||
|
"200",
|
||||||
|
"\\{[^}]*\\}"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 500,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Show example JSON response",
|
||||||
|
"Document all status codes",
|
||||||
|
"Explain response fields",
|
||||||
|
"Include success and error examples",
|
||||||
|
"Use proper JSON formatting in code blocks"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"required_patterns": [
|
||||||
|
"```bash",
|
||||||
|
"curl",
|
||||||
|
"```json"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 100,
|
||||||
|
"max_words": 1000,
|
||||||
|
"readability_target": "general"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Provide complete curl examples",
|
||||||
|
"Show request headers",
|
||||||
|
"Include example responses",
|
||||||
|
"Add explanatory comments",
|
||||||
|
"Cover common scenarios"
|
||||||
|
],
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": true,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 15
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 8,
|
||||||
|
"maxItems": 200
|
||||||
|
},
|
||||||
|
"code_blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 30
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 15,
|
||||||
|
"maxItems": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
examples/manpages/enhanced-manpage-schema.json
Normal file
229
examples/manpages/enhanced-manpage-schema.json
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Enhanced Markdown Manpage Schema with Classifications",
|
||||||
|
"description": "JSON schema for Unix-style manual pages with section classification and content control",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_title",
|
||||||
|
"content_instruction": "Brief command syntax showing all options and arguments in standard format",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 5,
|
||||||
|
"min_code_blocks": 0,
|
||||||
|
"max_code_blocks": 3,
|
||||||
|
"error_message": "SYNOPSIS section is mandatory for all manpages per Unix conventions"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Detailed explanation of what the command does, its purpose, and main functionality",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"max_paragraphs": 50,
|
||||||
|
"error_message": "DESCRIPTION section is mandatory for all manpages"
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Practical usage examples with explanations demonstrating common use cases",
|
||||||
|
"min_code_blocks": 3,
|
||||||
|
"max_code_blocks": 20,
|
||||||
|
"warning_if_missing": "Examples greatly improve manpage usability - highly recommended"
|
||||||
|
},
|
||||||
|
"SEE ALSO": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Related commands, configuration files, and documentation references",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"warning_if_missing": "Cross-references help users discover related functionality"
|
||||||
|
},
|
||||||
|
"OPTIONS": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Detailed option descriptions with all flags and their behaviors",
|
||||||
|
"alternatives": ["GLOBAL OPTIONS", "COMMAND OPTIONS", "FLAGS"],
|
||||||
|
"warning_if_missing": "Documenting command options helps users understand available functionality"
|
||||||
|
},
|
||||||
|
"BUGS": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Known issues, limitations, and bug reporting information"
|
||||||
|
},
|
||||||
|
"AUTHORS": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "List of contributors and maintainers"
|
||||||
|
},
|
||||||
|
"COPYRIGHT": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Copyright statement and license information"
|
||||||
|
},
|
||||||
|
"HISTORY": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Historical information about command development"
|
||||||
|
},
|
||||||
|
"DEPRECATED": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Consider moving deprecated content to historical documentation or HISTORY section"
|
||||||
|
},
|
||||||
|
"OLD_SYNTAX": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Old syntax should be documented in HISTORY or removed entirely"
|
||||||
|
},
|
||||||
|
"INTERNAL_NOTES": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Internal notes must not appear in published manpages - move to developer documentation"
|
||||||
|
},
|
||||||
|
"TODO": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "TODO sections are for development only - remove before publication"
|
||||||
|
},
|
||||||
|
"DRAFT": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "DRAFT markers must be removed before publication"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"synopsis": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z][a-z0-9-]*\\*\\*",
|
||||||
|
"\\[.*\\]"
|
||||||
|
],
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"TBD"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 5,
|
||||||
|
"max_words": 150,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Show command name in bold (e.g., **command**)",
|
||||||
|
"Use brackets [] for optional arguments",
|
||||||
|
"Use italic *ARG* for required arguments",
|
||||||
|
"Keep synopsis concise (1-5 lines maximum)",
|
||||||
|
"Use ellipsis ... to indicate repeatable arguments"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"\\bWIP\\b",
|
||||||
|
"\\bXXX\\b"
|
||||||
|
],
|
||||||
|
"forbidden_patterns": [
|
||||||
|
"password\\s*=\\s*[\"'].*[\"']",
|
||||||
|
"api[_-]?key\\s*=\\s*[\"'].*[\"']",
|
||||||
|
"secret\\s*=\\s*[\"'].*[\"']"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 1000,
|
||||||
|
"readability_target": "technical",
|
||||||
|
"min_sentences": 3
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Start with what the command does",
|
||||||
|
"Explain why users would use it",
|
||||||
|
"Describe main functionality and features",
|
||||||
|
"Mention any prerequisites or requirements",
|
||||||
|
"Keep technical but accessible"
|
||||||
|
],
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": false,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"required_patterns": [
|
||||||
|
"```",
|
||||||
|
"#"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 100,
|
||||||
|
"max_words": 2000,
|
||||||
|
"readability_target": "general"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Use bash code blocks for command examples",
|
||||||
|
"Include comments explaining what each example does",
|
||||||
|
"Start with simple examples, progress to complex",
|
||||||
|
"Show actual output when helpful",
|
||||||
|
"Cover common use cases first"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Document heading structure",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Title heading in format: command(section) - description",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9-]+\\([0-9]\\) - .+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Main section headings",
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 30
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Subsection headings",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Text paragraphs",
|
||||||
|
"minItems": 10,
|
||||||
|
"maxItems": 500
|
||||||
|
},
|
||||||
|
"code_blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Code examples",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 50
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Lists for options and structured information",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 100
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Bold and italic text for commands and arguments",
|
||||||
|
"minItems": 20,
|
||||||
|
"maxItems": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs", "code_blocks", "emphasis"]
|
||||||
|
}
|
||||||
246
examples/manpages/markdown-manpage-schema.json
Normal file
246
examples/manpages/markdown-manpage-schema.json
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"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",
|
||||||
|
"description": "Document heading structure following Unix manpage conventions",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Title heading: command(section) - brief description",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9-]+\\([0-9]\\) - .+",
|
||||||
|
"description": "Must follow format: command(section) - description"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content", "level"]
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Main section headings (SYNOPSIS, DESCRIPTION, etc.)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Section name in UPPERCASE"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content", "level"]
|
||||||
|
},
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 30
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Subsection headings (optional, for grouping commands or options)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content", "level"]
|
||||||
|
},
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Text paragraphs containing descriptions and explanations",
|
||||||
|
"minItems": 5,
|
||||||
|
"maxItems": 500
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Lists for options, examples, or structured information",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 100
|
||||||
|
},
|
||||||
|
"code_blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Code examples and command demonstrations",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 50
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Bold and italic emphasis for commands, options, and arguments",
|
||||||
|
"minItems": 10,
|
||||||
|
"maxItems": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs", "code_blocks", "emphasis"]
|
||||||
|
}
|
||||||
901
examples/manpages/markdown-schema-validation.1.md
Normal file
901
examples/manpages/markdown-schema-validation.1.md
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
# markdown-schema-validation(7) - Structured Document Validation with JSON Schema
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
|
||||||
|
**markitect schema-generate** *SOURCE_FILE* [**--output** *SCHEMA_FILE*]
|
||||||
|
|
||||||
|
**markitect schema-ingest** *SCHEMA_FILE*
|
||||||
|
|
||||||
|
**markitect validate** *DOCUMENT* *SCHEMA*
|
||||||
|
|
||||||
|
**markitect generate-stub** *SCHEMA* [**--output** *FILE*]
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
Markdown Schema Validation is MarkiTect's system for enforcing structural consistency in markdown documents. Unlike traditional markdown linters that check syntax, schema validation ensures documents conform to predefined structural patterns by validating their Abstract Syntax Tree (AST) representation against JSON Schema definitions.
|
||||||
|
|
||||||
|
This approach enables content management workflows where document structure is as important as content, making it ideal for technical documentation, business documents, and any scenario requiring consistent document templates.
|
||||||
|
|
||||||
|
### How Schema Validation Works
|
||||||
|
|
||||||
|
MarkiTect parses markdown files into an AST representation, then validates the AST structure against JSON schemas. The validation process checks:
|
||||||
|
|
||||||
|
- **Heading hierarchy** - Required heading levels and counts
|
||||||
|
- **Content elements** - Minimum and maximum paragraph counts
|
||||||
|
- **Structural patterns** - Presence of lists, code blocks, tables
|
||||||
|
- **Section organization** - Required and optional document sections
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
MarkiTect schemas are standard JSON Schema (draft-07) documents with custom extensions for markdown-specific validation.
|
||||||
|
|
||||||
|
#### Standard Properties
|
||||||
|
|
||||||
|
**properties.headings**
|
||||||
|
: Defines heading structure by level (level_1, level_2, level_3)
|
||||||
|
: Each level specifies minItems, maxItems, and content patterns
|
||||||
|
|
||||||
|
**properties.paragraphs**
|
||||||
|
: Array constraints for paragraph counts
|
||||||
|
: Validates document length and content density
|
||||||
|
|
||||||
|
**properties.code_blocks**
|
||||||
|
: Array constraints for code examples
|
||||||
|
: Ensures technical documentation includes examples
|
||||||
|
|
||||||
|
**properties.lists**
|
||||||
|
: Array constraints for list elements
|
||||||
|
: Validates presence of structured information
|
||||||
|
|
||||||
|
**properties.emphasis**
|
||||||
|
: Array constraints for bold and italic text
|
||||||
|
: Ensures appropriate use of emphasis
|
||||||
|
|
||||||
|
#### MarkiTect Extensions
|
||||||
|
|
||||||
|
MarkiTect extends JSON Schema with custom properties prefixed with **x-markitect-**:
|
||||||
|
|
||||||
|
**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-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
|
||||||
|
: Focuses on heading structure without content validation
|
||||||
|
|
||||||
|
**x-markitect-heading-text-capture**
|
||||||
|
: Boolean enabling exact heading text validation
|
||||||
|
: Enforces specific section names
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
|
||||||
|
### Schema Generation
|
||||||
|
|
||||||
|
**markitect schema-generate** *SOURCE_FILE*
|
||||||
|
: Analyzes markdown file AST and generates JSON schema
|
||||||
|
: Schema describes actual structure found in source document
|
||||||
|
|
||||||
|
**--output** *SCHEMA_FILE*
|
||||||
|
: Write schema to file instead of stdout
|
||||||
|
: Default: outputs to terminal
|
||||||
|
|
||||||
|
**--max-depth** *N*
|
||||||
|
: Limit heading analysis to depth N
|
||||||
|
: Useful for outline-focused schemas
|
||||||
|
|
||||||
|
### Schema Management
|
||||||
|
|
||||||
|
**markitect schema-ingest** *SCHEMA_FILE*
|
||||||
|
: Store schema in MarkiTect database
|
||||||
|
: Registers schema for reuse with validation commands
|
||||||
|
|
||||||
|
**markitect schema-list**
|
||||||
|
: Display all stored schemas
|
||||||
|
: Shows schema names and metadata
|
||||||
|
|
||||||
|
**markitect schema-get** *SCHEMA_NAME*
|
||||||
|
: Retrieve stored schema
|
||||||
|
: Outputs JSON schema to stdout
|
||||||
|
|
||||||
|
**markitect schema-delete** *SCHEMA_NAME*
|
||||||
|
: Remove schema from database
|
||||||
|
: Permanently deletes schema definition
|
||||||
|
|
||||||
|
### Document Validation
|
||||||
|
|
||||||
|
**markitect validate** *DOCUMENT* *SCHEMA*
|
||||||
|
: Validate markdown document against schema
|
||||||
|
: Returns exit code 0 for valid, 4 for invalid
|
||||||
|
|
||||||
|
**--detailed-errors**
|
||||||
|
: Show detailed validation error messages
|
||||||
|
: Includes suggestions for fixing violations
|
||||||
|
|
||||||
|
**--quiet**
|
||||||
|
: Suppress output, exit code only
|
||||||
|
: Useful for scripting and automation
|
||||||
|
|
||||||
|
### Template Generation
|
||||||
|
|
||||||
|
**markitect generate-stub** *SCHEMA*
|
||||||
|
: Generate markdown template from schema
|
||||||
|
: Creates document outline following schema structure
|
||||||
|
|
||||||
|
**--output** *FILE*
|
||||||
|
: Write template to file
|
||||||
|
: Default: outputs to stdout
|
||||||
|
|
||||||
|
## WORKFLOW
|
||||||
|
|
||||||
|
### Schema-Driven Development Workflow
|
||||||
|
|
||||||
|
The typical workflow for schema-based document management:
|
||||||
|
|
||||||
|
**1. Generate Schema from Example**
|
||||||
|
|
||||||
|
Create or identify an exemplar document with the desired structure, then generate its schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-generate exemplar.md --output doc-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Refine Schema**
|
||||||
|
|
||||||
|
Edit the generated schema to adjust constraints:
|
||||||
|
|
||||||
|
- Change minItems/maxItems for flexibility
|
||||||
|
- Add required-sections extensions
|
||||||
|
- Adjust heading patterns
|
||||||
|
- Add content instructions
|
||||||
|
|
||||||
|
**3. Store Schema**
|
||||||
|
|
||||||
|
Register schema for reuse:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-ingest doc-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Generate Templates**
|
||||||
|
|
||||||
|
Create document templates from schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect generate-stub doc-schema.json --output template.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Create Documents**
|
||||||
|
|
||||||
|
Write new documents using template as starting point, or use existing documents.
|
||||||
|
|
||||||
|
**6. Validate Documents**
|
||||||
|
|
||||||
|
Ensure documents conform to schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate new-document.md doc-schema.json
|
||||||
|
|
||||||
|
markitect validate new-document.md doc-schema.json --detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
**7. Iterate**
|
||||||
|
|
||||||
|
Fix validation errors and re-validate until document passes.
|
||||||
|
|
||||||
|
### Batch Validation Workflow
|
||||||
|
|
||||||
|
For managing multiple documents:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for doc in docs/*.md; do
|
||||||
|
markitect validate "$doc" doc-schema.json --quiet || echo "Failed: $doc"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## VALIDATION RULES
|
||||||
|
|
||||||
|
### Heading Validation
|
||||||
|
|
||||||
|
Schemas validate heading structure through the **headings** property:
|
||||||
|
|
||||||
|
**level_1** headings must appear exactly once (document title)
|
||||||
|
|
||||||
|
**level_2** headings represent major sections (minItems/maxItems set bounds)
|
||||||
|
|
||||||
|
**level_3** headings provide subsections (often optional with minItems: 0)
|
||||||
|
|
||||||
|
Heading content can be validated with **pattern** or **enum** constraints for exact section names.
|
||||||
|
|
||||||
|
### Content Element Validation
|
||||||
|
|
||||||
|
**Paragraphs** - Validates document has sufficient descriptive content
|
||||||
|
|
||||||
|
**Code blocks** - Ensures technical documents include examples
|
||||||
|
|
||||||
|
**Lists** - Validates structured information presence
|
||||||
|
|
||||||
|
**Emphasis** - Checks for appropriate use of bold/italic formatting
|
||||||
|
|
||||||
|
Constraints use **minItems** and **maxItems** to set acceptable ranges.
|
||||||
|
|
||||||
|
### Metadata Validation
|
||||||
|
|
||||||
|
The **metadata** property validates overall document characteristics:
|
||||||
|
|
||||||
|
**total_elements** - Total AST node count
|
||||||
|
|
||||||
|
**structure_types** - Array of AST node types present
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
**Missing Required Section**
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Required section 'SYNOPSIS' not found
|
||||||
|
Suggestion: Add H2 heading '## SYNOPSIS' near document start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Insufficient Content**
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Too few paragraphs (found 3, minimum 5 required)
|
||||||
|
Suggestion: Add descriptive content to meet minimum paragraph count
|
||||||
|
```
|
||||||
|
|
||||||
|
**Heading Count Mismatch**
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Too many H2 headings (found 15, maximum 13 allowed)
|
||||||
|
Suggestion: Combine related sections or adjust schema maxItems
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Type Mismatch**
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Expected structure types not found: code_blocks
|
||||||
|
Suggestion: Add code examples using fenced code blocks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Detailed Error Mode
|
||||||
|
|
||||||
|
Enable detailed errors for actionable feedback:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate document.md schema.json --detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
Output includes:
|
||||||
|
- Specific constraint violations
|
||||||
|
- Location information when available
|
||||||
|
- Suggestions for fixes
|
||||||
|
- Schema path to failing constraint
|
||||||
|
|
||||||
|
## SCHEMA DESIGN
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
**Start with Real Documents**
|
||||||
|
|
||||||
|
Generate schemas from actual documents rather than writing from scratch. Real documents provide realistic constraints.
|
||||||
|
|
||||||
|
**Use Ranges, Not Exact Counts**
|
||||||
|
|
||||||
|
Allow flexibility with minItems/maxItems ranges:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"paragraphs": {
|
||||||
|
"minItems": 10,
|
||||||
|
"maxItems": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid exact counts (**const**) unless structure is truly rigid.
|
||||||
|
|
||||||
|
**Section Classification**
|
||||||
|
|
||||||
|
Use the five-level classification system to define section requirements:
|
||||||
|
|
||||||
|
```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**
|
||||||
|
|
||||||
|
Use regex patterns for flexible heading validation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pattern": "^[A-Z][A-Z ]+$"
|
||||||
|
```
|
||||||
|
|
||||||
|
Matches UPPERCASE section names while allowing variation.
|
||||||
|
|
||||||
|
**Progressive Refinement**
|
||||||
|
|
||||||
|
Start with loose constraints, tighten based on validation experience with real documents.
|
||||||
|
|
||||||
|
### Anti-Patterns
|
||||||
|
|
||||||
|
**Over-Specification**
|
||||||
|
|
||||||
|
Avoid schemas that are too specific:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"paragraphs": { "const": 47 }
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires exactly 47 paragraphs, which is too rigid for most use cases.
|
||||||
|
|
||||||
|
**Under-Specification**
|
||||||
|
|
||||||
|
Avoid schemas that validate nothing:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"paragraphs": { "minItems": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Provide meaningful constraints that ensure document quality.
|
||||||
|
|
||||||
|
**Semantic Validation**
|
||||||
|
|
||||||
|
Schemas validate structure, not content. Don't expect schemas to validate:
|
||||||
|
|
||||||
|
- Correct grammar or spelling
|
||||||
|
- Factual accuracy
|
||||||
|
- Code correctness
|
||||||
|
- Logical flow
|
||||||
|
|
||||||
|
Use other tools for semantic validation.
|
||||||
|
|
||||||
|
## INTEGRATION
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
|
||||||
|
Validate documentation in continuous integration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate README.md readme-schema.json --quiet
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo "Documentation valid"
|
||||||
|
else
|
||||||
|
echo "Documentation validation failed"
|
||||||
|
markitect validate README.md readme-schema.json --detailed-errors
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
Pre-commit hook for automatic validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
changed_docs=$(git diff --cached --name-only --diff-filter=ACM | grep '.md$')
|
||||||
|
|
||||||
|
for doc in $changed_docs; do
|
||||||
|
schema="${doc%.md}-schema.json"
|
||||||
|
if [ -f "$schema" ]; then
|
||||||
|
markitect validate "$doc" "$schema" || exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Systems
|
||||||
|
|
||||||
|
Makefile integration:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
.PHONY: validate-docs
|
||||||
|
validate-docs:
|
||||||
|
@for doc in docs/*.md; do \
|
||||||
|
markitect validate "$$doc" doc-schema.json || exit 1; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: validate-docs
|
||||||
|
# Build process continues only if docs validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
### Generate Schema from Document
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-generate examples/invoice.md --output invoice-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Store Schema for Reuse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-ingest invoice-schema.json
|
||||||
|
markitect schema-list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Single Document
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate draft-invoice.md invoice-schema.json
|
||||||
|
|
||||||
|
markitect validate draft-invoice.md invoice-schema.json --detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for invoice in invoices/*.md; do
|
||||||
|
markitect validate "$invoice" invoice-schema.json --quiet
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Invalid: $invoice"
|
||||||
|
markitect validate "$invoice" invoice-schema.json --detailed-errors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Generation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect generate-stub invoice-schema.json --output new-invoice-template.md
|
||||||
|
|
||||||
|
cat new-invoice-template.md
|
||||||
|
|
||||||
|
markitect validate new-invoice-template.md invoice-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Refinement Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-generate example.md --output v1-schema.json
|
||||||
|
|
||||||
|
markitect validate test-doc.md v1-schema.json --detailed-errors
|
||||||
|
|
||||||
|
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**
|
||||||
|
: JSON schema files defining document structure
|
||||||
|
: Standard JSON Schema draft-07 format with MarkiTect extensions
|
||||||
|
|
||||||
|
**markitect.db**
|
||||||
|
: Database storing ingested schemas
|
||||||
|
: SQLite database in current directory or specified path
|
||||||
|
|
||||||
|
**.markitect.yml**
|
||||||
|
: Configuration file for default schemas
|
||||||
|
: YAML format with schema paths and validation rules
|
||||||
|
|
||||||
|
## EXIT STATUS
|
||||||
|
|
||||||
|
**0**
|
||||||
|
: Success - document is valid
|
||||||
|
|
||||||
|
**1**
|
||||||
|
: General error - file not found, invalid arguments
|
||||||
|
|
||||||
|
**2**
|
||||||
|
: Configuration error - invalid schema file
|
||||||
|
|
||||||
|
**3**
|
||||||
|
: Database error - schema storage/retrieval failed
|
||||||
|
|
||||||
|
**4**
|
||||||
|
: Validation error - document does not conform to schema
|
||||||
|
|
||||||
|
## ENVIRONMENT
|
||||||
|
|
||||||
|
**MARKITECT_DATABASE**
|
||||||
|
: Path to database file for schema storage
|
||||||
|
: Default: markitect.db in current directory
|
||||||
|
|
||||||
|
**MARKITECT_SCHEMA_PATH**
|
||||||
|
: Search path for schema files
|
||||||
|
: Colon-separated list of directories
|
||||||
|
|
||||||
|
**MARKITECT_VALIDATION_STRICT**
|
||||||
|
: Enable strict validation mode
|
||||||
|
: Any non-empty value enables strict mode
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
|
||||||
|
**markitect**(1), **json-schema**(7), **markdown-it**(7)
|
||||||
|
|
||||||
|
Related documentation:
|
||||||
|
- JSON Schema Specification (https://json-schema.org/)
|
||||||
|
- MarkiTect Schema Reference
|
||||||
|
- AST Structure Documentation
|
||||||
|
- Template System Guide
|
||||||
|
|
||||||
|
## LIMITATIONS
|
||||||
|
|
||||||
|
Schema validation has inherent limitations:
|
||||||
|
|
||||||
|
**Structure Only**
|
||||||
|
|
||||||
|
Schemas validate document structure, not content semantics. Cannot validate:
|
||||||
|
- Factual correctness
|
||||||
|
- Code functionality
|
||||||
|
- Logical consistency
|
||||||
|
- Language quality
|
||||||
|
|
||||||
|
**AST-Based**
|
||||||
|
|
||||||
|
Validation operates on parsed AST, not raw markdown. Some markdown formatting details may not be preserved or validated.
|
||||||
|
|
||||||
|
**Performance**
|
||||||
|
|
||||||
|
Large documents with complex schemas may have performance implications. AST caching mitigates this for repeated validations.
|
||||||
|
|
||||||
|
**Schema Complexity**
|
||||||
|
|
||||||
|
Very complex schemas can become difficult to maintain. Keep schemas as simple as possible while meeting requirements.
|
||||||
|
|
||||||
|
## BUGS
|
||||||
|
|
||||||
|
Report bugs at: https://github.com/markitect/markitect/issues
|
||||||
|
|
||||||
|
Known issues:
|
||||||
|
- Schema generation from very large documents may be slow
|
||||||
|
- Some edge cases in heading pattern matching
|
||||||
|
- Limited support for custom markdown extensions
|
||||||
|
|
||||||
|
## AUTHORS
|
||||||
|
|
||||||
|
MarkiTect development team
|
||||||
|
|
||||||
|
Schema validation system designed for structured content management and documentation consistency.
|
||||||
|
|
||||||
|
## COPYRIGHT
|
||||||
|
|
||||||
|
Copyright (c) 2025 MarkiTect Project. Licensed under MIT License.
|
||||||
|
|
||||||
|
## VERSION
|
||||||
|
|
||||||
|
This manual documents schema validation in MarkiTect version 1.0 and later.
|
||||||
309
examples/schemas/manpage-schema-v1.md
Normal file
309
examples/schemas/manpage-schema-v1.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Manpage Schema v1.0
|
||||||
|
|
||||||
|
**Schema ID:** `https://markitect.dev/schemas/manpage/v1`
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Status:** Stable
|
||||||
|
**Created:** 2026-01-04
|
||||||
|
**Document Types:** Manual pages, CLI documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This schema validates Unix/Linux manual page documentation following standard conventions. Manual pages (man pages) are the traditional form of documentation for Unix commands and system calls.
|
||||||
|
|
||||||
|
## Typical Structure
|
||||||
|
|
||||||
|
A well-formed manual page includes:
|
||||||
|
|
||||||
|
1. **NAME** - Command name and one-line description
|
||||||
|
2. **SYNOPSIS** - Command syntax showing all options
|
||||||
|
3. **DESCRIPTION** - Detailed explanation of what the command does
|
||||||
|
4. **OPTIONS** - Description of each command-line option
|
||||||
|
5. **EXAMPLES** - Practical usage examples
|
||||||
|
6. **SEE ALSO** - Related commands or documentation
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Validate a Manpage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate mycommand.1.md --schema manpage-schema-v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Manpage Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect generate-stub manpage-schema-v1.json --output newcommand.1.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refine Existing Schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-refine manpage-schema-v1.json --loosen-counts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Complete examples can be found in:
|
||||||
|
- [examples/manpages/markdown-schema-validation.1.md](../manpages/markdown-schema-validation.1.md)
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Required Sections (Level 2 Headings)
|
||||||
|
|
||||||
|
**NAME**
|
||||||
|
- **Classification:** Required
|
||||||
|
- **Content:** Command name in bold followed by brief description
|
||||||
|
- **Format:** `**command** - one line description`
|
||||||
|
- **Example:** `**markitect** - validate and analyze markdown documents`
|
||||||
|
|
||||||
|
**SYNOPSIS**
|
||||||
|
- **Classification:** Required
|
||||||
|
- **Content:** Command syntax with all options
|
||||||
|
- **Min code blocks:** 1
|
||||||
|
- **Max paragraphs:** 3
|
||||||
|
- **Example:**
|
||||||
|
```bash
|
||||||
|
markitect [OPTIONS] COMMAND [ARGS]
|
||||||
|
```
|
||||||
|
|
||||||
|
**DESCRIPTION**
|
||||||
|
- **Classification:** Required
|
||||||
|
- **Content:** Detailed explanation of the command
|
||||||
|
- **Min paragraphs:** 2
|
||||||
|
- **Min words:** 50
|
||||||
|
|
||||||
|
### Recommended Sections
|
||||||
|
|
||||||
|
**OPTIONS**
|
||||||
|
- **Classification:** Recommended
|
||||||
|
- **Content:** Definition list or table of command-line options
|
||||||
|
- **Format:** Each option as `**--option** *type*` followed by description
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
- **Classification:** Recommended
|
||||||
|
- **Content:** Practical usage examples with explanations
|
||||||
|
- **Min code blocks:** 1
|
||||||
|
- **Benefit:** Greatly improves documentation usability
|
||||||
|
|
||||||
|
### Optional Sections
|
||||||
|
|
||||||
|
**ENVIRONMENT**
|
||||||
|
- Environment variables affecting command behavior
|
||||||
|
|
||||||
|
**FILES**
|
||||||
|
- Important files used or created by the command
|
||||||
|
|
||||||
|
**EXIT STATUS**
|
||||||
|
- Explanation of exit codes
|
||||||
|
|
||||||
|
**BUGS**
|
||||||
|
- Known issues or limitations
|
||||||
|
|
||||||
|
**AUTHORS**
|
||||||
|
- Command authors and contributors
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
- Related commands or documentation
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/manpage/v1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Unix Manual Page Schema",
|
||||||
|
"description": "Schema for validating Unix/Linux manual page documentation",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Document title (command name)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": ".*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Section headings",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"NAME",
|
||||||
|
"SYNOPSIS",
|
||||||
|
"DESCRIPTION",
|
||||||
|
"OPTIONS",
|
||||||
|
"EXAMPLES",
|
||||||
|
"ENVIRONMENT",
|
||||||
|
"FILES",
|
||||||
|
"EXIT STATUS",
|
||||||
|
"BUGS",
|
||||||
|
"AUTHORS",
|
||||||
|
"SEE ALSO"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Paragraph content",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 3
|
||||||
|
},
|
||||||
|
"code_blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Code examples and command syntax",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"language": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs"],
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"NAME": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Command name in bold followed by brief description",
|
||||||
|
"pattern": "\\*\\*[a-z-]+\\*\\*.*",
|
||||||
|
"error_if_missing": "NAME section is required in all manual pages"
|
||||||
|
},
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Command syntax showing all options and arguments",
|
||||||
|
"min_code_blocks": 1,
|
||||||
|
"error_if_missing": "SYNOPSIS section is required to show command usage"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Detailed explanation of what the command does and when to use it",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"min_words": 50,
|
||||||
|
"error_if_missing": "DESCRIPTION section is required for detailed explanation"
|
||||||
|
},
|
||||||
|
"OPTIONS": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Description of each command-line option with syntax and explanation",
|
||||||
|
"warning_if_missing": "OPTIONS section recommended for commands with options"
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Practical usage examples with explanations",
|
||||||
|
"min_code_blocks": 1,
|
||||||
|
"warning_if_missing": "EXAMPLES greatly improve documentation usability"
|
||||||
|
},
|
||||||
|
"ENVIRONMENT": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Environment variables that affect command behavior"
|
||||||
|
},
|
||||||
|
"FILES": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Important files used or created by the command"
|
||||||
|
},
|
||||||
|
"SEE ALSO": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Related commands or documentation references"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"name_section": {
|
||||||
|
"required_pattern": "\\*\\*[a-z-]+\\*\\*",
|
||||||
|
"description": "Command name must be in bold",
|
||||||
|
"example": "**markitect** - validate and analyze markdown documents"
|
||||||
|
},
|
||||||
|
"synopsis_section": {
|
||||||
|
"min_code_blocks": 1,
|
||||||
|
"code_block_language": "bash",
|
||||||
|
"description": "Must include at least one code block showing command syntax"
|
||||||
|
},
|
||||||
|
"description_section": {
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"min_words": 50,
|
||||||
|
"description": "Detailed description with at least 50 words across 2+ paragraphs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-metadata": {
|
||||||
|
"schema-type": "document-schema",
|
||||||
|
"domain": "manpage",
|
||||||
|
"document-types": ["manual-page", "man-page", "cli-documentation"],
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "MarkiTect Project",
|
||||||
|
"created": "2026-01-04",
|
||||||
|
"updated": "2026-01-04",
|
||||||
|
"example": "examples/manpages/markdown-schema-validation.1.md",
|
||||||
|
"supersedes": null,
|
||||||
|
"superseded-by": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial stable release
|
||||||
|
- Validates standard manpage sections
|
||||||
|
- Classification system (required/recommended/optional)
|
||||||
|
- Content control for NAME, SYNOPSIS, DESCRIPTION
|
||||||
|
- MarkiTect extensions for improved validation
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Planned for v2.0:
|
||||||
|
- Multi-language support (internationalization)
|
||||||
|
- Extended sections (DIAGNOSTICS, SECURITY, STANDARDS)
|
||||||
|
- Cross-reference validation
|
||||||
|
- Version-specific section variants (man1, man5, man8)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To suggest improvements to this schema:
|
||||||
|
1. Open an issue describing the enhancement
|
||||||
|
2. Provide example documents demonstrating the need
|
||||||
|
3. Propose specific schema changes
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This schema is part of the MarkiTect project and follows the same license.
|
||||||
287
examples/terminology/README.md
Normal file
287
examples/terminology/README.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# Terminology Document Example
|
||||||
|
|
||||||
|
This example demonstrates how to use MarkiTect schemas to validate terminology and glossary documents.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Terminology documents (glossaries, dictionaries, lexicons) benefit from consistent structure and validation. This example shows how to:
|
||||||
|
|
||||||
|
1. Structure terminology documents with clear categories and term definitions
|
||||||
|
2. Validate terminology documents using JSON schemas
|
||||||
|
3. Use MarkiTect's schema extensions for content control
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **terminology-example.md** - Example terminology document with proper structure
|
||||||
|
- **terminology-schema.json** - JSON schema for validating terminology documents
|
||||||
|
- **README.md** - This file
|
||||||
|
|
||||||
|
## Terminology Document Structure
|
||||||
|
|
||||||
|
A well-structured terminology document includes:
|
||||||
|
|
||||||
|
### Required Elements
|
||||||
|
|
||||||
|
1. **Main Title (Level 1 Heading)**
|
||||||
|
- Should include keywords: "Terminology", "Glossary", "Terms", or "Definitions"
|
||||||
|
|
||||||
|
2. **Category Sections (Level 2 Headings)**
|
||||||
|
- Organize terms into logical groups
|
||||||
|
- Examples: "Core Concepts", "Document Types", "Process Terms"
|
||||||
|
|
||||||
|
3. **Term Definitions (Level 3 Headings)**
|
||||||
|
- Each term as a level 3 heading
|
||||||
|
- Followed by structured content
|
||||||
|
|
||||||
|
### Term Structure
|
||||||
|
|
||||||
|
Each term should include:
|
||||||
|
|
||||||
|
**Required:**
|
||||||
|
- **Definition:** Clear, concise explanation of the term
|
||||||
|
|
||||||
|
**Optional (but recommended):**
|
||||||
|
- **Synonyms:** Alternative names or abbreviations
|
||||||
|
- **Related Terms:** Links to related concepts
|
||||||
|
- **Example:** Practical usage example
|
||||||
|
- **Use Cases:** Common scenarios
|
||||||
|
- **Format:** For document type terms
|
||||||
|
- **Components:** For complex concepts
|
||||||
|
- **Steps:** For process terms
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Using the Registered Schema
|
||||||
|
|
||||||
|
The terminology schema is registered in markitect's database and can be used by name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all registered schemas (terminology-schema.json should appear)
|
||||||
|
markitect schema-list
|
||||||
|
|
||||||
|
# Validate using the registered schema
|
||||||
|
markitect validate my-glossary.md --schema terminology-schema.json
|
||||||
|
|
||||||
|
# Or use the local file directly
|
||||||
|
markitect validate my-glossary.md --schema examples/terminology/terminology-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate with Detailed Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate my-glossary.md --schema terminology-schema.json --detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register the Schema (if needed)
|
||||||
|
|
||||||
|
If the schema isn't already registered, you can add it to markitect's database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-ingest markitect/schemas/terminology-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Schema from Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-generate terminology-example.md --output my-terminology-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Features
|
||||||
|
|
||||||
|
This schema demonstrates several MarkiTect features:
|
||||||
|
|
||||||
|
### 1. Structural Validation
|
||||||
|
|
||||||
|
- Enforces consistent heading hierarchy (H1 → H2 → H3)
|
||||||
|
- Validates minimum term count
|
||||||
|
- Ensures proper document structure
|
||||||
|
|
||||||
|
### 2. Content Pattern Validation
|
||||||
|
|
||||||
|
- Validates title pattern (must contain terminology-related keywords)
|
||||||
|
- Checks for required field labels (Definition:, Synonyms:, etc.)
|
||||||
|
- Enforces consistent formatting
|
||||||
|
|
||||||
|
### 3. MarkiTect Extensions
|
||||||
|
|
||||||
|
The schema uses MarkiTect-specific extensions:
|
||||||
|
|
||||||
|
#### `x-markitect-sections`
|
||||||
|
Defines section classifications and requirements:
|
||||||
|
- `document_title` (required)
|
||||||
|
- `category_sections` (required, min 1)
|
||||||
|
- `term_definitions` (required, min 1)
|
||||||
|
|
||||||
|
#### `x-markitect-content-control`
|
||||||
|
Specifies content requirements:
|
||||||
|
- Required vs optional components
|
||||||
|
- Content quality metrics (word counts)
|
||||||
|
- Content instructions for authors
|
||||||
|
|
||||||
|
#### `x-markitect-validation-rules`
|
||||||
|
Custom validation rules:
|
||||||
|
- Minimum term count (3 required, 10+ recommended)
|
||||||
|
- Category balance (min 2 terms per category)
|
||||||
|
- Definition quality checks
|
||||||
|
- Consistency validation
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Consistent Field Labels
|
||||||
|
|
||||||
|
Always use the same labels for metadata:
|
||||||
|
```markdown
|
||||||
|
**Definition:** ...
|
||||||
|
**Synonyms:** ...
|
||||||
|
**Related Terms:** ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Write Clear Definitions
|
||||||
|
|
||||||
|
- Start with the term's primary meaning
|
||||||
|
- Use 10-200 words
|
||||||
|
- Be self-contained (don't require reading other terms)
|
||||||
|
- Avoid circular definitions
|
||||||
|
|
||||||
|
### 3. Group Related Terms
|
||||||
|
|
||||||
|
Organize terms into logical categories:
|
||||||
|
- Core Concepts
|
||||||
|
- Document Types
|
||||||
|
- Process Terms
|
||||||
|
- Quality Attributes
|
||||||
|
- Deprecated Terms
|
||||||
|
|
||||||
|
### 4. Include Examples
|
||||||
|
|
||||||
|
Add practical examples for complex terms:
|
||||||
|
```markdown
|
||||||
|
**Example:**
|
||||||
|
\`\`\`markdown
|
||||||
|
# Heading
|
||||||
|
Paragraph text
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Link Related Terms
|
||||||
|
|
||||||
|
Use **Related Terms:** to create a terminology graph:
|
||||||
|
```markdown
|
||||||
|
**Related Terms:** Parser, Token, Node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending the Schema
|
||||||
|
|
||||||
|
You can customize the schema for your project:
|
||||||
|
|
||||||
|
### Add Custom Field Labels
|
||||||
|
|
||||||
|
Extend the `bold_text` enum:
|
||||||
|
```json
|
||||||
|
"enum": [
|
||||||
|
"Definition:",
|
||||||
|
"Synonyms:",
|
||||||
|
"Your Custom Label:"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adjust Quality Metrics
|
||||||
|
|
||||||
|
Modify content quality requirements:
|
||||||
|
```json
|
||||||
|
"content_quality": {
|
||||||
|
"min_words_per_definition": 20,
|
||||||
|
"max_words_per_definition": 300,
|
||||||
|
"readability_target": "business"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Domain-Specific Validation
|
||||||
|
|
||||||
|
Include specialized validation rules:
|
||||||
|
```json
|
||||||
|
"x-markitect-validation-rules": {
|
||||||
|
"domain_specific": {
|
||||||
|
"require_acronym_expansion": true,
|
||||||
|
"require_source_citations": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Documentation Projects
|
||||||
|
|
||||||
|
- Software project glossaries
|
||||||
|
- API terminology reference
|
||||||
|
- Architecture decision records (ADR) glossary
|
||||||
|
- Domain-driven design (DDD) ubiquitous language
|
||||||
|
|
||||||
|
### Technical Writing
|
||||||
|
|
||||||
|
- Standards documentation
|
||||||
|
- Compliance documentation (ISO, SOC2)
|
||||||
|
- Technical specifications
|
||||||
|
- Research papers
|
||||||
|
|
||||||
|
### Knowledge Management
|
||||||
|
|
||||||
|
- Company wikis
|
||||||
|
- Team handbooks
|
||||||
|
- Onboarding documentation
|
||||||
|
- Training materials
|
||||||
|
|
||||||
|
## Integration with CI/CD
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# .git/hooks/pre-commit
|
||||||
|
markitect validate docs/glossary.md --schema schemas/terminology-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Validate Terminology
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Install MarkiTect
|
||||||
|
run: pip install markitect
|
||||||
|
- name: Validate Glossary
|
||||||
|
run: |
|
||||||
|
markitect validate docs/glossary.md \
|
||||||
|
--schema schemas/terminology-schema.json \
|
||||||
|
--detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Examples
|
||||||
|
|
||||||
|
- **manpages/** - Manual page documentation validation
|
||||||
|
- **templates/** - Document template examples
|
||||||
|
- **design-patterns/** - Software pattern documentation
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
- [Schema Extensions Specification](../../docs/specifications/schema-extensions-spec.md)
|
||||||
|
- [MarkiTect Documentation](../../README.md)
|
||||||
|
- [JSON Schema Documentation](https://json-schema.org/)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To improve this example:
|
||||||
|
|
||||||
|
1. Add more terms to demonstrate edge cases
|
||||||
|
2. Enhance the schema with additional validation rules
|
||||||
|
3. Create alternative terminology document styles
|
||||||
|
4. Add multilingual terminology examples
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This example is part of the MarkiTect project and follows the same license.
|
||||||
91
examples/terminology/terminology-example.md
Normal file
91
examples/terminology/terminology-example.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Project Terminology
|
||||||
|
|
||||||
|
A glossary of key terms and concepts for this project.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Abstract Syntax Tree
|
||||||
|
|
||||||
|
**Definition:** A tree representation of the abstract syntactic structure of source code or markup, where each node represents a construct occurring in the source.
|
||||||
|
|
||||||
|
**Synonyms:** AST, Parse Tree
|
||||||
|
|
||||||
|
**Related Terms:** Parser, Token, Node
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```markdown
|
||||||
|
# Heading
|
||||||
|
Paragraph text
|
||||||
|
```
|
||||||
|
|
||||||
|
The AST representation would include nodes for heading (level 1) and paragraph elements.
|
||||||
|
|
||||||
|
### Schema Validation
|
||||||
|
|
||||||
|
**Definition:** The process of verifying that a document's structure conforms to a predefined schema specification.
|
||||||
|
|
||||||
|
**Synonyms:** Structural Validation, Schema Conformance
|
||||||
|
|
||||||
|
**Related Terms:** JSON Schema, Validator, Metaschema
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Ensuring documentation completeness
|
||||||
|
- Enforcing content standards
|
||||||
|
- Automated quality checks
|
||||||
|
|
||||||
|
## Document Types
|
||||||
|
|
||||||
|
### Manpage
|
||||||
|
|
||||||
|
**Definition:** A manual page document following Unix/Linux documentation conventions, typically including sections like SYNOPSIS, DESCRIPTION, and OPTIONS.
|
||||||
|
|
||||||
|
**Format:** Markdown with specific heading structure
|
||||||
|
|
||||||
|
**Related Terms:** Documentation, Manual, Help Text
|
||||||
|
|
||||||
|
### Blueprint
|
||||||
|
|
||||||
|
**Definition:** A template specification that combines schemas, content instructions, and data templates for generating documents.
|
||||||
|
|
||||||
|
**Components:**
|
||||||
|
- Schema references
|
||||||
|
- Content model
|
||||||
|
- Data schema
|
||||||
|
- Generation rules
|
||||||
|
|
||||||
|
## Process Terms
|
||||||
|
|
||||||
|
### Schema Refinement
|
||||||
|
|
||||||
|
**Definition:** The process of transforming a rigid, auto-generated schema into a flexible, classification-aware schema with content guidance.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Analyze existing schema for rigidity
|
||||||
|
2. Loosen exact constraints to ranges
|
||||||
|
3. Add classification metadata
|
||||||
|
4. Include content instructions
|
||||||
|
|
||||||
|
**Tools:** `markitect schema-analyze`, `markitect schema-refine`
|
||||||
|
|
||||||
|
## Quality Attributes
|
||||||
|
|
||||||
|
### Classification Levels
|
||||||
|
|
||||||
|
**Definition:** A hierarchical system for categorizing document elements based on their importance and requirements.
|
||||||
|
|
||||||
|
**Levels:**
|
||||||
|
- **Required**: Must be present (validation fails if missing)
|
||||||
|
- **Recommended**: Should be present (warning if missing)
|
||||||
|
- **Optional**: May be present (no impact)
|
||||||
|
- **Discouraged**: Should not be present (warning if present)
|
||||||
|
- **Improper**: Must not be present (validation fails if present)
|
||||||
|
|
||||||
|
## Deprecated Terms
|
||||||
|
|
||||||
|
### Rigid Schema
|
||||||
|
|
||||||
|
**Status:** DEPRECATED - Use "Unrefined Schema" instead
|
||||||
|
|
||||||
|
**Definition:** A schema with exact count constraints that make it unusable as a pattern.
|
||||||
|
|
||||||
|
**Migration:** Use schema refinement tools to convert to flexible schemas.
|
||||||
214
examples/terminology/terminology-schema.json
Normal file
214
examples/terminology/terminology-schema.json
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/terminology-v1.json",
|
||||||
|
"title": "Terminology Document Schema",
|
||||||
|
"description": "Schema for validating terminology and glossary documents with consistent structure",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Main document title",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": ".*(Terminology|Glossary|Terms|Definitions).*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Category headings (Core Concepts, Document Types, etc.)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 20
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Individual term headings",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Term name - should be title case"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2", "level_3"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Content paragraphs including definitions and descriptions",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 3
|
||||||
|
},
|
||||||
|
"bold_text": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Bold text used for field labels (Definition, Synonyms, etc.)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Definition:",
|
||||||
|
"Synonyms:",
|
||||||
|
"Related Terms:",
|
||||||
|
"Example:",
|
||||||
|
"Examples:",
|
||||||
|
"Use Cases:",
|
||||||
|
"Usage:",
|
||||||
|
"Format:",
|
||||||
|
"Components:",
|
||||||
|
"Steps:",
|
||||||
|
"Tools:",
|
||||||
|
"Levels:",
|
||||||
|
"Status:",
|
||||||
|
"Migration:",
|
||||||
|
"Required:",
|
||||||
|
"Recommended:",
|
||||||
|
"Optional:",
|
||||||
|
"Discouraged:",
|
||||||
|
"Improper:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs"],
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"document_title": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 1,
|
||||||
|
"content_instruction": "Main title should include words like 'Terminology', 'Glossary', or 'Definitions'",
|
||||||
|
"pattern": ".*(Terminology|Glossary|Terms|Definitions).*"
|
||||||
|
},
|
||||||
|
"category_sections": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"min_sections": 1,
|
||||||
|
"content_instruction": "Organize terms into logical categories (e.g., Core Concepts, Document Types, Process Terms)"
|
||||||
|
},
|
||||||
|
"term_definitions": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 3,
|
||||||
|
"min_sections": 1,
|
||||||
|
"content_instruction": "Each term should be a level 3 heading followed by its definition and optional metadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"term_structure": {
|
||||||
|
"required_components": [
|
||||||
|
{
|
||||||
|
"label": "Definition:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Clear, concise definition of the term"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optional_components": [
|
||||||
|
{
|
||||||
|
"label": "Synonyms:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Alternative names or abbreviations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Related Terms:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Links to related concepts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Example:",
|
||||||
|
"type": "bold_text_or_code",
|
||||||
|
"description": "Practical example demonstrating the term"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Use Cases:",
|
||||||
|
"type": "list",
|
||||||
|
"description": "Common scenarios where term applies"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words_per_definition": 10,
|
||||||
|
"max_words_per_definition": 200,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Start each term with a level 3 heading containing the term name",
|
||||||
|
"Follow immediately with 'Definition:' in bold",
|
||||||
|
"Provide a clear, self-contained definition",
|
||||||
|
"Add optional fields (Synonyms, Related Terms, Examples) as needed",
|
||||||
|
"Use consistent formatting across all terms",
|
||||||
|
"Group related terms under category headings (level 2)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"definition_pattern": {
|
||||||
|
"description": "Each definition should follow: Term heading (###) → Definition: (bold) → Definition text",
|
||||||
|
"validation": {
|
||||||
|
"heading_level_3_followed_by": "bold_text_starting_with_Definition",
|
||||||
|
"definition_length": {
|
||||||
|
"min_words": 10,
|
||||||
|
"max_words": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated_terms": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Optional section for deprecated terms with migration guidance",
|
||||||
|
"required_fields": [
|
||||||
|
"Status: DEPRECATED",
|
||||||
|
"Migration:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-validation-rules": {
|
||||||
|
"term_count": {
|
||||||
|
"min": 3,
|
||||||
|
"recommended_min": 10,
|
||||||
|
"description": "Terminology document should define at least 3 terms, 10+ recommended"
|
||||||
|
},
|
||||||
|
"category_balance": {
|
||||||
|
"description": "Each category should have at least 2 terms",
|
||||||
|
"min_terms_per_category": 2
|
||||||
|
},
|
||||||
|
"definition_quality": {
|
||||||
|
"all_terms_must_have_definition": true,
|
||||||
|
"definition_must_follow_term_heading": true,
|
||||||
|
"definition_min_words": 10
|
||||||
|
},
|
||||||
|
"consistency": {
|
||||||
|
"use_consistent_field_labels": true,
|
||||||
|
"maintain_heading_hierarchy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1284,11 +1284,25 @@ MISSING: {len(missing_components)} components
|
|||||||
html_content = markdown_content_with_dogtag.replace('\n\n', '</p><p>').replace('\n', '<br>')
|
html_content = markdown_content_with_dogtag.replace('\n\n', '</p><p>').replace('\n', '<br>')
|
||||||
html_content = f'<p>{html_content}</p>'
|
html_content = f'<p>{html_content}</p>'
|
||||||
|
|
||||||
|
# Generate or read CSS content
|
||||||
|
if css:
|
||||||
|
# If css is a file path, read it
|
||||||
|
css_path = Path(css)
|
||||||
|
if css_path.exists() and css_path.is_file():
|
||||||
|
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
|
||||||
|
else:
|
||||||
|
# Assume it's raw CSS content
|
||||||
|
css_content = f'<style>\n{css}\n</style>'
|
||||||
|
else:
|
||||||
|
# Use template-based CSS generation
|
||||||
|
css_content = self._get_template_css(template, image_max_width, image_max_height)
|
||||||
|
|
||||||
# Replace template placeholders using safe string replacement
|
# Replace template placeholders using safe string replacement
|
||||||
# This avoids conflicts with CSS curly braces
|
# This avoids conflicts with CSS curly braces
|
||||||
html_template = template_content.replace('{title}', title)
|
html_template = template_content.replace('{title}', title)
|
||||||
html_template = html_template.replace('{version}', version_str)
|
html_template = html_template.replace('{version}', version_str)
|
||||||
html_template = html_template.replace('{content}', html_content)
|
html_template = html_template.replace('{content}', html_content)
|
||||||
|
html_template = html_template.replace('{css_content}', css_content)
|
||||||
|
|
||||||
return html_template
|
return html_template
|
||||||
|
|
||||||
@@ -1302,8 +1316,18 @@ MISSING: {len(missing_components)} components
|
|||||||
|
|
||||||
template_content = template_path.read_text(encoding='utf-8')
|
template_content = template_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
# Generate CSS
|
# Generate or read CSS content
|
||||||
css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css
|
if css:
|
||||||
|
# If css is a file path, read it
|
||||||
|
css_path = Path(css)
|
||||||
|
if css_path.exists() and css_path.is_file():
|
||||||
|
css_content = f'<style>\n{css_path.read_text(encoding="utf-8")}\n</style>'
|
||||||
|
else:
|
||||||
|
# Assume it's raw CSS content
|
||||||
|
css_content = f'<style>\n{css}\n</style>'
|
||||||
|
else:
|
||||||
|
# Use template-based CSS generation
|
||||||
|
css_content = self._get_template_css(template, image_max_width, image_max_height)
|
||||||
|
|
||||||
# Create configuration object - ONLY dynamic data interface
|
# Create configuration object - ONLY dynamic data interface
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
228
markitect/cli.py
228
markitect/cli.py
@@ -1740,15 +1740,46 @@ def schema_list(config, output_format, names_only):
|
|||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
for schema_info in schemas:
|
for schema_info in schemas:
|
||||||
click.echo(f"🔧 {schema_info['filename']}")
|
# Format timestamp for display (remove microseconds)
|
||||||
|
created = schema_info['created_at']
|
||||||
|
if created:
|
||||||
|
# Format: YYYY-MM-DD HH:MM:SS (remove microseconds if present)
|
||||||
|
if '.' in created:
|
||||||
|
created_display = created.split('.')[0]
|
||||||
|
else:
|
||||||
|
created_display = created
|
||||||
|
click.echo(f"🔧 {schema_info['filename']:<40} (added: {created_display})")
|
||||||
|
else:
|
||||||
|
click.echo(f"🔧 {schema_info['filename']}")
|
||||||
|
|
||||||
if config.get('verbose'):
|
if config.get('verbose'):
|
||||||
click.echo(f" Title: {schema_info['title']}")
|
click.echo(f" Title: {schema_info['title']}")
|
||||||
click.echo(f" Created: {schema_info['created_at']}")
|
click.echo(f" Created: {schema_info['created_at']}")
|
||||||
|
click.echo(f" Updated: {schema_info['updated_at']}")
|
||||||
if schema_info['description']:
|
if schema_info['description']:
|
||||||
click.echo(f" Description: {schema_info['description']}")
|
click.echo(f" Description: {schema_info['description']}")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
elif output_format == 'table':
|
||||||
|
# Custom table format for better readability
|
||||||
|
table_data = []
|
||||||
|
for schema in schemas:
|
||||||
|
# Format timestamps (remove microseconds)
|
||||||
|
created_date = schema['created_at'].split('.')[0] if schema['created_at'] and '.' in schema['created_at'] else schema['created_at']
|
||||||
|
updated_date = schema['updated_at'].split('.')[0] if schema['updated_at'] and '.' in schema['updated_at'] else schema['updated_at']
|
||||||
|
|
||||||
|
table_data.append({
|
||||||
|
'Name': schema['filename'],
|
||||||
|
'Title': schema['title'] or '',
|
||||||
|
'Created': created_date or '',
|
||||||
|
'Updated': updated_date or ''
|
||||||
|
})
|
||||||
|
|
||||||
|
if table_data:
|
||||||
|
headers = ['Name', 'Title', 'Created', 'Updated']
|
||||||
|
rows = [[row[h] for h in headers] for row in table_data]
|
||||||
|
click.echo(tabulate(rows, headers=headers, tablefmt='simple'))
|
||||||
else:
|
else:
|
||||||
# Use structured format (table, json, yaml)
|
# Use structured format (json, yaml)
|
||||||
formatted_output = format_output(schemas, output_format)
|
formatted_output = format_output(schemas, output_format)
|
||||||
click.echo(formatted_output)
|
click.echo(formatted_output)
|
||||||
|
|
||||||
@@ -1872,6 +1903,199 @@ def schema_delete(config, schema_name, confirm):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command('schema-validate')
|
||||||
|
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||||
|
@click.option('--detailed-errors', is_flag=True, help='Show detailed validation errors')
|
||||||
|
@pass_config
|
||||||
|
def schema_validate_cmd(config, schema_file, detailed_errors):
|
||||||
|
"""
|
||||||
|
Validate a schema file against the schema-for-schemas metaschema.
|
||||||
|
|
||||||
|
Ensures schema files follow MarkiTect conventions and standards:
|
||||||
|
- Required fields ($schema, $id, title, description, version)
|
||||||
|
- Version format (SemVer: major.minor.patch)
|
||||||
|
- $id URL format (HTTPS with version)
|
||||||
|
- MarkiTect extensions (x-markitect-*)
|
||||||
|
- Section classification structures
|
||||||
|
|
||||||
|
SCHEMA_FILE: Path to the schema file to validate (markdown or JSON)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect schema-validate manpage-schema-v1.0.md
|
||||||
|
markitect schema-validate my-schema-v2.0.md --detailed-errors
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .schema_loader import MarkdownSchemaLoader
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
from jsonschema import Draft7Validator, ValidationError
|
||||||
|
except ImportError:
|
||||||
|
click.echo("❌ Error: jsonschema package not installed", err=True)
|
||||||
|
click.echo("Install it with: pip install jsonschema", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
# Load the schema to validate
|
||||||
|
click.echo(f"Loading schema: {schema_file.name}")
|
||||||
|
try:
|
||||||
|
if schema_file.suffix == '.md':
|
||||||
|
schema_data = loader.load_schema(schema_file)
|
||||||
|
schema = schema_data['schema']
|
||||||
|
else:
|
||||||
|
# Assume JSON
|
||||||
|
schema = json.loads(schema_file.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to load schema: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Load metaschema
|
||||||
|
metaschema_path = Path(__file__).parent / 'schemas' / 'schema-schema-v1.0.md'
|
||||||
|
if not metaschema_path.exists():
|
||||||
|
click.echo(f"❌ Metaschema not found: {metaschema_path}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
metaschema_data = loader.load_schema(metaschema_path)
|
||||||
|
metaschema = metaschema_data['schema']
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to load metaschema: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Validate schema against metaschema
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
click.echo(f"✅ Schema is valid: {schema_file.name}")
|
||||||
|
click.echo(f" Title: {schema.get('title', 'N/A')}")
|
||||||
|
click.echo(f" Version: {schema.get('version', 'N/A')}")
|
||||||
|
click.echo(f" $id: {schema.get('$id', 'N/A')}")
|
||||||
|
|
||||||
|
# Additional structure validation
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
if issues:
|
||||||
|
click.echo(f"\n⚠️ Structure recommendations:")
|
||||||
|
for issue in issues:
|
||||||
|
click.echo(f" - {issue}")
|
||||||
|
else:
|
||||||
|
click.echo(f"❌ Schema validation failed: {schema_file.name}", err=True)
|
||||||
|
click.echo(f"\nFound {len(errors)} validation error(s):\n", err=True)
|
||||||
|
|
||||||
|
for i, error in enumerate(errors, 1):
|
||||||
|
path = ' → '.join(str(p) for p in error.path) if error.path else 'root'
|
||||||
|
click.echo(f"{i}. At {path}:", err=True)
|
||||||
|
click.echo(f" {error.message}", err=True)
|
||||||
|
|
||||||
|
if detailed_errors and error.context:
|
||||||
|
click.echo(f" Context:", err=True)
|
||||||
|
for ctx_error in error.context:
|
||||||
|
click.echo(f" - {ctx_error.message}", err=True)
|
||||||
|
|
||||||
|
if detailed_errors:
|
||||||
|
click.echo(f" Schema path: {' → '.join(str(p) for p in error.schema_path)}", err=True)
|
||||||
|
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Schema validation error: {e}", err=True)
|
||||||
|
if config and config.get('verbose'):
|
||||||
|
import traceback
|
||||||
|
click.echo(traceback.format_exc(), err=True)
|
||||||
|
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')
|
@cli.command('generate-stub')
|
||||||
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||||
@click.option('--output', '-o', type=click.Path(path_type=Path),
|
@click.option('--output', '-o', type=click.Path(path_type=Path),
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class MetaschemaValidator:
|
|||||||
"x-markitect-instruction-type": self._validate_instruction_type,
|
"x-markitect-instruction-type": self._validate_instruction_type,
|
||||||
"x-markitect-generation-mode": self._validate_generation_mode,
|
"x-markitect-generation-mode": self._validate_generation_mode,
|
||||||
"x-markitect-generated-from": self._validate_generated_from,
|
"x-markitect-generated-from": self._validate_generated_from,
|
||||||
|
"x-markitect-sections": self._validate_sections,
|
||||||
|
"x-markitect-content-control": self._validate_content_control,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply validation rules
|
# Apply validation rules
|
||||||
@@ -194,3 +196,189 @@ class MetaschemaValidator:
|
|||||||
property_name
|
property_name
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _validate_sections(self, value: Any, property_name: str) -> Optional[ValidationError]:
|
||||||
|
"""Validate x-markitect-sections property."""
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return ValidationError(
|
||||||
|
"x-markitect-sections must be an object",
|
||||||
|
property_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate each section definition
|
||||||
|
for section_name, section_def in value.items():
|
||||||
|
# Section name should be UPPERCASE (convention)
|
||||||
|
if not isinstance(section_name, str):
|
||||||
|
return ValidationError(
|
||||||
|
f"Section name must be a string: {section_name}",
|
||||||
|
f"{property_name}.{section_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(section_def, dict):
|
||||||
|
return ValidationError(
|
||||||
|
f"Section definition must be an object: {section_name}",
|
||||||
|
f"{property_name}.{section_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate required 'classification' field
|
||||||
|
if "classification" not in section_def:
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' missing required 'classification' field",
|
||||||
|
f"{property_name}.{section_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
classification = section_def["classification"]
|
||||||
|
valid_classifications = ["required", "recommended", "optional", "discouraged", "improper"]
|
||||||
|
if classification not in valid_classifications:
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' has invalid classification '{classification}'. "
|
||||||
|
f"Must be one of {valid_classifications}",
|
||||||
|
f"{property_name}.{section_name}.classification"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate optional fields if present
|
||||||
|
if "heading_level" in section_def:
|
||||||
|
level = section_def["heading_level"]
|
||||||
|
if not isinstance(level, int) or level < 1 or level > 6:
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' heading_level must be integer 1-6, got {level}",
|
||||||
|
f"{property_name}.{section_name}.heading_level"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "position" in section_def:
|
||||||
|
position = section_def["position"]
|
||||||
|
valid_positions = ["after_title", "before_section_name", "after_section_name", "anywhere"]
|
||||||
|
if position not in valid_positions:
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' has invalid position '{position}'. "
|
||||||
|
f"Must be one of {valid_positions}",
|
||||||
|
f"{property_name}.{section_name}.position"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate content constraints are non-negative integers
|
||||||
|
for constraint in ["min_paragraphs", "max_paragraphs", "min_code_blocks",
|
||||||
|
"max_code_blocks", "min_lists", "max_lists"]:
|
||||||
|
if constraint in section_def:
|
||||||
|
value_check = section_def[constraint]
|
||||||
|
if not isinstance(value_check, int) or value_check < 0:
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' {constraint} must be non-negative integer, got {value_check}",
|
||||||
|
f"{property_name}.{section_name}.{constraint}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate alternatives is array of strings
|
||||||
|
if "alternatives" in section_def:
|
||||||
|
alternatives = section_def["alternatives"]
|
||||||
|
if not isinstance(alternatives, list):
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' alternatives must be an array",
|
||||||
|
f"{property_name}.{section_name}.alternatives"
|
||||||
|
)
|
||||||
|
for alt in alternatives:
|
||||||
|
if not isinstance(alt, str):
|
||||||
|
return ValidationError(
|
||||||
|
f"Section '{section_name}' alternative names must be strings",
|
||||||
|
f"{property_name}.{section_name}.alternatives"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _validate_content_control(self, value: Any, property_name: str) -> Optional[ValidationError]:
|
||||||
|
"""Validate x-markitect-content-control property."""
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return ValidationError(
|
||||||
|
"x-markitect-content-control must be an object",
|
||||||
|
property_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate each section's content control rules
|
||||||
|
for section_name, control_def in value.items():
|
||||||
|
if not isinstance(section_name, str):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control section name must be a string: {section_name}",
|
||||||
|
f"{property_name}.{section_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(control_def, dict):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control definition must be an object: {section_name}",
|
||||||
|
f"{property_name}.{section_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate pattern arrays
|
||||||
|
for pattern_type in ["required_patterns", "discouraged_patterns", "forbidden_patterns"]:
|
||||||
|
if pattern_type in control_def:
|
||||||
|
patterns = control_def[pattern_type]
|
||||||
|
if not isinstance(patterns, list):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' {pattern_type} must be an array",
|
||||||
|
f"{property_name}.{section_name}.{pattern_type}"
|
||||||
|
)
|
||||||
|
for pattern in patterns:
|
||||||
|
if not isinstance(pattern, str):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' pattern must be string",
|
||||||
|
f"{property_name}.{section_name}.{pattern_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate content_quality object
|
||||||
|
if "content_quality" in control_def:
|
||||||
|
quality = control_def["content_quality"]
|
||||||
|
if not isinstance(quality, dict):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' content_quality must be an object",
|
||||||
|
f"{property_name}.{section_name}.content_quality"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate word/sentence counts
|
||||||
|
for count_field in ["min_words", "max_words", "min_sentences", "max_sentences"]:
|
||||||
|
if count_field in quality:
|
||||||
|
count = quality[count_field]
|
||||||
|
if not isinstance(count, int) or count < 0:
|
||||||
|
return ValidationError(
|
||||||
|
f"Content quality '{section_name}' {count_field} must be non-negative integer",
|
||||||
|
f"{property_name}.{section_name}.content_quality.{count_field}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate readability_target
|
||||||
|
if "readability_target" in quality:
|
||||||
|
target = quality["readability_target"]
|
||||||
|
valid_targets = ["simple", "general", "technical", "advanced"]
|
||||||
|
if target not in valid_targets:
|
||||||
|
return ValidationError(
|
||||||
|
f"Content quality '{section_name}' readability_target must be one of {valid_targets}",
|
||||||
|
f"{property_name}.{section_name}.content_quality.readability_target"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate content_instructions array
|
||||||
|
if "content_instructions" in control_def:
|
||||||
|
instructions = control_def["content_instructions"]
|
||||||
|
if not isinstance(instructions, list):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' content_instructions must be an array",
|
||||||
|
f"{property_name}.{section_name}.content_instructions"
|
||||||
|
)
|
||||||
|
for instruction in instructions:
|
||||||
|
if not isinstance(instruction, str):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' instruction must be string",
|
||||||
|
f"{property_name}.{section_name}.content_instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate link_validation object
|
||||||
|
if "link_validation" in control_def:
|
||||||
|
link_val = control_def["link_validation"]
|
||||||
|
if not isinstance(link_val, dict):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' link_validation must be an object",
|
||||||
|
f"{property_name}.{section_name}.link_validation"
|
||||||
|
)
|
||||||
|
for field in ["check_internal", "check_external", "allow_fragments"]:
|
||||||
|
if field in link_val:
|
||||||
|
if not isinstance(link_val[field], bool):
|
||||||
|
return ValidationError(
|
||||||
|
f"Content control '{section_name}' link_validation.{field} must be boolean",
|
||||||
|
f"{property_name}.{section_name}.link_validation.{field}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
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
|
||||||
503
markitect/schema_loader.py
Normal file
503
markitect/schema_loader.py
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
"""
|
||||||
|
Schema Loader - Extract JSON schemas from markdown files.
|
||||||
|
|
||||||
|
This module provides functionality to load schemas from markdown files that
|
||||||
|
contain embedded JSON schemas in code blocks, along with YAML frontmatter
|
||||||
|
metadata and rich documentation.
|
||||||
|
|
||||||
|
Markdown Schema Format:
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/domain/v1"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable|draft|deprecated"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schema Title v1.0
|
||||||
|
|
||||||
|
## Documentation sections...
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables:
|
||||||
|
- Rich documentation alongside schemas
|
||||||
|
- Version history in same file
|
||||||
|
- Human-readable schema files
|
||||||
|
- Markdown-first approach aligned with MarkiTect philosophy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaLoaderError(Exception):
|
||||||
|
"""Base exception for schema loading errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSchemaFormatError(SchemaLoaderError):
|
||||||
|
"""Schema file format is invalid."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaNotFoundError(SchemaLoaderError):
|
||||||
|
"""No JSON schema found in markdown file."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownSchemaLoader:
|
||||||
|
"""
|
||||||
|
Load and parse markdown schema files.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- YAML frontmatter for metadata
|
||||||
|
- JSON code blocks for schema definition
|
||||||
|
- Validation of schema structure
|
||||||
|
- Metadata merging
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> loader = MarkdownSchemaLoader()
|
||||||
|
>>> schema_data = loader.load_schema(Path("manpage-schema-v1.0.md"))
|
||||||
|
>>> schema = schema_data['schema']
|
||||||
|
>>> metadata = schema_data['metadata']
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the schema loader with regex patterns."""
|
||||||
|
# Pattern to match YAML frontmatter
|
||||||
|
# Matches: --- ... --- at start of file
|
||||||
|
self.frontmatter_pattern = re.compile(
|
||||||
|
r'^---\s*\n(.*?)\n---\s*\n',
|
||||||
|
re.DOTALL | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern to match JSON code blocks
|
||||||
|
# Matches: ```json ... ```
|
||||||
|
self.json_code_block_pattern = re.compile(
|
||||||
|
r'```json\s*\n(.*?)\n```',
|
||||||
|
re.DOTALL | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern to find Schema Definition section
|
||||||
|
# This helps us find the right JSON block if there are multiple
|
||||||
|
self.schema_section_pattern = re.compile(
|
||||||
|
r'##\s+Schema Definition\s*\n',
|
||||||
|
re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_schema(self, md_path: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load schema from markdown file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
md_path: Path to markdown schema file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- schema: Extracted JSON schema (dict)
|
||||||
|
- metadata: Frontmatter metadata (dict)
|
||||||
|
- documentation: Full markdown content (str)
|
||||||
|
- source_file: Source file path (str)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If schema file doesn't exist
|
||||||
|
InvalidSchemaFormatError: If file format is invalid
|
||||||
|
SchemaNotFoundError: If no JSON schema found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> loader = MarkdownSchemaLoader()
|
||||||
|
>>> data = loader.load_schema(Path("manpage-schema-v1.0.md"))
|
||||||
|
>>> print(data['schema']['title'])
|
||||||
|
'Unix Manual Page Schema'
|
||||||
|
"""
|
||||||
|
if not md_path.exists():
|
||||||
|
raise FileNotFoundError(f"Schema file not found: {md_path}")
|
||||||
|
|
||||||
|
# Read file content
|
||||||
|
try:
|
||||||
|
content = md_path.read_text(encoding='utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
raise InvalidSchemaFormatError(f"Failed to read schema file: {e}")
|
||||||
|
|
||||||
|
# Extract frontmatter
|
||||||
|
metadata = self._extract_frontmatter(content)
|
||||||
|
|
||||||
|
# Extract JSON schema
|
||||||
|
schema = self._extract_json_schema(content)
|
||||||
|
|
||||||
|
if not schema:
|
||||||
|
raise SchemaNotFoundError(
|
||||||
|
f"No JSON schema found in {md_path}. "
|
||||||
|
f"Expected a ```json code block with schema definition."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge metadata into schema
|
||||||
|
schema = self._merge_metadata(schema, metadata, md_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'schema': schema,
|
||||||
|
'metadata': metadata,
|
||||||
|
'documentation': content,
|
||||||
|
'source_file': str(md_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_frontmatter(self, content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract YAML frontmatter from markdown content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Markdown file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of frontmatter metadata (empty if none found)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidSchemaFormatError: If YAML is malformed
|
||||||
|
"""
|
||||||
|
match = self.frontmatter_pattern.search(content)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
yaml_content = match.group(1)
|
||||||
|
try:
|
||||||
|
metadata = yaml.safe_load(yaml_content) or {}
|
||||||
|
if not isinstance(metadata, dict):
|
||||||
|
raise InvalidSchemaFormatError(
|
||||||
|
f"Frontmatter must be a YAML dictionary, got {type(metadata)}"
|
||||||
|
)
|
||||||
|
return metadata
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
raise InvalidSchemaFormatError(f"Invalid YAML frontmatter: {e}")
|
||||||
|
|
||||||
|
def _extract_json_schema(self, content: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Extract JSON schema from markdown code blocks.
|
||||||
|
|
||||||
|
Prefers JSON blocks under "## Schema Definition" section,
|
||||||
|
but will use first JSON block if no Schema Definition section found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Markdown file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON schema dictionary or None if not found
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidSchemaFormatError: If JSON is malformed
|
||||||
|
"""
|
||||||
|
# Find all JSON code blocks
|
||||||
|
json_blocks = self.json_code_block_pattern.findall(content)
|
||||||
|
|
||||||
|
if not json_blocks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try to find the Schema Definition section
|
||||||
|
schema_section_match = self.schema_section_pattern.search(content)
|
||||||
|
|
||||||
|
if schema_section_match:
|
||||||
|
# Find JSON block that comes after Schema Definition section
|
||||||
|
section_pos = schema_section_match.end()
|
||||||
|
|
||||||
|
# Re-search for JSON blocks starting from section position
|
||||||
|
remaining_content = content[section_pos:]
|
||||||
|
section_json_blocks = self.json_code_block_pattern.findall(remaining_content)
|
||||||
|
|
||||||
|
if section_json_blocks:
|
||||||
|
json_text = section_json_blocks[0]
|
||||||
|
else:
|
||||||
|
# Fallback to first JSON block in entire document
|
||||||
|
json_text = json_blocks[0]
|
||||||
|
else:
|
||||||
|
# No Schema Definition section, use first JSON block
|
||||||
|
json_text = json_blocks[0]
|
||||||
|
|
||||||
|
# Parse JSON
|
||||||
|
try:
|
||||||
|
schema = json.loads(json_text)
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
raise InvalidSchemaFormatError(
|
||||||
|
f"Schema must be a JSON object, got {type(schema)}"
|
||||||
|
)
|
||||||
|
return schema
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise InvalidSchemaFormatError(f"Invalid JSON schema: {e}")
|
||||||
|
|
||||||
|
def _merge_metadata(
|
||||||
|
self,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
metadata: Dict[str, Any],
|
||||||
|
source_file: Path
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Merge frontmatter metadata into schema.
|
||||||
|
|
||||||
|
Adds x-markitect-source extension with file info and metadata.
|
||||||
|
Optionally overrides schema fields with frontmatter values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dictionary
|
||||||
|
metadata: Frontmatter metadata dictionary
|
||||||
|
source_file: Path to source file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Schema with merged metadata
|
||||||
|
"""
|
||||||
|
# Create a copy to avoid modifying original
|
||||||
|
merged_schema = schema.copy()
|
||||||
|
|
||||||
|
# Add MarkiTect-specific source metadata
|
||||||
|
merged_schema['x-markitect-source'] = {
|
||||||
|
'file': str(source_file),
|
||||||
|
'filename': source_file.name,
|
||||||
|
'format': 'markdown',
|
||||||
|
'frontmatter': metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override schema fields with frontmatter if present
|
||||||
|
# This allows frontmatter to be the source of truth for metadata
|
||||||
|
if 'version' in metadata:
|
||||||
|
merged_schema['version'] = metadata['version']
|
||||||
|
|
||||||
|
if 'schema-id' in metadata:
|
||||||
|
merged_schema['$id'] = metadata['schema-id']
|
||||||
|
|
||||||
|
if 'status' in metadata:
|
||||||
|
if 'x-markitect-metadata' not in merged_schema:
|
||||||
|
merged_schema['x-markitect-metadata'] = {}
|
||||||
|
merged_schema['x-markitect-metadata']['status'] = metadata['status']
|
||||||
|
|
||||||
|
return merged_schema
|
||||||
|
|
||||||
|
def save_schema(
|
||||||
|
self,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
md_path: Path,
|
||||||
|
template: Optional[str] = None,
|
||||||
|
frontmatter: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Save schema as markdown file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dictionary to save
|
||||||
|
md_path: Output path for markdown file
|
||||||
|
template: Optional markdown template string
|
||||||
|
frontmatter: Optional frontmatter metadata (extracted from schema if not provided)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidSchemaFormatError: If schema is invalid
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> loader = MarkdownSchemaLoader()
|
||||||
|
>>> loader.save_schema(
|
||||||
|
... schema={'title': 'My Schema', ...},
|
||||||
|
... md_path=Path('my-schema-v1.0.md')
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
if template:
|
||||||
|
# Use provided template
|
||||||
|
content = self._render_template(template, schema, frontmatter)
|
||||||
|
else:
|
||||||
|
# Generate basic markdown
|
||||||
|
content = self._generate_markdown(schema, frontmatter)
|
||||||
|
|
||||||
|
# Create parent directory if needed
|
||||||
|
md_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Write file
|
||||||
|
try:
|
||||||
|
md_path.write_text(content, encoding='utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
raise InvalidSchemaFormatError(f"Failed to write schema file: {e}")
|
||||||
|
|
||||||
|
def _generate_markdown(
|
||||||
|
self,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
frontmatter: Optional[Dict[str, Any]] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate markdown from schema.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dictionary
|
||||||
|
frontmatter: Optional frontmatter metadata
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown content as string
|
||||||
|
"""
|
||||||
|
# Extract metadata from schema
|
||||||
|
title = schema.get('title', 'Untitled Schema')
|
||||||
|
version = schema.get('version', '1.0.0')
|
||||||
|
description = schema.get('description', '')
|
||||||
|
schema_id = schema.get('$id', '')
|
||||||
|
|
||||||
|
# Build frontmatter
|
||||||
|
if frontmatter is None:
|
||||||
|
frontmatter = {}
|
||||||
|
|
||||||
|
# Set defaults
|
||||||
|
if 'schema-id' not in frontmatter and schema_id:
|
||||||
|
frontmatter['schema-id'] = schema_id
|
||||||
|
if 'version' not in frontmatter:
|
||||||
|
frontmatter['version'] = version
|
||||||
|
if 'status' not in frontmatter:
|
||||||
|
frontmatter['status'] = 'draft'
|
||||||
|
|
||||||
|
# Generate frontmatter YAML
|
||||||
|
frontmatter_yaml = yaml.dump(
|
||||||
|
frontmatter,
|
||||||
|
default_flow_style=False,
|
||||||
|
allow_unicode=True
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# Generate JSON (pretty-printed)
|
||||||
|
schema_json = json.dumps(schema, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Build markdown content
|
||||||
|
md_content = f"""---
|
||||||
|
{frontmatter_yaml}
|
||||||
|
---
|
||||||
|
|
||||||
|
# {title} v{version}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate document.md --schema {Path(frontmatter.get('schema-id', 'schema')).name}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{schema_json}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v{version}
|
||||||
|
- Initial version
|
||||||
|
"""
|
||||||
|
|
||||||
|
return md_content
|
||||||
|
|
||||||
|
def _render_template(
|
||||||
|
self,
|
||||||
|
template: str,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
frontmatter: Optional[Dict[str, Any]] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Render markdown from template.
|
||||||
|
|
||||||
|
Simple template rendering using string formatting.
|
||||||
|
For complex templates, consider using Jinja2 or similar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template: Template string
|
||||||
|
schema: JSON schema dictionary
|
||||||
|
frontmatter: Optional frontmatter metadata
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Rendered markdown content
|
||||||
|
"""
|
||||||
|
# Build context for template
|
||||||
|
context = {
|
||||||
|
'title': schema.get('title', 'Untitled'),
|
||||||
|
'version': schema.get('version', '1.0.0'),
|
||||||
|
'description': schema.get('description', ''),
|
||||||
|
'schema_id': schema.get('$id', ''),
|
||||||
|
'schema_json': json.dumps(schema, indent=2, ensure_ascii=False),
|
||||||
|
'frontmatter': frontmatter or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Simple template rendering
|
||||||
|
try:
|
||||||
|
return template.format(**context)
|
||||||
|
except KeyError as e:
|
||||||
|
raise InvalidSchemaFormatError(f"Template missing key: {e}")
|
||||||
|
|
||||||
|
def list_json_blocks(self, content: str) -> List[Tuple[int, str]]:
|
||||||
|
"""
|
||||||
|
List all JSON code blocks in markdown content.
|
||||||
|
|
||||||
|
Useful for debugging or when multiple JSON blocks exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Markdown file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (position, json_content) tuples
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> loader = MarkdownSchemaLoader()
|
||||||
|
>>> content = Path('schema.md').read_text()
|
||||||
|
>>> blocks = loader.list_json_blocks(content)
|
||||||
|
>>> print(f"Found {len(blocks)} JSON blocks")
|
||||||
|
"""
|
||||||
|
blocks = []
|
||||||
|
for match in self.json_code_block_pattern.finditer(content):
|
||||||
|
blocks.append((match.start(), match.group(1)))
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def validate_schema_structure(self, schema: Dict[str, Any]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Validate basic schema structure.
|
||||||
|
|
||||||
|
Checks for required JSON Schema fields and MarkiTect conventions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of warning/error messages (empty if valid)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> loader = MarkdownSchemaLoader()
|
||||||
|
>>> issues = loader.validate_schema_structure(schema)
|
||||||
|
>>> if issues:
|
||||||
|
... print("Schema issues:", issues)
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check required JSON Schema fields
|
||||||
|
if '$schema' not in schema:
|
||||||
|
issues.append("Missing required field: $schema")
|
||||||
|
|
||||||
|
if 'type' not in schema:
|
||||||
|
issues.append("Missing recommended field: type")
|
||||||
|
|
||||||
|
if 'title' not in schema:
|
||||||
|
issues.append("Missing recommended field: title")
|
||||||
|
|
||||||
|
if 'description' not in schema:
|
||||||
|
issues.append("Missing recommended field: description")
|
||||||
|
|
||||||
|
# Check MarkiTect conventions
|
||||||
|
if 'version' not in schema:
|
||||||
|
issues.append("Missing MarkiTect convention: version field")
|
||||||
|
|
||||||
|
if '$id' not in schema:
|
||||||
|
issues.append("Missing recommended field: $id")
|
||||||
|
|
||||||
|
# Check $id format if present
|
||||||
|
if '$id' in schema:
|
||||||
|
schema_id = schema['$id']
|
||||||
|
if not isinstance(schema_id, str):
|
||||||
|
issues.append("$id must be a string")
|
||||||
|
elif not schema_id.startswith('https://'):
|
||||||
|
issues.append("$id should be a full HTTPS URL")
|
||||||
|
|
||||||
|
return issues
|
||||||
309
markitect/schema_naming.py
Normal file
309
markitect/schema_naming.py
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
"""
|
||||||
|
Schema Naming Validation - Enforce filename conventions for schemas.
|
||||||
|
|
||||||
|
This module provides validation and utilities for schema filename conventions
|
||||||
|
to ensure consistency across the MarkiTect schema ecosystem.
|
||||||
|
|
||||||
|
Naming Convention:
|
||||||
|
Format: {domain}-schema-v{major}.{minor}.md
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- domain: lowercase, hyphen-separated identifier (e.g., "manpage", "api-documentation")
|
||||||
|
- schema: literal string "schema"
|
||||||
|
- version: SemVer major.minor (e.g., "v1.0", "v2.1")
|
||||||
|
- extension: ".md" (markdown)
|
||||||
|
|
||||||
|
Valid Examples:
|
||||||
|
✓ manpage-schema-v1.0.md
|
||||||
|
✓ terminology-schema-v1.0.md
|
||||||
|
✓ api-documentation-schema-v1.0.md
|
||||||
|
✓ my-custom-type-schema-v2.1.md
|
||||||
|
|
||||||
|
Invalid Examples:
|
||||||
|
✗ manpage.json (missing version and wrong extension)
|
||||||
|
✗ manpage-v1.md (missing "schema" keyword)
|
||||||
|
✗ ManPage-Schema-v1.0.md (wrong case - must be lowercase)
|
||||||
|
✗ manpage-schema-1.0.md (missing 'v' prefix)
|
||||||
|
✗ manpage-schema-v1.md (missing minor version)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
# Regex pattern for schema filename validation
|
||||||
|
# Matches: {domain}-schema-v{major}.{minor}.md
|
||||||
|
# Where domain is lowercase letters/numbers/hyphens starting with letter
|
||||||
|
SCHEMA_FILENAME_PATTERN = re.compile(
|
||||||
|
r'^(?P<domain>[a-z][a-z0-9-]*)-schema-v(?P<major>\d+)\.(?P<minor>\d+)\.md$'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaFilenameError(Exception):
|
||||||
|
"""Exception raised for invalid schema filenames."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_schema_filename(filename: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
Validate schema filename against naming convention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The filename to validate (e.g., "manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, metadata_dict or None)
|
||||||
|
|
||||||
|
If valid, metadata_dict contains:
|
||||||
|
- domain: str - The domain identifier
|
||||||
|
- version: str - Full version string (e.g., "1.0")
|
||||||
|
- major: int - Major version number
|
||||||
|
- minor: int - Minor version number
|
||||||
|
- filename: str - The original filename
|
||||||
|
|
||||||
|
If invalid, metadata_dict is None
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> validate_schema_filename("manpage-schema-v1.0.md")
|
||||||
|
(True, {'domain': 'manpage', 'version': '1.0', ...})
|
||||||
|
|
||||||
|
>>> validate_schema_filename("invalid.json")
|
||||||
|
(False, None)
|
||||||
|
"""
|
||||||
|
match = SCHEMA_FILENAME_PATTERN.match(filename)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
return True, {
|
||||||
|
'domain': match.group('domain'),
|
||||||
|
'version': f"{match.group('major')}.{match.group('minor')}",
|
||||||
|
'major': int(match.group('major')),
|
||||||
|
'minor': int(match.group('minor')),
|
||||||
|
'filename': filename
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def suggest_schema_filename(
|
||||||
|
domain: str,
|
||||||
|
version: str = "1.0",
|
||||||
|
normalize: bool = True
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate a valid schema filename from domain and version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: The schema domain (e.g., "manpage", "API Documentation")
|
||||||
|
version: Version string in format "major.minor" (default: "1.0")
|
||||||
|
normalize: Whether to normalize domain to lowercase/hyphenated
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Valid schema filename
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If domain or version format is invalid
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> suggest_schema_filename("manpage", "1.0")
|
||||||
|
'manpage-schema-v1.0.md'
|
||||||
|
|
||||||
|
>>> suggest_schema_filename("API Documentation", "2.1")
|
||||||
|
'api-documentation-schema-v2.1.md'
|
||||||
|
|
||||||
|
>>> suggest_schema_filename("My_Custom_Type", "1.0")
|
||||||
|
'my-custom-type-schema-v1.0.md'
|
||||||
|
"""
|
||||||
|
if not domain:
|
||||||
|
raise ValueError("Domain cannot be empty")
|
||||||
|
|
||||||
|
if normalize:
|
||||||
|
# Normalize domain: lowercase, replace spaces/underscores with hyphens
|
||||||
|
domain_clean = domain.lower()
|
||||||
|
domain_clean = domain_clean.replace(' ', '-').replace('_', '-')
|
||||||
|
# Remove consecutive hyphens
|
||||||
|
domain_clean = re.sub(r'-+', '-', domain_clean)
|
||||||
|
# Remove leading/trailing hyphens
|
||||||
|
domain_clean = domain_clean.strip('-')
|
||||||
|
else:
|
||||||
|
domain_clean = domain
|
||||||
|
|
||||||
|
# Validate domain format (must start with letter, contain only lowercase, numbers, hyphens)
|
||||||
|
if not re.match(r'^[a-z][a-z0-9-]*$', domain_clean):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid domain '{domain_clean}': must start with lowercase letter "
|
||||||
|
"and contain only lowercase letters, numbers, and hyphens"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse and validate version
|
||||||
|
version_parts = version.split('.')
|
||||||
|
if len(version_parts) != 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid version '{version}': must be in format 'major.minor' (e.g., '1.0')"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
major = int(version_parts[0])
|
||||||
|
minor = int(version_parts[1])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid version '{version}': major and minor must be integers"
|
||||||
|
)
|
||||||
|
|
||||||
|
if major < 0 or minor < 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid version '{version}': major and minor must be non-negative"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"{domain_clean}-schema-v{major}.{minor}.md"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_schema_metadata(filename: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract metadata from a valid schema filename.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Schema filename to parse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with metadata
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SchemaFilenameError: If filename is invalid
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> extract_schema_metadata("manpage-schema-v1.0.md")
|
||||||
|
{'domain': 'manpage', 'version': '1.0', 'major': 1, 'minor': 0}
|
||||||
|
"""
|
||||||
|
is_valid, metadata = validate_schema_filename(filename)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
raise SchemaFilenameError(
|
||||||
|
f"Invalid schema filename: {filename}\n"
|
||||||
|
f"Expected format: {{domain}}-schema-v{{major}}.{{minor}}.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def get_validation_errors(filename: str) -> list:
|
||||||
|
"""
|
||||||
|
Get detailed validation errors for a filename.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Filename to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of error messages (empty if valid)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> get_validation_errors("manpage-schema-v1.0.md")
|
||||||
|
[]
|
||||||
|
|
||||||
|
>>> get_validation_errors("invalid.json")
|
||||||
|
['Filename does not match pattern: {domain}-schema-v{major}.{minor}.md', ...]
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Check basic pattern match
|
||||||
|
is_valid, _ = validate_schema_filename(filename)
|
||||||
|
if is_valid:
|
||||||
|
return errors
|
||||||
|
|
||||||
|
# Provide detailed feedback
|
||||||
|
errors.append(
|
||||||
|
f"Filename does not match pattern: {{domain}}-schema-v{{major}}.{{minor}}.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check extension
|
||||||
|
if not filename.endswith('.md'):
|
||||||
|
errors.append(f"Extension must be '.md', got: {Path(filename).suffix}")
|
||||||
|
|
||||||
|
# Check for version
|
||||||
|
if '-v' not in filename:
|
||||||
|
errors.append("Missing version: filename must include '-v{major}.{minor}'")
|
||||||
|
elif not re.search(r'-v\d+\.\d+', filename):
|
||||||
|
errors.append(
|
||||||
|
"Invalid version format: must be '-v{major}.{minor}' (e.g., '-v1.0')"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for schema keyword
|
||||||
|
if '-schema-' not in filename:
|
||||||
|
errors.append("Missing '-schema-' keyword in filename")
|
||||||
|
|
||||||
|
# Check for uppercase (must be lowercase)
|
||||||
|
if any(c.isupper() for c in filename):
|
||||||
|
errors.append("Filename must be lowercase")
|
||||||
|
|
||||||
|
# Check domain format (if we can isolate it)
|
||||||
|
parts = filename.split('-schema-')
|
||||||
|
if len(parts) >= 1:
|
||||||
|
domain = parts[0]
|
||||||
|
if domain and not re.match(r'^[a-z][a-z0-9-]*$', domain):
|
||||||
|
errors.append(
|
||||||
|
f"Invalid domain '{domain}': must start with lowercase letter "
|
||||||
|
"and contain only lowercase letters, numbers, and hyphens"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_schema_filename(filename: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if filename is valid (convenience function).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Filename to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> is_valid_schema_filename("manpage-schema-v1.0.md")
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> is_valid_schema_filename("invalid.json")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
is_valid, _ = validate_schema_filename(filename)
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def format_validation_message(filename: str) -> str:
|
||||||
|
"""
|
||||||
|
Format a user-friendly validation message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Filename that failed validation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted error message with suggestions
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> print(format_validation_message("manpage.json"))
|
||||||
|
❌ Invalid schema filename: manpage.json
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
errors = get_validation_errors(filename)
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return f"✅ Valid schema filename: {filename}"
|
||||||
|
|
||||||
|
message = f"❌ Invalid schema filename: {filename}\n\n"
|
||||||
|
message += "Errors:\n"
|
||||||
|
for i, error in enumerate(errors, 1):
|
||||||
|
message += f" {i}. {error}\n"
|
||||||
|
|
||||||
|
message += "\nExpected format: {domain}-schema-v{major}.{minor}.md\n"
|
||||||
|
message += "Example: manpage-schema-v1.0.md\n"
|
||||||
|
|
||||||
|
# Try to suggest a corrected filename
|
||||||
|
try:
|
||||||
|
# Extract domain guess (everything before first hyphen or dot)
|
||||||
|
domain_guess = filename.split('-')[0].split('.')[0]
|
||||||
|
suggestion = suggest_schema_filename(domain_guess, "1.0")
|
||||||
|
message += f"\nSuggested filename: {suggestion}\n"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return message
|
||||||
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
|
||||||
333
markitect/schemas/manpage-schema-v1.0.md
Normal file
333
markitect/schemas/manpage-schema-v1.0.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/manpage/v1.0"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable"
|
||||||
|
domain: "manpage"
|
||||||
|
description: "JSON schema for Unix-style manual pages with section classification and content control"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Unix Manual Page Schema v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This schema defines the structure and validation rules for Unix-style manual pages (manpages) in MarkiTect's markdown format. It includes comprehensive section classification, content control patterns, and quality guidelines to ensure consistent, high-quality documentation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Section Classification System**: Categorizes manpage sections as required, recommended, optional, discouraged, or improper
|
||||||
|
- **Content Control**: Validates content patterns, quality metrics, and structural requirements
|
||||||
|
- **Flexible Section Names**: Supports alternative section names (e.g., "FLAGS" as alternative to "OPTIONS")
|
||||||
|
- **Quality Enforcement**: Minimum/maximum content requirements for paragraphs, code blocks, and words
|
||||||
|
|
||||||
|
## Section Classifications
|
||||||
|
|
||||||
|
### Required Sections
|
||||||
|
- **SYNOPSIS**: Brief command syntax with all options and arguments
|
||||||
|
- **DESCRIPTION**: Detailed explanation of command purpose and functionality
|
||||||
|
|
||||||
|
### Recommended Sections
|
||||||
|
- **EXAMPLES**: Practical usage examples demonstrating common use cases
|
||||||
|
- **OPTIONS**: Detailed option descriptions with all flags and behaviors
|
||||||
|
- **SEE ALSO**: Related commands and documentation references
|
||||||
|
|
||||||
|
### Optional Sections
|
||||||
|
- **BUGS**: Known issues and bug reporting information
|
||||||
|
- **AUTHORS**: Contributors and maintainers
|
||||||
|
- **COPYRIGHT**: License information
|
||||||
|
- **HISTORY**: Historical development information
|
||||||
|
|
||||||
|
### Discouraged Sections
|
||||||
|
- **DEPRECATED**: Legacy content (should move to HISTORY)
|
||||||
|
- **OLD_SYNTAX**: Outdated syntax (should move to HISTORY or be removed)
|
||||||
|
|
||||||
|
### Improper Sections
|
||||||
|
- **INTERNAL_NOTES**: Development notes (must not appear in published docs)
|
||||||
|
- **TODO**: Development tasks (remove before publication)
|
||||||
|
- **DRAFT**: Draft markers (remove before publication)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Validating a Manpage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate my-command.1.md --schema manpage-schema-v1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Validation Errors
|
||||||
|
|
||||||
|
1. **Missing Required Sections**: Ensure SYNOPSIS and DESCRIPTION are present
|
||||||
|
2. **Content Too Brief**: DESCRIPTION should have at least 50 words
|
||||||
|
3. **No Examples**: While optional, EXAMPLES are highly recommended
|
||||||
|
4. **Improper Sections**: Remove TODO, DRAFT, and INTERNAL_NOTES before publication
|
||||||
|
|
||||||
|
## Content Quality Guidelines
|
||||||
|
|
||||||
|
### SYNOPSIS Section
|
||||||
|
- Show command name in bold: `**command**`
|
||||||
|
- Use brackets `[]` for optional arguments
|
||||||
|
- Use italic `*ARG*` for required arguments
|
||||||
|
- Keep concise (1-5 lines maximum)
|
||||||
|
- Include 5-150 words
|
||||||
|
|
||||||
|
### DESCRIPTION Section
|
||||||
|
- Start with what the command does
|
||||||
|
- Explain why users would use it
|
||||||
|
- Describe main functionality and features
|
||||||
|
- Minimum 50 words, maximum 1000 words
|
||||||
|
- At least 3 sentences
|
||||||
|
|
||||||
|
### EXAMPLES Section
|
||||||
|
- Use bash code blocks for commands
|
||||||
|
- Include comments explaining each example
|
||||||
|
- Start simple, progress to complex
|
||||||
|
- Show actual output when helpful
|
||||||
|
- Cover common use cases first
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Enhanced Markdown Manpage Schema with Classifications",
|
||||||
|
"description": "JSON schema for Unix-style manual pages with section classification and content control",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_title",
|
||||||
|
"content_instruction": "Brief command syntax showing all options and arguments in standard format",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 5,
|
||||||
|
"min_code_blocks": 0,
|
||||||
|
"max_code_blocks": 3,
|
||||||
|
"error_message": "SYNOPSIS section is mandatory for all manpages per Unix conventions"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Detailed explanation of what the command does, its purpose, and main functionality",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"max_paragraphs": 50,
|
||||||
|
"error_message": "DESCRIPTION section is mandatory for all manpages"
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Practical usage examples with explanations demonstrating common use cases",
|
||||||
|
"min_code_blocks": 3,
|
||||||
|
"max_code_blocks": 20,
|
||||||
|
"warning_if_missing": "Examples greatly improve manpage usability - highly recommended"
|
||||||
|
},
|
||||||
|
"SEE ALSO": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Related commands, configuration files, and documentation references",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"warning_if_missing": "Cross-references help users discover related functionality"
|
||||||
|
},
|
||||||
|
"OPTIONS": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Detailed option descriptions with all flags and their behaviors",
|
||||||
|
"alternatives": ["GLOBAL OPTIONS", "COMMAND OPTIONS", "FLAGS"],
|
||||||
|
"warning_if_missing": "Documenting command options helps users understand available functionality"
|
||||||
|
},
|
||||||
|
"BUGS": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Known issues, limitations, and bug reporting information"
|
||||||
|
},
|
||||||
|
"AUTHORS": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "List of contributors and maintainers"
|
||||||
|
},
|
||||||
|
"COPYRIGHT": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Copyright statement and license information"
|
||||||
|
},
|
||||||
|
"HISTORY": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Historical information about command development"
|
||||||
|
},
|
||||||
|
"DEPRECATED": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Consider moving deprecated content to historical documentation or HISTORY section"
|
||||||
|
},
|
||||||
|
"OLD_SYNTAX": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_if_missing": "Old syntax should be documented in HISTORY or removed entirely"
|
||||||
|
},
|
||||||
|
"INTERNAL_NOTES": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Internal notes must not appear in published manpages - move to developer documentation"
|
||||||
|
},
|
||||||
|
"TODO": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "TODO sections are for development only - remove before publication"
|
||||||
|
},
|
||||||
|
"DRAFT": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "DRAFT markers must be removed before publication"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"synopsis": {
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z][a-z0-9-]*\\*\\*",
|
||||||
|
"\\[.*\\]"
|
||||||
|
],
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"TBD"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 5,
|
||||||
|
"max_words": 150,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Show command name in bold (e.g., **command**)",
|
||||||
|
"Use brackets [] for optional arguments",
|
||||||
|
"Use italic *ARG* for required arguments",
|
||||||
|
"Keep synopsis concise (1-5 lines maximum)",
|
||||||
|
"Use ellipsis ... to indicate repeatable arguments"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"discouraged_patterns": [
|
||||||
|
"TODO",
|
||||||
|
"FIXME",
|
||||||
|
"\\bWIP\\b",
|
||||||
|
"\\bXXX\\b"
|
||||||
|
],
|
||||||
|
"forbidden_patterns": [
|
||||||
|
"password\\s*=\\s*[\"'].*[\"']",
|
||||||
|
"api[_-]?key\\s*=\\s*[\"'].*[\"']",
|
||||||
|
"secret\\s*=\\s*[\"'].*[\"']"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 1000,
|
||||||
|
"readability_target": "technical",
|
||||||
|
"min_sentences": 3
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Start with what the command does",
|
||||||
|
"Explain why users would use it",
|
||||||
|
"Describe main functionality and features",
|
||||||
|
"Mention any prerequisites or requirements",
|
||||||
|
"Keep technical but accessible"
|
||||||
|
],
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": false,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"required_patterns": [
|
||||||
|
"```",
|
||||||
|
"#"
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 100,
|
||||||
|
"max_words": 2000,
|
||||||
|
"readability_target": "general"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Use bash code blocks for command examples",
|
||||||
|
"Include comments explaining what each example does",
|
||||||
|
"Start with simple examples, progress to complex",
|
||||||
|
"Show actual output when helpful",
|
||||||
|
"Cover common use cases first"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Document heading structure",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Title heading in format: command(section) - description",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9-]+\\([0-9]\\) - .+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Main section headings",
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 30
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Subsection headings",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Text paragraphs",
|
||||||
|
"minItems": 10,
|
||||||
|
"maxItems": 500
|
||||||
|
},
|
||||||
|
"code_blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Code examples",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 50
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Lists for options and structured information",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 100
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Bold and italic text for commands and arguments",
|
||||||
|
"minItems": 20,
|
||||||
|
"maxItems": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs", "code_blocks", "emphasis"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial markdown schema version
|
||||||
|
- Migrated from enhanced-manpage JSON schema
|
||||||
|
- Added comprehensive documentation
|
||||||
|
- Implemented section classification system
|
||||||
|
- Added content control and quality guidelines
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Schema Naming Specification](../../roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md)
|
||||||
|
- [Schema Management Workplan](../../roadmap/schema-of-schemas/WORKPLAN.md)
|
||||||
|
- [MarkiTect Documentation](../../README.md)
|
||||||
@@ -40,6 +40,163 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["outline", "full"],
|
"enum": ["outline", "full"],
|
||||||
"description": "Mode used to generate this schema"
|
"description": "Mode used to generate this schema"
|
||||||
|
},
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Section classification and content control for document sections",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Z][A-Z0-9_ ]*$": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Section definition with classification and constraints",
|
||||||
|
"properties": {
|
||||||
|
"classification": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["required", "recommended", "optional", "discouraged", "improper"],
|
||||||
|
"description": "Classification level determining validation behavior"
|
||||||
|
},
|
||||||
|
"heading_level": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 6,
|
||||||
|
"description": "Expected heading level (H1-H6) for this section"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["after_title", "before_section_name", "after_section_name", "anywhere"],
|
||||||
|
"description": "Where this section should appear in the document"
|
||||||
|
},
|
||||||
|
"content_instruction": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable instruction for section content"
|
||||||
|
},
|
||||||
|
"min_paragraphs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum number of paragraphs in this section"
|
||||||
|
},
|
||||||
|
"max_paragraphs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum number of paragraphs in this section"
|
||||||
|
},
|
||||||
|
"min_code_blocks": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum number of code blocks in this section"
|
||||||
|
},
|
||||||
|
"max_code_blocks": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum number of code blocks in this section"
|
||||||
|
},
|
||||||
|
"min_lists": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum number of lists in this section"
|
||||||
|
},
|
||||||
|
"max_lists": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum number of lists in this section"
|
||||||
|
},
|
||||||
|
"warning_if_missing": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom warning message for missing recommended sections"
|
||||||
|
},
|
||||||
|
"error_message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom error message for required/improper section violations"
|
||||||
|
},
|
||||||
|
"alternatives": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Alternative section names that satisfy the requirement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["classification"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Content validation rules including patterns and quality metrics",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-z][a-z0-9_]*$": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Content control rules for a specific section",
|
||||||
|
"properties": {
|
||||||
|
"required_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Regex patterns that must appear in section content"
|
||||||
|
},
|
||||||
|
"discouraged_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Regex patterns that should not appear in content (warning)"
|
||||||
|
},
|
||||||
|
"forbidden_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Regex patterns that must not appear in content (error)"
|
||||||
|
},
|
||||||
|
"content_quality": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Quality metrics for section content",
|
||||||
|
"properties": {
|
||||||
|
"min_words": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum word count"
|
||||||
|
},
|
||||||
|
"max_words": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum word count"
|
||||||
|
},
|
||||||
|
"readability_target": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["simple", "general", "technical", "advanced"],
|
||||||
|
"description": "Target readability level"
|
||||||
|
},
|
||||||
|
"min_sentences": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum sentence count"
|
||||||
|
},
|
||||||
|
"max_sentences": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum sentence count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content_instructions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Array of human-readable content creation instructions"
|
||||||
|
},
|
||||||
|
"link_validation": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Link checking configuration",
|
||||||
|
"properties": {
|
||||||
|
"check_internal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Validate internal document links"
|
||||||
|
},
|
||||||
|
"check_external": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Validate external URLs"
|
||||||
|
},
|
||||||
|
"allow_fragments": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Allow fragment-only links like #section"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
|
|||||||
77
markitect/schemas/schema-catalog.yaml
Normal file
77
markitect/schemas/schema-catalog.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# MarkiTect Schema Catalog
|
||||||
|
#
|
||||||
|
# This catalog provides metadata about available schemas for markdown document validation.
|
||||||
|
# Schemas can be referenced by name or loaded from their file path.
|
||||||
|
|
||||||
|
version: "1.0"
|
||||||
|
description: "Catalog of registered MarkiTect schemas for document validation"
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
- id: "markitect-metaschema"
|
||||||
|
name: "MarkiTect Metaschema"
|
||||||
|
file: "markitect-metaschema.json"
|
||||||
|
version: "1.0"
|
||||||
|
description: "Metaschema for validating MarkiTect schema extensions"
|
||||||
|
type: "metaschema"
|
||||||
|
usage: "Used internally to validate schema files with MarkiTect-specific extensions"
|
||||||
|
tags:
|
||||||
|
- internal
|
||||||
|
- validation
|
||||||
|
- metaschema
|
||||||
|
|
||||||
|
- id: "terminology-v1"
|
||||||
|
name: "Terminology Document Schema"
|
||||||
|
file: "terminology-schema.json"
|
||||||
|
version: "1.0"
|
||||||
|
description: "Schema for validating terminology and glossary documents"
|
||||||
|
type: "document-schema"
|
||||||
|
usage: "Validates technical glossaries, terminology documents, and definition lists"
|
||||||
|
document_types:
|
||||||
|
- glossary
|
||||||
|
- terminology
|
||||||
|
- lexicon
|
||||||
|
- dictionary
|
||||||
|
features:
|
||||||
|
- Heading hierarchy validation (H1 → H2 → H3)
|
||||||
|
- Term structure validation (Definition, Synonyms, Related Terms, etc.)
|
||||||
|
- Content quality metrics (word counts, readability)
|
||||||
|
- MarkiTect extensions (x-markitect-sections, x-markitect-content-control)
|
||||||
|
- Classification system (required/recommended/optional)
|
||||||
|
example: "examples/terminology/terminology-example.md"
|
||||||
|
tags:
|
||||||
|
- documentation
|
||||||
|
- glossary
|
||||||
|
- terminology
|
||||||
|
- definitions
|
||||||
|
related_schemas: []
|
||||||
|
author: "MarkiTect Project"
|
||||||
|
created: "2026-01-04"
|
||||||
|
updated: "2026-01-04"
|
||||||
|
|
||||||
|
# Future schemas to add:
|
||||||
|
#
|
||||||
|
# - id: "manpage-v1"
|
||||||
|
# name: "Unix Manual Page Schema"
|
||||||
|
# description: "Schema for Unix/Linux manual page documentation"
|
||||||
|
#
|
||||||
|
# - id: "api-reference-v1"
|
||||||
|
# name: "API Reference Schema"
|
||||||
|
# description: "Schema for API endpoint documentation"
|
||||||
|
#
|
||||||
|
# - id: "arc42-v1"
|
||||||
|
# name: "arc42 Architecture Documentation Schema"
|
||||||
|
# description: "Schema for arc42 architecture documentation template"
|
||||||
|
#
|
||||||
|
# - id: "adr-v1"
|
||||||
|
# name: "Architecture Decision Record Schema"
|
||||||
|
# description: "Schema for ADR (Architecture Decision Record) documents"
|
||||||
|
#
|
||||||
|
# - id: "rfc-v1"
|
||||||
|
# name: "RFC/Specification Schema"
|
||||||
|
# description: "Schema for RFC-style specification documents"
|
||||||
|
|
||||||
|
# Schema discovery paths:
|
||||||
|
# - Built-in: markitect/schemas/*.json
|
||||||
|
# - User-defined: ~/.markitect/schemas/*.json
|
||||||
|
# - Project-specific: .markitect/schemas/*.json
|
||||||
|
# - Custom paths via MARKITECT_SCHEMA_PATH environment variable
|
||||||
519
markitect/schemas/schema-schema-v1.0.md
Normal file
519
markitect/schemas/schema-schema-v1.0.md
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/schema/v1.0"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable"
|
||||||
|
domain: "schema"
|
||||||
|
description: "Metaschema for validating MarkiTect schema files"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schema-for-Schemas v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This metaschema validates that MarkiTect schema files follow conventions and standards. It ensures schemas are well-formed, properly versioned, and include required MarkiTect extensions.
|
||||||
|
|
||||||
|
**Purpose**: Quality assurance for schema authors
|
||||||
|
|
||||||
|
**Validates**:
|
||||||
|
- Core JSON Schema fields (title, description, $schema, $id)
|
||||||
|
- Version format (SemVer: major.minor.patch)
|
||||||
|
- $id URL format (HTTPS with version)
|
||||||
|
- MarkiTect extensions (x-markitect-*)
|
||||||
|
- Section classification structures
|
||||||
|
- Content control patterns
|
||||||
|
|
||||||
|
## Schema Conventions
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
Every MarkiTect schema MUST include:
|
||||||
|
|
||||||
|
1. **$schema**: JSON Schema version (draft-07)
|
||||||
|
2. **$id**: Canonical HTTPS URL with version
|
||||||
|
3. **title**: Human-readable schema name
|
||||||
|
4. **description**: Brief explanation of what the schema validates
|
||||||
|
5. **version**: SemVer version string (major.minor.patch)
|
||||||
|
|
||||||
|
### Recommended Fields
|
||||||
|
|
||||||
|
Schemas SHOULD include:
|
||||||
|
|
||||||
|
- **type**: Root schema type (usually "object")
|
||||||
|
- **properties**: Object properties definition
|
||||||
|
- **required**: Array of required property names
|
||||||
|
|
||||||
|
### MarkiTect Extensions
|
||||||
|
|
||||||
|
#### x-markitect-sections
|
||||||
|
|
||||||
|
Defines document sections with classifications and content rules.
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"SECTION_NAME": {
|
||||||
|
"classification": "required|recommended|optional|discouraged|improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "What this section should contain",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 10,
|
||||||
|
"min_code_blocks": 0,
|
||||||
|
"max_code_blocks": 5,
|
||||||
|
"alternatives": ["ALTERNATIVE_NAME"],
|
||||||
|
"error_message": "Error if validation fails",
|
||||||
|
"warning_if_missing": "Warning if section absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Classifications**:
|
||||||
|
- `required`: Section must be present
|
||||||
|
- `recommended`: Section should be present (warning if missing)
|
||||||
|
- `optional`: Section may be present
|
||||||
|
- `discouraged`: Section should be avoided (warning if present)
|
||||||
|
- `improper`: Section must not be present (error if present)
|
||||||
|
|
||||||
|
#### x-markitect-content-control
|
||||||
|
|
||||||
|
Defines content patterns and quality metrics.
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"section_name": {
|
||||||
|
"required_patterns": ["regex1", "regex2"],
|
||||||
|
"discouraged_patterns": ["regex3"],
|
||||||
|
"forbidden_patterns": ["regex4"],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 50,
|
||||||
|
"max_words": 1000,
|
||||||
|
"readability_target": "technical|general",
|
||||||
|
"min_sentences": 3
|
||||||
|
},
|
||||||
|
"content_instructions": ["instruction1", "instruction2"],
|
||||||
|
"link_validation": {
|
||||||
|
"check_internal": true,
|
||||||
|
"check_external": false,
|
||||||
|
"allow_fragments": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### x-markitect-metadata
|
||||||
|
|
||||||
|
Additional schema metadata.
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "stable|draft|deprecated",
|
||||||
|
"authors": ["Author Name <email@example.com>"],
|
||||||
|
"created": "2026-01-04",
|
||||||
|
"updated": "2026-01-04",
|
||||||
|
"tags": ["tag1", "tag2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### x-markitect-source
|
||||||
|
|
||||||
|
Automatically added by schema loader (not in schema file).
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file": "/path/to/schema-v1.0.md",
|
||||||
|
"filename": "schema-v1.0.md",
|
||||||
|
"format": "markdown",
|
||||||
|
"frontmatter": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### $id Format
|
||||||
|
|
||||||
|
Must be HTTPS URL with version:
|
||||||
|
```
|
||||||
|
https://markitect.dev/schemas/{domain}/v{major}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- ✅ `https://markitect.dev/schemas/manpage/v1.0`
|
||||||
|
- ✅ `https://markitect.dev/schemas/api-documentation/v2.0`
|
||||||
|
- ❌ `http://example.com/schema` (not HTTPS)
|
||||||
|
- ❌ `https://markitect.dev/schemas/manpage` (no version)
|
||||||
|
|
||||||
|
### Version Format
|
||||||
|
|
||||||
|
Must be SemVer (major.minor.patch):
|
||||||
|
```
|
||||||
|
{major}.{minor}.{patch}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- ✅ `1.0.0`
|
||||||
|
- ✅ `2.5.3`
|
||||||
|
- ❌ `1.0` (missing patch)
|
||||||
|
- ❌ `v1.0.0` (has 'v' prefix)
|
||||||
|
|
||||||
|
### Title Format
|
||||||
|
|
||||||
|
Should be descriptive and end with "Schema":
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- ✅ "Unix Manual Page Schema"
|
||||||
|
- ✅ "API Documentation Schema"
|
||||||
|
- ❌ "Schema" (too generic)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Validating a Schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate a schema file
|
||||||
|
markitect schema-validate manpage-schema-v1.0.md
|
||||||
|
|
||||||
|
# Show detailed errors
|
||||||
|
markitect schema-validate manpage-schema-v1.0.md --detailed-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from markitect.schema_loader import MarkdownSchemaLoader
|
||||||
|
|
||||||
|
# Load schema to validate
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
schema_data = loader.load_schema(Path("my-schema-v1.0.md"))
|
||||||
|
|
||||||
|
# Check structure
|
||||||
|
issues = loader.validate_schema_structure(schema_data['schema'])
|
||||||
|
if issues:
|
||||||
|
for issue in issues:
|
||||||
|
print(f"⚠️ {issue}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Validation Errors
|
||||||
|
|
||||||
|
### Missing Required Fields
|
||||||
|
|
||||||
|
**Error**: `Missing required field: $schema`
|
||||||
|
|
||||||
|
**Solution**: Add `$schema` field:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid $id Format
|
||||||
|
|
||||||
|
**Error**: `$id should be a full HTTPS URL`
|
||||||
|
|
||||||
|
**Solution**: Use proper format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$id": "https://markitect.dev/schemas/my-domain/v1.0",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid Version Format
|
||||||
|
|
||||||
|
**Error**: `version must be in SemVer format (major.minor.patch)`
|
||||||
|
|
||||||
|
**Solution**: Use three-part version:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid Section Classification
|
||||||
|
|
||||||
|
**Error**: `Invalid classification value: 'mandatory'`
|
||||||
|
|
||||||
|
**Solution**: Use valid classification:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid values: `required`, `recommended`, `optional`, `discouraged`, `improper`
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/schema/v1.0",
|
||||||
|
"title": "MarkiTect Schema-for-Schemas",
|
||||||
|
"description": "Metaschema for validating MarkiTect schema files",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["$schema", "$id", "title", "description", "version"],
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "JSON Schema version (must be draft-07)"
|
||||||
|
},
|
||||||
|
"$id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^https://[a-z0-9.-]+/schemas/[a-z0-9-]+/v[0-9]+\\.[0-9]+$",
|
||||||
|
"description": "Canonical schema URI with HTTPS and version"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 5,
|
||||||
|
"maxLength": 200,
|
||||||
|
"description": "Human-readable schema name"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 500,
|
||||||
|
"description": "Brief explanation of what this schema validates"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
|
||||||
|
"description": "Semantic version (major.minor.patch)"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["object", "array", "string", "number", "boolean", "null"],
|
||||||
|
"description": "Root schema type"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Object property definitions"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Required property names"
|
||||||
|
},
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Section definitions with classifications",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Z][A-Z0-9_ ]*$": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["classification", "heading_level"],
|
||||||
|
"properties": {
|
||||||
|
"classification": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["required", "recommended", "optional", "discouraged", "improper"],
|
||||||
|
"description": "Section requirement level"
|
||||||
|
},
|
||||||
|
"heading_level": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 6,
|
||||||
|
"description": "Markdown heading level (1-6)"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["after_title", "before_title", "anywhere"],
|
||||||
|
"description": "Section position constraint"
|
||||||
|
},
|
||||||
|
"content_instruction": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What this section should contain"
|
||||||
|
},
|
||||||
|
"min_paragraphs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum paragraph count"
|
||||||
|
},
|
||||||
|
"max_paragraphs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Maximum paragraph count"
|
||||||
|
},
|
||||||
|
"min_code_blocks": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum code block count"
|
||||||
|
},
|
||||||
|
"max_code_blocks": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Maximum code block count"
|
||||||
|
},
|
||||||
|
"alternatives": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Alternative section names"
|
||||||
|
},
|
||||||
|
"error_message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error message if validation fails"
|
||||||
|
},
|
||||||
|
"warning_if_missing": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Warning message if section absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Content pattern and quality rules",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-z_]+$": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"required_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Required regex patterns"
|
||||||
|
},
|
||||||
|
"discouraged_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Patterns to warn about"
|
||||||
|
},
|
||||||
|
"forbidden_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Patterns that cause errors"
|
||||||
|
},
|
||||||
|
"content_quality": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"min_words": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Minimum word count"
|
||||||
|
},
|
||||||
|
"max_words": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Maximum word count"
|
||||||
|
},
|
||||||
|
"readability_target": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["technical", "general"],
|
||||||
|
"description": "Target readability level"
|
||||||
|
},
|
||||||
|
"min_sentences": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Minimum sentence count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content_instructions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Content writing guidelines"
|
||||||
|
},
|
||||||
|
"link_validation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"check_internal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Validate internal links"
|
||||||
|
},
|
||||||
|
"check_external": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Validate external links"
|
||||||
|
},
|
||||||
|
"allow_fragments": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Allow fragment identifiers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Additional schema metadata",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["stable", "draft", "deprecated"],
|
||||||
|
"description": "Schema lifecycle status"
|
||||||
|
},
|
||||||
|
"authors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Schema authors"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"description": "Creation date (ISO 8601)"
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"description": "Last update date (ISO 8601)"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Schema tags for categorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-source": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Source file metadata (added by loader)",
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full file path"
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "File name only"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["markdown", "json"],
|
||||||
|
"description": "Source file format"
|
||||||
|
},
|
||||||
|
"frontmatter": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "YAML frontmatter from markdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial metaschema version
|
||||||
|
- Validates core JSON Schema fields
|
||||||
|
- Validates MarkiTect extensions
|
||||||
|
- Supports section classifications
|
||||||
|
- Supports content control patterns
|
||||||
|
- SemVer version validation
|
||||||
|
- HTTPS $id URL validation
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Schema Naming Specification](../../roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md)
|
||||||
|
- [Schema Loader Guide](../../roadmap/schema-of-schemas/SCHEMA_LOADER_GUIDE.md)
|
||||||
|
- [Schema Management Workplan](../../roadmap/schema-of-schemas/WORKPLAN.md)
|
||||||
214
markitect/schemas/terminology-schema.json
Normal file
214
markitect/schemas/terminology-schema.json
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/terminology-v1.json",
|
||||||
|
"title": "Terminology Document Schema",
|
||||||
|
"description": "Schema for validating terminology and glossary documents with consistent structure",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level_1": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Main document title",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": ".*(Terminology|Glossary|Terms|Definitions).*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
},
|
||||||
|
"level_2": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Category headings (Core Concepts, Document Types, etc.)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 20
|
||||||
|
},
|
||||||
|
"level_3": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Individual term headings",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Term name - should be title case"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["level_1", "level_2", "level_3"]
|
||||||
|
},
|
||||||
|
"paragraphs": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Content paragraphs including definitions and descriptions",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 3
|
||||||
|
},
|
||||||
|
"bold_text": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Bold text used for field labels (Definition, Synonyms, etc.)",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Definition:",
|
||||||
|
"Synonyms:",
|
||||||
|
"Related Terms:",
|
||||||
|
"Example:",
|
||||||
|
"Examples:",
|
||||||
|
"Use Cases:",
|
||||||
|
"Usage:",
|
||||||
|
"Format:",
|
||||||
|
"Components:",
|
||||||
|
"Steps:",
|
||||||
|
"Tools:",
|
||||||
|
"Levels:",
|
||||||
|
"Status:",
|
||||||
|
"Migration:",
|
||||||
|
"Required:",
|
||||||
|
"Recommended:",
|
||||||
|
"Optional:",
|
||||||
|
"Discouraged:",
|
||||||
|
"Improper:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["headings", "paragraphs"],
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"document_title": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 1,
|
||||||
|
"content_instruction": "Main title should include words like 'Terminology', 'Glossary', or 'Definitions'",
|
||||||
|
"pattern": ".*(Terminology|Glossary|Terms|Definitions).*"
|
||||||
|
},
|
||||||
|
"category_sections": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"min_sections": 1,
|
||||||
|
"content_instruction": "Organize terms into logical categories (e.g., Core Concepts, Document Types, Process Terms)"
|
||||||
|
},
|
||||||
|
"term_definitions": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 3,
|
||||||
|
"min_sections": 1,
|
||||||
|
"content_instruction": "Each term should be a level 3 heading followed by its definition and optional metadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"term_structure": {
|
||||||
|
"required_components": [
|
||||||
|
{
|
||||||
|
"label": "Definition:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Clear, concise definition of the term"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optional_components": [
|
||||||
|
{
|
||||||
|
"label": "Synonyms:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Alternative names or abbreviations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Related Terms:",
|
||||||
|
"type": "bold_text",
|
||||||
|
"description": "Links to related concepts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Example:",
|
||||||
|
"type": "bold_text_or_code",
|
||||||
|
"description": "Practical example demonstrating the term"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Use Cases:",
|
||||||
|
"type": "list",
|
||||||
|
"description": "Common scenarios where term applies"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words_per_definition": 10,
|
||||||
|
"max_words_per_definition": 200,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Start each term with a level 3 heading containing the term name",
|
||||||
|
"Follow immediately with 'Definition:' in bold",
|
||||||
|
"Provide a clear, self-contained definition",
|
||||||
|
"Add optional fields (Synonyms, Related Terms, Examples) as needed",
|
||||||
|
"Use consistent formatting across all terms",
|
||||||
|
"Group related terms under category headings (level 2)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"definition_pattern": {
|
||||||
|
"description": "Each definition should follow: Term heading (###) → Definition: (bold) → Definition text",
|
||||||
|
"validation": {
|
||||||
|
"heading_level_3_followed_by": "bold_text_starting_with_Definition",
|
||||||
|
"definition_length": {
|
||||||
|
"min_words": 10,
|
||||||
|
"max_words": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated_terms": {
|
||||||
|
"classification": "optional",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Optional section for deprecated terms with migration guidance",
|
||||||
|
"required_fields": [
|
||||||
|
"Status: DEPRECATED",
|
||||||
|
"Migration:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-validation-rules": {
|
||||||
|
"term_count": {
|
||||||
|
"min": 3,
|
||||||
|
"recommended_min": 10,
|
||||||
|
"description": "Terminology document should define at least 3 terms, 10+ recommended"
|
||||||
|
},
|
||||||
|
"category_balance": {
|
||||||
|
"description": "Each category should have at least 2 terms",
|
||||||
|
"min_terms_per_category": 2
|
||||||
|
},
|
||||||
|
"definition_quality": {
|
||||||
|
"all_terms_must_have_definition": true,
|
||||||
|
"definition_must_follow_term_heading": true,
|
||||||
|
"definition_min_words": 10
|
||||||
|
},
|
||||||
|
"consistency": {
|
||||||
|
"use_consistent_field_labels": true,
|
||||||
|
"maintain_heading_hierarchy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
<meta name="generator" content="Markitect {version}">
|
<meta name="generator" content="Markitect {version}">
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
||||||
|
{css_content}
|
||||||
|
|
||||||
<!-- Base styling for document content -->
|
<!-- Base styling for document content -->
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|||||||
787
roadmap/SCHEMA_EVOLUTION_WORKPLAN.md
Normal file
787
roadmap/SCHEMA_EVOLUTION_WORKPLAN.md
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
# MarkiTect Schema Evolution Workplan
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Current State**: MarkiTect validates document structure via JSON Schema, but is too rigid (exact counts) and structure-only (no content guidance).
|
||||||
|
|
||||||
|
**Target State**: A flexible schema system with content control, section classification, multi-schema conformance, and blueprint-based document generation.
|
||||||
|
|
||||||
|
**Timeline**: 5 phases, 15-20 development sessions, approximately 8-10 weeks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Analysis
|
||||||
|
|
||||||
|
### Current Limitations
|
||||||
|
|
||||||
|
#### 1. Structural Rigidity
|
||||||
|
**Problem**: Auto-generated schemas use exact counts
|
||||||
|
```json
|
||||||
|
"paragraphs": { "minItems": 86, "maxItems": 86 }
|
||||||
|
```
|
||||||
|
**Impact**: Schemas are document-specific, not reusable patterns.
|
||||||
|
|
||||||
|
#### 2. Binary Structure Validation
|
||||||
|
**Problem**: Elements are either valid or invalid, no classification.
|
||||||
|
**Need**: Required, Recommended, Optional, Discouraged, Improper classifications.
|
||||||
|
|
||||||
|
#### 3. No Content Guidance
|
||||||
|
**Problem**: Schemas validate structure exists, not what content belongs there.
|
||||||
|
**Need**: Content instructions, semantic patterns, quality expectations.
|
||||||
|
|
||||||
|
#### 4. Single Schema Limitation
|
||||||
|
**Problem**: Documents can only conform to one schema.
|
||||||
|
**Need**: Multi-schema conformance (e.g., "manpage" + "API reference" + "tutorial").
|
||||||
|
|
||||||
|
#### 5. Template Generation Gap
|
||||||
|
**Problem**: `generate-stub` creates outline, but no content guidance or data binding.
|
||||||
|
**Need**: Blueprint system with content instructions and data templates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
### Three-Layer System
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ BLUEPRINT LAYER │
|
||||||
|
│ (Multi-schema + Content + Data Templates) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SCHEMA LAYER (Enhanced) │
|
||||||
|
│ (Structure + Classification + Instructions) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ VALIDATION LAYER │
|
||||||
|
│ (AST Validation + Content Analysis) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
**1. Schema Classification System**
|
||||||
|
- **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
|
||||||
|
|
||||||
|
**2. Content Control**
|
||||||
|
- **Content Instructions**: Human-readable guidance for section content
|
||||||
|
- **Content Patterns**: Regex/template patterns for content validation
|
||||||
|
- **Content Quality Metrics**: Word count, readability, completeness scoring
|
||||||
|
|
||||||
|
**3. Multi-Schema Conformance**
|
||||||
|
- Documents can conform to multiple schemas simultaneously
|
||||||
|
- Schema composition and inheritance
|
||||||
|
- Conflict resolution strategies
|
||||||
|
|
||||||
|
**4. Blueprint System**
|
||||||
|
- Schemas + Instructions + Data Templates = Blueprints
|
||||||
|
- Blueprints generate documents with content guidance
|
||||||
|
- Data binding for dynamic document generation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Enhanced Schema Format
|
||||||
|
|
||||||
|
**Goal**: Extend JSON Schema with MarkiTect-specific content control extensions.
|
||||||
|
|
||||||
|
### 1.1 Schema Classification Extensions
|
||||||
|
|
||||||
|
**New Properties**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"position": "after_title",
|
||||||
|
"content_instruction": "Brief command syntax showing all options",
|
||||||
|
"min_code_blocks": 1,
|
||||||
|
"max_code_blocks": 3
|
||||||
|
},
|
||||||
|
"EXAMPLES": {
|
||||||
|
"classification": "recommended",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Practical usage examples with explanations",
|
||||||
|
"min_code_blocks": 3,
|
||||||
|
"warning_if_missing": "Examples greatly improve documentation usability"
|
||||||
|
},
|
||||||
|
"DEPRECATED": {
|
||||||
|
"classification": "discouraged",
|
||||||
|
"heading_level": 2,
|
||||||
|
"warning_message": "DEPRECATED sections should be moved to historical docs"
|
||||||
|
},
|
||||||
|
"INTERNAL_NOTES": {
|
||||||
|
"classification": "improper",
|
||||||
|
"heading_level": 2,
|
||||||
|
"error_message": "Internal notes must not appear in published documentation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Content Control Extensions
|
||||||
|
|
||||||
|
**New Properties**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"synopsis_section": {
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 3,
|
||||||
|
"required_patterns": [
|
||||||
|
"\\*\\*[a-z-]+\\*\\*.*\\[.*\\]" // Bold command with args
|
||||||
|
],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 10,
|
||||||
|
"max_words": 100,
|
||||||
|
"readability_target": "technical"
|
||||||
|
},
|
||||||
|
"content_instructions": [
|
||||||
|
"Show command name in bold",
|
||||||
|
"Include all major options in synopsis",
|
||||||
|
"Use italic for arguments and placeholders"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Flexible Structure Constraints
|
||||||
|
|
||||||
|
**Replace rigid counts with ranges and classifications**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"properties": {
|
||||||
|
"level_2": {
|
||||||
|
"items": {
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"oneOf": [
|
||||||
|
{"const": "SYNOPSIS", "x-markitect-classification": "required"},
|
||||||
|
{"const": "DESCRIPTION", "x-markitect-classification": "required"},
|
||||||
|
{"const": "EXAMPLES", "x-markitect-classification": "recommended"},
|
||||||
|
{"const": "SEE ALSO", "x-markitect-classification": "optional"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minItems": 2, // At least required sections
|
||||||
|
"maxItems": 30 // Reasonable upper bound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] **Task 1.1**: Define `x-markitect-sections` schema extension format
|
||||||
|
- [ ] **Task 1.2**: Define `x-markitect-content-control` schema extension format
|
||||||
|
- [ ] **Task 1.3**: Update metaschema to validate new extensions
|
||||||
|
- [ ] **Task 1.4**: Create schema examples demonstrating all classifications
|
||||||
|
- [ ] **Task 1.5**: Document schema extension format
|
||||||
|
|
||||||
|
**Duration**: 3-4 sessions
|
||||||
|
**Dependencies**: None
|
||||||
|
**Deliverables**: Enhanced schema format specification, updated metaschema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Schema Refinement Tools
|
||||||
|
|
||||||
|
**Goal**: Tools to transform rigid auto-generated schemas into flexible, classified schemas.
|
||||||
|
|
||||||
|
### 2.1 Schema Analysis Tool
|
||||||
|
|
||||||
|
**Command**: `markitect schema-analyze`
|
||||||
|
|
||||||
|
Analyzes existing schema and suggests improvements:
|
||||||
|
```bash
|
||||||
|
markitect schema-analyze rigid-schema.json
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
⚠️ Exact counts detected (86 paragraphs)
|
||||||
|
Suggestion: Use range 50-150 for flexibility
|
||||||
|
|
||||||
|
⚠️ All sections unclassified
|
||||||
|
Suggestion: Classify sections as required/recommended/optional
|
||||||
|
|
||||||
|
⚠️ No content instructions
|
||||||
|
Suggestion: Add content guidance for key sections
|
||||||
|
|
||||||
|
✨ Run: markitect schema-refine rigid-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Schema Refinement Tool
|
||||||
|
|
||||||
|
**Command**: `markitect schema-refine`
|
||||||
|
|
||||||
|
Interactive or automated schema refinement:
|
||||||
|
```bash
|
||||||
|
# Automated: Apply common refinements
|
||||||
|
markitect schema-refine rigid-schema.json \
|
||||||
|
--loosen-counts \
|
||||||
|
--add-classifications \
|
||||||
|
--output flexible-schema.json
|
||||||
|
|
||||||
|
# Interactive: Guided refinement
|
||||||
|
markitect schema-refine rigid-schema.json --interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refinement Operations**:
|
||||||
|
- Convert exact counts to ranges (configurable tolerance)
|
||||||
|
- Classify sections based on conventions
|
||||||
|
- Add content instructions from templates
|
||||||
|
- Merge multiple schemas for common patterns
|
||||||
|
|
||||||
|
### 2.3 Schema Composition Tool
|
||||||
|
|
||||||
|
**Command**: `markitect schema-compose`
|
||||||
|
|
||||||
|
Combine multiple schemas:
|
||||||
|
```bash
|
||||||
|
# Create composite schema
|
||||||
|
markitect schema-compose \
|
||||||
|
--base manpage-schema.json \
|
||||||
|
--extend api-reference-schema.json \
|
||||||
|
--extend tutorial-schema.json \
|
||||||
|
--output composite-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] **Task 2.1**: Implement `schema-analyze` command
|
||||||
|
- [ ] **Task 2.2**: Implement `schema-refine` command with loosening logic
|
||||||
|
- [ ] **Task 2.3**: Implement `schema-refine --interactive` mode
|
||||||
|
- [ ] **Task 2.4**: Implement `schema-compose` command
|
||||||
|
- [ ] **Task 2.5**: Create schema refinement rule library
|
||||||
|
|
||||||
|
**Duration**: 3-4 sessions
|
||||||
|
**Dependencies**: Phase 1 complete
|
||||||
|
**Deliverables**: Schema analysis, refinement, and composition tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Enhanced Validation Engine
|
||||||
|
|
||||||
|
**Goal**: Validate classification levels, content patterns, and multi-schema conformance.
|
||||||
|
|
||||||
|
### 3.1 Classification-Aware Validation
|
||||||
|
|
||||||
|
**Validation Levels**:
|
||||||
|
```python
|
||||||
|
class ValidationResult:
|
||||||
|
status: Literal["valid", "valid_with_warnings", "invalid"]
|
||||||
|
errors: List[ValidationError] # Required/Improper violations
|
||||||
|
warnings: List[ValidationWarning] # Recommended/Discouraged violations
|
||||||
|
suggestions: List[str] # Optional improvements
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Output**:
|
||||||
|
```bash
|
||||||
|
markitect validate document.md schema.json --detailed-errors
|
||||||
|
|
||||||
|
❌ ERRORS (validation failed)
|
||||||
|
- Missing required section: SYNOPSIS
|
||||||
|
- Improper section present: INTERNAL_NOTES
|
||||||
|
|
||||||
|
⚠️ WARNINGS
|
||||||
|
- Missing recommended section: EXAMPLES
|
||||||
|
- Discouraged section present: DEPRECATED
|
||||||
|
|
||||||
|
💡 SUGGESTIONS
|
||||||
|
- Consider adding optional section: PERFORMANCE
|
||||||
|
- Content quality: DESCRIPTION section below recommended word count (45/100)
|
||||||
|
|
||||||
|
Status: INVALID (2 errors, 2 warnings)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Content Pattern Validation
|
||||||
|
|
||||||
|
**Validate content patterns**:
|
||||||
|
```python
|
||||||
|
# Schema specifies required patterns
|
||||||
|
"synopsis_section": {
|
||||||
|
"required_patterns": [
|
||||||
|
r"\*\*command\*\*", # Bold command name
|
||||||
|
r"\[.*\]" # Options in brackets
|
||||||
|
],
|
||||||
|
"discouraged_patterns": [
|
||||||
|
r"TODO", # No TODOs in published docs
|
||||||
|
r"FIXME"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Multi-Schema Validation
|
||||||
|
|
||||||
|
**Command**: `markitect validate --schemas`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate against multiple schemas
|
||||||
|
markitect validate api-doc.md \
|
||||||
|
--schemas manpage.json,api-reference.json,tutorial.json \
|
||||||
|
--require-all
|
||||||
|
|
||||||
|
# Output shows conformance to each schema
|
||||||
|
✅ manpage.json: VALID
|
||||||
|
✅ api-reference.json: VALID (2 warnings)
|
||||||
|
❌ tutorial.json: INVALID (missing required section: GETTING STARTED)
|
||||||
|
|
||||||
|
Overall: INVALID (must conform to all schemas)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Content Quality Metrics
|
||||||
|
|
||||||
|
**Validate content quality**:
|
||||||
|
```bash
|
||||||
|
markitect validate document.md schema.json --quality-check
|
||||||
|
|
||||||
|
📊 Content Quality Report
|
||||||
|
- Word count: 487 (target: 300-1000) ✅
|
||||||
|
- Code examples: 3 (minimum: 3) ✅
|
||||||
|
- Readability: Technical (appropriate) ✅
|
||||||
|
- Link validity: 12/12 valid ✅
|
||||||
|
- Heading hierarchy: Valid ✅
|
||||||
|
|
||||||
|
Quality Score: 95/100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] **Task 3.1**: Implement classification-aware validator
|
||||||
|
- [ ] **Task 3.2**: Implement content pattern validation
|
||||||
|
- [ ] **Task 3.3**: Implement multi-schema validation
|
||||||
|
- [ ] **Task 3.4**: Implement content quality metrics
|
||||||
|
- [ ] **Task 3.5**: Enhanced error reporting with suggestions
|
||||||
|
|
||||||
|
**Duration**: 4-5 sessions
|
||||||
|
**Dependencies**: Phase 1 complete
|
||||||
|
**Deliverables**: Enhanced validation engine, quality metrics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Blueprint System
|
||||||
|
|
||||||
|
**Goal**: Document generation system with schemas + content instructions + data templates.
|
||||||
|
|
||||||
|
### 4.1 Blueprint Format
|
||||||
|
|
||||||
|
**Blueprint Structure**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$blueprint": "1.0",
|
||||||
|
"name": "api-documentation-blueprint",
|
||||||
|
"description": "Blueprint for API endpoint documentation",
|
||||||
|
|
||||||
|
"schemas": [
|
||||||
|
"manpage-schema.json",
|
||||||
|
"api-reference-schema.json"
|
||||||
|
],
|
||||||
|
|
||||||
|
"content_model": {
|
||||||
|
"synopsis": {
|
||||||
|
"template": "**{{command}}** [*OPTIONS*] *{{primary_argument}}*",
|
||||||
|
"data_source": "command_metadata.json",
|
||||||
|
"instruction": "Brief command syntax"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"template": "{{description}}\n\nThis endpoint {{purpose}}.",
|
||||||
|
"min_paragraphs": 2,
|
||||||
|
"instruction": "Explain what the endpoint does and why to use it"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"template": "{{#each parameters}}\n**{{name}}** *{{type}}*\n: {{description}}\n{{/each}}",
|
||||||
|
"data_source": "parameters",
|
||||||
|
"instruction": "Document all parameters with types and descriptions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"data_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {"type": "string"},
|
||||||
|
"primary_argument": {"type": "string"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"purpose": {"type": "string"},
|
||||||
|
"parameters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"type": {"type": "string"},
|
||||||
|
"description": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"generation_rules": {
|
||||||
|
"heading_style": "atx",
|
||||||
|
"code_fence_style": "backticks",
|
||||||
|
"line_length": 80,
|
||||||
|
"include_metadata": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Blueprint Commands
|
||||||
|
|
||||||
|
**Create Blueprint**:
|
||||||
|
```bash
|
||||||
|
# From existing schema
|
||||||
|
markitect blueprint-create --from-schema api-schema.json \
|
||||||
|
--output api-blueprint.json
|
||||||
|
|
||||||
|
# Interactive creation
|
||||||
|
markitect blueprint-create --interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate from Blueprint**:
|
||||||
|
```bash
|
||||||
|
# Generate with data file
|
||||||
|
markitect blueprint-generate api-blueprint.json \
|
||||||
|
--data endpoint-data.json \
|
||||||
|
--output api-doc.md
|
||||||
|
|
||||||
|
# Generate with inline data
|
||||||
|
markitect blueprint-generate api-blueprint.json \
|
||||||
|
--data '{"command": "api-call", "description": "Make API call"}' \
|
||||||
|
--output api-doc.md
|
||||||
|
|
||||||
|
# Batch generation
|
||||||
|
markitect blueprint-generate-batch api-blueprint.json \
|
||||||
|
--data-dir ./endpoints/ \
|
||||||
|
--output-dir ./docs/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validate Blueprint**:
|
||||||
|
```bash
|
||||||
|
# Validate blueprint format
|
||||||
|
markitect blueprint-validate api-blueprint.json
|
||||||
|
|
||||||
|
# Test blueprint generation
|
||||||
|
markitect blueprint-test api-blueprint.json \
|
||||||
|
--sample-data test-data.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Template Engine Integration
|
||||||
|
|
||||||
|
**Handlebars-style templates with MarkiTect extensions**:
|
||||||
|
```markdown
|
||||||
|
# {{command}}(1) - {{title}}
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
|
||||||
|
**{{command}}** {{#each options}}[*{{this}}*] {{/each}}*{{argument}}*
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
{{description}}
|
||||||
|
|
||||||
|
{{#markitect-section "technical-details"}}
|
||||||
|
Technical implementation details for {{command}}.
|
||||||
|
{{/markitect-section}}
|
||||||
|
|
||||||
|
## PARAMETERS
|
||||||
|
|
||||||
|
{{#each parameters}}
|
||||||
|
**--{{name}}** *{{type}}*
|
||||||
|
: {{description}}
|
||||||
|
: {{#if default}}Default: `{{default}}`{{/if}}
|
||||||
|
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#markitect-code-block "bash"}}
|
||||||
|
# Example usage
|
||||||
|
{{command}} {{#each examples.[0].args}}{{this}} {{/each}}
|
||||||
|
{{/markitect-code-block}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] **Task 4.1**: Define blueprint format specification
|
||||||
|
- [ ] **Task 4.2**: Implement `blueprint-create` command
|
||||||
|
- [ ] **Task 4.3**: Implement `blueprint-generate` command
|
||||||
|
- [ ] **Task 4.4**: Implement template engine with Handlebars
|
||||||
|
- [ ] **Task 4.5**: Implement `blueprint-validate` command
|
||||||
|
- [ ] **Task 4.6**: Implement batch generation
|
||||||
|
- [ ] **Task 4.7**: Create blueprint library (common patterns)
|
||||||
|
|
||||||
|
**Duration**: 5-6 sessions
|
||||||
|
**Dependencies**: Phases 1 and 3 complete
|
||||||
|
**Deliverables**: Blueprint system, template engine, generation commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Documentation and Integration
|
||||||
|
|
||||||
|
**Goal**: Comprehensive documentation, examples, and ecosystem integration.
|
||||||
|
|
||||||
|
### 5.1 Documentation Suite
|
||||||
|
|
||||||
|
**Documents to Create**:
|
||||||
|
- [ ] Schema Evolution Guide (why and how)
|
||||||
|
- [ ] Schema Classification Reference
|
||||||
|
- [ ] Content Control Specification
|
||||||
|
- [ ] Blueprint System Guide
|
||||||
|
- [ ] Schema Design Best Practices
|
||||||
|
- [ ] Migration Guide (old schemas → new format)
|
||||||
|
- [ ] API Reference for programmatic usage
|
||||||
|
|
||||||
|
### 5.2 Example Gallery
|
||||||
|
|
||||||
|
**Create comprehensive examples**:
|
||||||
|
- [ ] Manpage blueprint (already started)
|
||||||
|
- [ ] API documentation blueprint
|
||||||
|
- [ ] Tutorial document blueprint
|
||||||
|
- [ ] Architecture Decision Record (ADR) blueprint
|
||||||
|
- [ ] RFC/specification blueprint
|
||||||
|
- [ ] Meeting notes blueprint
|
||||||
|
- [ ] Project README blueprint
|
||||||
|
|
||||||
|
### 5.3 CLI Integration
|
||||||
|
|
||||||
|
**Update existing commands**:
|
||||||
|
```bash
|
||||||
|
# schema-generate with classification
|
||||||
|
markitect schema-generate example.md \
|
||||||
|
--classify-sections \
|
||||||
|
--add-instructions \
|
||||||
|
--flexible \
|
||||||
|
--output smart-schema.json
|
||||||
|
|
||||||
|
# validate with multiple schemas
|
||||||
|
markitect validate doc.md \
|
||||||
|
--schemas schema1.json,schema2.json \
|
||||||
|
--classification-aware \
|
||||||
|
--quality-check
|
||||||
|
|
||||||
|
# generate-stub enhanced
|
||||||
|
markitect generate-stub schema.json \
|
||||||
|
--include-instructions \
|
||||||
|
--sample-content \
|
||||||
|
--output template.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 CI/CD Integration Templates
|
||||||
|
|
||||||
|
**Provide ready-to-use integrations**:
|
||||||
|
|
||||||
|
GitHub Actions:
|
||||||
|
```yaml
|
||||||
|
- name: Validate Documentation
|
||||||
|
uses: markitect/validate-action@v1
|
||||||
|
with:
|
||||||
|
schemas: docs/schemas/*.json
|
||||||
|
files: docs/**/*.md
|
||||||
|
classification-aware: true
|
||||||
|
fail-on: errors
|
||||||
|
warn-on: missing-recommended
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-commit hook:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
markitect validate-changed --schemas docs/schemas/ \
|
||||||
|
--classification-aware \
|
||||||
|
--fail-on errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [ ] **Task 5.1**: Write comprehensive documentation suite
|
||||||
|
- [ ] **Task 5.2**: Create example gallery with 7+ blueprints
|
||||||
|
- [ ] **Task 5.3**: Update all CLI commands for new features
|
||||||
|
- [ ] **Task 5.4**: Create CI/CD integration templates
|
||||||
|
- [ ] **Task 5.5**: Write migration guide for existing schemas
|
||||||
|
- [ ] **Task 5.6**: Create video tutorials/screencasts
|
||||||
|
|
||||||
|
**Duration**: 3-4 sessions
|
||||||
|
**Dependencies**: All previous phases complete
|
||||||
|
**Deliverables**: Complete documentation, examples, integrations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Development Approach
|
||||||
|
|
||||||
|
**1. Test-Driven Development**
|
||||||
|
- Write tests for each classification level
|
||||||
|
- Test schema refinement transformations
|
||||||
|
- Test blueprint generation with various data
|
||||||
|
- Test multi-schema validation
|
||||||
|
|
||||||
|
**2. Backward Compatibility**
|
||||||
|
- Existing schemas continue to work
|
||||||
|
- New features are opt-in via extensions
|
||||||
|
- Clear migration path documented
|
||||||
|
|
||||||
|
**3. Incremental Rollout**
|
||||||
|
- Phase 1: Can be used immediately after completion
|
||||||
|
- Each phase delivers user value independently
|
||||||
|
- Later phases build on earlier phases
|
||||||
|
|
||||||
|
**4. Community Feedback**
|
||||||
|
- Alpha release after Phase 1
|
||||||
|
- Beta release after Phase 3
|
||||||
|
- Stable release after Phase 5
|
||||||
|
|
||||||
|
### Technical Considerations
|
||||||
|
|
||||||
|
**Schema Format**:
|
||||||
|
- JSON Schema draft-07 as foundation
|
||||||
|
- MarkiTect extensions namespaced with `x-markitect-`
|
||||||
|
- Validation via metaschema
|
||||||
|
- Clear upgrade path to future JSON Schema versions
|
||||||
|
|
||||||
|
**Performance**:
|
||||||
|
- Cache compiled schemas
|
||||||
|
- Lazy validation for large documents
|
||||||
|
- Parallel validation for multiple schemas
|
||||||
|
- Optimize content pattern matching
|
||||||
|
|
||||||
|
**API Design**:
|
||||||
|
- Programmatic access to all features
|
||||||
|
- Python API for schema manipulation
|
||||||
|
- Plugin system for custom validators
|
||||||
|
- Extensible template engine
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Phase 1 Success
|
||||||
|
- ✅ Schema with all 5 classifications validates correctly
|
||||||
|
- ✅ Content instructions appear in generated stubs
|
||||||
|
- ✅ Metaschema validates all extension formats
|
||||||
|
|
||||||
|
### Phase 2 Success
|
||||||
|
- ✅ Rigid schema refined to flexible schema automatically
|
||||||
|
- ✅ Multiple schemas composed without conflicts
|
||||||
|
- ✅ Interactive refinement completes end-to-end
|
||||||
|
|
||||||
|
### Phase 3 Success
|
||||||
|
- ✅ Validation distinguishes errors from warnings
|
||||||
|
- ✅ Content patterns detected and reported
|
||||||
|
- ✅ Multi-schema validation works with 3+ schemas
|
||||||
|
- ✅ Quality metrics provide actionable feedback
|
||||||
|
|
||||||
|
### Phase 4 Success
|
||||||
|
- ✅ Blueprint generates valid document from data
|
||||||
|
- ✅ Generated document validates against source schemas
|
||||||
|
- ✅ Batch generation processes 100+ documents
|
||||||
|
- ✅ Template engine supports complex logic
|
||||||
|
|
||||||
|
### Phase 5 Success
|
||||||
|
- ✅ Documentation covers all features
|
||||||
|
- ✅ 7+ working blueprint examples
|
||||||
|
- ✅ CI/CD integrations work in real projects
|
||||||
|
- ✅ Migration guide successfully upgrades old schemas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Technical Risks
|
||||||
|
|
||||||
|
**Risk**: Schema format complexity
|
||||||
|
**Mitigation**: Clear examples, validation tools, gradual adoption
|
||||||
|
|
||||||
|
**Risk**: Performance degradation with complex schemas
|
||||||
|
**Mitigation**: Caching, optimization, benchmarking
|
||||||
|
|
||||||
|
**Risk**: Template engine security (code injection)
|
||||||
|
**Mitigation**: Sandboxed execution, no eval, strict parsing
|
||||||
|
|
||||||
|
### Adoption Risks
|
||||||
|
|
||||||
|
**Risk**: Breaking changes to existing workflows
|
||||||
|
**Mitigation**: Full backward compatibility, opt-in features
|
||||||
|
|
||||||
|
**Risk**: Learning curve for new features
|
||||||
|
**Mitigation**: Excellent documentation, examples, tutorials
|
||||||
|
|
||||||
|
**Risk**: Feature bloat
|
||||||
|
**Mitigation**: Keep core simple, advanced features optional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Post-MVP)
|
||||||
|
|
||||||
|
### Potential Future Features
|
||||||
|
|
||||||
|
**1. Semantic Validation**
|
||||||
|
- AI-powered content quality checking
|
||||||
|
- Grammar and style validation
|
||||||
|
- Factual consistency checking
|
||||||
|
- Link and reference validation
|
||||||
|
|
||||||
|
**2. Visual Schema Editor**
|
||||||
|
- Web-based GUI for schema creation
|
||||||
|
- Visual blueprint designer
|
||||||
|
- Live preview of generated documents
|
||||||
|
- Drag-and-drop section arrangement
|
||||||
|
|
||||||
|
**3. Schema Marketplace**
|
||||||
|
- Community schema repository
|
||||||
|
- Reusable blueprint library
|
||||||
|
- Rating and reviews system
|
||||||
|
- Version management
|
||||||
|
|
||||||
|
**4. Advanced Blueprint Features**
|
||||||
|
- Conditional sections based on data
|
||||||
|
- Dynamic schema selection
|
||||||
|
- Multi-language support
|
||||||
|
- Custom helper functions
|
||||||
|
|
||||||
|
**5. Integration Ecosystem**
|
||||||
|
- IDE plugins (VS Code, JetBrains)
|
||||||
|
- Documentation platforms (Read the Docs, Docusaurus)
|
||||||
|
- CMS integrations (Contentful, Strapi)
|
||||||
|
- Static site generators (Hugo, Jekyll)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This workplan transforms MarkiTect from a structural validator to a comprehensive document control system:
|
||||||
|
|
||||||
|
**Current**: Rigid structure validation
|
||||||
|
**Target**: Flexible content control with blueprints
|
||||||
|
|
||||||
|
**Key Improvements**:
|
||||||
|
1. ✨ Classification system (required → improper)
|
||||||
|
2. ✨ Content guidance and instructions
|
||||||
|
3. ✨ Multi-schema conformance
|
||||||
|
4. ✨ Blueprint-based generation
|
||||||
|
5. ✨ Quality metrics and analysis
|
||||||
|
|
||||||
|
**Timeline**: ~8-10 weeks for full implementation
|
||||||
|
**Value**: Complete CMS-like document control for markdown
|
||||||
|
|
||||||
|
The system remains true to MarkiTect's philosophy of treating markdown as structured data while adding the flexibility and guidance needed for real-world content management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Review and refine** this workplan
|
||||||
|
2. **Prioritize phases** based on user needs
|
||||||
|
3. **Create detailed specifications** for Phase 1
|
||||||
|
4. **Set up development environment** for new features
|
||||||
|
5. **Begin implementation** with TDD approach
|
||||||
|
|
||||||
|
**First Implementation Task**: Define `x-markitect-sections` format specification
|
||||||
94
roadmap/schema-of-schemas/README.md
Normal file
94
roadmap/schema-of-schemas/README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Schema-of-Schemas Implementation
|
||||||
|
|
||||||
|
**Project:** Markdown-First Schema System
|
||||||
|
**Status:** Planning → Implementation
|
||||||
|
**Timeline:** 8-10 days
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- **[WORKPLAN.md](./WORKPLAN.md)** - Detailed implementation plan with phases
|
||||||
|
- **[SCHEMA_MANAGEMENT_PROPOSAL.md](./SCHEMA_MANAGEMENT_PROPOSAL.md)** - Full analysis and options
|
||||||
|
- **[SCHEMA_MANAGEMENT_SUMMARY.md](./SCHEMA_MANAGEMENT_SUMMARY.md)** - Executive summary
|
||||||
|
|
||||||
|
## What We're Building
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
1. ✅ Filename convention: `{domain}-schema-v{version}.md`
|
||||||
|
2. ✅ Markdown-first schema format (documentation + embedded JSON)
|
||||||
|
3. ✅ Schema-for-schemas to validate all schemas
|
||||||
|
4. ✅ Migrate existing schemas to new format
|
||||||
|
5. ✅ Clean up duplicate/legacy schemas
|
||||||
|
|
||||||
|
### Why
|
||||||
|
- **Consistency:** Enforced naming and versioning
|
||||||
|
- **Alignment:** Markdown-first matches MarkiTect philosophy
|
||||||
|
- **Documentation:** Rich docs alongside schemas
|
||||||
|
- **Validation:** Schema-for-schemas ensures quality
|
||||||
|
- **Maintainability:** Clear versions and structure
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
1. **Phase 0:** Planning & Setup (0.5 days) ← **Current**
|
||||||
|
2. **Phase 1:** Filename Convention (1 day)
|
||||||
|
3. **Phase 2:** Markdown Loader (2-3 days)
|
||||||
|
4. **Phase 3:** Schema-for-Schemas (2 days)
|
||||||
|
5. **Phase 4:** Schema Migration (1-2 days)
|
||||||
|
6. **Phase 5:** CLI & Docs (1 day)
|
||||||
|
7. **Phase 6:** Testing (1 day)
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
- [x] Directory structure created
|
||||||
|
- [x] Planning documents moved to roadmap
|
||||||
|
- [x] Comprehensive workplan written
|
||||||
|
- [x] Example markdown schema created
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
1. Complete Phase 0 planning artifacts
|
||||||
|
2. Begin Phase 1 implementation
|
||||||
|
3. Checkpoint review after Phase 1
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
**Format:** `{domain}-schema-v{major}.{minor}.md`
|
||||||
|
**Example:** `manpage-schema-v1.0.md`
|
||||||
|
|
||||||
|
### Schema Format
|
||||||
|
**Markdown with embedded JSON:**
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/manpage/v1"
|
||||||
|
version: "1.0.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Manpage Schema v1.0
|
||||||
|
|
||||||
|
[Documentation...]
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ ... JSON schema ... }
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Migration Plan
|
||||||
|
```
|
||||||
|
Old → New
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
terminology-schema.json → terminology-schema-v1.0.md
|
||||||
|
api-documentation → api-documentation-schema-v1.0.md
|
||||||
|
enhanced-manpage → manpage-schema-v2.0.md
|
||||||
|
markdown-manpage → REMOVE (duplicate)
|
||||||
|
markdown-manpage-schema.json → REMOVE (duplicate)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
Track progress in: `roadmap/schema-of-schemas/IMPLEMENTATION_LOG.md` (to be created)
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
See the full workplan for detailed implementation steps, risks, and mitigation strategies.
|
||||||
579
roadmap/schema-of-schemas/SCHEMA_LOADER_GUIDE.md
Normal file
579
roadmap/schema-of-schemas/SCHEMA_LOADER_GUIDE.md
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
# Markdown Schema Loader - User Guide
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Status:** Implemented
|
||||||
|
**Created:** 2026-01-04
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Markdown Schema Loader enables MarkiTect to load JSON schemas from markdown files, combining rich documentation with machine-readable validation rules. This aligns with MarkiTect's markdown-first philosophy while maintaining JSON Schema compatibility.
|
||||||
|
|
||||||
|
## Markdown Schema Format
|
||||||
|
|
||||||
|
A markdown schema file consists of three parts:
|
||||||
|
|
||||||
|
1. **YAML Frontmatter**: Metadata about the schema
|
||||||
|
2. **Documentation**: Rich markdown content explaining the schema
|
||||||
|
3. **Schema Definition**: JSON schema in a code block
|
||||||
|
|
||||||
|
### Example Structure
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/domain/v1.0"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schema Title v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Description of what this schema validates...
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
How to use this schema...
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "My Schema",
|
||||||
|
"type": "object",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
- v1.0.0 - Initial version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontmatter Metadata
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
None are strictly required, but these are recommended:
|
||||||
|
|
||||||
|
| Field | Type | Description | Example |
|
||||||
|
|-------|------|-------------|---------|
|
||||||
|
| `schema-id` | string | Canonical URI for the schema | `https://markitect.dev/schemas/manpage/v1.0` |
|
||||||
|
| `version` | string | SemVer version | `1.0.0` |
|
||||||
|
| `status` | string | Lifecycle status | `stable`, `draft`, `deprecated` |
|
||||||
|
|
||||||
|
### Optional Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `domain` | string | Schema domain name |
|
||||||
|
| `description` | string | Brief schema description |
|
||||||
|
| `authors` | array | List of authors |
|
||||||
|
| `created` | string | Creation date (ISO 8601) |
|
||||||
|
| `updated` | string | Last update date (ISO 8601) |
|
||||||
|
|
||||||
|
### Metadata Merging
|
||||||
|
|
||||||
|
Frontmatter metadata takes precedence over schema fields:
|
||||||
|
|
||||||
|
- `schema-id` → `$id` in the schema
|
||||||
|
- `version` → `version` in the schema
|
||||||
|
- `status` → `x-markitect-metadata.status` in the schema
|
||||||
|
|
||||||
|
All frontmatter is preserved in `x-markitect-source.frontmatter`.
|
||||||
|
|
||||||
|
## JSON Schema Extraction
|
||||||
|
|
||||||
|
### Schema Definition Section
|
||||||
|
|
||||||
|
The loader prefers JSON blocks under a `## Schema Definition` heading:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Behavior
|
||||||
|
|
||||||
|
If no `## Schema Definition` section exists, the loader uses the **first** JSON code block in the file.
|
||||||
|
|
||||||
|
### Multiple JSON Blocks
|
||||||
|
|
||||||
|
You can include multiple JSON blocks in documentation:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"version": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader will use the schema under `## Schema Definition` heading.
|
||||||
|
|
||||||
|
## Using the Loader
|
||||||
|
|
||||||
|
### Python API
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from markitect.schema_loader import MarkdownSchemaLoader
|
||||||
|
|
||||||
|
# Create loader instance
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
# Load schema from markdown
|
||||||
|
schema_data = loader.load_schema(Path("manpage-schema-v1.0.md"))
|
||||||
|
|
||||||
|
# Access components
|
||||||
|
schema = schema_data['schema'] # JSON Schema dict
|
||||||
|
metadata = schema_data['metadata'] # Frontmatter dict
|
||||||
|
docs = schema_data['documentation'] # Full markdown content
|
||||||
|
source = schema_data['source_file'] # Source file path
|
||||||
|
|
||||||
|
# Use the schema
|
||||||
|
print(f"Loaded: {schema['title']}")
|
||||||
|
print(f"Version: {schema['version']}")
|
||||||
|
print(f"Status: {metadata['status']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading from Markdown
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Load schema
|
||||||
|
schema_data = loader.load_schema(Path("my-schema-v1.0.md"))
|
||||||
|
|
||||||
|
# Check for issues
|
||||||
|
issues = loader.validate_schema_structure(schema_data['schema'])
|
||||||
|
if issues:
|
||||||
|
for issue in issues:
|
||||||
|
print(f"⚠️ {issue}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Saving to Markdown
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create a schema
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "My Schema",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save as markdown
|
||||||
|
loader.save_schema(
|
||||||
|
schema=schema,
|
||||||
|
md_path=Path("my-schema-v1.0.md"),
|
||||||
|
frontmatter={
|
||||||
|
"schema-id": "https://example.com/schemas/my-schema/v1.0",
|
||||||
|
"status": "draft"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Round-Trip Conversion
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Load existing JSON schema
|
||||||
|
import json
|
||||||
|
json_schema = json.loads(Path("old-schema.json").read_text())
|
||||||
|
|
||||||
|
# Save as markdown
|
||||||
|
loader.save_schema(
|
||||||
|
schema=json_schema,
|
||||||
|
md_path=Path("new-schema-v1.0.md")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load it back
|
||||||
|
schema_data = loader.load_schema(Path("new-schema-v1.0.md"))
|
||||||
|
|
||||||
|
# Schemas are equivalent
|
||||||
|
assert schema_data['schema']['title'] == json_schema['title']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Listing JSON Blocks
|
||||||
|
|
||||||
|
Useful for debugging when multiple JSON blocks exist:
|
||||||
|
|
||||||
|
```python
|
||||||
|
content = Path("schema.md").read_text()
|
||||||
|
blocks = loader.list_json_blocks(content)
|
||||||
|
|
||||||
|
print(f"Found {len(blocks)} JSON blocks:")
|
||||||
|
for position, json_content in blocks:
|
||||||
|
print(f" Position {position}: {len(json_content)} chars")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Structure Validation
|
||||||
|
|
||||||
|
Check for recommended fields and conventions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
print(f"⚠️ {issue}")
|
||||||
|
|
||||||
|
# Example output:
|
||||||
|
# ⚠️ Missing recommended field: $id
|
||||||
|
# ⚠️ Missing MarkiTect convention: version field
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Templates
|
||||||
|
|
||||||
|
Use custom markdown templates for saving schemas:
|
||||||
|
|
||||||
|
```python
|
||||||
|
template = """---
|
||||||
|
{frontmatter_yaml}
|
||||||
|
---
|
||||||
|
|
||||||
|
# {title}
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{schema_json}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
loader.save_schema(
|
||||||
|
schema=schema,
|
||||||
|
md_path=Path("custom-schema-v1.0.md"),
|
||||||
|
template=template
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| `FileNotFoundError` | Schema file doesn't exist | Check file path |
|
||||||
|
| `SchemaNotFoundError` | No JSON block in markdown | Add ```json code block |
|
||||||
|
| `InvalidSchemaFormatError` | Invalid JSON or YAML | Check syntax |
|
||||||
|
| `SchemaFilenameError` | Invalid filename format | Use `{domain}-schema-v{major}.{minor}.md` |
|
||||||
|
|
||||||
|
### Example Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect.schema_loader import (
|
||||||
|
MarkdownSchemaLoader,
|
||||||
|
SchemaNotFoundError,
|
||||||
|
InvalidSchemaFormatError
|
||||||
|
)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
try:
|
||||||
|
schema_data = loader.load_schema(Path("my-schema.md"))
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"❌ File not found: {e}")
|
||||||
|
except SchemaNotFoundError as e:
|
||||||
|
print(f"❌ No schema in file: {e}")
|
||||||
|
except InvalidSchemaFormatError as e:
|
||||||
|
print(f"❌ Invalid format: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Schema Definition Section
|
||||||
|
|
||||||
|
Always place the main schema under `## Schema Definition`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{...}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Include Frontmatter
|
||||||
|
|
||||||
|
Provide metadata for better discoverability:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/domain/v1.0"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add Rich Documentation
|
||||||
|
|
||||||
|
Explain the schema purpose, usage, and examples:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Overview
|
||||||
|
This schema validates...
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```bash
|
||||||
|
markitect validate doc.md --schema my-schema-v1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Version Your Schemas
|
||||||
|
|
||||||
|
Follow the naming convention:
|
||||||
|
|
||||||
|
- Initial: `my-schema-v1.0.md`
|
||||||
|
- Minor update: `my-schema-v1.1.md`
|
||||||
|
- Breaking change: `my-schema-v2.0.md`
|
||||||
|
|
||||||
|
### 5. Validate Structure
|
||||||
|
|
||||||
|
Always check for common issues:
|
||||||
|
|
||||||
|
```python
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
if not issues:
|
||||||
|
print("✅ Schema structure is valid")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with MarkiTect
|
||||||
|
|
||||||
|
### CLI Usage (Future)
|
||||||
|
|
||||||
|
Once integrated with the CLI, you'll be able to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ingest markdown schema
|
||||||
|
markitect schema-ingest manpage-schema-v1.0.md
|
||||||
|
|
||||||
|
# Validate against markdown schema
|
||||||
|
markitect validate document.md --schema manpage-schema-v1.0
|
||||||
|
|
||||||
|
# Export schema
|
||||||
|
markitect schema-get manpage-schema-v1.0 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validator Integration
|
||||||
|
|
||||||
|
The SchemaValidator will automatically detect `.md` schemas:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect.validator import SchemaValidator
|
||||||
|
|
||||||
|
validator = SchemaValidator()
|
||||||
|
validator.validate(
|
||||||
|
document="my-doc.md",
|
||||||
|
schema="manpage-schema-v1.0.md" # .md extension auto-detected
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Markdown Schema Template
|
||||||
|
|
||||||
|
Here's a complete template for creating new schemas:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/YOUR-DOMAIN/v1.0"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "draft"
|
||||||
|
domain: "YOUR-DOMAIN"
|
||||||
|
description: "Brief description of what this schema validates"
|
||||||
|
authors:
|
||||||
|
- "Your Name <email@example.com>"
|
||||||
|
created: "2026-01-04"
|
||||||
|
---
|
||||||
|
|
||||||
|
# YOUR-DOMAIN Schema v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Detailed description of what this schema validates and why it exists.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Feature 1
|
||||||
|
- Feature 2
|
||||||
|
- Feature 3
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Validating Documents
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect validate document.md --schema YOUR-DOMAIN-schema-v1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Validation Errors
|
||||||
|
|
||||||
|
1. **Error Type 1**: Description and solution
|
||||||
|
2. **Error Type 2**: Description and solution
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "YOUR DOMAIN Schema",
|
||||||
|
"description": "Schema description",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"field1": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of field1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["field1"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Valid Document
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Example of valid content...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid Document
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Example of invalid content...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial version
|
||||||
|
- Feature A
|
||||||
|
- Feature B
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Related Schema 1](../other-schema-v1.0.md)
|
||||||
|
- [MarkiTect Documentation](../../README.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The loader has comprehensive test coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all loader tests
|
||||||
|
pytest tests/test_schema_loader.py -v
|
||||||
|
|
||||||
|
# Run specific test class
|
||||||
|
pytest tests/test_schema_loader.py::TestMarkdownSchemaLoader -v
|
||||||
|
|
||||||
|
# Check coverage
|
||||||
|
pytest tests/test_schema_loader.py --cov=markitect.schema_loader
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Results**: 35/35 tests passing (100%)
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Regex Patterns
|
||||||
|
|
||||||
|
The loader uses these regex patterns:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Frontmatter pattern
|
||||||
|
r'^---\s*\n(.*?)\n---\s*\n'
|
||||||
|
|
||||||
|
# JSON code block pattern
|
||||||
|
r'```json\s*\n(.*?)\n```'
|
||||||
|
|
||||||
|
# Schema Definition section pattern
|
||||||
|
r'##\s+Schema Definition\s*\n'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Merging
|
||||||
|
|
||||||
|
The `_merge_metadata` method:
|
||||||
|
|
||||||
|
1. Copies the original schema
|
||||||
|
2. Adds `x-markitect-source` with file metadata
|
||||||
|
3. Merges frontmatter fields:
|
||||||
|
- `schema-id` → `$id`
|
||||||
|
- `version` → `version`
|
||||||
|
- `status` → `x-markitect-metadata.status`
|
||||||
|
|
||||||
|
### File Encoding
|
||||||
|
|
||||||
|
All files are read/written as UTF-8. Invalid UTF-8 sequences raise `InvalidSchemaFormatError`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Schema Not Found
|
||||||
|
|
||||||
|
**Problem**: `SchemaNotFoundError: No JSON schema found`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Ensure you have a ```json code block
|
||||||
|
- Check the JSON syntax is valid
|
||||||
|
- Verify the code block is properly closed with ```
|
||||||
|
|
||||||
|
### Invalid YAML Frontmatter
|
||||||
|
|
||||||
|
**Problem**: `InvalidSchemaFormatError: Invalid YAML frontmatter`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Check YAML syntax (indentation, colons, quotes)
|
||||||
|
- Ensure frontmatter is between `---` delimiters
|
||||||
|
- Verify frontmatter is at the start of file
|
||||||
|
|
||||||
|
### Binary File Error
|
||||||
|
|
||||||
|
**Problem**: `InvalidSchemaFormatError: Failed to read schema file`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
- Ensure file is text, not binary
|
||||||
|
- Check file encoding is UTF-8
|
||||||
|
- Verify file isn't corrupted
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Schema Naming Specification](SCHEMA_NAMING_SPEC.md)
|
||||||
|
- [Schema Management Workplan](WORKPLAN.md)
|
||||||
|
- [Phase 2 Documentation](WORKPLAN.md#phase-2-markdown-schema-loader)
|
||||||
|
- [Example Markdown Schema](../../markitect/schemas/manpage-schema-v1.0.md)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial implementation
|
||||||
|
- 35 unit tests (100% passing)
|
||||||
|
- Frontmatter extraction with YAML parsing
|
||||||
|
- JSON code block extraction with section preference
|
||||||
|
- Metadata merging with x-markitect-source tracking
|
||||||
|
- Schema saving with template support
|
||||||
|
- Round-trip save/load capability
|
||||||
|
- Helper methods for validation and debugging
|
||||||
569
roadmap/schema-of-schemas/SCHEMA_MANAGEMENT_PROPOSAL.md
Normal file
569
roadmap/schema-of-schemas/SCHEMA_MANAGEMENT_PROPOSAL.md
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
# Schema Management Proposal
|
||||||
|
|
||||||
|
**Status:** Draft
|
||||||
|
**Created:** 2026-01-04
|
||||||
|
**Author:** Analysis of current state and proposed improvements
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
### 1. Inconsistent Schema Naming
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
```
|
||||||
|
terminology-schema.json ← Has ".json" suffix
|
||||||
|
api-documentation ← No suffix
|
||||||
|
enhanced-manpage ← No suffix
|
||||||
|
markdown-manpage ← No suffix, duplicate title
|
||||||
|
markdown-manpage-schema.json ← Has ".json" suffix, duplicate title
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- No naming convention enforced
|
||||||
|
- Duplicate schemas (3 manpage schemas!)
|
||||||
|
- Mix of suffixed (.json) and non-suffixed names
|
||||||
|
- No way to distinguish versions
|
||||||
|
|
||||||
|
### 2. Missing Versioning
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- No version in filenames
|
||||||
|
- No version in schema metadata (beyond optional `$id`)
|
||||||
|
- No way to track schema evolution
|
||||||
|
- Breaking changes not apparent
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
- Can't have multiple versions simultaneously
|
||||||
|
- No migration path when schemas change
|
||||||
|
- Unclear which schema version a document uses
|
||||||
|
|
||||||
|
### 3. Format Mismatch: JSON vs Markdown
|
||||||
|
|
||||||
|
**The Philosophical Problem:**
|
||||||
|
> MarkiTect is a markdown-centric tool, yet schemas are JSON files.
|
||||||
|
> This creates a conceptual and practical mismatch.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- Documents: Markdown (.md)
|
||||||
|
- Schemas: JSON (.json)
|
||||||
|
- No unified format for documentation + schema
|
||||||
|
- Schemas lack rich documentation capabilities
|
||||||
|
|
||||||
|
## Proposed Solutions
|
||||||
|
|
||||||
|
### Part 1: Naming Convention & Versioning
|
||||||
|
|
||||||
|
#### Option A: Filename-Based Versioning (Recommended)
|
||||||
|
|
||||||
|
**Format:** `{domain}-{type}-schema-v{major}.{minor}.json`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
manpage-schema-v1.0.json # Manpage schema v1.0
|
||||||
|
manpage-schema-v2.0.json # Breaking change → v2.0
|
||||||
|
terminology-schema-v1.0.json # Terminology schema
|
||||||
|
api-documentation-schema-v1.0.json
|
||||||
|
arc42-schema-v1.0.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Clear versioning in filename
|
||||||
|
- Easy to see multiple versions
|
||||||
|
- SemVer compatible (major.minor)
|
||||||
|
- Searchable/sortable
|
||||||
|
|
||||||
|
**Migration Strategy:**
|
||||||
|
```bash
|
||||||
|
# Rename existing schemas
|
||||||
|
markdown-manpage → manpage-schema-v1.0.json
|
||||||
|
enhanced-manpage → manpage-schema-v2.0.json # (breaking changes)
|
||||||
|
terminology-schema.json → terminology-schema-v1.0.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: $id-Based Versioning
|
||||||
|
|
||||||
|
**Keep simple filenames, use `$id` for versioning:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$id": "https://markitect.dev/schemas/manpage/v1",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"version": "1.0.0",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Filenames:** `manpage-schema.json`, `terminology-schema.json`
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Clean filenames
|
||||||
|
- Versioning in metadata
|
||||||
|
- Follows JSON Schema best practices
|
||||||
|
|
||||||
|
**Drawbacks:**
|
||||||
|
- Can't have multiple versions in same database
|
||||||
|
- Harder to see versions at a glance
|
||||||
|
|
||||||
|
#### Recommendation: **Hybrid Approach**
|
||||||
|
|
||||||
|
Combine both for maximum clarity:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// File: manpage-schema-v1.json
|
||||||
|
{
|
||||||
|
"$id": "https://markitect.dev/schemas/manpage/v1",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Unix Manual Page Schema",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Part 2: Schema Metadata Standard
|
||||||
|
|
||||||
|
Add required metadata to all schemas:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/{domain}/v{major}",
|
||||||
|
|
||||||
|
// Required metadata
|
||||||
|
"version": "1.0.0", // SemVer
|
||||||
|
"title": "Human Readable Title",
|
||||||
|
"description": "Detailed description",
|
||||||
|
|
||||||
|
// Optional metadata
|
||||||
|
"x-markitect-schema-type": "document-schema",
|
||||||
|
"x-markitect-version": {
|
||||||
|
"major": 1,
|
||||||
|
"minor": 0,
|
||||||
|
"patch": 0
|
||||||
|
},
|
||||||
|
"x-markitect-author": "MarkiTect Project",
|
||||||
|
"x-markitect-created": "2026-01-04",
|
||||||
|
"x-markitect-updated": "2026-01-04",
|
||||||
|
"x-markitect-deprecated": false,
|
||||||
|
"x-markitect-superseded-by": null,
|
||||||
|
"x-markitect-document-types": ["manpage", "manual"],
|
||||||
|
"x-markitect-example": "examples/manpages/example.md",
|
||||||
|
|
||||||
|
// Schema content
|
||||||
|
"type": "object",
|
||||||
|
"properties": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Part 3: Format Mismatch Solutions
|
||||||
|
|
||||||
|
#### Option 1: Markdown-First with Embedded JSON (Recommended)
|
||||||
|
|
||||||
|
**File Format:** Markdown with frontmatter and JSON code block
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-version: "1.0.0"
|
||||||
|
schema-id: "https://markitect.dev/schemas/manpage/v1"
|
||||||
|
document-type: manpage
|
||||||
|
status: stable
|
||||||
|
---
|
||||||
|
|
||||||
|
# Manpage Schema v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This schema validates Unix/Linux manual page documentation following
|
||||||
|
standard conventions (SYNOPSIS, DESCRIPTION, OPTIONS, etc.).
|
||||||
|
|
||||||
|
## Document Types
|
||||||
|
|
||||||
|
- Manual pages (man pages)
|
||||||
|
- CLI command documentation
|
||||||
|
- API reference pages
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
markitect validate mycommand.1.md --schema manpage-schema-v1
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See [examples/manpages/](../../examples/manpages/) for complete examples.
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/manpage/v1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Unix Manual Page Schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level_1": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-sections": { ... }
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Required Sections
|
||||||
|
- **NAME** - Command name and brief description
|
||||||
|
- **SYNOPSIS** - Command syntax
|
||||||
|
- **DESCRIPTION** - Detailed description
|
||||||
|
|
||||||
|
### Optional Sections
|
||||||
|
- **OPTIONS** - Command-line options
|
||||||
|
- **EXAMPLES** - Usage examples
|
||||||
|
- **SEE ALSO** - Related commands
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (2026-01-04)
|
||||||
|
- Initial release
|
||||||
|
- Basic manpage structure validation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
```python
|
||||||
|
class MarkdownSchemaLoader:
|
||||||
|
"""Load schemas from markdown files with embedded JSON."""
|
||||||
|
|
||||||
|
def load_schema_from_markdown(self, md_path: Path) -> dict:
|
||||||
|
"""Extract JSON schema from markdown file."""
|
||||||
|
content = md_path.read_text()
|
||||||
|
|
||||||
|
# Parse frontmatter
|
||||||
|
frontmatter = self._extract_frontmatter(content)
|
||||||
|
|
||||||
|
# Extract JSON from code block
|
||||||
|
schema_json = self._extract_json_from_code_block(content)
|
||||||
|
|
||||||
|
# Merge metadata
|
||||||
|
schema = json.loads(schema_json)
|
||||||
|
schema['x-markitect-metadata'] = frontmatter
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def save_schema_to_markdown(self, schema: dict, md_path: Path):
|
||||||
|
"""Save schema as markdown with embedded JSON."""
|
||||||
|
# Generate markdown documentation
|
||||||
|
doc = self._generate_schema_documentation(schema)
|
||||||
|
|
||||||
|
# Embed JSON schema
|
||||||
|
json_block = f"```json\n{json.dumps(schema, indent=2)}\n```"
|
||||||
|
|
||||||
|
# Combine
|
||||||
|
full_content = f"{doc}\n\n## Schema Definition\n\n{json_block}"
|
||||||
|
md_path.write_text(full_content)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Markdown-first (aligns with MarkiTect philosophy)
|
||||||
|
- ✅ Rich documentation alongside schema
|
||||||
|
- ✅ Human-readable and editable
|
||||||
|
- ✅ Version history in same file
|
||||||
|
- ✅ Examples and usage inline
|
||||||
|
- ✅ Can extract JSON when needed
|
||||||
|
|
||||||
|
**Drawbacks:**
|
||||||
|
- ⚠️ Requires parsing logic
|
||||||
|
- ⚠️ Two sources of truth (markdown + embedded JSON)
|
||||||
|
- ⚠️ More complex than pure JSON
|
||||||
|
|
||||||
|
#### Option 2: Markdown Documentation Generator
|
||||||
|
|
||||||
|
**Keep JSON schemas, auto-generate markdown docs:**
|
||||||
|
|
||||||
|
```
|
||||||
|
schemas/
|
||||||
|
manpage-schema-v1.json # Source of truth
|
||||||
|
manpage-schema-v1.md # Auto-generated docs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```bash
|
||||||
|
markitect schema-document manpage-schema-v1.json
|
||||||
|
# Generates: manpage-schema-v1.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Simple implementation
|
||||||
|
- ✅ JSON remains source of truth
|
||||||
|
- ✅ Auto-generated docs always in sync
|
||||||
|
|
||||||
|
**Drawbacks:**
|
||||||
|
- ⚠️ Two files to manage
|
||||||
|
- ⚠️ Can't hand-edit documentation (gets overwritten)
|
||||||
|
|
||||||
|
#### Option 3: Markdown Schema Language (DSL)
|
||||||
|
|
||||||
|
**Define schemas in markdown-native syntax:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Manpage Schema v1.0
|
||||||
|
|
||||||
|
## Document Structure
|
||||||
|
|
||||||
|
### Required Sections (Level 1 Heading)
|
||||||
|
|
||||||
|
**NAME**
|
||||||
|
- Classification: required
|
||||||
|
- Content: Command name in bold, followed by description
|
||||||
|
- Pattern: `**command** - description`
|
||||||
|
|
||||||
|
**SYNOPSIS**
|
||||||
|
- Classification: required
|
||||||
|
- Content: Command syntax with options
|
||||||
|
- Min paragraphs: 1
|
||||||
|
- Max paragraphs: 3
|
||||||
|
|
||||||
|
### Optional Sections
|
||||||
|
|
||||||
|
**OPTIONS**
|
||||||
|
- Classification: recommended
|
||||||
|
- Content: Definition list of command-line options
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parser generates JSON schema from markdown:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
markitect schema-compile manpage-schema-v1.md --output manpage-schema-v1.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Pure markdown
|
||||||
|
- ✅ Human-friendly syntax
|
||||||
|
- ✅ No JSON editing needed
|
||||||
|
|
||||||
|
**Drawbacks:**
|
||||||
|
- ⚠️ Complex parser implementation
|
||||||
|
- ⚠️ Limited to MarkiTect-specific features
|
||||||
|
- ⚠️ Can't use standard JSON Schema tools
|
||||||
|
|
||||||
|
#### Option 4: Literate Schema Programming
|
||||||
|
|
||||||
|
**Inspired by literate programming, mix documentation and schema:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Manpage Schema v1.0
|
||||||
|
|
||||||
|
Manual pages follow a standard structure. The NAME section is required:
|
||||||
|
|
||||||
|
<<define-name-section>>=
|
||||||
|
{
|
||||||
|
"NAME": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Command name and brief description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The SYNOPSIS section shows command syntax:
|
||||||
|
|
||||||
|
<<define-synopsis-section>>=
|
||||||
|
{
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"min_code_blocks": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Complete schema:
|
||||||
|
|
||||||
|
<<manpage-schema.json>>=
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
<<define-name-section>>,
|
||||||
|
<<define-synopsis-section>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Documentation and schema interleaved
|
||||||
|
- ✅ Literate programming benefits
|
||||||
|
- ✅ Reusable schema fragments
|
||||||
|
|
||||||
|
**Drawbacks:**
|
||||||
|
- ⚠️ Complex tangling/weaving
|
||||||
|
- ⚠️ Unfamiliar paradigm
|
||||||
|
- ⚠️ Overkill for simple schemas
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Short-Term (Immediate)
|
||||||
|
|
||||||
|
1. **Naming Convention:**
|
||||||
|
- Format: `{domain}-schema-v{major}.{minor}.json`
|
||||||
|
- Example: `manpage-schema-v1.0.json`
|
||||||
|
|
||||||
|
2. **Schema Metadata:**
|
||||||
|
- Add required `version`, `title`, `description` fields
|
||||||
|
- Add `x-markitect-*` metadata extensions
|
||||||
|
- Document in schema-catalog.yaml
|
||||||
|
|
||||||
|
3. **Duplicate Cleanup:**
|
||||||
|
- Consolidate 3 manpage schemas into versioned series
|
||||||
|
- Keep enhanced-manpage as v2.0 (breaking changes)
|
||||||
|
- Archive old schemas
|
||||||
|
|
||||||
|
### Medium-Term (Next Phase)
|
||||||
|
|
||||||
|
4. **Markdown Schema Format (Option 1):**
|
||||||
|
- Implement markdown-first schema format
|
||||||
|
- Markdown file with embedded JSON in code block
|
||||||
|
- Parser extracts JSON for validation
|
||||||
|
- Rich documentation alongside schema
|
||||||
|
|
||||||
|
5. **Schema Documentation Generator:**
|
||||||
|
- Auto-generate markdown docs from JSON schemas
|
||||||
|
- Include examples, usage, version history
|
||||||
|
- Link to example documents
|
||||||
|
|
||||||
|
### Long-Term (Future)
|
||||||
|
|
||||||
|
6. **Schema DSL (Option 3):**
|
||||||
|
- Evaluate markdown schema language
|
||||||
|
- Prototype parser for common patterns
|
||||||
|
- Consider if DSL adds value over JSON
|
||||||
|
|
||||||
|
7. **Schema Registry API:**
|
||||||
|
- REST API for schema discovery
|
||||||
|
- Version negotiation
|
||||||
|
- Schema evolution tracking
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Naming & Versioning (1-2 days)
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
1. Define naming convention spec
|
||||||
|
2. Create schema metadata template
|
||||||
|
3. Rename existing schemas
|
||||||
|
4. Update schema-catalog.yaml
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Schema naming convention spec
|
||||||
|
- Migrated schemas with versions
|
||||||
|
- Updated catalog
|
||||||
|
|
||||||
|
### Phase 2: Markdown Schema Format (3-5 days)
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
1. Design markdown schema format
|
||||||
|
2. Implement parser (extract JSON from markdown)
|
||||||
|
3. Implement generator (create markdown from JSON)
|
||||||
|
4. Convert existing schemas to markdown format
|
||||||
|
5. Update CLI to support .md schemas
|
||||||
|
6. Write documentation and examples
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Markdown schema parser/generator
|
||||||
|
- All schemas in markdown format
|
||||||
|
- Updated CLI commands
|
||||||
|
- Migration guide
|
||||||
|
|
||||||
|
### Phase 3: Schema Validation (2-3 days)
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
1. Create metaschema for validating schemas
|
||||||
|
2. Add schema validation command
|
||||||
|
3. Validate all existing schemas
|
||||||
|
4. Add CI check for schema validity
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Schema-for-schemas (metaschema)
|
||||||
|
- Validation command
|
||||||
|
- CI integration
|
||||||
|
|
||||||
|
## Cost-Benefit Analysis
|
||||||
|
|
||||||
|
### Option 1: Markdown-First (Recommended)
|
||||||
|
|
||||||
|
**Cost:**
|
||||||
|
- Parser implementation: ~200 lines
|
||||||
|
- CLI updates: ~100 lines
|
||||||
|
- Migration effort: 2-3 days
|
||||||
|
- Testing: 1 day
|
||||||
|
|
||||||
|
**Benefit:**
|
||||||
|
- Aligned with markdown philosophy ⭐⭐⭐⭐⭐
|
||||||
|
- Rich documentation ⭐⭐⭐⭐⭐
|
||||||
|
- Version history inline ⭐⭐⭐⭐
|
||||||
|
- Human-friendly ⭐⭐⭐⭐⭐
|
||||||
|
- Lower barrier to entry ⭐⭐⭐⭐
|
||||||
|
|
||||||
|
**Total:** High value for reasonable cost
|
||||||
|
|
||||||
|
### Option 2: Documentation Generator
|
||||||
|
|
||||||
|
**Cost:**
|
||||||
|
- Generator implementation: ~150 lines
|
||||||
|
- Template design: 1 day
|
||||||
|
- Testing: 0.5 days
|
||||||
|
|
||||||
|
**Benefit:**
|
||||||
|
- Simple implementation ⭐⭐⭐⭐
|
||||||
|
- Auto-sync docs ⭐⭐⭐⭐
|
||||||
|
- JSON remains source ⭐⭐⭐
|
||||||
|
|
||||||
|
**Total:** Good value, lower cost
|
||||||
|
|
||||||
|
### Option 3: Schema DSL
|
||||||
|
|
||||||
|
**Cost:**
|
||||||
|
- DSL design: 2-3 days
|
||||||
|
- Parser implementation: ~500 lines
|
||||||
|
- Compiler: ~300 lines
|
||||||
|
- Testing: 2 days
|
||||||
|
- Documentation: 1 day
|
||||||
|
|
||||||
|
**Benefit:**
|
||||||
|
- Pure markdown ⭐⭐⭐⭐⭐
|
||||||
|
- No JSON editing ⭐⭐⭐⭐
|
||||||
|
- Limited ecosystem ⭐⭐
|
||||||
|
|
||||||
|
**Total:** High cost, uncertain value
|
||||||
|
|
||||||
|
## Decision Matrix
|
||||||
|
|
||||||
|
| Criterion | Option 1: Markdown-First | Option 2: Doc Generator | Option 3: DSL |
|
||||||
|
|-----------|-------------------------|------------------------|---------------|
|
||||||
|
| Markdown alignment | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| Implementation cost | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| Documentation quality | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||||
|
| Tool ecosystem | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| Maintainability | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| User-friendliness | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||||
|
|
||||||
|
## Recommendation Summary
|
||||||
|
|
||||||
|
1. **Immediate:** Implement naming convention and versioning
|
||||||
|
2. **Short-term:** Choose **Option 1 (Markdown-First)** for schema format
|
||||||
|
3. **Fallback:** If Option 1 proves too complex, use **Option 2 (Doc Generator)**
|
||||||
|
4. **Future:** Evaluate DSL if community demand emerges
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Review and approve this proposal
|
||||||
|
2. Create naming convention specification
|
||||||
|
3. Prototype markdown schema parser
|
||||||
|
4. Migrate one schema as proof-of-concept
|
||||||
|
5. Gather feedback and iterate
|
||||||
|
6. Full migration of all schemas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Example Markdown Schema
|
||||||
|
|
||||||
|
See `examples/schemas/manpage-schema-v1.md` for a complete example of the proposed format.
|
||||||
154
roadmap/schema-of-schemas/SCHEMA_MANAGEMENT_SUMMARY.md
Normal file
154
roadmap/schema-of-schemas/SCHEMA_MANAGEMENT_SUMMARY.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Schema Management: Executive Summary
|
||||||
|
|
||||||
|
**TL;DR:** Implement naming conventions, versioning, and markdown-first schema format to solve current schema management issues.
|
||||||
|
|
||||||
|
## Problems Identified
|
||||||
|
|
||||||
|
1. **Inconsistent Naming** - Mix of `schema.json` suffix and no suffix
|
||||||
|
2. **No Versioning** - Can't track schema evolution or maintain multiple versions
|
||||||
|
3. **Duplicate Schemas** - 3 manpage schemas with similar content
|
||||||
|
4. **Format Mismatch** - JSON schemas in markdown-centric tool
|
||||||
|
|
||||||
|
## Recommended Solution
|
||||||
|
|
||||||
|
### 1. Naming Convention (Immediate)
|
||||||
|
|
||||||
|
**Format:** `{domain}-schema-v{major}.{minor}.json` or `.md`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
manpage-schema-v1.0.json
|
||||||
|
terminology-schema-v1.0.json
|
||||||
|
api-documentation-schema-v1.0.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration:**
|
||||||
|
```
|
||||||
|
markdown-manpage → manpage-schema-v1.0.json
|
||||||
|
enhanced-manpage → manpage-schema-v2.0.json (breaking changes)
|
||||||
|
terminology-schema.json → terminology-schema-v1.0.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Markdown-First Format (Short-term)
|
||||||
|
|
||||||
|
**Proposal:** Store schemas as markdown files with embedded JSON
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Aligns with markdown philosophy ✅
|
||||||
|
- Rich documentation alongside schema ✅
|
||||||
|
- Version history in same file ✅
|
||||||
|
- Examples and usage inline ✅
|
||||||
|
- Lower barrier to entry ✅
|
||||||
|
|
||||||
|
**Example:** See `examples/schemas/manpage-schema-v1.md`
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```markdown
|
||||||
|
# Schema Title v1.0
|
||||||
|
|
||||||
|
## Documentation sections...
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{ schema here }
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Schema Metadata Standard (Immediate)
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/{domain}/v{major}",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Human Readable Title",
|
||||||
|
"description": "Detailed description",
|
||||||
|
"x-markitect-metadata": {
|
||||||
|
"domain": "manpage",
|
||||||
|
"document-types": ["manual-page"],
|
||||||
|
"created": "2026-01-04",
|
||||||
|
"example": "examples/manpages/example.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Foundation (1-2 days)
|
||||||
|
- [x] Analyze current state
|
||||||
|
- [ ] Define naming convention spec
|
||||||
|
- [ ] Create schema metadata template
|
||||||
|
- [ ] Rename existing schemas
|
||||||
|
- [ ] Update schema-catalog.yaml
|
||||||
|
|
||||||
|
### Phase 2: Markdown Format (3-5 days)
|
||||||
|
- [ ] Design markdown schema format
|
||||||
|
- [ ] Implement parser (extract JSON from markdown)
|
||||||
|
- [ ] Convert 1 schema as proof-of-concept
|
||||||
|
- [ ] Test and iterate
|
||||||
|
- [ ] Migrate all schemas
|
||||||
|
|
||||||
|
### Phase 3: Tooling (2-3 days)
|
||||||
|
- [ ] Update CLI to support .md schemas
|
||||||
|
- [ ] Add schema validation command
|
||||||
|
- [ ] Create migration guide
|
||||||
|
- [ ] Update documentation
|
||||||
|
|
||||||
|
## Cost-Benefit Analysis
|
||||||
|
|
||||||
|
**Cost:** 6-10 days total effort
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Professional schema management ⭐⭐⭐⭐⭐
|
||||||
|
- Better discoverability ⭐⭐⭐⭐
|
||||||
|
- Easier maintenance ⭐⭐⭐⭐⭐
|
||||||
|
- Markdown alignment ⭐⭐⭐⭐⭐
|
||||||
|
- Version tracking ⭐⭐⭐⭐⭐
|
||||||
|
|
||||||
|
**ROI:** High - Foundational improvement that benefits all future schema work
|
||||||
|
|
||||||
|
## Alternative Considered
|
||||||
|
|
||||||
|
**Alternative:** Keep JSON, generate markdown docs automatically
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Simpler implementation (2-3 days)
|
||||||
|
- JSON remains source of truth
|
||||||
|
- Standard tooling works
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Doesn't solve format mismatch
|
||||||
|
- Documentation generated, not authored
|
||||||
|
- Two files to manage
|
||||||
|
|
||||||
|
**Verdict:** Markdown-first better aligns with project philosophy
|
||||||
|
|
||||||
|
## Quick Wins (Today)
|
||||||
|
|
||||||
|
1. **Rename schemas** with versioned names (30 minutes)
|
||||||
|
2. **Add metadata** to existing schemas (1 hour)
|
||||||
|
3. **Update catalog** with proper versioning (30 minutes)
|
||||||
|
|
||||||
|
## Questions to Resolve
|
||||||
|
|
||||||
|
1. **File extension:** `.md` or `.schema.md` for markdown schemas?
|
||||||
|
2. **JSON extraction:** Real-time or pre-compiled cache?
|
||||||
|
3. **Backward compatibility:** Support both formats during transition?
|
||||||
|
4. **CLI changes:** `--schema file.md` or auto-detect format?
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Review** this proposal and example (examples/schemas/manpage-schema-v1.md)
|
||||||
|
2. **Decide** on markdown-first vs generated docs approach
|
||||||
|
3. **Prototype** parser for markdown schemas
|
||||||
|
4. **Migrate** one schema as proof-of-concept
|
||||||
|
5. **Iterate** based on feedback
|
||||||
|
6. **Full rollout** to all schemas
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Full proposal: [SCHEMA_MANAGEMENT_PROPOSAL.md](./SCHEMA_MANAGEMENT_PROPOSAL.md)
|
||||||
|
- Example markdown schema: [examples/schemas/manpage-schema-v1.md](../../examples/schemas/manpage-schema-v1.md)
|
||||||
|
- Current schema catalog: [markitect/schemas/schema-catalog.yaml](../../markitect/schemas/schema-catalog.yaml)
|
||||||
408
roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md
Normal file
408
roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# Schema Naming Convention Specification
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Status:** Implemented
|
||||||
|
**Created:** 2026-01-04
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This specification defines the filename convention for all MarkiTect schema files to ensure consistency, discoverability, and version tracking across the schema ecosystem.
|
||||||
|
|
||||||
|
## Filename Format
|
||||||
|
|
||||||
|
### Standard Format
|
||||||
|
|
||||||
|
```
|
||||||
|
{domain}-schema-v{major}.{minor}.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
| Component | Description | Rules | Examples |
|
||||||
|
|-----------|-------------|-------|----------|
|
||||||
|
| **domain** | Schema domain identifier | - Lowercase only<br>- Start with letter<br>- Letters, numbers, hyphens<br>- No consecutive hyphens<br>- No leading/trailing hyphens | `manpage`<br>`api-documentation`<br>`arc42` |
|
||||||
|
| **schema** | Literal keyword | - Must be exactly `schema` | `schema` |
|
||||||
|
| **version** | SemVer major.minor | - Format: `v{major}.{minor}`<br>- Non-negative integers<br>- Must include both major and minor | `v1.0`<br>`v2.5`<br>`v10.25` |
|
||||||
|
| **extension** | File extension | - Must be `.md` (markdown) | `.md` |
|
||||||
|
|
||||||
|
### Regular Expression
|
||||||
|
|
||||||
|
```regex
|
||||||
|
^[a-z][a-z0-9-]*-schema-v\d+\.\d+\.md$
|
||||||
|
```
|
||||||
|
|
||||||
|
**Breakdown:**
|
||||||
|
- `^[a-z]` - Start with lowercase letter
|
||||||
|
- `[a-z0-9-]*` - Followed by lowercase letters, numbers, or hyphens
|
||||||
|
- `-schema-` - Literal string
|
||||||
|
- `v\d+\.\d+` - Version (v + digits + dot + digits)
|
||||||
|
- `\.md$` - Extension
|
||||||
|
|
||||||
|
## Valid Examples
|
||||||
|
|
||||||
|
### Simple Domains
|
||||||
|
|
||||||
|
```
|
||||||
|
manpage-schema-v1.0.md
|
||||||
|
terminology-schema-v1.0.md
|
||||||
|
glossary-schema-v1.0.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Word Domains
|
||||||
|
|
||||||
|
```
|
||||||
|
api-documentation-schema-v1.0.md
|
||||||
|
architecture-decision-record-schema-v1.0.md
|
||||||
|
software-requirements-specification-schema-v1.0.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Numbers
|
||||||
|
|
||||||
|
```
|
||||||
|
arc42-schema-v1.0.md
|
||||||
|
rfc2119-keywords-schema-v1.0.md
|
||||||
|
iso27001-schema-v1.0.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Variations
|
||||||
|
|
||||||
|
```
|
||||||
|
manpage-schema-v1.0.md # Initial version
|
||||||
|
manpage-schema-v1.1.md # Minor update
|
||||||
|
manpage-schema-v2.0.md # Breaking change
|
||||||
|
manpage-schema-v10.25.md # Double-digit versions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invalid Examples
|
||||||
|
|
||||||
|
### Wrong Extension
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ manpage-schema-v1.0.json # Must be .md
|
||||||
|
❌ manpage-schema-v1.0.yaml # Must be .md
|
||||||
|
❌ manpage-schema-v1.0 # Missing extension
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Components
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ manpage-v1.0.md # Missing "schema" keyword
|
||||||
|
❌ manpage-schema.md # Missing version
|
||||||
|
❌ manpage.md # Missing "schema" and version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Format Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ manpage-schema-1.0.md # Missing 'v' prefix
|
||||||
|
❌ manpage-schema-v1.md # Missing minor version
|
||||||
|
❌ manpage-schema-v1.0.0.md # Too many version parts (patch not used)
|
||||||
|
❌ manpage-schema-v1-0.md # Hyphen instead of dot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ ManPage-schema-v1.0.md # Uppercase in domain
|
||||||
|
❌ manpage-Schema-v1.0.md # Uppercase in keyword
|
||||||
|
❌ MANPAGE-SCHEMA-V1.0.MD # All uppercase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain Format Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ 42answers-schema-v1.0.md # Starts with number
|
||||||
|
❌ -manpage-schema-v1.0.md # Starts with hyphen
|
||||||
|
❌ man_page-schema-v1.0.md # Underscore (use hyphen)
|
||||||
|
❌ man page-schema-v1.0.md # Space (use hyphen)
|
||||||
|
❌ my--schema-v1.0.md # Consecutive hyphens
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Numbering Guidelines
|
||||||
|
|
||||||
|
### Semantic Versioning
|
||||||
|
|
||||||
|
We use simplified SemVer with major.minor only:
|
||||||
|
|
||||||
|
**Major Version (X.0):**
|
||||||
|
- Breaking changes to schema structure
|
||||||
|
- Incompatible with previous version
|
||||||
|
- Documents validated against v1.0 may fail v2.0
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `manpage-schema-v1.0.md` → `manpage-schema-v2.0.md` (breaking change)
|
||||||
|
- `api-schema-v1.0.md` → `api-schema-v2.0.md` (new required sections)
|
||||||
|
|
||||||
|
**Minor Version (X.Y):**
|
||||||
|
- Backward-compatible additions
|
||||||
|
- New optional sections or fields
|
||||||
|
- Relaxed constraints
|
||||||
|
- Documents validated against v1.0 still validate against v1.1
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `manpage-schema-v1.0.md` → `manpage-schema-v1.1.md` (new optional section)
|
||||||
|
- `api-schema-v2.0.md` → `api-schema-v2.1.md` (additional metadata)
|
||||||
|
|
||||||
|
### Version Incrementing
|
||||||
|
|
||||||
|
```
|
||||||
|
v1.0 → v1.1 → v1.2 → ... → v1.9 → v1.10 → v1.11
|
||||||
|
↓
|
||||||
|
v2.0 (breaking change)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initial Version
|
||||||
|
|
||||||
|
All new schemas start at `v1.0.md`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# New schema
|
||||||
|
my-new-type-schema-v1.0.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain Naming Guidelines
|
||||||
|
|
||||||
|
### Good Domain Names
|
||||||
|
|
||||||
|
**Descriptive and Specific:**
|
||||||
|
```
|
||||||
|
✓ manpage-schema-v1.0.md # Clear: Unix manual pages
|
||||||
|
✓ api-documentation-schema-v1.0.md # Clear: API docs
|
||||||
|
✓ architecture-decision-record-schema-v1.0.md # Full ADR name
|
||||||
|
```
|
||||||
|
|
||||||
|
**Concise but Meaningful:**
|
||||||
|
```
|
||||||
|
✓ adr-schema-v1.0.md # Common abbreviation
|
||||||
|
✓ rfc-schema-v1.0.md # Well-known acronym
|
||||||
|
✓ arc42-schema-v1.0.md # Standard name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Poor Domain Names
|
||||||
|
|
||||||
|
**Too Generic:**
|
||||||
|
```
|
||||||
|
❌ document-schema-v1.0.md # Too vague
|
||||||
|
❌ markdown-schema-v1.0.md # All schemas are markdown
|
||||||
|
❌ schema-schema-v1.0.md # Redundant (use "metaschema")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Too Verbose:**
|
||||||
|
```
|
||||||
|
❌ my-custom-documentation-template-for-apis-v1.0.md # Too long
|
||||||
|
→ api-documentation-schema-v1.0.md # Better
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unclear Abbreviations:**
|
||||||
|
```
|
||||||
|
❌ mt-schema-v1.0.md # What is "mt"?
|
||||||
|
❌ doc-schema-v1.0.md # Too generic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Normalization Rules
|
||||||
|
|
||||||
|
When converting arbitrary strings to valid domain names:
|
||||||
|
|
||||||
|
1. **Convert to lowercase**
|
||||||
|
- `API Documentation` → `api documentation`
|
||||||
|
|
||||||
|
2. **Replace separators with hyphens**
|
||||||
|
- Spaces: `api documentation` → `api-documentation`
|
||||||
|
- Underscores: `my_type` → `my-type`
|
||||||
|
- Multiple separators: `my type` → `my--type`
|
||||||
|
|
||||||
|
3. **Remove consecutive hyphens**
|
||||||
|
- `my--type` → `my-type`
|
||||||
|
|
||||||
|
4. **Remove leading/trailing hyphens**
|
||||||
|
- `-my-type-` → `my-type`
|
||||||
|
|
||||||
|
5. **Validate result**
|
||||||
|
- Must start with letter
|
||||||
|
- Only lowercase letters, numbers, hyphens
|
||||||
|
|
||||||
|
### Example Normalizations
|
||||||
|
|
||||||
|
```python
|
||||||
|
"API Documentation" → "api-documentation-schema-v1.0.md"
|
||||||
|
"My_Custom_Type" → "my-custom-type-schema-v1.0.md"
|
||||||
|
"arc42 Architecture" → "arc42-architecture-schema-v1.0.md"
|
||||||
|
"--leading-hyphen" → "leading-hyphen-schema-v1.0.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Validation Function
|
||||||
|
|
||||||
|
The naming convention is enforced by `markitect.schema_naming.validate_schema_filename()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect.schema_naming import validate_schema_filename
|
||||||
|
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
print(f"Domain: {metadata['domain']}")
|
||||||
|
print(f"Version: {metadata['version']}")
|
||||||
|
print(f"Major: {metadata['major']}, Minor: {metadata['minor']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suggestion Function
|
||||||
|
|
||||||
|
Generate valid filenames from arbitrary input:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect.schema_naming import suggest_schema_filename
|
||||||
|
|
||||||
|
# From clean input
|
||||||
|
filename = suggest_schema_filename("manpage", "1.0")
|
||||||
|
# → "manpage-schema-v1.0.md"
|
||||||
|
|
||||||
|
# From messy input (with normalization)
|
||||||
|
filename = suggest_schema_filename("API Documentation", "2.1")
|
||||||
|
# → "api-documentation-schema-v1.0.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Integration
|
||||||
|
|
||||||
|
The `schema-ingest` command validates filenames:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Valid filename - accepted
|
||||||
|
$ markitect schema-ingest manpage-schema-v1.0.md
|
||||||
|
✅ Schema stored successfully
|
||||||
|
|
||||||
|
# Invalid filename - rejected (unless --force)
|
||||||
|
$ markitect schema-ingest manpage.json
|
||||||
|
❌ Invalid schema filename: manpage.json
|
||||||
|
|
||||||
|
Expected format: {domain}-schema-v{major}.{minor}.md
|
||||||
|
Example: manpage-schema-v1.0.md
|
||||||
|
|
||||||
|
Suggested filename: manpage-schema-v1.0.md
|
||||||
|
|
||||||
|
Use --force to skip validation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Legacy Naming
|
||||||
|
|
||||||
|
### Current State Analysis
|
||||||
|
|
||||||
|
Existing schemas with inconsistent naming:
|
||||||
|
|
||||||
|
```
|
||||||
|
terminology-schema.json # Has .json extension
|
||||||
|
api-documentation # No version, no extension
|
||||||
|
enhanced-manpage # No version, no extension, unclear name
|
||||||
|
markdown-manpage # No version, no extension
|
||||||
|
markdown-manpage-schema.json # Has .json extension
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Strategy
|
||||||
|
|
||||||
|
1. **Identify domain and version**
|
||||||
|
2. **Apply naming convention**
|
||||||
|
3. **Update database registration**
|
||||||
|
4. **Remove legacy entries**
|
||||||
|
|
||||||
|
### Migration Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
Old Name → New Name
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
terminology-schema.json → terminology-schema-v1.0.md
|
||||||
|
api-documentation → api-documentation-schema-v1.0.md
|
||||||
|
enhanced-manpage → manpage-schema-v2.0.md
|
||||||
|
markdown-manpage → (DELETE - duplicate)
|
||||||
|
markdown-manpage-schema.json → (DELETE - duplicate)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- `enhanced-manpage` → v2.0 (has breaking changes: classification system)
|
||||||
|
- `markdown-manpage` variants → DELETE (superseded by v1.0 and v2.0)
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### Metaschema
|
||||||
|
|
||||||
|
The schema-for-schemas follows the same convention:
|
||||||
|
|
||||||
|
```
|
||||||
|
schema-schema-v1.0.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Domain is `schema`, indicating it validates schemas themselves.
|
||||||
|
|
||||||
|
### Multiple Schemas for Same Domain
|
||||||
|
|
||||||
|
Use version numbers to distinguish:
|
||||||
|
|
||||||
|
```
|
||||||
|
manpage-schema-v1.0.md # Original
|
||||||
|
manpage-schema-v2.0.md # Enhanced with classifications
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use more specific domain names:
|
||||||
|
|
||||||
|
```
|
||||||
|
manpage-simple-schema-v1.0.md # Simplified variant
|
||||||
|
manpage-extended-schema-v1.0.md # Extended variant
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Testing
|
||||||
|
|
||||||
|
All schemas should pass the naming convention validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test a filename
|
||||||
|
python -c "
|
||||||
|
from markitect.schema_naming import is_valid_schema_filename
|
||||||
|
print(is_valid_schema_filename('manpage-schema-v1.0.md'))
|
||||||
|
"
|
||||||
|
# → True
|
||||||
|
|
||||||
|
# Get detailed errors
|
||||||
|
python -c "
|
||||||
|
from markitect.schema_naming import get_validation_errors
|
||||||
|
errors = get_validation_errors('invalid.json')
|
||||||
|
for error in errors:
|
||||||
|
print(error)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Consistency
|
||||||
|
- All schemas follow same pattern
|
||||||
|
- Easy to recognize schema files
|
||||||
|
- Predictable naming
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
- Clear version tracking
|
||||||
|
- Multiple versions can coexist
|
||||||
|
- Breaking changes explicit (major version bump)
|
||||||
|
|
||||||
|
### Discoverability
|
||||||
|
- Glob patterns work: `*-schema-v*.md`
|
||||||
|
- Easy to list all schemas: `ls *-schema-*.md`
|
||||||
|
- Domain easily extractable
|
||||||
|
|
||||||
|
### Tooling
|
||||||
|
- Programmatic validation
|
||||||
|
- Automatic suggestion
|
||||||
|
- Migration support
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **Implementation:** `markitect/schema_naming.py`
|
||||||
|
- **Tests:** `tests/test_schema_naming.py`
|
||||||
|
- **Workplan:** `roadmap/schema-of-schemas/WORKPLAN.md`
|
||||||
|
- **Examples:** `examples/schemas/manpage-schema-v1.0.md`
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0 (2026-01-04)
|
||||||
|
- Initial specification
|
||||||
|
- Implemented validation and suggestion functions
|
||||||
|
- 50 unit tests (100% passing)
|
||||||
|
- CLI integration planned
|
||||||
962
roadmap/schema-of-schemas/WORKPLAN.md
Normal file
962
roadmap/schema-of-schemas/WORKPLAN.md
Normal file
@@ -0,0 +1,962 @@
|
|||||||
|
# Schema-of-Schemas Implementation Workplan
|
||||||
|
|
||||||
|
**Project:** Implement Markdown-First Schema System with Self-Description
|
||||||
|
**Created:** 2026-01-04
|
||||||
|
**Status:** Planning
|
||||||
|
**Duration:** 6-10 days
|
||||||
|
**Priority:** High - Foundation for all schema work
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This workplan implements a comprehensive schema management system:
|
||||||
|
1. Filename conventions and versioning
|
||||||
|
2. Markdown-first schema format (`.md` with embedded JSON)
|
||||||
|
3. Schema-for-schemas (metaschema) for validation
|
||||||
|
4. Migration of existing schemas
|
||||||
|
5. Cleanup of legacy schemas from registry
|
||||||
|
|
||||||
|
## Project Goals
|
||||||
|
|
||||||
|
### Primary Goals
|
||||||
|
- [x] Establish filename convention: `{domain}-schema-v{version}.md`
|
||||||
|
- [ ] Implement markdown schema parser (extract JSON from markdown)
|
||||||
|
- [ ] Create schema-for-schemas to validate all schemas
|
||||||
|
- [ ] Migrate existing schemas to new format
|
||||||
|
- [ ] Remove legacy/duplicate schemas from registry
|
||||||
|
|
||||||
|
### Success Criteria
|
||||||
|
- ✅ All schemas follow naming convention
|
||||||
|
- ✅ Schemas stored as markdown files with embedded JSON
|
||||||
|
- ✅ Schema-for-schemas validates all schemas successfully
|
||||||
|
- ✅ No duplicate schemas in registry
|
||||||
|
- ✅ CLI commands work with `.md` schema files
|
||||||
|
- ✅ Documentation updated
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
```
|
||||||
|
Schemas: JSON files (.json)
|
||||||
|
Naming: Inconsistent (api-documentation, markdown-manpage-schema.json)
|
||||||
|
Versioning: None
|
||||||
|
Documentation: Separate or missing
|
||||||
|
Registry: Database with 5 schemas (3 duplicates)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target State
|
||||||
|
```
|
||||||
|
Schemas: Markdown files (.md) with embedded JSON
|
||||||
|
Naming: {domain}-schema-v{major}.{minor}.md
|
||||||
|
Versioning: SemVer in filename and metadata
|
||||||
|
Documentation: Inline with schema
|
||||||
|
Registry: Clean, versioned, no duplicates
|
||||||
|
Validation: Schema-for-schemas validates all schemas
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components to Build
|
||||||
|
|
||||||
|
```
|
||||||
|
markitect/
|
||||||
|
├── schema_loader.py # NEW: Load schemas from markdown
|
||||||
|
├── schema_validator.py # UPDATED: Support .md schemas
|
||||||
|
├── cli.py # UPDATED: Accept .md schema files
|
||||||
|
└── schemas/
|
||||||
|
├── schema-schema-v1.md # NEW: Schema-for-schemas
|
||||||
|
└── ...versioned schemas...
|
||||||
|
|
||||||
|
examples/schemas/ # Markdown schema examples
|
||||||
|
└── manpage-schema-v1.md # Already created
|
||||||
|
|
||||||
|
roadmap/schema-of-schemas/ # Planning artifacts
|
||||||
|
├── WORKPLAN.md # This file
|
||||||
|
├── SCHEMA_NAMING_SPEC.md # Naming convention spec
|
||||||
|
└── IMPLEMENTATION_LOG.md # Progress tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase Breakdown
|
||||||
|
|
||||||
|
### Phase 0: Planning & Setup ✅ (0.5 days)
|
||||||
|
|
||||||
|
**Goal:** Establish project structure and specifications
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [x] Create roadmap/schema-of-schemas directory
|
||||||
|
- [x] Move planning documents to roadmap
|
||||||
|
- [ ] Write naming convention specification
|
||||||
|
- [ ] Document schema metadata standard
|
||||||
|
- [ ] Create implementation checklist
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [x] Directory structure
|
||||||
|
- [ ] SCHEMA_NAMING_SPEC.md
|
||||||
|
- [ ] SCHEMA_METADATA_SPEC.md
|
||||||
|
- [ ] This workplan
|
||||||
|
|
||||||
|
**Duration:** 0.5 days
|
||||||
|
**Status:** In Progress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Filename Convention & Validation (1 day)
|
||||||
|
|
||||||
|
**Goal:** Establish and enforce filename conventions
|
||||||
|
|
||||||
|
**1.1 Define Naming Convention**
|
||||||
|
|
||||||
|
**Specification:**
|
||||||
|
```
|
||||||
|
Format: {domain}-schema-v{major}.{minor}.md
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- domain: lowercase, hyphen-separated (e.g., "manpage", "api-documentation")
|
||||||
|
- schema: literal string "schema"
|
||||||
|
- version: SemVer major.minor (e.g., "v1.0", "v2.1")
|
||||||
|
- extension: ".md" (markdown)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
✓ manpage-schema-v1.0.md
|
||||||
|
✓ terminology-schema-v1.0.md
|
||||||
|
✓ api-documentation-schema-v1.0.md
|
||||||
|
✗ manpage.json (missing version)
|
||||||
|
✗ manpage-v1.md (missing "schema")
|
||||||
|
✗ ManPage-Schema-v1.0.md (wrong case)
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.2 Implement Validation Function**
|
||||||
|
|
||||||
|
**File:** `markitect/schema_naming.py` (NEW)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
SCHEMA_FILENAME_PATTERN = re.compile(
|
||||||
|
r'^(?P<domain>[a-z][a-z0-9-]*)-schema-v(?P<major>\d+)\.(?P<minor>\d+)\.md$'
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_schema_filename(filename: str) -> Tuple[bool, Optional[dict]]:
|
||||||
|
"""
|
||||||
|
Validate schema filename against convention.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(is_valid, metadata_dict)
|
||||||
|
"""
|
||||||
|
match = SCHEMA_FILENAME_PATTERN.match(filename)
|
||||||
|
if not match:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
return True, {
|
||||||
|
'domain': match.group('domain'),
|
||||||
|
'version': f"{match.group('major')}.{match.group('minor')}",
|
||||||
|
'major': int(match.group('major')),
|
||||||
|
'minor': int(match.group('minor'))
|
||||||
|
}
|
||||||
|
|
||||||
|
def suggest_schema_filename(domain: str, version: str) -> str:
|
||||||
|
"""Generate correct schema filename from domain and version."""
|
||||||
|
# Normalize domain: lowercase, replace spaces with hyphens
|
||||||
|
domain_clean = domain.lower().replace(' ', '-').replace('_', '-')
|
||||||
|
return f"{domain_clean}-schema-v{version}.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.3 Add CLI Validation**
|
||||||
|
|
||||||
|
**Update:** `markitect/cli.py` - schema-ingest command
|
||||||
|
|
||||||
|
```python
|
||||||
|
@cli.command('schema-ingest')
|
||||||
|
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||||
|
@click.option('--force', is_flag=True, help='Skip filename validation')
|
||||||
|
def schema_ingest(config, schema_file, force):
|
||||||
|
"""Ingest schema file with filename validation."""
|
||||||
|
from .schema_naming import validate_schema_filename, suggest_schema_filename
|
||||||
|
|
||||||
|
filename = schema_file.name
|
||||||
|
is_valid, metadata = validate_schema_filename(filename)
|
||||||
|
|
||||||
|
if not is_valid and not force:
|
||||||
|
click.echo(f"❌ Invalid schema filename: {filename}", err=True)
|
||||||
|
click.echo("\nExpected format: {domain}-schema-v{major}.{minor}.md")
|
||||||
|
click.echo("Example: manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
# Try to suggest correct name
|
||||||
|
# ... extract domain/version from file content ...
|
||||||
|
suggestion = suggest_schema_filename(domain, version)
|
||||||
|
click.echo(f"\nSuggested filename: {suggestion}")
|
||||||
|
click.echo("\nUse --force to skip validation")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Continue with ingestion...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Write `markitect/schema_naming.py`
|
||||||
|
- [ ] Add unit tests for filename validation
|
||||||
|
- [ ] Update `schema-ingest` command with validation
|
||||||
|
- [ ] Test with valid and invalid filenames
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] schema_naming.py with validation logic
|
||||||
|
- [ ] Unit tests (tests/test_schema_naming.py)
|
||||||
|
- [ ] Updated CLI with validation
|
||||||
|
- [ ] SCHEMA_NAMING_SPEC.md documentation
|
||||||
|
|
||||||
|
**Duration:** 1 day
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Markdown Schema Loader (2-3 days)
|
||||||
|
|
||||||
|
**Goal:** Parse markdown files to extract JSON schemas
|
||||||
|
|
||||||
|
**2.1 Design Markdown Schema Format**
|
||||||
|
|
||||||
|
**Format Specification:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/{domain}/v{major}"
|
||||||
|
version: "{major}.{minor}.{patch}"
|
||||||
|
status: "stable|draft|deprecated"
|
||||||
|
---
|
||||||
|
|
||||||
|
# {Title} v{version}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
[Human-readable description]
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
[Examples of how to use this schema]
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/{domain}/v{major}",
|
||||||
|
"version": "{major}.{minor}.{patch}",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
\```
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
[Explanation of schema rules]
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
[Changelog]
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.2 Implement Markdown Schema Loader**
|
||||||
|
|
||||||
|
**File:** `markitect/schema_loader.py` (NEW)
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Schema Loader - Extract JSON schemas from markdown files.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- YAML frontmatter for metadata
|
||||||
|
- JSON code block for schema definition
|
||||||
|
- Validation of schema structure
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, Tuple
|
||||||
|
|
||||||
|
class MarkdownSchemaLoader:
|
||||||
|
"""Load and parse markdown schema files."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.frontmatter_pattern = re.compile(
|
||||||
|
r'^---\s*\n(.*?)\n---\s*\n',
|
||||||
|
re.DOTALL | re.MULTILINE
|
||||||
|
)
|
||||||
|
self.json_code_block_pattern = re.compile(
|
||||||
|
r'```json\s*\n(.*?)\n```',
|
||||||
|
re.DOTALL | re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_schema(self, md_path: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load schema from markdown file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
'schema': {...}, # Extracted JSON schema
|
||||||
|
'metadata': {...}, # Frontmatter metadata
|
||||||
|
'documentation': '...' # Full markdown content
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not md_path.exists():
|
||||||
|
raise FileNotFoundError(f"Schema file not found: {md_path}")
|
||||||
|
|
||||||
|
content = md_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
# Extract frontmatter
|
||||||
|
metadata = self._extract_frontmatter(content)
|
||||||
|
|
||||||
|
# Extract JSON schema
|
||||||
|
schema = self._extract_json_schema(content)
|
||||||
|
|
||||||
|
if not schema:
|
||||||
|
raise ValueError(f"No JSON schema found in {md_path}")
|
||||||
|
|
||||||
|
# Merge metadata into schema
|
||||||
|
schema = self._merge_metadata(schema, metadata, md_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'schema': schema,
|
||||||
|
'metadata': metadata,
|
||||||
|
'documentation': content,
|
||||||
|
'source_file': str(md_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_frontmatter(self, content: str) -> Dict[str, Any]:
|
||||||
|
"""Extract YAML frontmatter from markdown."""
|
||||||
|
match = self.frontmatter_pattern.search(content)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(match.group(1)) or {}
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
raise ValueError(f"Invalid YAML frontmatter: {e}")
|
||||||
|
|
||||||
|
def _extract_json_schema(self, content: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Extract JSON schema from code block."""
|
||||||
|
matches = self.json_code_block_pattern.findall(content)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use the first JSON code block as schema
|
||||||
|
# (or could look for specific heading like "## Schema Definition")
|
||||||
|
try:
|
||||||
|
return json.loads(matches[0])
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Invalid JSON schema: {e}")
|
||||||
|
|
||||||
|
def _merge_metadata(
|
||||||
|
self,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
metadata: Dict[str, Any],
|
||||||
|
source_file: Path
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Merge frontmatter metadata into schema."""
|
||||||
|
|
||||||
|
# Add MarkiTect-specific metadata
|
||||||
|
schema['x-markitect-source'] = {
|
||||||
|
'file': str(source_file),
|
||||||
|
'format': 'markdown',
|
||||||
|
'frontmatter': metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override schema fields with frontmatter if present
|
||||||
|
if 'version' in metadata:
|
||||||
|
schema['version'] = metadata['version']
|
||||||
|
if 'schema-id' in metadata:
|
||||||
|
schema['$id'] = metadata['schema-id']
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def save_schema(
|
||||||
|
self,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
md_path: Path,
|
||||||
|
template: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Save schema as markdown file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: JSON schema dict
|
||||||
|
md_path: Output path
|
||||||
|
template: Optional markdown template
|
||||||
|
"""
|
||||||
|
if template:
|
||||||
|
# Use provided template
|
||||||
|
content = self._render_template(template, schema)
|
||||||
|
else:
|
||||||
|
# Generate basic markdown
|
||||||
|
content = self._generate_markdown(schema)
|
||||||
|
|
||||||
|
md_path.write_text(content, encoding='utf-8')
|
||||||
|
|
||||||
|
def _generate_markdown(self, schema: Dict[str, Any]) -> str:
|
||||||
|
"""Generate markdown from schema."""
|
||||||
|
title = schema.get('title', 'Untitled Schema')
|
||||||
|
version = schema.get('version', '1.0.0')
|
||||||
|
description = schema.get('description', '')
|
||||||
|
|
||||||
|
# Generate frontmatter
|
||||||
|
frontmatter = yaml.dump({
|
||||||
|
'schema-id': schema.get('$id', ''),
|
||||||
|
'version': version,
|
||||||
|
'status': 'draft'
|
||||||
|
}, default_flow_style=False)
|
||||||
|
|
||||||
|
# Generate markdown
|
||||||
|
md = f"""---
|
||||||
|
{frontmatter}---
|
||||||
|
|
||||||
|
# {title} v{version}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{json.dumps(schema, indent=2)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v{version}
|
||||||
|
- Initial version
|
||||||
|
"""
|
||||||
|
return md
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaLoaderError(Exception):
|
||||||
|
"""Base exception for schema loading errors."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.3 Update Schema Validator**
|
||||||
|
|
||||||
|
**Update:** `markitect/schema_validator.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .schema_loader import MarkdownSchemaLoader
|
||||||
|
|
||||||
|
class SchemaValidator:
|
||||||
|
def __init__(self):
|
||||||
|
self.schema_generator = SchemaGenerator()
|
||||||
|
self.jsonschema_available = JSONSCHEMA_AVAILABLE
|
||||||
|
self.md_loader = MarkdownSchemaLoader() # NEW
|
||||||
|
|
||||||
|
def validate_file_against_schema_file(
|
||||||
|
self,
|
||||||
|
file_path: Path,
|
||||||
|
schema_file_path: Path
|
||||||
|
) -> bool:
|
||||||
|
"""Validate file against schema (supports .json and .md)."""
|
||||||
|
|
||||||
|
# Detect schema file format
|
||||||
|
if schema_file_path.suffix == '.md':
|
||||||
|
# Load from markdown
|
||||||
|
schema_data = self.md_loader.load_schema(schema_file_path)
|
||||||
|
schema = schema_data['schema']
|
||||||
|
else:
|
||||||
|
# Load from JSON (legacy)
|
||||||
|
schema_content = schema_file_path.read_text(encoding='utf-8')
|
||||||
|
schema = json.loads(schema_content)
|
||||||
|
|
||||||
|
return self.validate_file_against_schema(file_path, schema)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Implement MarkdownSchemaLoader class
|
||||||
|
- [ ] Add frontmatter extraction (YAML)
|
||||||
|
- [ ] Add JSON code block extraction
|
||||||
|
- [ ] Add metadata merging logic
|
||||||
|
- [ ] Write comprehensive unit tests
|
||||||
|
- [ ] Update SchemaValidator to use loader
|
||||||
|
- [ ] Test with example markdown schemas
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] schema_loader.py implementation
|
||||||
|
- [ ] Unit tests (tests/test_schema_loader.py)
|
||||||
|
- [ ] Updated schema_validator.py
|
||||||
|
- [ ] Integration tests
|
||||||
|
|
||||||
|
**Duration:** 2-3 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Schema-for-Schemas (2 days)
|
||||||
|
|
||||||
|
**Goal:** Create metaschema to validate all schema files
|
||||||
|
|
||||||
|
**3.1 Design Schema-for-Schemas**
|
||||||
|
|
||||||
|
**File:** `markitect/schemas/schema-schema-v1.md`
|
||||||
|
|
||||||
|
**Purpose:** Validates that schema files follow MarkiTect conventions
|
||||||
|
|
||||||
|
**Validates:**
|
||||||
|
- Required fields ($schema, $id, version, title, description)
|
||||||
|
- Version format (SemVer)
|
||||||
|
- $id URL format
|
||||||
|
- x-markitect-* extensions
|
||||||
|
- Section classifications
|
||||||
|
- Content control structures
|
||||||
|
|
||||||
|
**3.2 Implement Schema-for-Schemas**
|
||||||
|
|
||||||
|
See separate file: `roadmap/schema-of-schemas/schema-schema-v1.md` (to be created)
|
||||||
|
|
||||||
|
**3.3 Add Schema Validation Command**
|
||||||
|
|
||||||
|
**New CLI command:** `markitect schema-validate`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@cli.command('schema-validate')
|
||||||
|
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||||
|
@click.option('--detailed-errors', is_flag=True)
|
||||||
|
def schema_validate(config, schema_file, detailed_errors):
|
||||||
|
"""
|
||||||
|
Validate a schema file against the schema-for-schemas.
|
||||||
|
|
||||||
|
Ensures schema files follow MarkiTect conventions and standards.
|
||||||
|
"""
|
||||||
|
from .schema_loader import MarkdownSchemaLoader
|
||||||
|
from .schema_validator import SchemaValidator
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
validator = SchemaValidator()
|
||||||
|
|
||||||
|
# Load the schema
|
||||||
|
try:
|
||||||
|
schema_data = loader.load_schema(schema_file)
|
||||||
|
schema = schema_data['schema']
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to load schema: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Load schema-for-schemas
|
||||||
|
metaschema_path = Path(__file__).parent / 'schemas' / 'schema-schema-v1.md'
|
||||||
|
metaschema_data = loader.load_schema(metaschema_path)
|
||||||
|
metaschema = metaschema_data['schema']
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
is_valid = validator.validate_schema_against_metaschema(schema, metaschema)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
click.echo(f"✅ Schema is valid: {schema_file.name}")
|
||||||
|
click.echo(f" Title: {schema.get('title')}")
|
||||||
|
click.echo(f" Version: {schema.get('version')}")
|
||||||
|
else:
|
||||||
|
click.echo(f"❌ Schema validation failed: {schema_file.name}", err=True)
|
||||||
|
if detailed_errors:
|
||||||
|
# Show detailed errors
|
||||||
|
pass
|
||||||
|
sys.exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Design schema-for-schemas structure
|
||||||
|
- [ ] Implement schema-schema-v1.md
|
||||||
|
- [ ] Add schema validation logic
|
||||||
|
- [ ] Create `schema-validate` CLI command
|
||||||
|
- [ ] Test all existing schemas against metaschema
|
||||||
|
- [ ] Document validation rules
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] schema-schema-v1.md (metaschema)
|
||||||
|
- [ ] schema-validate command
|
||||||
|
- [ ] Validation documentation
|
||||||
|
- [ ] Test suite
|
||||||
|
|
||||||
|
**Duration:** 2 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Schema Migration (1-2 days)
|
||||||
|
|
||||||
|
**Goal:** Convert existing schemas to new format
|
||||||
|
|
||||||
|
**4.1 Inventory Current Schemas**
|
||||||
|
|
||||||
|
Current schemas in database:
|
||||||
|
```
|
||||||
|
1. terminology-schema.json → terminology-schema-v1.0.md
|
||||||
|
2. api-documentation → api-documentation-schema-v1.0.md
|
||||||
|
3. enhanced-manpage → manpage-schema-v2.0.md
|
||||||
|
4. markdown-manpage → manpage-schema-v1.0.md (DUPLICATE)
|
||||||
|
5. markdown-manpage-schema.json → manpage-schema-v1.0.md (DUPLICATE)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decision matrix:**
|
||||||
|
- Keep enhanced-manpage as v2.0 (has classifications)
|
||||||
|
- Merge markdown-manpage variants into v1.0
|
||||||
|
- Update terminology to v1.0
|
||||||
|
- Update api-documentation to v1.0
|
||||||
|
|
||||||
|
**4.2 Create Migration Script**
|
||||||
|
|
||||||
|
**File:** `scripts/migrate_schemas.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Migrate schemas to markdown format with versioning."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from markitect.schema_loader import MarkdownSchemaLoader
|
||||||
|
from markitect.database import DatabaseManager
|
||||||
|
|
||||||
|
def migrate_schema(
|
||||||
|
db_manager: DatabaseManager,
|
||||||
|
old_name: str,
|
||||||
|
new_name: str,
|
||||||
|
version: str,
|
||||||
|
domain: str
|
||||||
|
):
|
||||||
|
"""Migrate single schema to new format."""
|
||||||
|
|
||||||
|
# Get old schema from database
|
||||||
|
old_schema = db_manager.get_schema_file(old_name)
|
||||||
|
if not old_schema:
|
||||||
|
print(f"❌ Schema not found: {old_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
schema_json = json.loads(old_schema['schema_content'])
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
schema_json['version'] = version
|
||||||
|
schema_json['$id'] = f"https://markitect.dev/schemas/{domain}/v{version.split('.')[0]}"
|
||||||
|
|
||||||
|
# Save as markdown
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
md_path = Path(f"markitect/schemas/{new_name}")
|
||||||
|
loader.save_schema(schema_json, md_path)
|
||||||
|
|
||||||
|
print(f"✓ Migrated: {old_name} → {new_name}")
|
||||||
|
|
||||||
|
# Ingest new schema
|
||||||
|
# ... ingest markdown schema to database ...
|
||||||
|
|
||||||
|
return md_path
|
||||||
|
|
||||||
|
def main():
|
||||||
|
migrations = [
|
||||||
|
('terminology-schema.json', 'terminology-schema-v1.0.md', '1.0.0', 'terminology'),
|
||||||
|
('api-documentation', 'api-documentation-schema-v1.0.md', '1.0.0', 'api-documentation'),
|
||||||
|
('enhanced-manpage', 'manpage-schema-v2.0.md', '2.0.0', 'manpage'),
|
||||||
|
('markdown-manpage', 'manpage-schema-v1.0.md', '1.0.0', 'manpage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
db = DatabaseManager('markitect.db')
|
||||||
|
|
||||||
|
for old, new, version, domain in migrations:
|
||||||
|
migrate_schema(db, old, new, version, domain)
|
||||||
|
```
|
||||||
|
|
||||||
|
**4.3 Execute Migration**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migration script
|
||||||
|
python scripts/migrate_schemas.py
|
||||||
|
|
||||||
|
# Validate all new schemas
|
||||||
|
for schema in markitect/schemas/*-schema-v*.md; do
|
||||||
|
markitect schema-validate "$schema"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ingest new schemas
|
||||||
|
for schema in markitect/schemas/*-schema-v*.md; do
|
||||||
|
markitect schema-ingest "$schema"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**4.4 Clean Up Registry**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove old schemas from database
|
||||||
|
markitect schema-delete markdown-manpage
|
||||||
|
markitect schema-delete markdown-manpage-schema.json
|
||||||
|
markitect schema-delete api-documentation
|
||||||
|
markitect schema-delete enhanced-manpage
|
||||||
|
markitect schema-delete terminology-schema.json
|
||||||
|
|
||||||
|
# Verify cleanup
|
||||||
|
markitect schema-list
|
||||||
|
# Should show only versioned .md schemas
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create schema inventory
|
||||||
|
- [ ] Write migration script
|
||||||
|
- [ ] Test migration on one schema
|
||||||
|
- [ ] Execute full migration
|
||||||
|
- [ ] Validate all migrated schemas
|
||||||
|
- [ ] Remove old schemas from database
|
||||||
|
- [ ] Update examples to use new schema names
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] scripts/migrate_schemas.py
|
||||||
|
- [ ] All schemas migrated to markdown
|
||||||
|
- [ ] Clean registry (no duplicates)
|
||||||
|
- [ ] Migration report
|
||||||
|
|
||||||
|
**Duration:** 1-2 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: CLI & Documentation Updates (1 day)
|
||||||
|
|
||||||
|
**Goal:** Update CLI and documentation for new system
|
||||||
|
|
||||||
|
**5.1 Update CLI Commands**
|
||||||
|
|
||||||
|
Commands to update:
|
||||||
|
- `schema-ingest` - Accept .md files, validate filename
|
||||||
|
- `schema-list` - Show version in output
|
||||||
|
- `schema-get` - Export as .md or .json
|
||||||
|
- `validate` - Accept .md schema files
|
||||||
|
- `generate-stub` - Work with .md schemas
|
||||||
|
- `schema-generate` - Output .md format option
|
||||||
|
- NEW: `schema-validate` - Validate against metaschema
|
||||||
|
|
||||||
|
**5.2 Update Documentation**
|
||||||
|
|
||||||
|
Files to update:
|
||||||
|
- README.md - Mention markdown schemas
|
||||||
|
- examples/terminology/README.md - Use new schema name
|
||||||
|
- docs/specifications/schema-extensions-spec.md - Document markdown format
|
||||||
|
- Create: docs/guides/schema-authoring-guide.md
|
||||||
|
|
||||||
|
**5.3 Add Schema Templates**
|
||||||
|
|
||||||
|
**File:** `templates/schema-template-v1.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
schema-id: "https://markitect.dev/schemas/DOMAIN/v1"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
# TITLE Schema v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[Description of what this schema validates]
|
||||||
|
|
||||||
|
## Document Types
|
||||||
|
|
||||||
|
- [Document type 1]
|
||||||
|
- [Document type 2]
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
markitect validate document.md --schema DOMAIN-schema-v1.0.md
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See [examples/DOMAIN/example.md](../../examples/DOMAIN/example.md)
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/DOMAIN/v1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "TITLE Schema",
|
||||||
|
"description": "Schema for validating DESCRIPTION",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"headings": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-sections": {},
|
||||||
|
"x-markitect-content-control": {}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Required Sections
|
||||||
|
- **SECTION** - Description
|
||||||
|
|
||||||
|
### Optional Sections
|
||||||
|
- **SECTION** - Description
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0 (YYYY-MM-DD)
|
||||||
|
- Initial release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Update all CLI commands for .md support
|
||||||
|
- [ ] Update documentation
|
||||||
|
- [ ] Create schema authoring guide
|
||||||
|
- [ ] Add schema template
|
||||||
|
- [ ] Update examples
|
||||||
|
- [ ] Test all workflows end-to-end
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] Updated CLI commands
|
||||||
|
- [ ] Schema authoring guide
|
||||||
|
- [ ] Schema template
|
||||||
|
- [ ] Updated examples
|
||||||
|
- [ ] End-to-end tests
|
||||||
|
|
||||||
|
**Duration:** 1 day
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Testing & Validation (1 day)
|
||||||
|
|
||||||
|
**Goal:** Comprehensive testing of new system
|
||||||
|
|
||||||
|
**6.1 Unit Tests**
|
||||||
|
|
||||||
|
Test coverage for:
|
||||||
|
- `schema_naming.py` - Filename validation
|
||||||
|
- `schema_loader.py` - Markdown parsing
|
||||||
|
- `schema_validator.py` - Validation with .md schemas
|
||||||
|
|
||||||
|
**6.2 Integration Tests**
|
||||||
|
|
||||||
|
End-to-end workflows:
|
||||||
|
1. Create new schema in markdown format
|
||||||
|
2. Validate schema against schema-for-schemas
|
||||||
|
3. Ingest schema to database
|
||||||
|
4. Use schema to validate documents
|
||||||
|
5. Generate stub from schema
|
||||||
|
6. Export schema
|
||||||
|
|
||||||
|
**6.3 Regression Tests**
|
||||||
|
|
||||||
|
Ensure existing functionality still works:
|
||||||
|
- JSON schemas still load (backward compatibility)
|
||||||
|
- All existing documents validate
|
||||||
|
- Schema generation still works
|
||||||
|
- Stub generation still works
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Write unit tests for new modules
|
||||||
|
- [ ] Create integration test suite
|
||||||
|
- [ ] Run regression tests
|
||||||
|
- [ ] Fix any issues found
|
||||||
|
- [ ] Achieve >80% code coverage
|
||||||
|
- [ ] Document test procedures
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- [ ] Unit tests (>80% coverage)
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Regression test suite
|
||||||
|
- [ ] Test documentation
|
||||||
|
|
||||||
|
**Duration:** 1 day
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Week 1:
|
||||||
|
Day 1: Phase 0 (Planning) + Phase 1 (Naming Convention)
|
||||||
|
Day 2-3: Phase 2 (Markdown Loader)
|
||||||
|
Day 4-5: Phase 3 (Schema-for-Schemas)
|
||||||
|
|
||||||
|
Week 2:
|
||||||
|
Day 6-7: Phase 4 (Migration)
|
||||||
|
Day 8: Phase 5 (CLI & Docs)
|
||||||
|
Day 9: Phase 6 (Testing)
|
||||||
|
Day 10: Buffer for issues/refinement
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total:** 8-10 days
|
||||||
|
|
||||||
|
## Risks & Mitigation
|
||||||
|
|
||||||
|
### Risk 1: Parsing Complexity
|
||||||
|
**Risk:** Markdown parsing more complex than expected
|
||||||
|
**Probability:** Medium
|
||||||
|
**Impact:** High
|
||||||
|
**Mitigation:**
|
||||||
|
- Start with simple regex-based parser
|
||||||
|
- Test extensively with edge cases
|
||||||
|
- Have fallback to simpler format
|
||||||
|
|
||||||
|
### Risk 2: Backward Compatibility
|
||||||
|
**Risk:** Breaking existing workflows
|
||||||
|
**Probability:** Low
|
||||||
|
**Impact:** High
|
||||||
|
**Mitigation:**
|
||||||
|
- Support both .json and .md during transition
|
||||||
|
- Provide migration script
|
||||||
|
- Test thoroughly with existing documents
|
||||||
|
|
||||||
|
### Risk 3: Schema-for-Schemas Complexity
|
||||||
|
**Risk:** Self-referential validation complex
|
||||||
|
**Probability:** Medium
|
||||||
|
**Impact:** Medium
|
||||||
|
**Mitigation:**
|
||||||
|
- Start with simple metaschema
|
||||||
|
- Iterate based on actual schemas
|
||||||
|
- Don't over-engineer initially
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- [ ] All schemas follow naming convention (5/5)
|
||||||
|
- [ ] All schemas in markdown format (5/5)
|
||||||
|
- [ ] All schemas validate against metaschema (5/5)
|
||||||
|
- [ ] Zero duplicate schemas in registry
|
||||||
|
- [ ] CLI commands work with .md schemas
|
||||||
|
- [ ] Documentation comprehensive
|
||||||
|
- [ ] Test coverage >80%
|
||||||
|
- [ ] No regression in existing functionality
|
||||||
|
|
||||||
|
## Deliverables Checklist
|
||||||
|
|
||||||
|
### Code
|
||||||
|
- [ ] markitect/schema_naming.py
|
||||||
|
- [ ] markitect/schema_loader.py
|
||||||
|
- [ ] markitect/schemas/schema-schema-v1.md
|
||||||
|
- [ ] scripts/migrate_schemas.py
|
||||||
|
- [ ] Updated CLI commands
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] SCHEMA_NAMING_SPEC.md
|
||||||
|
- [ ] SCHEMA_METADATA_SPEC.md
|
||||||
|
- [ ] Schema authoring guide
|
||||||
|
- [ ] Migration guide
|
||||||
|
- [ ] Updated examples
|
||||||
|
- [ ] IMPLEMENTATION_LOG.md
|
||||||
|
|
||||||
|
### Schemas
|
||||||
|
- [ ] terminology-schema-v1.0.md
|
||||||
|
- [ ] api-documentation-schema-v1.0.md
|
||||||
|
- [ ] manpage-schema-v1.0.md
|
||||||
|
- [ ] manpage-schema-v2.0.md
|
||||||
|
- [ ] schema-schema-v1.0.md
|
||||||
|
|
||||||
|
### Registry
|
||||||
|
- [ ] Clean schema database
|
||||||
|
- [ ] Updated schema-catalog.yaml
|
||||||
|
- [ ] No duplicates
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Review this workplan** - Get approval
|
||||||
|
2. **Phase 0** - Complete planning artifacts
|
||||||
|
3. **Phase 1** - Implement naming validation
|
||||||
|
4. **Checkpoint** - Review progress after Phase 1
|
||||||
|
5. **Continue** - Execute remaining phases
|
||||||
|
|
||||||
|
## Approval
|
||||||
|
|
||||||
|
- [ ] Workplan reviewed
|
||||||
|
- [ ] Approach approved
|
||||||
|
- [ ] Ready to begin implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** Awaiting approval
|
||||||
|
**Next Action:** Complete Phase 0 planning artifacts
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
"""
|
|
||||||
Test suite for the new clean architecture implementation
|
|
||||||
Tests the JSON configuration interface and separation of concerns
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
|
|
||||||
class TestCleanArchitecture:
|
|
||||||
"""Test suite for clean JavaScript-Python separation"""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Setup for each test"""
|
|
||||||
self.manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
def test_clean_edit_mode_json_configuration(self):
|
|
||||||
"""Test that edit mode uses clean JSON configuration interface"""
|
|
||||||
test_markdown = '''# Test Document
|
|
||||||
|
|
||||||
## Section with Problematic Content
|
|
||||||
```python
|
|
||||||
script = f"""
|
|
||||||
function test() {
|
|
||||||
console.log("Hello {name}");
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
This content has quotes that previously broke JavaScript generation.
|
|
||||||
'''
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
|
|
||||||
# Read generated HTML
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Test 1: Check for clean template usage
|
|
||||||
assert 'markitect-config' in html_content
|
|
||||||
assert 'type="application/json"' in html_content
|
|
||||||
|
|
||||||
# Test 2: Extract and validate JSON configuration
|
|
||||||
config_json = self.extract_config_json(html_content)
|
|
||||||
assert config_json is not None, "Configuration JSON not found"
|
|
||||||
|
|
||||||
config = json.loads(config_json)
|
|
||||||
|
|
||||||
# Test 3: Validate configuration structure
|
|
||||||
required_fields = ['markdownContent', 'mode', 'theme', 'originalFilename']
|
|
||||||
for field in required_fields:
|
|
||||||
assert field in config, f"Required field '{field}' missing from configuration"
|
|
||||||
|
|
||||||
# Test 4: Check that problematic content is properly escaped
|
|
||||||
assert 'script = f"""' in config['markdownContent'] # Should be in JSON
|
|
||||||
assert '"""' not in html_content.split('markitect-config')[1].split('</script>')[0], "Unescaped quotes in HTML"
|
|
||||||
|
|
||||||
def test_clean_architecture_no_python_js_mixing(self):
|
|
||||||
"""Test that no Python code generates JavaScript strings"""
|
|
||||||
test_markdown = "# Simple Test\n\nBasic content."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Test 1: No direct JavaScript variable assignments from Python
|
|
||||||
problematic_patterns = [
|
|
||||||
'const markdownContent = "', # Old way
|
|
||||||
'const markdownContentWithDogtag = "', # Old way
|
|
||||||
'var markdownContent = "',
|
|
||||||
'let markdownContent = "'
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in problematic_patterns:
|
|
||||||
assert pattern not in html_content, f"Found problematic pattern: {pattern}"
|
|
||||||
|
|
||||||
# Test 2: Configuration should be in JSON script tag only
|
|
||||||
config_sections = html_content.count('markitect-config')
|
|
||||||
assert config_sections >= 2, f"Expected at least 2 config references (opening and closing), found {config_sections}"
|
|
||||||
|
|
||||||
# Test 3: JavaScript files should be embedded inline (no external src attributes)
|
|
||||||
js_components = [
|
|
||||||
'config-loader',
|
|
||||||
'section-manager',
|
|
||||||
'dom-renderer'
|
|
||||||
]
|
|
||||||
|
|
||||||
for component in js_components:
|
|
||||||
# Check that the component JavaScript is embedded, not referenced externally
|
|
||||||
assert f'src="js/' not in html_content, "Found external JavaScript references - should be embedded"
|
|
||||||
|
|
||||||
# Check that components are embedded inline
|
|
||||||
assert '{js_config_loader}' not in html_content, "Template placeholder not replaced"
|
|
||||||
assert 'class MarkitectConfig' in html_content, "Config loader not embedded"
|
|
||||||
assert 'class SectionManager' in html_content, "Section manager not embedded"
|
|
||||||
|
|
||||||
def test_configuration_interface_completeness(self):
|
|
||||||
"""Test that all required data is passed through the configuration interface"""
|
|
||||||
test_markdown = "# Config Test\n\nTesting configuration completeness."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True,
|
|
||||||
editor_theme='dark',
|
|
||||||
keyboard_shortcuts=False
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
config_json = self.extract_config_json(html_content)
|
|
||||||
config = json.loads(config_json)
|
|
||||||
|
|
||||||
# Test configuration completeness
|
|
||||||
expected_config = {
|
|
||||||
'markdownContent': test_markdown,
|
|
||||||
'mode': 'edit',
|
|
||||||
'theme': 'dark',
|
|
||||||
'keyboardShortcuts': False,
|
|
||||||
'autosave': False,
|
|
||||||
'sections': True,
|
|
||||||
'base64References': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, expected_value in expected_config.items():
|
|
||||||
assert key in config, f"Configuration missing key: {key}"
|
|
||||||
if key == 'markdownContent':
|
|
||||||
assert config[key] == expected_value, f"Configuration {key} value mismatch"
|
|
||||||
|
|
||||||
def test_insert_mode_configuration(self):
|
|
||||||
"""Test insert mode specific configuration"""
|
|
||||||
test_markdown = "# Insert Mode Test"
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
insert_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Check body class
|
|
||||||
assert 'class="markitect-insert-mode"' in html_content
|
|
||||||
|
|
||||||
# Check configuration
|
|
||||||
config_json = self.extract_config_json(html_content)
|
|
||||||
config = json.loads(config_json)
|
|
||||||
|
|
||||||
assert config['mode'] == 'insert'
|
|
||||||
assert 'restrictedHeadingLevels' in config
|
|
||||||
assert config['restrictedHeadingLevels'] == [1, 2, 3]
|
|
||||||
|
|
||||||
def test_static_vs_edit_mode_separation(self):
|
|
||||||
"""Test that static mode and edit mode use different templates"""
|
|
||||||
test_markdown = "# Mode Test\n\nTesting template separation."
|
|
||||||
|
|
||||||
# Test static mode
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as static_file:
|
|
||||||
static_result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=static_file.name,
|
|
||||||
edit_mode=False
|
|
||||||
)
|
|
||||||
|
|
||||||
static_content = Path(static_file.name).read_text()
|
|
||||||
|
|
||||||
# Static mode should NOT have configuration interface
|
|
||||||
assert 'markitect-config' not in static_content
|
|
||||||
assert 'application/json' not in static_content
|
|
||||||
|
|
||||||
# Test edit mode
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as edit_file:
|
|
||||||
edit_result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=edit_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
edit_content = Path(edit_file.name).read_text()
|
|
||||||
|
|
||||||
# Edit mode should HAVE configuration interface
|
|
||||||
assert 'markitect-config' in edit_content
|
|
||||||
assert 'application/json' in edit_content
|
|
||||||
|
|
||||||
# Helper methods
|
|
||||||
|
|
||||||
def extract_config_json(self, html_content):
|
|
||||||
"""Extract JSON configuration from HTML"""
|
|
||||||
try:
|
|
||||||
# Find the config script tag
|
|
||||||
start_marker = 'id="markitect-config" type="application/json">'
|
|
||||||
end_marker = '</script>'
|
|
||||||
|
|
||||||
start_pos = html_content.find(start_marker)
|
|
||||||
if start_pos == -1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
start_pos += len(start_marker)
|
|
||||||
end_pos = html_content.find(end_marker, start_pos)
|
|
||||||
|
|
||||||
if end_pos == -1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
config_json = html_content[start_pos:end_pos].strip()
|
|
||||||
return config_json
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to extract config JSON: {e}")
|
|
||||||
return None
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for Issue #132: Basic HTML Generation and Rendering
|
|
||||||
|
|
||||||
This module tests the core functionality of the md-render command for
|
|
||||||
client-side markdown rendering with JavaScript.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Add project root to path for imports
|
|
||||||
import sys
|
|
||||||
project_root = Path(__file__).parent.parent.parent.parent
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
from markitect.plugins.builtin.markdown_commands import MarkdownCommandsPlugin
|
|
||||||
|
|
||||||
|
|
||||||
class TestIssue132BasicRendering:
|
|
||||||
"""Test basic HTML generation and markdown rendering functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test environment."""
|
|
||||||
self.plugin = MarkdownCommandsPlugin()
|
|
||||||
self.plugin.initialize()
|
|
||||||
|
|
||||||
# Create temporary directory for test outputs
|
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
def teardown_method(self):
|
|
||||||
"""Clean up test environment."""
|
|
||||||
# Clean up temporary files
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def test_md_render_command_exists(self):
|
|
||||||
"""Test that md-render command is registered in plugin - Issue #132."""
|
|
||||||
commands = self.plugin.get_commands()
|
|
||||||
|
|
||||||
# Should include md-render command
|
|
||||||
assert 'md-render' in commands
|
|
||||||
|
|
||||||
# Command should be callable
|
|
||||||
md_render_cmd = commands['md-render']
|
|
||||||
assert callable(md_render_cmd)
|
|
||||||
|
|
||||||
def test_generate_basic_html_from_simple_markdown(self):
|
|
||||||
"""Test generating HTML from simple markdown content - Issue #132."""
|
|
||||||
# Create test markdown content
|
|
||||||
markdown_content = """# Test Document
|
|
||||||
|
|
||||||
This is a **test** document with some *italic* text and a [link](https://example.com).
|
|
||||||
|
|
||||||
## Section 2
|
|
||||||
|
|
||||||
- List item 1
|
|
||||||
- List item 2
|
|
||||||
- List item 3
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create temporary input file
|
|
||||||
input_file = Path(self.temp_dir) / "test.md"
|
|
||||||
input_file.write_text(markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "output.html"
|
|
||||||
|
|
||||||
# Test actual command execution
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
# Should execute successfully
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
# Should generate HTML file with content
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
assert '<!DOCTYPE html>' in html_content
|
|
||||||
assert '<title>Test Document</title>' in html_content
|
|
||||||
|
|
||||||
def test_html_contains_embedded_markdown_payload(self):
|
|
||||||
"""Test that generated HTML contains markdown as JavaScript payload - Issue #132."""
|
|
||||||
markdown_content = "# Simple Test\n\nThis is test content."
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "simple.md"
|
|
||||||
input_file.write_text(markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "simple.html"
|
|
||||||
|
|
||||||
# Test actual rendering
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should contain JavaScript with embedded markdown
|
|
||||||
assert 'const markdownContent =' in html_content
|
|
||||||
assert json.dumps(markdown_content) in html_content
|
|
||||||
|
|
||||||
# Should contain script tag for rendering
|
|
||||||
assert '<script' in html_content
|
|
||||||
assert 'marked' in html_content.lower()
|
|
||||||
|
|
||||||
def test_html_includes_javascript_markdown_parser(self):
|
|
||||||
"""Test that generated HTML includes JavaScript markdown parser - Issue #132."""
|
|
||||||
markdown_content = "# Parser Test\n\nTesting parser inclusion."
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "parser_test.md"
|
|
||||||
input_file.write_text(markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "parser_test.html"
|
|
||||||
|
|
||||||
# Test actual rendering
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include markdown parser (marked.js or similar)
|
|
||||||
assert any(parser in html_content.lower() for parser in ['marked', 'markdown-it', 'showdown'])
|
|
||||||
|
|
||||||
# Should include rendering logic
|
|
||||||
assert 'DOMContentLoaded' in html_content or 'window.onload' in html_content
|
|
||||||
|
|
||||||
def test_generated_html_is_valid_structure(self):
|
|
||||||
"""Test that generated HTML has valid document structure - Issue #132."""
|
|
||||||
markdown_content = "# Structure Test\n\nTesting HTML structure."
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "structure.md"
|
|
||||||
input_file.write_text(markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "structure.html"
|
|
||||||
|
|
||||||
# Test actual rendering
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Valid HTML5 document structure
|
|
||||||
assert html_content.startswith('<!DOCTYPE html>')
|
|
||||||
assert '<html' in html_content
|
|
||||||
assert '<head>' in html_content
|
|
||||||
assert '<body>' in html_content
|
|
||||||
assert '</html>' in html_content
|
|
||||||
|
|
||||||
# Should have content div for rendering
|
|
||||||
assert 'id="markdown-content"' in html_content
|
|
||||||
|
|
||||||
def test_handles_empty_markdown_file(self):
|
|
||||||
"""Test behavior with empty markdown file - Issue #132."""
|
|
||||||
# Create empty markdown file
|
|
||||||
input_file = Path(self.temp_dir) / "empty.md"
|
|
||||||
input_file.write_text("")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "empty.html"
|
|
||||||
|
|
||||||
# Test actual rendering
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
# Should handle empty file gracefully
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
# Should still generate valid HTML structure
|
|
||||||
assert '<!DOCTYPE html>' in html_content
|
|
||||||
assert 'const markdownContent = "";' in html_content
|
|
||||||
|
|
||||||
def test_handles_markdown_with_code_blocks(self):
|
|
||||||
"""Test handling markdown with code blocks - Issue #132."""
|
|
||||||
markdown_content = """# Code Test
|
|
||||||
|
|
||||||
Here's some Python code:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def hello_world():
|
|
||||||
print("Hello, World!")
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
|
|
||||||
And some inline `code` too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "code_test.md"
|
|
||||||
input_file.write_text(markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "code_test.html"
|
|
||||||
|
|
||||||
# Test actual rendering with code blocks
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [str(input_file), '--output', str(output_file), '--nodogtag'])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should properly escape code content in JavaScript
|
|
||||||
assert 'def hello_world' in html_content
|
|
||||||
# Should handle backticks and quotes properly
|
|
||||||
assert json.dumps(markdown_content) in html_content
|
|
||||||
|
|
||||||
def test_cli_command_interface_exists(self):
|
|
||||||
"""Test that md-render CLI command interface exists - Issue #132."""
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
# Should have md-render command registered
|
|
||||||
assert 'md-render' in cli.commands
|
|
||||||
|
|
||||||
cmd = cli.commands['md-render']
|
|
||||||
assert cmd.name == 'md-render'
|
|
||||||
assert cmd.help is not None
|
|
||||||
assert 'markdown' in cmd.help.lower()
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for Issue #132: Template System and CSS Injection
|
|
||||||
|
|
||||||
This module tests template selection and custom CSS injection functionality
|
|
||||||
for client-side markdown rendering.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add project root to path for imports
|
|
||||||
import sys
|
|
||||||
project_root = Path(__file__).parent.parent.parent.parent
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
|
|
||||||
class TestIssue132TemplateSystem:
|
|
||||||
"""Test template selection and CSS injection functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test environment."""
|
|
||||||
# Create temporary directory for test outputs
|
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
|
||||||
self.markdown_content = """# Template Test
|
|
||||||
|
|
||||||
This is a test document for template system validation.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Multiple templates
|
|
||||||
- Custom CSS support
|
|
||||||
- Responsive design
|
|
||||||
"""
|
|
||||||
|
|
||||||
def teardown_method(self):
|
|
||||||
"""Clean up test environment."""
|
|
||||||
# Clean up temporary files
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def test_default_template_generates_basic_html(self):
|
|
||||||
"""Test that default template generates basic HTML structure - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "default.md"
|
|
||||||
input_file.write_text(self.markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "default.html"
|
|
||||||
|
|
||||||
# Template system IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should contain basic HTML5 structure
|
|
||||||
assert '<!DOCTYPE html>' in html_content
|
|
||||||
assert '<meta charset="utf-8">' in html_content
|
|
||||||
assert '<title>' in html_content
|
|
||||||
|
|
||||||
def test_github_template_option(self):
|
|
||||||
"""Test GitHub-style template selection - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "github.md"
|
|
||||||
input_file.write_text(self.markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "github.html"
|
|
||||||
|
|
||||||
# Template system IS implemented - test GitHub template
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--theme', 'github'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
assert 'border-bottom: 1px solid #d0d7de' in html_content # GitHub heading style
|
|
||||||
|
|
||||||
def test_template_loading_from_filesystem(self):
|
|
||||||
"""Test template system uses embedded templates - Issue #132."""
|
|
||||||
# Templates are embedded in code, not loaded from filesystem
|
|
||||||
# Test that template system provides all expected templates
|
|
||||||
from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES
|
|
||||||
|
|
||||||
# Should have all expected templates available
|
|
||||||
expected_templates = ['basic', 'github', 'academic', 'dark']
|
|
||||||
|
|
||||||
for template_name in expected_templates:
|
|
||||||
assert template_name in TEMPLATE_STYLES
|
|
||||||
template_config = TEMPLATE_STYLES[template_name]
|
|
||||||
|
|
||||||
# Each template should have required style properties
|
|
||||||
assert 'body_color' in template_config
|
|
||||||
assert 'font_family' in template_config
|
|
||||||
assert 'max_width' in template_config
|
|
||||||
|
|
||||||
# Test that templates are properly formatted with variable placeholders
|
|
||||||
from markitect.plugins.builtin.markdown_commands import generate_html_with_embedded_markdown
|
|
||||||
test_html = generate_html_with_embedded_markdown("# Test", "Test Title", "basic", "", {})
|
|
||||||
# HTML template should be properly formatted
|
|
||||||
assert '<!DOCTYPE html>' in test_html
|
|
||||||
assert 'Test Title' in test_html
|
|
||||||
assert '# Test' in test_html
|
|
||||||
|
|
||||||
def test_template_variable_substitution(self):
|
|
||||||
"""Test template variable substitution system - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "variables.md"
|
|
||||||
input_file.write_text("# Variable Test\n\nTesting substitution.")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "variables.html"
|
|
||||||
|
|
||||||
# Template engine IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Variables should be substituted with actual values
|
|
||||||
assert '{{ markdown_json }}' not in html_content # Should be replaced
|
|
||||||
assert '{{ title }}' not in html_content # Should be replaced
|
|
||||||
assert '{{ css_content }}' not in html_content # Should be replaced
|
|
||||||
|
|
||||||
# Should contain actual markdown content as JSON
|
|
||||||
assert '# Variable Test' in html_content
|
|
||||||
|
|
||||||
def test_custom_css_injection(self):
|
|
||||||
"""Test custom CSS injection into templates - Issue #132."""
|
|
||||||
custom_css = """
|
|
||||||
body {
|
|
||||||
font-family: 'Comic Sans MS', cursive;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
.markdown-content {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create CSS file
|
|
||||||
css_file = Path(self.temp_dir) / "custom.css"
|
|
||||||
css_file.write_text(custom_css)
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "styled.md"
|
|
||||||
input_file.write_text(self.markdown_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "styled.html"
|
|
||||||
|
|
||||||
# CSS injection IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--css', str(css_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
# Custom CSS should be injected
|
|
||||||
assert 'Comic Sans MS' in html_content
|
|
||||||
assert 'background-color: #f0f0f0' in html_content
|
|
||||||
|
|
||||||
def test_css_content_embedded_in_html(self):
|
|
||||||
"""Test that CSS content is properly embedded in HTML - Issue #132."""
|
|
||||||
custom_css = "body { color: red; }"
|
|
||||||
css_file = Path(self.temp_dir) / "red.css"
|
|
||||||
css_file.write_text(custom_css)
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "red_test.md"
|
|
||||||
input_file.write_text("# Red Test\n\nShould be red text.")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "red_test.html"
|
|
||||||
|
|
||||||
# CSS embedding IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--css', str(css_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# CSS should be embedded in <style> tags
|
|
||||||
assert '<style>' in html_content
|
|
||||||
assert 'body { color: red; }' in html_content
|
|
||||||
assert '</style>' in html_content
|
|
||||||
|
|
||||||
def test_template_with_markdown_parser_integration(self):
|
|
||||||
"""Test template integration with JavaScript markdown parser - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "integration.md"
|
|
||||||
input_file.write_text("# Integration Test\n\nTesting parser integration.")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "integration.html"
|
|
||||||
|
|
||||||
# Integration IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should contain markdown parser script
|
|
||||||
assert 'marked.min.js' in html_content
|
|
||||||
assert 'marked.parse' in html_content
|
|
||||||
assert 'Integration Test' in html_content
|
|
||||||
|
|
||||||
# Should contain rendering JavaScript
|
|
||||||
assert 'DOMContentLoaded' in html_content
|
|
||||||
assert 'getElementById' in html_content
|
|
||||||
assert 'innerHTML' in html_content
|
|
||||||
|
|
||||||
def test_multiple_templates_available(self):
|
|
||||||
"""Test that multiple template options are available - Issue #132."""
|
|
||||||
# Test template availability
|
|
||||||
theme_options = ['basic', 'github', 'academic', 'dark']
|
|
||||||
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
# Create test markdown file
|
|
||||||
input_file = Path(self.temp_dir) / "template_test.md"
|
|
||||||
input_file.write_text("# Template Test\n\nTesting multiple templates.")
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
for theme in theme_options:
|
|
||||||
output_file = Path(self.temp_dir) / f"{theme}_output.html"
|
|
||||||
|
|
||||||
result = runner.invoke(md_render_command, [
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--theme', theme
|
|
||||||
])
|
|
||||||
|
|
||||||
# Should be able to specify different templates
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
# Verify template-specific styling
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
assert '<title>Template Test</title>' in html_content
|
|
||||||
|
|
||||||
def test_dark_theme_template_specific_styling(self):
|
|
||||||
"""Test that dark theme has appropriate dark styling - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "dark_test.md"
|
|
||||||
input_file.write_text("# Dark Theme Test\n\n> Blockquote test\n\n```code block```")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "dark_test.html"
|
|
||||||
|
|
||||||
from markitect.plugins.builtin.markdown_commands import md_render_command
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(md_render_command, [
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--theme', 'dark'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Verify dark theme specific colors
|
|
||||||
assert 'background-color: #0d1117' in html_content # Dark background
|
|
||||||
assert 'color: #e6edf3' in html_content # Light text (updated in modular theme)
|
|
||||||
assert 'color: #58a6ff' in html_content # Blue headings
|
|
||||||
assert 'background-color: #161b22' in html_content # Dark code blocks
|
|
||||||
assert 'border-left: 4px solid #30363d' in html_content # Gray blockquote border (updated)
|
|
||||||
|
|
||||||
def test_invalid_template_handling(self):
|
|
||||||
"""Test error handling for invalid template names - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "invalid.md"
|
|
||||||
input_file.write_text("# Invalid Template Test")
|
|
||||||
|
|
||||||
# Error handling IS implemented - test invalid template
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--theme', 'nonexistent_template'
|
|
||||||
])
|
|
||||||
|
|
||||||
# Should exit with error code for invalid template choice
|
|
||||||
assert result.exit_code != 0
|
|
||||||
assert ('invalid choice' in result.output.lower() or
|
|
||||||
'not one of' in result.output.lower() or
|
|
||||||
'unknown theme' in result.output.lower())
|
|
||||||
|
|
||||||
def test_template_title_extraction_from_markdown(self):
|
|
||||||
"""Test title extraction from markdown for template variables - Issue #132."""
|
|
||||||
markdown_with_title = """# Main Title
|
|
||||||
|
|
||||||
This document should use "Main Title" as the HTML title.
|
|
||||||
"""
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "title_test.md"
|
|
||||||
input_file.write_text(markdown_with_title)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "title_test.html"
|
|
||||||
|
|
||||||
# Title extraction IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# HTML title should be extracted from first heading
|
|
||||||
assert '<title>Main Title</title>' in html_content
|
|
||||||
|
|
||||||
def test_responsive_template_css(self):
|
|
||||||
"""Test that default templates include responsive CSS - Issue #132."""
|
|
||||||
input_file = Path(self.temp_dir) / "responsive.md"
|
|
||||||
input_file.write_text("# Responsive Test\n\nTesting responsive design.")
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "responsive.html"
|
|
||||||
|
|
||||||
# Responsive CSS IS implemented - test actual functionality
|
|
||||||
from markitect.cli import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file)
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include viewport meta tag
|
|
||||||
assert '<meta name="viewport"' in html_content
|
|
||||||
|
|
||||||
# Should include responsive CSS patterns
|
|
||||||
assert 'max-width' in html_content
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for Issue #133: CLI Integration with Instant Markdown Editing Support
|
|
||||||
|
|
||||||
This module tests the CLI command enhancement that adds editing capabilities
|
|
||||||
to the existing md-render command through the --edit flag.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
# Add project root to path for imports
|
|
||||||
import sys
|
|
||||||
project_root = Path(__file__).parent.parent.parent.parent
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
|
|
||||||
class TestIssue133CLIIntegration:
|
|
||||||
"""Test CLI integration for instant markdown editing support."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test environment."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
# Sample markdown content for testing
|
|
||||||
self.test_markdown = """# Editing Test Document
|
|
||||||
|
|
||||||
This is a test document for instant markdown editing functionality.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Click-to-edit sections
|
|
||||||
- Live preview comparison
|
|
||||||
- Change tracking
|
|
||||||
- File saving
|
|
||||||
|
|
||||||
### Code Example
|
|
||||||
```bash
|
|
||||||
markitect md-render input.md --edit
|
|
||||||
```
|
|
||||||
|
|
||||||
Content paragraph that should be editable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def teardown_method(self):
|
|
||||||
"""Clean up test environment."""
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def test_edit_flag_adds_editing_capabilities(self):
|
|
||||||
"""Test that --edit flag enables editing mode - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "edit_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "edit_output.html"
|
|
||||||
|
|
||||||
# Edit flag functionality IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include editor library and edit mode flag
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
|
||||||
assert 'DOMRenderer' in html_content
|
|
||||||
|
|
||||||
def test_edit_flag_with_all_templates(self):
|
|
||||||
"""Test --edit flag works with all template types - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "template_edit_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
templates = ['basic', 'github', 'academic', 'dark']
|
|
||||||
|
|
||||||
# Template editing IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
for template in templates:
|
|
||||||
output_file = Path(self.temp_dir) / f"edit_{template}.html"
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--theme', template,
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
# Should work with template styles
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'DOMRenderer' in html_content
|
|
||||||
|
|
||||||
def test_editor_library_loading_configuration(self):
|
|
||||||
"""Test editor library loading and configuration options - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "config_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "config_output.html"
|
|
||||||
|
|
||||||
# Editor configuration IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit',
|
|
||||||
'--editor-theme', 'dark'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include editor configuration with theme: 'dark'
|
|
||||||
assert 'theme: \'dark\'' in html_content
|
|
||||||
assert 'MARKITECT_EDITOR_CONFIG' in html_content
|
|
||||||
|
|
||||||
def test_backward_compatibility_without_edit_flag(self):
|
|
||||||
"""Test that existing functionality remains unchanged without --edit - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "compatibility_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "compatibility_output.html"
|
|
||||||
|
|
||||||
# Existing functionality should continue to work
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--theme', 'github',
|
|
||||||
'--nodogtag'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should NOT include editor library without --edit flag
|
|
||||||
assert 'markitect-editor' not in html_content
|
|
||||||
assert 'const MARKITECT_EDIT_MODE = true' not in html_content
|
|
||||||
|
|
||||||
# Should include existing functionality
|
|
||||||
assert 'marked.min.js' in html_content
|
|
||||||
assert 'Editing Test Document' in html_content
|
|
||||||
|
|
||||||
def test_help_text_includes_edit_options(self):
|
|
||||||
"""Test that help text includes new editing options - Issue #133."""
|
|
||||||
# Help text IS updated with edit options
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, ['md-render', '--help'])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert '--edit' in result.output
|
|
||||||
assert 'editing' in result.output.lower()
|
|
||||||
assert 'instant' in result.output.lower() or 'edit' in result.output.lower()
|
|
||||||
|
|
||||||
def test_edit_flag_with_custom_css(self):
|
|
||||||
"""Test --edit flag works with custom CSS injection - Issue #133."""
|
|
||||||
# Create custom CSS file
|
|
||||||
css_content = """
|
|
||||||
.editor-section {
|
|
||||||
border: 2px dashed #007acc;
|
|
||||||
}
|
|
||||||
.edit-mode textarea {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
css_file = Path(self.temp_dir) / "editor.css"
|
|
||||||
css_file.write_text(css_content)
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "css_edit_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "css_edit_output.html"
|
|
||||||
|
|
||||||
# CSS + editing integration IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--css', str(css_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include both custom CSS and editor
|
|
||||||
assert 'Courier New' in html_content
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'DOMRenderer' in html_content
|
|
||||||
|
|
||||||
def test_large_document_editing_performance(self):
|
|
||||||
"""Test editing flag with large markdown documents - Issue #133."""
|
|
||||||
# Create large markdown document
|
|
||||||
large_content = self.test_markdown * 50 # Repeat content 50 times
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "large_edit_test.md"
|
|
||||||
input_file.write_text(large_content)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "large_edit_output.html"
|
|
||||||
|
|
||||||
# Large document handling IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should handle large documents gracefully
|
|
||||||
assert len(html_content) > 20000 # Should be substantial (adjusted from 50k)
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
|
||||||
|
|
||||||
def test_front_matter_preservation_with_editing(self):
|
|
||||||
"""Test YAML front matter preserved in editing mode - Issue #133."""
|
|
||||||
markdown_with_frontmatter = """---
|
|
||||||
title: "Editable Document"
|
|
||||||
author: "Test Author"
|
|
||||||
date: "2025-10-07"
|
|
||||||
tags: [editing, test, markdown]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Editable Content
|
|
||||||
|
|
||||||
This content should be editable while preserving front matter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "frontmatter_edit_test.md"
|
|
||||||
input_file.write_text(markdown_with_frontmatter)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "frontmatter_edit_output.html"
|
|
||||||
|
|
||||||
# Front matter + editing IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should preserve front matter in JavaScript payload and include editing
|
|
||||||
assert 'Test Author' in html_content or 'Editable Document' in html_content
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
|
||||||
|
|
||||||
def test_error_handling_invalid_edit_options(self):
|
|
||||||
"""Test error handling for invalid editing options - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "error_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
# Error handling IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
# Test invalid editor theme
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--edit',
|
|
||||||
'--editor-theme', 'invalid_theme'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code != 0
|
|
||||||
assert 'invalid' in result.output.lower() or 'not one of' in result.output.lower()
|
|
||||||
|
|
||||||
def test_editor_script_cdn_fallback(self):
|
|
||||||
"""Test graceful handling when editor CDN fails - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "fallback_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "fallback_output.html"
|
|
||||||
|
|
||||||
# Editor functionality IS implemented with bundled JavaScript
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include bundled editor (not relying on CDN)
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
|
||||||
# The implementation uses bundled JavaScript, not CDN, so no fallback needed
|
|
||||||
|
|
||||||
def test_mobile_responsive_editing_meta_tags(self):
|
|
||||||
"""Test that editing mode includes proper mobile meta tags - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "mobile_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "mobile_output.html"
|
|
||||||
|
|
||||||
# Mobile responsiveness IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include mobile-friendly meta tags
|
|
||||||
assert 'viewport' in html_content
|
|
||||||
assert 'width=device-width' in html_content
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
|
|
||||||
def test_keyboard_shortcuts_configuration(self):
|
|
||||||
"""Test keyboard shortcuts can be configured for editing - Issue #133."""
|
|
||||||
input_file = Path(self.temp_dir) / "shortcuts_test.md"
|
|
||||||
input_file.write_text(self.test_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "shortcuts_output.html"
|
|
||||||
|
|
||||||
# Keyboard shortcuts ARE implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit',
|
|
||||||
'--keyboard-shortcuts'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should include keyboard shortcut configuration
|
|
||||||
assert 'MARKITECT_EDITOR_CONFIG' in html_content
|
|
||||||
assert 'keyboardShortcuts' in html_content
|
|
||||||
# TODO: Keyboard shortcut handlers not yet implemented in current architecture
|
|
||||||
# assert 'keydown' in html_content # When keyboard shortcuts are implemented
|
|
||||||
|
|
||||||
def test_edit_mode_with_existing_command_patterns(self):
|
|
||||||
"""Test that editing follows existing CLI command patterns - Issue #133."""
|
|
||||||
# Command pattern consistency IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
# Should follow same patterns as other md-* commands
|
|
||||||
md_commands = [name for name in cli.commands.keys() if name.startswith('md-')]
|
|
||||||
|
|
||||||
assert 'md-render' in md_commands
|
|
||||||
|
|
||||||
# md-render command should have consistent help format
|
|
||||||
cmd = cli.commands['md-render']
|
|
||||||
assert cmd.help is not None
|
|
||||||
assert 'edit' in cmd.help.lower() or any('--edit' in str(param) for param in cmd.params)
|
|
||||||
|
|
||||||
def test_section_detection_configuration(self):
|
|
||||||
"""Test section detection can be configured for different markdown structures - Issue #133."""
|
|
||||||
complex_markdown = """# Main Title
|
|
||||||
|
|
||||||
## Section 1
|
|
||||||
Content for section 1.
|
|
||||||
|
|
||||||
### Subsection 1.1
|
|
||||||
- List item 1
|
|
||||||
- List item 2
|
|
||||||
|
|
||||||
```python
|
|
||||||
def example_function():
|
|
||||||
return "editable code"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Section 2
|
|
||||||
| Column 1 | Column 2 |
|
|
||||||
|----------|----------|
|
|
||||||
| Data 1 | Data 2 |
|
|
||||||
|
|
||||||
> This is a blockquote that should be editable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
input_file = Path(self.temp_dir) / "complex_test.md"
|
|
||||||
input_file.write_text(complex_markdown)
|
|
||||||
|
|
||||||
output_file = Path(self.temp_dir) / "complex_output.html"
|
|
||||||
|
|
||||||
# Complex section detection IS implemented
|
|
||||||
from markitect.cli import cli
|
|
||||||
|
|
||||||
result = self.runner.invoke(cli, [
|
|
||||||
'md-render',
|
|
||||||
str(input_file),
|
|
||||||
'--output', str(output_file),
|
|
||||||
'--edit'
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
html_content = output_file.read_text()
|
|
||||||
|
|
||||||
# Should detect and mark various section types
|
|
||||||
assert 'data-section' in html_content or 'markitect-section-editable' in html_content
|
|
||||||
assert 'SectionManager' in html_content
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
"""
|
|
||||||
Test suite for md-render --edit functionality to prevent regression.
|
|
||||||
|
|
||||||
This test suite specifically targets the critical JavaScript syntax errors
|
|
||||||
that were causing edit mode to fail completely, ensuring they never happen again.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
class TestEditModeRegression:
|
|
||||||
"""Tests to prevent regression of the md-render --edit functionality."""
|
|
||||||
|
|
||||||
def test_edit_mode_generates_valid_javascript(self):
|
|
||||||
"""Test that edit mode generates syntactically valid JavaScript."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
# Test markdown content
|
|
||||||
test_content = "# Test Header\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph."
|
|
||||||
|
|
||||||
# Generate HTML with edit mode
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test Document",
|
|
||||||
markdown_content=test_content,
|
|
||||||
edit_mode=True,
|
|
||||||
editor_theme='github',
|
|
||||||
keyboard_shortcuts=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript from HTML
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
assert js_match, "No JavaScript found in edit mode HTML"
|
|
||||||
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Write to temp file and validate syntax with Node.js
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
|
||||||
f.write(js_content)
|
|
||||||
temp_js_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use Node.js to check JavaScript syntax
|
|
||||||
result = subprocess.run(
|
|
||||||
['node', '-c', temp_js_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.returncode == 0, f"JavaScript syntax error: {result.stderr}"
|
|
||||||
|
|
||||||
finally:
|
|
||||||
Path(temp_js_path).unlink()
|
|
||||||
|
|
||||||
def test_edit_mode_contains_required_functions(self):
|
|
||||||
"""Test that edit mode HTML contains all required JavaScript functions."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for critical functions that must be present
|
|
||||||
required_functions = [
|
|
||||||
'SectionManager',
|
|
||||||
'Section',
|
|
||||||
'DOMRenderer',
|
|
||||||
'DebugPanel',
|
|
||||||
'DocumentControls'
|
|
||||||
]
|
|
||||||
|
|
||||||
for func_name in required_functions:
|
|
||||||
assert func_name in html_content, f"Required function '{func_name}' not found in edit mode HTML"
|
|
||||||
|
|
||||||
def test_edit_mode_no_broken_string_literals(self):
|
|
||||||
"""Test that there are no broken string literals in the generated JavaScript."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Check for broken string patterns that caused the original bug
|
|
||||||
broken_patterns = [
|
|
||||||
r"'\s*\n\s*'", # Broken string literal across lines
|
|
||||||
r'"\s*\n\s*"', # Broken string literal across lines
|
|
||||||
r'reconstructed \+= .*\'\n', # Unescaped newline in string
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in broken_patterns:
|
|
||||||
matches = re.findall(pattern, js_content)
|
|
||||||
assert not matches, f"Found broken string pattern: {pattern} - matches: {matches}"
|
|
||||||
|
|
||||||
def test_edit_mode_proper_brace_escaping(self):
|
|
||||||
"""Test that braces are properly escaped in f-string templates."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Check for inconsistent brace patterns
|
|
||||||
inconsistent_patterns = [
|
|
||||||
r'(?<!})} else if.*{{', # Single brace followed by double (incorrect)
|
|
||||||
r'}} else if.*}(?!})', # Double brace followed by single closing (incorrect)
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in inconsistent_patterns:
|
|
||||||
matches = re.findall(pattern, js_content)
|
|
||||||
assert not matches, f"Found inconsistent brace pattern: {pattern}"
|
|
||||||
|
|
||||||
def test_edit_mode_template_literal_syntax(self):
|
|
||||||
"""Test that template literals are properly escaped."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Check for problematic template literal patterns
|
|
||||||
# Should NOT find double-escaped template literals like ${{
|
|
||||||
problematic_patterns = [
|
|
||||||
r'\$\{\{.*?\}\}', # Double-escaped template literals
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in problematic_patterns:
|
|
||||||
matches = re.findall(pattern, js_content)
|
|
||||||
assert not matches, f"Found problematic template literal: {pattern}"
|
|
||||||
|
|
||||||
def test_edit_mode_contains_content_div(self):
|
|
||||||
"""Test that edit mode HTML contains the markdown-content div."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test Content",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should contain the content container
|
|
||||||
assert 'id="markdown-content"' in html_content
|
|
||||||
assert 'MARKITECT_EDIT_MODE = true' in html_content
|
|
||||||
assert 'markitect-edit-mode' in html_content
|
|
||||||
|
|
||||||
def test_edit_mode_error_handling_elements(self):
|
|
||||||
"""Test that edit mode includes proper error handling UI elements."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should contain clean editor elements
|
|
||||||
assert 'MARKITECT_EDIT_MODE' in html_content
|
|
||||||
assert 'class="markitect-edit-mode"' in html_content
|
|
||||||
assert 'initializeCleanEditor' in html_content
|
|
||||||
assert 'console.error' in html_content # Error handling
|
|
||||||
|
|
||||||
def test_edit_mode_vs_normal_mode_differences(self):
|
|
||||||
"""Test that edit mode and normal mode generate different output appropriately."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
test_content = "# Test Header\n\nTest content."
|
|
||||||
|
|
||||||
# Generate both modes
|
|
||||||
normal_html = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content=test_content,
|
|
||||||
edit_mode=False
|
|
||||||
)
|
|
||||||
|
|
||||||
edit_html = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content=test_content,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Edit mode should have additional elements
|
|
||||||
assert len(edit_html) > len(normal_html)
|
|
||||||
assert 'MARKITECT_EDIT_MODE = true' in edit_html
|
|
||||||
assert 'MARKITECT_EDIT_MODE = true' not in normal_html
|
|
||||||
assert 'markitect-edit-mode' in edit_html
|
|
||||||
assert 'markitect-edit-mode' not in normal_html
|
|
||||||
|
|
||||||
def test_edit_mode_javascript_execution_flow(self):
|
|
||||||
"""Test the logical flow of JavaScript execution in edit mode."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Check for proper execution flow elements
|
|
||||||
flow_elements = [
|
|
||||||
'DOMContentLoaded', # Event listener setup
|
|
||||||
'MARKITECT_EDIT_MODE', # Mode check
|
|
||||||
'initializeCleanEditor', # Editor initialization
|
|
||||||
'marked.parse', # Content rendering
|
|
||||||
'SectionManager' # Section management class
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in flow_elements:
|
|
||||||
assert element in js_content, f"Missing execution flow element: {element}"
|
|
||||||
|
|
||||||
def test_newline_escaping_in_javascript_strings(self):
|
|
||||||
"""Test that newlines in JavaScript strings are properly escaped."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test\n\nMultiple\nLines",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract JavaScript
|
|
||||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
|
||||||
js_content = js_match.group(1)
|
|
||||||
|
|
||||||
# Look for the specific section that was broken
|
|
||||||
# Should find properly escaped newlines like '\\n\\n' in the JavaScript
|
|
||||||
assert '\\n\\n' in js_content, "Newlines not properly escaped in JavaScript strings"
|
|
||||||
|
|
||||||
# Should NOT find unescaped newlines in string contexts
|
|
||||||
# This regex looks for string concatenation with actual newlines
|
|
||||||
broken_newline_pattern = r"'\s*\+\s*text\s*\+\s*'\s*\n"
|
|
||||||
matches = re.findall(broken_newline_pattern, js_content)
|
|
||||||
assert not matches, f"Found unescaped newlines in string concatenation: {matches}"
|
|
||||||
|
|
||||||
|
|
||||||
class TestEditModeIntegration:
|
|
||||||
"""Integration tests for the complete edit mode functionality."""
|
|
||||||
|
|
||||||
def test_save_functionality_javascript_presence(self):
|
|
||||||
"""Test that the save functionality JavaScript is properly included."""
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
# Create a CleanDocumentManager
|
|
||||||
doc_manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
html_content = doc_manager._generate_html_template(
|
|
||||||
title="Test",
|
|
||||||
markdown_content="# Test Content",
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for modular architecture components (current implementation)
|
|
||||||
# TODO: Save functionality not yet implemented in modular architecture
|
|
||||||
required_elements = [
|
|
||||||
'SectionManager', # Core modular component
|
|
||||||
'DOMRenderer', # Rendering component
|
|
||||||
'DocumentControls', # Control component
|
|
||||||
'MARKITECT_EDIT_MODE' # Edit mode flag
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in required_elements:
|
|
||||||
assert element in html_content, f"Required modular component missing: {element}"
|
|
||||||
|
|
||||||
# Future save functionality elements (when implemented):
|
|
||||||
# save_elements = [
|
|
||||||
# '💾 Save Document',
|
|
||||||
# 'generateSaveFilename',
|
|
||||||
# 'getDocumentMarkdown',
|
|
||||||
# 'Blob',
|
|
||||||
# 'download'
|
|
||||||
# ]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pytest.main([__file__, "-v"])
|
|
||||||
@@ -20,7 +20,7 @@ from markitect.production.error_handler import (
|
|||||||
ResourceExhaustionError
|
ResourceExhaustionError
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from .test_utils import test_workspace
|
from .test_utils import workspace_context
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback for missing test utilities
|
# Fallback for missing test utilities
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -29,16 +29,13 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _test_workspace_fallback(name=None):
|
def workspace_context(name=None):
|
||||||
temp_dir = Path(tempfile.mkdtemp(prefix=f"{name}_" if name else "test_"))
|
temp_dir = Path(tempfile.mkdtemp(prefix=f"{name}_" if name else "test_"))
|
||||||
try:
|
try:
|
||||||
yield temp_dir
|
yield temp_dir
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
# Assign to expected name
|
|
||||||
test_workspace = _test_workspace_fallback
|
|
||||||
|
|
||||||
|
|
||||||
class TestProductionErrorHandler:
|
class TestProductionErrorHandler:
|
||||||
"""Test production error handling and recovery capabilities."""
|
"""Test production error handling and recovery capabilities."""
|
||||||
@@ -46,7 +43,7 @@ class TestProductionErrorHandler:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_workspace(self):
|
def temp_workspace(self):
|
||||||
"""Create temporary workspace for testing."""
|
"""Create temporary workspace for testing."""
|
||||||
with test_workspace("error_handler") as temp_dir:
|
with workspace_context("error_handler") as temp_dir:
|
||||||
yield temp_dir
|
yield temp_dir
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,440 +0,0 @@
|
|||||||
"""
|
|
||||||
JavaScript Sanity Test Suite
|
|
||||||
Tests for basic JavaScript functionality, syntax validation, and initialization
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
|
||||||
|
|
||||||
|
|
||||||
class TestJSSanity:
|
|
||||||
"""Test suite for JavaScript sanity checks"""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Setup for each test"""
|
|
||||||
self.manager = CleanDocumentManager()
|
|
||||||
|
|
||||||
def test_basic_html_generation_no_edit_mode(self):
|
|
||||||
"""Test that basic HTML generation works without edit mode"""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write("# Test Document\n\nThis is a test.")
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=False
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
|
|
||||||
# Read generated HTML
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Basic HTML structure checks
|
|
||||||
assert '<!DOCTYPE html>' in html_content
|
|
||||||
assert '<html' in html_content
|
|
||||||
assert '</html>' in html_content
|
|
||||||
assert '<body' in html_content
|
|
||||||
assert '</body>' in html_content
|
|
||||||
assert 'Test Document' in html_content
|
|
||||||
|
|
||||||
def test_edit_mode_javascript_syntax_validation(self):
|
|
||||||
"""Test that edit mode generates syntactically valid JavaScript"""
|
|
||||||
test_markdown = '''# Test Document
|
|
||||||
|
|
||||||
## Code Block Test
|
|
||||||
```python
|
|
||||||
script = f"""
|
|
||||||
function test() {
|
|
||||||
console.log("test");
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
This contains quotes that could break JavaScript.
|
|
||||||
'''
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
|
|
||||||
# Read generated HTML
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Extract JavaScript content
|
|
||||||
js_content = self.extract_javascript_from_html(html_content)
|
|
||||||
|
|
||||||
# Test 1: Basic syntax validation
|
|
||||||
syntax_errors = self.check_javascript_syntax(js_content)
|
|
||||||
assert len(syntax_errors) == 0, f"JavaScript syntax errors found: {syntax_errors}"
|
|
||||||
|
|
||||||
# Test 2: Check for unescaped quotes
|
|
||||||
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
|
||||||
assert len(quote_errors) == 0, f"Quote escaping issues found: {quote_errors}"
|
|
||||||
|
|
||||||
# Test 3: Check for required constants
|
|
||||||
self.check_required_constants(js_content)
|
|
||||||
|
|
||||||
def test_edit_mode_component_loading(self):
|
|
||||||
"""Test that all required JavaScript components are loaded"""
|
|
||||||
test_markdown = "# Simple Test\n\nBasic content for component loading test."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Check for required components
|
|
||||||
required_components = [
|
|
||||||
'js/core/debug-system.js',
|
|
||||||
'js/core/section-manager.js',
|
|
||||||
'js/components/dom-renderer.js',
|
|
||||||
'js/controls/control-base.js',
|
|
||||||
'js/main.js'
|
|
||||||
]
|
|
||||||
|
|
||||||
for component in required_components:
|
|
||||||
assert f"// === {component} ===" in html_content, f"Component {component} not loaded"
|
|
||||||
|
|
||||||
def test_edit_mode_class_definitions(self):
|
|
||||||
"""Test that required JavaScript classes are defined"""
|
|
||||||
test_markdown = "# Class Definition Test\n\nTesting class loading."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Check for required class definitions
|
|
||||||
required_classes = [
|
|
||||||
'class Section',
|
|
||||||
'class SectionManager',
|
|
||||||
'class DOMRenderer',
|
|
||||||
'class MarkitectDebugSystem',
|
|
||||||
'const Control =',
|
|
||||||
'class StatusControl',
|
|
||||||
'class DebugControl',
|
|
||||||
'class EditControl'
|
|
||||||
]
|
|
||||||
|
|
||||||
for class_def in required_classes:
|
|
||||||
assert class_def in html_content, f"Class definition '{class_def}' not found"
|
|
||||||
|
|
||||||
def test_edit_mode_initialization_functions(self):
|
|
||||||
"""Test that required initialization functions are defined"""
|
|
||||||
test_markdown = "# Initialization Test\n\nTesting function definitions."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Check for required function definitions
|
|
||||||
required_functions = [
|
|
||||||
'function initializeCleanEditor',
|
|
||||||
'function initializeScrollIndicators',
|
|
||||||
'function debug'
|
|
||||||
]
|
|
||||||
|
|
||||||
for func_def in required_functions:
|
|
||||||
assert func_def in html_content, f"Function definition '{func_def}' not found"
|
|
||||||
|
|
||||||
def test_edit_mode_global_exports(self):
|
|
||||||
"""Test that required globals are exported to window"""
|
|
||||||
test_markdown = "# Global Exports Test\n\nTesting window exports."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Check for required window exports
|
|
||||||
required_exports = [
|
|
||||||
'window.MarkitectDebugSystem = new MarkitectDebugSystem',
|
|
||||||
'window.SectionManager = SectionManager',
|
|
||||||
'window.Control = Control',
|
|
||||||
'window.StatusControl = StatusControl'
|
|
||||||
]
|
|
||||||
|
|
||||||
for export in required_exports:
|
|
||||||
assert export in html_content, f"Window export '{export}' not found"
|
|
||||||
|
|
||||||
# Helper methods
|
|
||||||
|
|
||||||
def extract_javascript_from_html(self, html_content):
|
|
||||||
"""Extract JavaScript content from HTML"""
|
|
||||||
# Find all script tags and extract their content
|
|
||||||
script_pattern = r'<script[^>]*>(.*?)</script>'
|
|
||||||
scripts = re.findall(script_pattern, html_content, re.DOTALL)
|
|
||||||
return '\n'.join(scripts)
|
|
||||||
|
|
||||||
def check_javascript_syntax(self, js_content):
|
|
||||||
"""Basic JavaScript syntax validation"""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# Check for common syntax errors
|
|
||||||
|
|
||||||
# 1. Unmatched quotes
|
|
||||||
single_quotes = js_content.count("'") - js_content.count("\\'")
|
|
||||||
double_quotes = js_content.count('"') - js_content.count('\\"')
|
|
||||||
|
|
||||||
if single_quotes % 2 != 0:
|
|
||||||
errors.append("Unmatched single quotes detected")
|
|
||||||
if double_quotes % 2 != 0:
|
|
||||||
errors.append("Unmatched double quotes detected")
|
|
||||||
|
|
||||||
# 2. Unmatched braces
|
|
||||||
open_braces = js_content.count('{')
|
|
||||||
close_braces = js_content.count('}')
|
|
||||||
if open_braces != close_braces:
|
|
||||||
errors.append(f"Unmatched braces: {open_braces} open, {close_braces} close")
|
|
||||||
|
|
||||||
# 3. Unmatched parentheses
|
|
||||||
open_parens = js_content.count('(')
|
|
||||||
close_parens = js_content.count(')')
|
|
||||||
if open_parens != close_parens:
|
|
||||||
errors.append(f"Unmatched parentheses: {open_parens} open, {close_parens} close")
|
|
||||||
|
|
||||||
# 4. Check for unterminated string literals
|
|
||||||
# Look for patterns that suggest unterminated strings
|
|
||||||
unterminated_patterns = [
|
|
||||||
r'[^\\]"[^"]*$', # Double quote not followed by closing quote at line end
|
|
||||||
r'[^\\]\'[^\']*$' # Single quote not followed by closing quote at line end
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in unterminated_patterns:
|
|
||||||
matches = re.findall(pattern, js_content, re.MULTILINE)
|
|
||||||
if matches:
|
|
||||||
errors.append(f"Potential unterminated string literals: {len(matches)} found")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def check_for_quote_escaping_issues(self, js_content):
|
|
||||||
"""Check for common quote escaping problems"""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# Look for problematic patterns
|
|
||||||
|
|
||||||
# 1. Triple quotes in JSON strings (common Python -> JS issue)
|
|
||||||
if '"""' in js_content and 'const markdownContent' in js_content:
|
|
||||||
errors.append("Triple quotes found in markdownContent - likely escaping issue")
|
|
||||||
|
|
||||||
# 2. Unescaped newlines in strings
|
|
||||||
problem_patterns = [
|
|
||||||
r'"[^"]*\n[^"]*"', # Newline in double-quoted string
|
|
||||||
r"'[^']*\n[^']*'" # Newline in single-quoted string
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in problem_patterns:
|
|
||||||
matches = re.findall(pattern, js_content)
|
|
||||||
if matches:
|
|
||||||
errors.append(f"Unescaped newlines in strings: {len(matches)} found")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def check_required_constants(self, js_content):
|
|
||||||
"""Check that required constants are defined"""
|
|
||||||
required_constants = [
|
|
||||||
'const markdownContent =',
|
|
||||||
'const MARKITECT_EDIT_MODE =',
|
|
||||||
'const MARKITECT_EDITOR_CONFIG =',
|
|
||||||
'const EditState =',
|
|
||||||
'const SectionType ='
|
|
||||||
]
|
|
||||||
|
|
||||||
for constant in required_constants:
|
|
||||||
assert constant in js_content, f"Required constant '{constant}' not found"
|
|
||||||
|
|
||||||
def check_for_infinite_retry_loop(self, js_content):
|
|
||||||
"""Check for patterns that indicate infinite retry loops"""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# Pattern 1: Retry logic that can loop infinitely
|
|
||||||
if "setTimeout(() => this.initialize(), 50)" in js_content:
|
|
||||||
# Check if there's a proper termination condition
|
|
||||||
if "maxWait" not in js_content and "startTime" not in js_content:
|
|
||||||
errors.append("Found retry setTimeout without timeout protection")
|
|
||||||
|
|
||||||
# Pattern 2: Configuration loading that retries indefinitely
|
|
||||||
retry_patterns = [
|
|
||||||
r"setTimeout\([^)]*initialize[^)]*\)", # setTimeout calling initialize
|
|
||||||
r"if\s*\(\s*!.*\.loaded\s*\)\s*{[^}]*setTimeout" # if not loaded, setTimeout
|
|
||||||
]
|
|
||||||
|
|
||||||
import re
|
|
||||||
for pattern in retry_patterns:
|
|
||||||
matches = re.findall(pattern, js_content)
|
|
||||||
if matches:
|
|
||||||
# Check if there are proper safeguards
|
|
||||||
if "maxWait" not in js_content or "timeout" not in js_content.lower():
|
|
||||||
errors.append(f"Found retry pattern without timeout protection: {pattern}")
|
|
||||||
|
|
||||||
# Pattern 3: Check for MarkitectMain.initialize calling itself recursively
|
|
||||||
if js_content.count("MarkitectMain.initialize") > 2: # Once for definition, once for call
|
|
||||||
if "this.initialized" not in js_content:
|
|
||||||
errors.append("MarkitectMain.initialize may call itself recursively without proper guard")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def check_configuration_loading_logic(self, js_content):
|
|
||||||
"""Check for proper configuration loading setup"""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# Check 1: Configuration should be loaded via JSON element
|
|
||||||
if 'markitect-config' not in js_content:
|
|
||||||
errors.append("No markitect-config element found - configuration loading will fail")
|
|
||||||
|
|
||||||
# Check 2: Configuration loader should wait for DOM
|
|
||||||
if 'DOMContentLoaded' not in js_content and 'document.readyState' not in js_content:
|
|
||||||
errors.append("Configuration loading doesn't wait for DOM ready")
|
|
||||||
|
|
||||||
# Check 3: Should have proper error handling for missing config element
|
|
||||||
if "getElementById('markitect-config')" in js_content:
|
|
||||||
if "throw new Error" not in js_content and "console.error" not in js_content:
|
|
||||||
errors.append("No error handling for missing configuration element")
|
|
||||||
|
|
||||||
# Check 4: Check for proper retry logic with timeout
|
|
||||||
if "setTimeout" in js_content and "initialize" in js_content:
|
|
||||||
if "maxWait" not in js_content and "startTime" not in js_content:
|
|
||||||
errors.append("Retry logic present but no timeout mechanism found")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def test_comprehensive_edit_mode_validation(self):
|
|
||||||
"""Comprehensive test that validates the complete edit mode functionality"""
|
|
||||||
# Use the actual GUARDRAILS.md that was causing issues
|
|
||||||
test_markdown = '''# Development Guardrails
|
|
||||||
|
|
||||||
## JavaScript Code Principles
|
|
||||||
|
|
||||||
### 1. No Inline JavaScript in Python
|
|
||||||
**NEVER write JavaScript code directly from Python code**
|
|
||||||
|
|
||||||
❌ **Wrong:**
|
|
||||||
```python
|
|
||||||
script = f"""
|
|
||||||
function myFunction() {{
|
|
||||||
console.log("Hello {name}");
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
✅ **Correct:**
|
|
||||||
```python
|
|
||||||
# Load from external files only
|
|
||||||
components = [
|
|
||||||
'js/core/section-manager.js',
|
|
||||||
'js/components/debug-panel.js'
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the content that was breaking the JavaScript generation.
|
|
||||||
'''
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
# This should not raise an exception
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
|
|
||||||
# Read and validate the generated HTML
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
js_content = self.extract_javascript_from_html(html_content)
|
|
||||||
|
|
||||||
# Comprehensive validation
|
|
||||||
syntax_errors = self.check_javascript_syntax(js_content)
|
|
||||||
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
|
||||||
|
|
||||||
# If these fail, we have the exact same problem as reported
|
|
||||||
assert len(syntax_errors) == 0, f"SYNTAX ERRORS: {syntax_errors}"
|
|
||||||
assert len(quote_errors) == 0, f"QUOTE ESCAPING ERRORS: {quote_errors}"
|
|
||||||
|
|
||||||
# Verify all required components loaded
|
|
||||||
self.check_required_constants(js_content)
|
|
||||||
|
|
||||||
# CRITICAL: Test for infinite retry loop
|
|
||||||
retry_errors = self.check_for_infinite_retry_loop(js_content)
|
|
||||||
assert len(retry_errors) == 0, f"INFINITE RETRY LOOP DETECTED: {retry_errors}"
|
|
||||||
|
|
||||||
def test_configuration_loading_not_stuck_in_loop(self):
|
|
||||||
"""Test specifically for infinite configuration loading retry loops"""
|
|
||||||
test_markdown = "# Simple Test\n\nBasic content for testing configuration loading."
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
||||||
md_file.write(test_markdown)
|
|
||||||
md_file.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
||||||
result = self.manager.render_file(
|
|
||||||
input_file=md_file.name,
|
|
||||||
output_file=html_file.name,
|
|
||||||
edit_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['success'] is True
|
|
||||||
html_content = Path(html_file.name).read_text()
|
|
||||||
|
|
||||||
# Test for infinite retry patterns
|
|
||||||
retry_issues = self.check_for_infinite_retry_loop(html_content)
|
|
||||||
assert len(retry_issues) == 0, f"INFINITE RETRY LOOP ISSUES: {retry_issues}"
|
|
||||||
|
|
||||||
# Test for proper configuration loading setup
|
|
||||||
config_issues = self.check_configuration_loading_logic(html_content)
|
|
||||||
assert len(config_issues) == 0, f"CONFIGURATION LOADING ISSUES: {config_issues}"
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
"""
|
|
||||||
Comprehensive tests for the Gitea facade/integration layer.
|
|
||||||
|
|
||||||
This test suite covers all Gitea API operations through the facade pattern,
|
|
||||||
ensuring the gitea.client module provides reliable, well-tested functionality
|
|
||||||
for the rest of the application.
|
|
||||||
|
|
||||||
NOTE: This test suite needs to be updated for the new capability-based architecture
|
|
||||||
where Gitea functionality has been moved to capabilities/release-management.
|
|
||||||
Skipping for now until the test can be restructured or moved to the appropriate capability.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock, MagicMock, patch
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Skip all tests in this file until gitea tests are moved to release-management capability
|
|
||||||
pytestmark = pytest.mark.skip(reason="Gitea functionality moved to release-management capability - tests need restructuring")
|
|
||||||
|
|
||||||
|
|
||||||
class TestGiteaConfig:
|
|
||||||
"""Test GiteaConfig functionality."""
|
|
||||||
|
|
||||||
def test_config_creation(self):
|
|
||||||
"""Test basic config creation."""
|
|
||||||
config = GiteaConfig(
|
|
||||||
gitea_url="https://gitea.example.com",
|
|
||||||
repo_owner="test_owner",
|
|
||||||
repo_name="test_repo",
|
|
||||||
auth_token="test_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert config.gitea_url == "https://gitea.example.com"
|
|
||||||
assert config.repo_owner == "test_owner"
|
|
||||||
assert config.repo_name == "test_repo"
|
|
||||||
assert config.auth_token == "test_token"
|
|
||||||
|
|
||||||
def test_api_url_properties(self):
|
|
||||||
"""Test API URL property generation."""
|
|
||||||
config = GiteaConfig(
|
|
||||||
gitea_url="https://gitea.example.com",
|
|
||||||
repo_owner="test_owner",
|
|
||||||
repo_name="test_repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert config.base_api_url == "https://gitea.example.com/api/v1"
|
|
||||||
assert config.repo_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo"
|
|
||||||
assert config.issues_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo/issues"
|
|
||||||
|
|
||||||
@patch('gitea.config.subprocess.run')
|
|
||||||
def test_from_git_repository(self, mock_run):
|
|
||||||
"""Test config creation from git repository."""
|
|
||||||
mock_run.return_value = Mock(
|
|
||||||
stdout="https://gitea.example.com/owner/repo.git",
|
|
||||||
returncode=0
|
|
||||||
)
|
|
||||||
|
|
||||||
config = GiteaConfig.from_git_repository()
|
|
||||||
|
|
||||||
assert config.gitea_url == "https://gitea.example.com"
|
|
||||||
assert config.repo_owner == "owner"
|
|
||||||
assert config.repo_name == "repo"
|
|
||||||
|
|
||||||
def test_config_validation(self):
|
|
||||||
"""Test config validation."""
|
|
||||||
# Valid config should not raise
|
|
||||||
config = GiteaConfig(
|
|
||||||
gitea_url="https://gitea.example.com",
|
|
||||||
repo_owner="owner",
|
|
||||||
repo_name="repo"
|
|
||||||
)
|
|
||||||
config.validate() # Should not raise
|
|
||||||
|
|
||||||
# Invalid URL should raise
|
|
||||||
invalid_config = GiteaConfig(
|
|
||||||
gitea_url="invalid-url",
|
|
||||||
repo_owner="owner",
|
|
||||||
repo_name="repo"
|
|
||||||
)
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
invalid_config.validate()
|
|
||||||
|
|
||||||
|
|
||||||
class TestIssuesClient:
|
|
||||||
"""Test IssuesClient functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.mock_api = Mock()
|
|
||||||
self.client = IssuesClient(self.mock_api)
|
|
||||||
|
|
||||||
# Mock issue for responses
|
|
||||||
self.mock_issue = Mock(spec=Issue)
|
|
||||||
self.mock_issue.number = 1
|
|
||||||
self.mock_issue.title = "Test Issue"
|
|
||||||
self.mock_issue.body = "Test body"
|
|
||||||
self.mock_issue.state = "open"
|
|
||||||
self.mock_issue.html_url = "https://gitea.example.com/owner/repo/issues/1"
|
|
||||||
self.mock_issue.created_at = datetime(2023, 1, 1, 12, 0, 0)
|
|
||||||
self.mock_issue.updated_at = datetime(2023, 1, 1, 12, 0, 0)
|
|
||||||
self.mock_issue.assignee = None
|
|
||||||
self.mock_issue.labels = []
|
|
||||||
self.mock_issue.milestone = None
|
|
||||||
|
|
||||||
def test_get_issue(self):
|
|
||||||
"""Test getting a single issue."""
|
|
||||||
self.mock_api.get_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.get(1)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.get_issue.assert_called_once_with(1)
|
|
||||||
|
|
||||||
def test_list_issues(self):
|
|
||||||
"""Test listing issues."""
|
|
||||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
|
||||||
|
|
||||||
result = self.client.list()
|
|
||||||
|
|
||||||
assert result == [self.mock_issue]
|
|
||||||
self.mock_api.list_issues.assert_called_once_with("all", 1, 50)
|
|
||||||
|
|
||||||
def test_list_issues_with_filters(self):
|
|
||||||
"""Test listing issues with filters."""
|
|
||||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
|
||||||
|
|
||||||
result = self.client.list(state="open", page=2, per_page=25)
|
|
||||||
|
|
||||||
assert result == [self.mock_issue]
|
|
||||||
self.mock_api.list_issues.assert_called_once_with("open", 2, 25)
|
|
||||||
|
|
||||||
def test_create_issue(self):
|
|
||||||
"""Test creating an issue."""
|
|
||||||
self.mock_api.create_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.create("Test Title", "Test Body")
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.create_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_create_issue_with_options(self):
|
|
||||||
"""Test creating an issue with optional fields."""
|
|
||||||
self.mock_api.create_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.create(
|
|
||||||
"Test Title",
|
|
||||||
"Test Body",
|
|
||||||
assignees=["user1"],
|
|
||||||
milestone=1,
|
|
||||||
labels=["bug", "priority:high"]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.create_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_update_issue(self):
|
|
||||||
"""Test updating an issue."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.update(1, title="New Title")
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_close_issue(self):
|
|
||||||
"""Test closing an issue."""
|
|
||||||
closed_issue = Mock(spec=Issue)
|
|
||||||
closed_issue.state = "closed"
|
|
||||||
self.mock_api.update_issue.return_value = closed_issue
|
|
||||||
|
|
||||||
result = self.client.close(1)
|
|
||||||
|
|
||||||
assert result.state == "closed"
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_reopen_issue(self):
|
|
||||||
"""Test reopening an issue."""
|
|
||||||
opened_issue = Mock(spec=Issue)
|
|
||||||
opened_issue.state = "open"
|
|
||||||
self.mock_api.update_issue.return_value = opened_issue
|
|
||||||
|
|
||||||
result = self.client.reopen(1)
|
|
||||||
|
|
||||||
assert result.state == "open"
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_add_labels(self):
|
|
||||||
"""Test adding labels to an issue."""
|
|
||||||
# Mock getting current issue
|
|
||||||
self.mock_issue.labels = [Mock(name="existing")]
|
|
||||||
self.mock_api.get_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
# Mock update result
|
|
||||||
updated_issue = Mock(spec=Issue)
|
|
||||||
updated_issue.labels = [Mock(name="existing"), Mock(name="new")]
|
|
||||||
self.mock_api.update_issue.return_value = updated_issue
|
|
||||||
|
|
||||||
result = self.client.add_labels(1, ["new"])
|
|
||||||
|
|
||||||
assert len(result.labels) == 2
|
|
||||||
self.mock_api.get_issue.assert_called_once_with(1)
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_remove_labels(self):
|
|
||||||
"""Test removing labels from an issue."""
|
|
||||||
# Mock getting current issue
|
|
||||||
label1 = Mock(name="keep")
|
|
||||||
label2 = Mock(name="remove")
|
|
||||||
self.mock_issue.labels = [label1, label2]
|
|
||||||
self.mock_api.get_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
# Mock update result
|
|
||||||
updated_issue = Mock(spec=Issue)
|
|
||||||
updated_issue.labels = [label1]
|
|
||||||
self.mock_api.update_issue.return_value = updated_issue
|
|
||||||
|
|
||||||
result = self.client.remove_labels(1, ["remove"])
|
|
||||||
|
|
||||||
assert len(result.labels) == 1
|
|
||||||
self.mock_api.get_issue.assert_called_once_with(1)
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_assign_to_milestone(self):
|
|
||||||
"""Test assigning issue to milestone."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.assign_to_milestone(1, 5)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_remove_from_milestone(self):
|
|
||||||
"""Test removing issue from milestone."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.remove_from_milestone(1)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_set_labels(self):
|
|
||||||
"""Test replacing all labels on an issue."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.set_labels(1, ["bug", "priority:high"])
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_update_title(self):
|
|
||||||
"""Test updating only issue title."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.update_title(1, "New Title")
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_update_body(self):
|
|
||||||
"""Test updating only issue body."""
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.update_body(1, "New Body")
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_set_priority(self):
|
|
||||||
"""Test setting issue priority."""
|
|
||||||
# Mock getting current issue
|
|
||||||
self.mock_issue.labels = [Mock(name="bug")]
|
|
||||||
self.mock_api.get_issue.return_value = self.mock_issue
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.set_priority(1, Priority.HIGH)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.get_issue.assert_called_once_with(1)
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_set_status(self):
|
|
||||||
"""Test setting issue status."""
|
|
||||||
# Mock getting current issue
|
|
||||||
self.mock_issue.labels = [Mock(name="bug")]
|
|
||||||
self.mock_api.get_issue.return_value = self.mock_issue
|
|
||||||
self.mock_api.update_issue.return_value = self.mock_issue
|
|
||||||
|
|
||||||
result = self.client.set_status(1, ProjectState.ACTIVE)
|
|
||||||
|
|
||||||
assert result == self.mock_issue
|
|
||||||
self.mock_api.get_issue.assert_called_once_with(1)
|
|
||||||
self.mock_api.update_issue.assert_called_once()
|
|
||||||
|
|
||||||
def test_to_dict(self):
|
|
||||||
"""Test converting issue to dictionary."""
|
|
||||||
result = self.client.to_dict(self.mock_issue)
|
|
||||||
|
|
||||||
expected_keys = ['number', 'title', 'body', 'state', 'html_url',
|
|
||||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
|
||||||
|
|
||||||
assert all(key in result for key in expected_keys)
|
|
||||||
assert result['number'] == 1
|
|
||||||
assert result['title'] == "Test Issue"
|
|
||||||
assert result['state'] == "open"
|
|
||||||
|
|
||||||
|
|
||||||
class TestMilestonesClient:
|
|
||||||
"""Test MilestonesClient functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.mock_api = Mock()
|
|
||||||
self.client = MilestonesClient(self.mock_api)
|
|
||||||
|
|
||||||
self.mock_milestone = Mock(spec=Milestone)
|
|
||||||
self.mock_milestone.id = 1
|
|
||||||
self.mock_milestone.title = "Test Milestone"
|
|
||||||
|
|
||||||
def test_list_milestones(self):
|
|
||||||
"""Test listing milestones."""
|
|
||||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
|
||||||
|
|
||||||
result = self.client.list()
|
|
||||||
|
|
||||||
assert result == [self.mock_milestone]
|
|
||||||
self.mock_api.list_milestones.assert_called_once_with("all")
|
|
||||||
|
|
||||||
def test_list_open_milestones(self):
|
|
||||||
"""Test listing open milestones."""
|
|
||||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
|
||||||
|
|
||||||
result = self.client.list_open()
|
|
||||||
|
|
||||||
assert result == [self.mock_milestone]
|
|
||||||
self.mock_api.list_milestones.assert_called_once_with("open")
|
|
||||||
|
|
||||||
def test_create_milestone(self):
|
|
||||||
"""Test creating a milestone."""
|
|
||||||
self.mock_api.create_milestone.return_value = self.mock_milestone
|
|
||||||
|
|
||||||
result = self.client.create("Test Milestone", "Description")
|
|
||||||
|
|
||||||
assert result == self.mock_milestone
|
|
||||||
self.mock_api.create_milestone.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestLabelsClient:
|
|
||||||
"""Test LabelsClient functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.mock_api = Mock()
|
|
||||||
self.client = LabelsClient(self.mock_api)
|
|
||||||
|
|
||||||
self.mock_label = Mock(spec=Label)
|
|
||||||
self.mock_label.id = 1
|
|
||||||
self.mock_label.name = "bug"
|
|
||||||
|
|
||||||
def test_list_labels(self):
|
|
||||||
"""Test listing labels."""
|
|
||||||
self.mock_api.list_labels.return_value = [self.mock_label]
|
|
||||||
|
|
||||||
result = self.client.list()
|
|
||||||
|
|
||||||
assert result == [self.mock_label]
|
|
||||||
self.mock_api.list_labels.assert_called_once()
|
|
||||||
|
|
||||||
def test_create_label(self):
|
|
||||||
"""Test creating a label."""
|
|
||||||
self.mock_api.create_label.return_value = self.mock_label
|
|
||||||
|
|
||||||
result = self.client.create("bug", "red", "Bug reports")
|
|
||||||
|
|
||||||
assert result == self.mock_label
|
|
||||||
self.mock_api.create_label.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGiteaClient:
|
|
||||||
"""Test the main GiteaClient facade."""
|
|
||||||
|
|
||||||
@patch('gitea.client.GiteaApiClient')
|
|
||||||
def test_client_initialization(self, mock_api_client):
|
|
||||||
"""Test GiteaClient initialization."""
|
|
||||||
config = GiteaConfig(
|
|
||||||
gitea_url="https://gitea.example.com",
|
|
||||||
repo_owner="test_owner",
|
|
||||||
repo_name="test_repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
client = GiteaClient(config)
|
|
||||||
|
|
||||||
assert isinstance(client.issues, IssuesClient)
|
|
||||||
assert isinstance(client.milestones, MilestonesClient)
|
|
||||||
assert isinstance(client.labels, LabelsClient)
|
|
||||||
mock_api_client.assert_called_once_with(config)
|
|
||||||
|
|
||||||
@patch('gitea.client.GiteaConfig.from_git_repository')
|
|
||||||
@patch('gitea.client.GiteaApiClient')
|
|
||||||
def test_client_auto_config(self, mock_api_client, mock_from_git):
|
|
||||||
"""Test GiteaClient with auto-detected config."""
|
|
||||||
mock_config = Mock()
|
|
||||||
mock_from_git.return_value = mock_config
|
|
||||||
|
|
||||||
client = GiteaClient()
|
|
||||||
|
|
||||||
mock_from_git.assert_called_once()
|
|
||||||
mock_api_client.assert_called_once_with(mock_config)
|
|
||||||
|
|
||||||
|
|
||||||
class TestErrorHandling:
|
|
||||||
"""Test error handling throughout the facade."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.mock_api = Mock()
|
|
||||||
self.client = IssuesClient(self.mock_api)
|
|
||||||
|
|
||||||
def test_gitea_error_propagation(self):
|
|
||||||
"""Test that GiteaError is properly propagated."""
|
|
||||||
self.mock_api.get_issue.side_effect = GiteaError("API Error")
|
|
||||||
|
|
||||||
with pytest.raises(GiteaError):
|
|
||||||
self.client.get(1)
|
|
||||||
|
|
||||||
def test_not_found_error_propagation(self):
|
|
||||||
"""Test that GiteaNotFoundError is properly propagated."""
|
|
||||||
self.mock_api.get_issue.side_effect = GiteaNotFoundError("Issue not found")
|
|
||||||
|
|
||||||
with pytest.raises(GiteaNotFoundError):
|
|
||||||
self.client.get(999)
|
|
||||||
|
|
||||||
def test_auth_error_propagation(self):
|
|
||||||
"""Test that GiteaAuthError is properly propagated."""
|
|
||||||
self.mock_api.create_issue.side_effect = GiteaAuthError("Unauthorized")
|
|
||||||
|
|
||||||
with pytest.raises(GiteaAuthError):
|
|
||||||
self.client.create("Title", "Body")
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationPatterns:
|
|
||||||
"""Test integration patterns and best practices."""
|
|
||||||
|
|
||||||
@patch('gitea.client.GiteaApiClient')
|
|
||||||
def test_consistent_interface(self, mock_api_client):
|
|
||||||
"""Test that the facade provides consistent interfaces."""
|
|
||||||
config = GiteaConfig(gitea_url="https://gitea.example.com",
|
|
||||||
repo_owner="owner", repo_name="repo")
|
|
||||||
client = GiteaClient(config)
|
|
||||||
|
|
||||||
# All sub-clients should be available
|
|
||||||
assert hasattr(client, 'issues')
|
|
||||||
assert hasattr(client, 'milestones')
|
|
||||||
assert hasattr(client, 'labels')
|
|
||||||
|
|
||||||
# All should have consistent method patterns
|
|
||||||
assert hasattr(client.issues, 'list')
|
|
||||||
assert hasattr(client.issues, 'get')
|
|
||||||
assert hasattr(client.issues, 'create')
|
|
||||||
assert hasattr(client.issues, 'update')
|
|
||||||
|
|
||||||
assert hasattr(client.milestones, 'list')
|
|
||||||
assert hasattr(client.milestones, 'create')
|
|
||||||
|
|
||||||
assert hasattr(client.labels, 'list')
|
|
||||||
assert hasattr(client.labels, 'create')
|
|
||||||
|
|
||||||
def test_backward_compatibility_dict_conversion(self):
|
|
||||||
"""Test that to_dict provides backward compatibility."""
|
|
||||||
mock_api = Mock()
|
|
||||||
client = IssuesClient(mock_api)
|
|
||||||
|
|
||||||
# Create a mock issue with all expected attributes
|
|
||||||
mock_issue = Mock(spec=Issue)
|
|
||||||
mock_issue.number = 1
|
|
||||||
mock_issue.title = "Test"
|
|
||||||
mock_issue.body = "Body"
|
|
||||||
mock_issue.state = "open"
|
|
||||||
mock_issue.html_url = "https://example.com"
|
|
||||||
mock_issue.created_at = datetime(2023, 1, 1)
|
|
||||||
mock_issue.updated_at = datetime(2023, 1, 1)
|
|
||||||
mock_issue.assignee = None
|
|
||||||
mock_issue.labels = []
|
|
||||||
mock_issue.milestone = None
|
|
||||||
|
|
||||||
result = client.to_dict(mock_issue)
|
|
||||||
|
|
||||||
# Should contain all expected fields for backward compatibility
|
|
||||||
required_fields = ['number', 'title', 'body', 'state', 'html_url',
|
|
||||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
|
||||||
|
|
||||||
for field in required_fields:
|
|
||||||
assert field in result, f"Missing required field: {field}"
|
|
||||||
|
|
||||||
def test_label_operations_consistency(self):
|
|
||||||
"""Test that label operations work consistently."""
|
|
||||||
mock_api = Mock()
|
|
||||||
client = IssuesClient(mock_api)
|
|
||||||
|
|
||||||
# Mock issue with labels
|
|
||||||
mock_issue = Mock()
|
|
||||||
mock_issue.labels = [Mock(name="bug"), Mock(name="priority:high")]
|
|
||||||
mock_api.get_issue.return_value = mock_issue
|
|
||||||
mock_api.update_issue.return_value = mock_issue
|
|
||||||
|
|
||||||
# Test all label operations
|
|
||||||
client.add_labels(1, ["new-label"])
|
|
||||||
client.remove_labels(1, ["old-label"])
|
|
||||||
client.set_labels(1, ["label1", "label2"])
|
|
||||||
|
|
||||||
# Should have made appropriate API calls
|
|
||||||
assert mock_api.get_issue.call_count == 2 # add_labels and remove_labels
|
|
||||||
assert mock_api.update_issue.call_count == 3 # all three operations
|
|
||||||
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
|
||||||
688
tests/test_schema_loader.py
Normal file
688
tests/test_schema_loader.py
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for schema_loader.py - Markdown schema loading.
|
||||||
|
|
||||||
|
Tests the markdown schema loader functionality including:
|
||||||
|
- Frontmatter extraction (YAML)
|
||||||
|
- JSON schema extraction from code blocks
|
||||||
|
- Metadata merging
|
||||||
|
- Schema saving
|
||||||
|
- Error handling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from markitect.schema_loader import (
|
||||||
|
MarkdownSchemaLoader,
|
||||||
|
SchemaLoaderError,
|
||||||
|
InvalidSchemaFormatError,
|
||||||
|
SchemaNotFoundError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_schema_dir(tmp_path):
|
||||||
|
"""Create temporary directory for schema files."""
|
||||||
|
schema_dir = tmp_path / "schemas"
|
||||||
|
schema_dir.mkdir()
|
||||||
|
return schema_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_schema_md():
|
||||||
|
"""Simple valid markdown schema content."""
|
||||||
|
return """---
|
||||||
|
schema-id: "https://markitect.dev/schemas/test/v1"
|
||||||
|
version: "1.0.0"
|
||||||
|
status: "stable"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Schema v1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a test schema for validation.
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Schema for testing",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- Initial version
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def schema_without_frontmatter():
|
||||||
|
"""Schema without YAML frontmatter."""
|
||||||
|
return """# Test Schema v1.0
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def schema_multiple_json_blocks():
|
||||||
|
"""Schema with multiple JSON code blocks."""
|
||||||
|
return """---
|
||||||
|
version: "1.0.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Schema
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"example": "This is not the schema"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Examples
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"another": "example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarkdownSchemaLoader:
|
||||||
|
"""Tests for MarkdownSchemaLoader class."""
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""Test loader initialization."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
assert loader is not None
|
||||||
|
assert hasattr(loader, 'frontmatter_pattern')
|
||||||
|
assert hasattr(loader, 'json_code_block_pattern')
|
||||||
|
|
||||||
|
def test_load_simple_schema(self, temp_schema_dir, simple_schema_md):
|
||||||
|
"""Test loading a simple valid schema."""
|
||||||
|
schema_file = temp_schema_dir / "test-schema-v1.0.md"
|
||||||
|
schema_file.write_text(simple_schema_md)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
result = loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
assert 'schema' in result
|
||||||
|
assert 'metadata' in result
|
||||||
|
assert 'documentation' in result
|
||||||
|
assert 'source_file' in result
|
||||||
|
|
||||||
|
# Check schema content
|
||||||
|
schema = result['schema']
|
||||||
|
assert schema['title'] == 'Test Schema'
|
||||||
|
assert schema['version'] == '1.0.0'
|
||||||
|
assert schema['type'] == 'object'
|
||||||
|
|
||||||
|
# Check metadata
|
||||||
|
metadata = result['metadata']
|
||||||
|
assert metadata['version'] == '1.0.0'
|
||||||
|
assert metadata['status'] == 'stable'
|
||||||
|
|
||||||
|
# Check source tracking
|
||||||
|
assert result['source_file'] == str(schema_file)
|
||||||
|
assert 'x-markitect-source' in schema
|
||||||
|
assert schema['x-markitect-source']['format'] == 'markdown'
|
||||||
|
|
||||||
|
def test_load_schema_file_not_found(self):
|
||||||
|
"""Test loading non-existent file raises FileNotFoundError."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError, match="Schema file not found"):
|
||||||
|
loader.load_schema(Path("/nonexistent/schema.md"))
|
||||||
|
|
||||||
|
def test_load_schema_without_json(self, temp_schema_dir):
|
||||||
|
"""Test loading markdown without JSON schema raises error."""
|
||||||
|
schema_file = temp_schema_dir / "no-schema.md"
|
||||||
|
schema_file.write_text("# Just a heading\n\nNo schema here.")
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(SchemaNotFoundError, match="No JSON schema found"):
|
||||||
|
loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
def test_load_schema_invalid_json(self, temp_schema_dir):
|
||||||
|
"""Test loading markdown with invalid JSON raises error."""
|
||||||
|
content = """# Test
|
||||||
|
|
||||||
|
```json
|
||||||
|
{invalid json}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
schema_file = temp_schema_dir / "invalid.md"
|
||||||
|
schema_file.write_text(content)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError, match="Invalid JSON"):
|
||||||
|
loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractFrontmatter:
|
||||||
|
"""Tests for frontmatter extraction."""
|
||||||
|
|
||||||
|
def test_extract_valid_frontmatter(self, simple_schema_md):
|
||||||
|
"""Test extracting valid YAML frontmatter."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
metadata = loader._extract_frontmatter(simple_schema_md)
|
||||||
|
|
||||||
|
assert metadata['schema-id'] == 'https://markitect.dev/schemas/test/v1'
|
||||||
|
assert metadata['version'] == '1.0.0'
|
||||||
|
assert metadata['status'] == 'stable'
|
||||||
|
|
||||||
|
def test_extract_no_frontmatter(self, schema_without_frontmatter):
|
||||||
|
"""Test extracting from content without frontmatter returns empty dict."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
metadata = loader._extract_frontmatter(schema_without_frontmatter)
|
||||||
|
|
||||||
|
assert metadata == {}
|
||||||
|
|
||||||
|
def test_extract_invalid_yaml_frontmatter(self):
|
||||||
|
"""Test extracting invalid YAML raises error."""
|
||||||
|
content = """---
|
||||||
|
invalid: yaml: syntax: error
|
||||||
|
---
|
||||||
|
|
||||||
|
# Content
|
||||||
|
"""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError, match="Invalid YAML"):
|
||||||
|
loader._extract_frontmatter(content)
|
||||||
|
|
||||||
|
def test_extract_non_dict_frontmatter(self):
|
||||||
|
"""Test extracting non-dictionary YAML raises error."""
|
||||||
|
content = """---
|
||||||
|
- list
|
||||||
|
- not
|
||||||
|
- dict
|
||||||
|
---
|
||||||
|
|
||||||
|
# Content
|
||||||
|
"""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError, match="must be a YAML dictionary"):
|
||||||
|
loader._extract_frontmatter(content)
|
||||||
|
|
||||||
|
def test_extract_complex_frontmatter(self):
|
||||||
|
"""Test extracting complex frontmatter with nested structures."""
|
||||||
|
content = """---
|
||||||
|
schema-id: "https://example.com/schema"
|
||||||
|
version: "1.0.0"
|
||||||
|
tags:
|
||||||
|
- documentation
|
||||||
|
- schema
|
||||||
|
metadata:
|
||||||
|
author: "Test Author"
|
||||||
|
created: "2026-01-04"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Content
|
||||||
|
"""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
metadata = loader._extract_frontmatter(content)
|
||||||
|
|
||||||
|
assert metadata['tags'] == ['documentation', 'schema']
|
||||||
|
assert metadata['metadata']['author'] == 'Test Author'
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractJsonSchema:
|
||||||
|
"""Tests for JSON schema extraction."""
|
||||||
|
|
||||||
|
def test_extract_single_json_block(self, schema_without_frontmatter):
|
||||||
|
"""Test extracting single JSON block."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
schema = loader._extract_json_schema(schema_without_frontmatter)
|
||||||
|
|
||||||
|
assert schema is not None
|
||||||
|
assert schema['title'] == 'Test Schema'
|
||||||
|
assert schema['type'] == 'object'
|
||||||
|
|
||||||
|
def test_extract_from_schema_definition_section(self, schema_multiple_json_blocks):
|
||||||
|
"""Test preferring JSON block under Schema Definition heading."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
schema = loader._extract_json_schema(schema_multiple_json_blocks)
|
||||||
|
|
||||||
|
assert schema is not None
|
||||||
|
assert schema['title'] == 'Test Schema'
|
||||||
|
# Should get the schema from Schema Definition section, not the example
|
||||||
|
|
||||||
|
def test_extract_no_json_block(self):
|
||||||
|
"""Test extracting from content with no JSON blocks returns None."""
|
||||||
|
content = "# Just text\n\nNo code blocks here."
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
schema = loader._extract_json_schema(content)
|
||||||
|
|
||||||
|
assert schema is None
|
||||||
|
|
||||||
|
def test_extract_invalid_json_block(self):
|
||||||
|
"""Test extracting invalid JSON raises error."""
|
||||||
|
content = """# Test
|
||||||
|
|
||||||
|
```json
|
||||||
|
{invalid}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError, match="Invalid JSON"):
|
||||||
|
loader._extract_json_schema(content)
|
||||||
|
|
||||||
|
def test_extract_non_object_json(self):
|
||||||
|
"""Test extracting JSON array (non-object) raises error."""
|
||||||
|
content = """# Test
|
||||||
|
|
||||||
|
```json
|
||||||
|
["array", "not", "object"]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError, match="must be a JSON object"):
|
||||||
|
loader._extract_json_schema(content)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMergeMetadata:
|
||||||
|
"""Tests for metadata merging."""
|
||||||
|
|
||||||
|
def test_merge_basic_metadata(self):
|
||||||
|
"""Test merging frontmatter into schema."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'title': 'Test Schema',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'version': '2.0.0',
|
||||||
|
'schema-id': 'https://example.com/v2',
|
||||||
|
'status': 'draft'
|
||||||
|
}
|
||||||
|
|
||||||
|
merged = loader._merge_metadata(schema, metadata, Path('test.md'))
|
||||||
|
|
||||||
|
# Version should be overridden
|
||||||
|
assert merged['version'] == '2.0.0'
|
||||||
|
|
||||||
|
# $id should be set from schema-id
|
||||||
|
assert merged['$id'] == 'https://example.com/v2'
|
||||||
|
|
||||||
|
# Status should be in x-markitect-metadata
|
||||||
|
assert merged['x-markitect-metadata']['status'] == 'draft'
|
||||||
|
|
||||||
|
# Source tracking should be added
|
||||||
|
assert merged['x-markitect-source']['file'] == 'test.md'
|
||||||
|
assert merged['x-markitect-source']['format'] == 'markdown'
|
||||||
|
|
||||||
|
def test_merge_preserves_schema_fields(self):
|
||||||
|
"""Test merging doesn't remove existing schema fields."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'title': 'Test',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {'name': {'type': 'string'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged = loader._merge_metadata(schema, {}, Path('test.md'))
|
||||||
|
|
||||||
|
assert merged['title'] == 'Test'
|
||||||
|
assert merged['type'] == 'object'
|
||||||
|
assert 'properties' in merged
|
||||||
|
|
||||||
|
def test_merge_frontmatter_takes_precedence(self):
|
||||||
|
"""Test frontmatter overrides schema values."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'version': '1.0.0',
|
||||||
|
'$id': 'old-id'
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'version': '2.0.0',
|
||||||
|
'schema-id': 'new-id'
|
||||||
|
}
|
||||||
|
|
||||||
|
merged = loader._merge_metadata(schema, metadata, Path('test.md'))
|
||||||
|
|
||||||
|
assert merged['version'] == '2.0.0'
|
||||||
|
assert merged['$id'] == 'new-id'
|
||||||
|
|
||||||
|
|
||||||
|
class TestSaveSchema:
|
||||||
|
"""Tests for saving schemas to markdown."""
|
||||||
|
|
||||||
|
def test_save_simple_schema(self, temp_schema_dir):
|
||||||
|
"""Test saving a schema to markdown file."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'$id': 'https://example.com/schema/v1',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'title': 'Test Schema',
|
||||||
|
'description': 'A test schema',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
output_file = temp_schema_dir / 'output-schema-v1.0.md'
|
||||||
|
loader.save_schema(schema, output_file)
|
||||||
|
|
||||||
|
assert output_file.exists()
|
||||||
|
|
||||||
|
# Verify content
|
||||||
|
content = output_file.read_text()
|
||||||
|
assert '---' in content # Frontmatter
|
||||||
|
assert 'Test Schema v1.0.0' in content # Title
|
||||||
|
assert '```json' in content # JSON block
|
||||||
|
assert '"title": "Test Schema"' in content
|
||||||
|
|
||||||
|
def test_save_creates_parent_directory(self, temp_schema_dir):
|
||||||
|
"""Test saving creates parent directories if needed."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {'title': 'Test', 'type': 'object'}
|
||||||
|
output_file = temp_schema_dir / 'nested' / 'dir' / 'schema.md'
|
||||||
|
|
||||||
|
loader.save_schema(schema, output_file)
|
||||||
|
|
||||||
|
assert output_file.exists()
|
||||||
|
assert output_file.parent.exists()
|
||||||
|
|
||||||
|
def test_save_with_custom_frontmatter(self, temp_schema_dir):
|
||||||
|
"""Test saving with custom frontmatter."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {'title': 'Test', 'type': 'object'}
|
||||||
|
frontmatter = {
|
||||||
|
'schema-id': 'https://custom.com/schema',
|
||||||
|
'status': 'experimental',
|
||||||
|
'tags': ['test', 'custom']
|
||||||
|
}
|
||||||
|
|
||||||
|
output_file = temp_schema_dir / 'custom.md'
|
||||||
|
loader.save_schema(schema, output_file, frontmatter=frontmatter)
|
||||||
|
|
||||||
|
content = output_file.read_text()
|
||||||
|
assert 'experimental' in content
|
||||||
|
assert 'https://custom.com/schema' in content
|
||||||
|
|
||||||
|
def test_save_and_reload_roundtrip(self, temp_schema_dir):
|
||||||
|
"""Test saving and reloading produces same schema."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
original_schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'title': 'Roundtrip Test',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'age': {'type': 'integer'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_file = temp_schema_dir / 'roundtrip-schema-v1.0.md'
|
||||||
|
loader.save_schema(original_schema, schema_file)
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
loaded = loader.load_schema(schema_file)
|
||||||
|
loaded_schema = loaded['schema']
|
||||||
|
|
||||||
|
# Compare key fields (ignoring x-markitect-source added during load)
|
||||||
|
assert loaded_schema['title'] == original_schema['title']
|
||||||
|
assert loaded_schema['type'] == original_schema['type']
|
||||||
|
assert loaded_schema['properties'] == original_schema['properties']
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateMarkdown:
|
||||||
|
"""Tests for markdown generation."""
|
||||||
|
|
||||||
|
def test_generate_basic_markdown(self):
|
||||||
|
"""Test generating basic markdown from schema."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'title': 'Test Schema',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'description': 'Test description',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
md = loader._generate_markdown(schema)
|
||||||
|
|
||||||
|
assert 'Test Schema v1.0.0' in md
|
||||||
|
assert 'Test description' in md
|
||||||
|
assert '```json' in md
|
||||||
|
assert '"title": "Test Schema"' in md
|
||||||
|
assert '---' in md # Frontmatter
|
||||||
|
|
||||||
|
def test_generate_includes_frontmatter(self):
|
||||||
|
"""Test generated markdown includes frontmatter."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'$id': 'https://example.com/schema',
|
||||||
|
'title': 'Test',
|
||||||
|
'version': '2.0.0',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
md = loader._generate_markdown(schema)
|
||||||
|
|
||||||
|
# Parse frontmatter
|
||||||
|
lines = md.split('\n')
|
||||||
|
assert lines[0] == '---'
|
||||||
|
|
||||||
|
# Find end of frontmatter
|
||||||
|
end_idx = lines[1:].index('---') + 1
|
||||||
|
|
||||||
|
frontmatter_yaml = '\n'.join(lines[1:end_idx])
|
||||||
|
frontmatter = yaml.safe_load(frontmatter_yaml)
|
||||||
|
|
||||||
|
assert frontmatter['version'] == '2.0.0'
|
||||||
|
assert frontmatter['schema-id'] == 'https://example.com/schema'
|
||||||
|
|
||||||
|
|
||||||
|
class TestListJsonBlocks:
|
||||||
|
"""Tests for listing JSON blocks."""
|
||||||
|
|
||||||
|
def test_list_single_block(self, schema_without_frontmatter):
|
||||||
|
"""Test listing single JSON block."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
blocks = loader.list_json_blocks(schema_without_frontmatter)
|
||||||
|
|
||||||
|
assert len(blocks) == 1
|
||||||
|
assert '"title": "Test Schema"' in blocks[0][1]
|
||||||
|
|
||||||
|
def test_list_multiple_blocks(self, schema_multiple_json_blocks):
|
||||||
|
"""Test listing multiple JSON blocks."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
blocks = loader.list_json_blocks(schema_multiple_json_blocks)
|
||||||
|
|
||||||
|
assert len(blocks) == 3
|
||||||
|
# First block
|
||||||
|
assert '"example"' in blocks[0][1]
|
||||||
|
# Second block (schema)
|
||||||
|
assert '"title": "Test Schema"' in blocks[1][1]
|
||||||
|
# Third block
|
||||||
|
assert '"another"' in blocks[2][1]
|
||||||
|
|
||||||
|
def test_list_no_blocks(self):
|
||||||
|
"""Test listing with no JSON blocks."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
blocks = loader.list_json_blocks("# Just text\n\nNo code blocks.")
|
||||||
|
|
||||||
|
assert len(blocks) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateSchemaStructure:
|
||||||
|
"""Tests for schema structure validation."""
|
||||||
|
|
||||||
|
def test_validate_complete_schema(self):
|
||||||
|
"""Test validating complete schema returns no issues."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'$id': 'https://example.com/schema',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'title': 'Test Schema',
|
||||||
|
'description': 'Test description',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
assert len(issues) == 0
|
||||||
|
|
||||||
|
def test_validate_missing_required_fields(self):
|
||||||
|
"""Test validation detects missing required fields."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {'type': 'object'}
|
||||||
|
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
|
||||||
|
assert len(issues) > 0
|
||||||
|
assert any('$schema' in issue for issue in issues)
|
||||||
|
assert any('title' in issue for issue in issues)
|
||||||
|
assert any('description' in issue for issue in issues)
|
||||||
|
|
||||||
|
def test_validate_missing_version(self):
|
||||||
|
"""Test validation detects missing version field."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'title': 'Test',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
|
||||||
|
assert any('version' in issue for issue in issues)
|
||||||
|
|
||||||
|
def test_validate_invalid_id_format(self):
|
||||||
|
"""Test validation detects non-HTTPS $id."""
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'$id': 'http://example.com/schema', # HTTP not HTTPS
|
||||||
|
'version': '1.0.0',
|
||||||
|
'title': 'Test',
|
||||||
|
'type': 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
|
||||||
|
assert any('HTTPS' in issue for issue in issues)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Tests for edge cases and error conditions."""
|
||||||
|
|
||||||
|
def test_load_empty_file(self, temp_schema_dir):
|
||||||
|
"""Test loading empty file raises error."""
|
||||||
|
schema_file = temp_schema_dir / 'empty.md'
|
||||||
|
schema_file.write_text('')
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(SchemaNotFoundError):
|
||||||
|
loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
def test_load_binary_file(self, temp_schema_dir):
|
||||||
|
"""Test loading binary file with invalid UTF-8 raises error."""
|
||||||
|
schema_file = temp_schema_dir / 'binary.md'
|
||||||
|
# Use invalid UTF-8 sequences that will trigger UnicodeDecodeError
|
||||||
|
schema_file.write_bytes(b'\xff\xfe\x00\x00\x80\x81\x82')
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSchemaFormatError):
|
||||||
|
loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
def test_malformed_code_block(self, temp_schema_dir):
|
||||||
|
"""Test handling malformed code block delimiters."""
|
||||||
|
content = """# Test
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"valid": "json"
|
||||||
|
# Missing closing backticks
|
||||||
|
"""
|
||||||
|
schema_file = temp_schema_dir / 'malformed.md'
|
||||||
|
schema_file.write_text(content)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
with pytest.raises(SchemaNotFoundError):
|
||||||
|
loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
def test_very_large_schema(self, temp_schema_dir):
|
||||||
|
"""Test loading very large schema."""
|
||||||
|
# Create large schema with many properties
|
||||||
|
large_schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-07/schema#',
|
||||||
|
'title': 'Large Schema',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
f'prop_{i}': {'type': 'string'}
|
||||||
|
for i in range(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = f"""# Large Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{json.dumps(large_schema, indent=2)}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
schema_file = temp_schema_dir / 'large.md'
|
||||||
|
schema_file.write_text(content)
|
||||||
|
|
||||||
|
loader = MarkdownSchemaLoader()
|
||||||
|
result = loader.load_schema(schema_file)
|
||||||
|
|
||||||
|
assert len(result['schema']['properties']) == 1000
|
||||||
300
tests/test_schema_metaschema.py
Normal file
300
tests/test_schema_metaschema.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for schema metaschema validation.
|
||||||
|
|
||||||
|
Tests that schemas validate correctly against the schema-for-schemas metaschema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
try:
|
||||||
|
from jsonschema import Draft7Validator
|
||||||
|
JSONSCHEMA_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
JSONSCHEMA_AVAILABLE = False
|
||||||
|
|
||||||
|
from markitect.schema_loader import MarkdownSchemaLoader
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def loader():
|
||||||
|
"""Create a schema loader instance."""
|
||||||
|
return MarkdownSchemaLoader()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def metaschema(loader):
|
||||||
|
"""Load the metaschema."""
|
||||||
|
metaschema_path = Path(__file__).parent.parent / 'markitect' / 'schemas' / 'schema-schema-v1.0.md'
|
||||||
|
metaschema_data = loader.load_schema(metaschema_path)
|
||||||
|
return metaschema_data['schema']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not JSONSCHEMA_AVAILABLE, reason="jsonschema not installed")
|
||||||
|
class TestMetaschemaValidation:
|
||||||
|
"""Tests for validating schemas against the metaschema."""
|
||||||
|
|
||||||
|
def test_metaschema_self_validation(self, loader, metaschema):
|
||||||
|
"""Test that metaschema validates itself."""
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(metaschema))
|
||||||
|
|
||||||
|
assert len(errors) == 0, f"Metaschema should validate itself, but got errors: {errors}"
|
||||||
|
|
||||||
|
def test_manpage_schema_validation(self, loader, metaschema):
|
||||||
|
"""Test that manpage schema validates against metaschema."""
|
||||||
|
manpage_path = Path(__file__).parent.parent / 'markitect' / 'schemas' / 'manpage-schema-v1.0.md'
|
||||||
|
manpage_data = loader.load_schema(manpage_path)
|
||||||
|
manpage_schema = manpage_data['schema']
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(manpage_schema))
|
||||||
|
|
||||||
|
assert len(errors) == 0, f"Manpage schema should be valid, but got errors: {[e.message for e in errors]}"
|
||||||
|
|
||||||
|
def test_required_fields_enforced(self, metaschema):
|
||||||
|
"""Test that metaschema enforces required fields."""
|
||||||
|
# Schema missing required fields
|
||||||
|
invalid_schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
# Missing: $id, title, description, version
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(invalid_schema))
|
||||||
|
|
||||||
|
# Should have errors for missing required fields
|
||||||
|
assert len(errors) > 0
|
||||||
|
error_messages = [e.message for e in errors]
|
||||||
|
|
||||||
|
# Check that required fields are mentioned
|
||||||
|
required_fields = ['$id', 'title', 'description', 'version']
|
||||||
|
for field in required_fields:
|
||||||
|
assert any(field in msg for msg in error_messages), \
|
||||||
|
f"Should report missing required field: {field}"
|
||||||
|
|
||||||
|
def test_version_format_validation(self, metaschema):
|
||||||
|
"""Test that metaschema validates version format."""
|
||||||
|
# Invalid version formats
|
||||||
|
invalid_versions = [
|
||||||
|
"1.0", # Missing patch
|
||||||
|
"v1.0.0", # Has v prefix
|
||||||
|
"1", # Only major
|
||||||
|
"1.0.0.0", # Too many parts
|
||||||
|
]
|
||||||
|
|
||||||
|
for invalid_version in invalid_versions:
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": invalid_version
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
assert len(errors) > 0, f"Should reject invalid version: {invalid_version}"
|
||||||
|
assert any('pattern' in str(e.schema_path) for e in errors), \
|
||||||
|
f"Should be a pattern error for version: {invalid_version}"
|
||||||
|
|
||||||
|
def test_valid_version_format(self, metaschema):
|
||||||
|
"""Test that valid version formats are accepted."""
|
||||||
|
valid_versions = [
|
||||||
|
"1.0.0",
|
||||||
|
"2.5.3",
|
||||||
|
"10.25.99",
|
||||||
|
"0.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
for valid_version in valid_versions:
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": valid_version
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
# Filter out errors not related to version
|
||||||
|
version_errors = [e for e in errors if 'version' in str(e.path)]
|
||||||
|
assert len(version_errors) == 0, \
|
||||||
|
f"Should accept valid version: {valid_version}, but got errors: {version_errors}"
|
||||||
|
|
||||||
|
def test_id_format_validation(self, metaschema):
|
||||||
|
"""Test that metaschema validates $id format."""
|
||||||
|
invalid_ids = [
|
||||||
|
"http://example.com/schema", # Not HTTPS
|
||||||
|
"https://example.com/schema", # No version
|
||||||
|
"schema/v1.0", # Not a URL
|
||||||
|
"https://example.com/schemas/test/1.0", # No 'v' prefix
|
||||||
|
]
|
||||||
|
|
||||||
|
for invalid_id in invalid_ids:
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": invalid_id,
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
assert len(errors) > 0, f"Should reject invalid $id: {invalid_id}"
|
||||||
|
|
||||||
|
def test_valid_id_format(self, metaschema):
|
||||||
|
"""Test that valid $id formats are accepted."""
|
||||||
|
valid_ids = [
|
||||||
|
"https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"https://example.com/schemas/my-schema/v2.5",
|
||||||
|
"https://api.example.com/schemas/domain/v10.25",
|
||||||
|
]
|
||||||
|
|
||||||
|
for valid_id in valid_ids:
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": valid_id,
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
# Filter out errors not related to $id
|
||||||
|
id_errors = [e for e in errors if '$id' in str(e.path)]
|
||||||
|
assert len(id_errors) == 0, \
|
||||||
|
f"Should accept valid $id: {valid_id}, but got errors: {id_errors}"
|
||||||
|
|
||||||
|
def test_section_classification_validation(self, metaschema):
|
||||||
|
"""Test that section classifications are validated."""
|
||||||
|
# Invalid classification
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "mandatory", # Invalid, should be 'required'
|
||||||
|
"heading_level": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
# Should have error about invalid enum value
|
||||||
|
assert any('enum' in str(e.schema_path) or 'mandatory' in e.message for e in errors)
|
||||||
|
|
||||||
|
def test_valid_section_classifications(self, metaschema):
|
||||||
|
"""Test that valid section classifications are accepted."""
|
||||||
|
classifications = ['required', 'recommended', 'optional', 'discouraged', 'improper']
|
||||||
|
|
||||||
|
for classification in classifications:
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"title": "Test Schema",
|
||||||
|
"description": "Test schema for validation",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"TEST_SECTION": {
|
||||||
|
"classification": classification,
|
||||||
|
"heading_level": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
# Filter errors related to classification
|
||||||
|
classification_errors = [e for e in errors if 'classification' in str(e.path)]
|
||||||
|
assert len(classification_errors) == 0, \
|
||||||
|
f"Should accept valid classification: {classification}"
|
||||||
|
|
||||||
|
def test_schema_with_all_extensions(self, metaschema):
|
||||||
|
"""Test schema with all MarkiTect extensions."""
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://markitect.dev/schemas/test/v1.0",
|
||||||
|
"title": "Complete Test Schema",
|
||||||
|
"description": "Schema with all MarkiTect extensions",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "object",
|
||||||
|
"x-markitect-sections": {
|
||||||
|
"SYNOPSIS": {
|
||||||
|
"classification": "required",
|
||||||
|
"heading_level": 2,
|
||||||
|
"content_instruction": "Brief overview",
|
||||||
|
"min_paragraphs": 1,
|
||||||
|
"max_paragraphs": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-content-control": {
|
||||||
|
"synopsis": {
|
||||||
|
"required_patterns": ["\\*\\*.*\\*\\*"],
|
||||||
|
"content_quality": {
|
||||||
|
"min_words": 10,
|
||||||
|
"max_words": 100,
|
||||||
|
"readability_target": "technical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-markitect-metadata": {
|
||||||
|
"status": "stable",
|
||||||
|
"authors": ["Test Author <test@example.com>"],
|
||||||
|
"tags": ["test", "example"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validator = Draft7Validator(metaschema)
|
||||||
|
errors = list(validator.iter_errors(schema))
|
||||||
|
|
||||||
|
assert len(errors) == 0, f"Complete schema should be valid, but got errors: {[e.message for e in errors]}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchemaLoaderIntegration:
|
||||||
|
"""Integration tests for schema loader with metaschema."""
|
||||||
|
|
||||||
|
def test_load_and_validate_manpage_schema(self, loader, metaschema):
|
||||||
|
"""Test loading and validating manpage schema."""
|
||||||
|
manpage_path = Path(__file__).parent.parent / 'markitect' / 'schemas' / 'manpage-schema-v1.0.md'
|
||||||
|
|
||||||
|
# Load schema
|
||||||
|
schema_data = loader.load_schema(manpage_path)
|
||||||
|
schema = schema_data['schema']
|
||||||
|
|
||||||
|
# Check metadata was merged
|
||||||
|
assert 'x-markitect-source' in schema
|
||||||
|
assert schema['x-markitect-source']['format'] == 'markdown'
|
||||||
|
assert schema['x-markitect-source']['filename'] == 'manpage-schema-v1.0.md'
|
||||||
|
|
||||||
|
# Validate structure
|
||||||
|
issues = loader.validate_schema_structure(schema)
|
||||||
|
# Should have no critical issues
|
||||||
|
assert all('Missing' not in issue or 'recommended' in issue.lower() for issue in issues)
|
||||||
|
|
||||||
|
def test_metaschema_structure_validation(self, loader):
|
||||||
|
"""Test metaschema structure with loader's validator."""
|
||||||
|
metaschema_path = Path(__file__).parent.parent / 'markitect' / 'schemas' / 'schema-schema-v1.0.md'
|
||||||
|
metaschema_data = loader.load_schema(metaschema_path)
|
||||||
|
metaschema = metaschema_data['schema']
|
||||||
|
|
||||||
|
# Validate structure
|
||||||
|
issues = loader.validate_schema_structure(metaschema)
|
||||||
|
|
||||||
|
# Metaschema should have minimal issues
|
||||||
|
critical_issues = [i for i in issues if 'Missing required field' in i]
|
||||||
|
assert len(critical_issues) == 0, f"Metaschema has critical issues: {critical_issues}"
|
||||||
390
tests/test_schema_naming.py
Normal file
390
tests/test_schema_naming.py
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for schema_naming.py - Schema filename validation.
|
||||||
|
|
||||||
|
Tests the schema naming convention enforcement including:
|
||||||
|
- Valid filename validation
|
||||||
|
- Invalid filename detection
|
||||||
|
- Metadata extraction
|
||||||
|
- Filename suggestion
|
||||||
|
- Error message generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from markitect.schema_naming import (
|
||||||
|
validate_schema_filename,
|
||||||
|
suggest_schema_filename,
|
||||||
|
extract_schema_metadata,
|
||||||
|
get_validation_errors,
|
||||||
|
is_valid_schema_filename,
|
||||||
|
format_validation_message,
|
||||||
|
SchemaFilenameError,
|
||||||
|
SCHEMA_FILENAME_PATTERN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateSchemaFilename:
|
||||||
|
"""Tests for validate_schema_filename function."""
|
||||||
|
|
||||||
|
def test_valid_simple_schema(self):
|
||||||
|
"""Test validation of simple valid schema filename."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is True
|
||||||
|
assert metadata is not None
|
||||||
|
assert metadata['domain'] == 'manpage'
|
||||||
|
assert metadata['version'] == '1.0'
|
||||||
|
assert metadata['major'] == 1
|
||||||
|
assert metadata['minor'] == 0
|
||||||
|
assert metadata['filename'] == 'manpage-schema-v1.0.md'
|
||||||
|
|
||||||
|
def test_valid_hyphenated_domain(self):
|
||||||
|
"""Test validation with multi-word hyphenated domain."""
|
||||||
|
is_valid, metadata = validate_schema_filename("api-documentation-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is True
|
||||||
|
assert metadata['domain'] == 'api-documentation'
|
||||||
|
assert metadata['version'] == '1.0'
|
||||||
|
|
||||||
|
def test_valid_with_numbers_in_domain(self):
|
||||||
|
"""Test validation with numbers in domain name."""
|
||||||
|
is_valid, metadata = validate_schema_filename("arc42-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is True
|
||||||
|
assert metadata['domain'] == 'arc42'
|
||||||
|
|
||||||
|
def test_valid_higher_version(self):
|
||||||
|
"""Test validation with version > 1.0."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v2.5.md")
|
||||||
|
|
||||||
|
assert is_valid is True
|
||||||
|
assert metadata['version'] == '2.5'
|
||||||
|
assert metadata['major'] == 2
|
||||||
|
assert metadata['minor'] == 5
|
||||||
|
|
||||||
|
def test_valid_double_digit_version(self):
|
||||||
|
"""Test validation with double-digit version numbers."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v10.25.md")
|
||||||
|
|
||||||
|
assert is_valid is True
|
||||||
|
assert metadata['major'] == 10
|
||||||
|
assert metadata['minor'] == 25
|
||||||
|
|
||||||
|
def test_invalid_wrong_extension(self):
|
||||||
|
"""Test that .json extension is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v1.0.json")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_no_extension(self):
|
||||||
|
"""Test that filename without extension is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v1.0")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_missing_schema_keyword(self):
|
||||||
|
"""Test that filename without 'schema' keyword is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_missing_version(self):
|
||||||
|
"""Test that filename without version is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_wrong_version_format(self):
|
||||||
|
"""Test that version without 'v' prefix is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_missing_minor_version(self):
|
||||||
|
"""Test that version without minor number is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("manpage-schema-v1.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_uppercase_letters(self):
|
||||||
|
"""Test that uppercase letters make filename invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("ManPage-Schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_starting_with_number(self):
|
||||||
|
"""Test that domain starting with number is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("42answers-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_starting_with_hyphen(self):
|
||||||
|
"""Test that domain starting with hyphen is invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("-manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
def test_invalid_special_characters(self):
|
||||||
|
"""Test that special characters in domain are invalid."""
|
||||||
|
is_valid, metadata = validate_schema_filename("man_page-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert is_valid is False
|
||||||
|
assert metadata is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuggestSchemaFilename:
|
||||||
|
"""Tests for suggest_schema_filename function."""
|
||||||
|
|
||||||
|
def test_suggest_simple_domain(self):
|
||||||
|
"""Test suggestion for simple domain."""
|
||||||
|
filename = suggest_schema_filename("manpage", "1.0")
|
||||||
|
assert filename == "manpage-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_with_spaces(self):
|
||||||
|
"""Test suggestion normalizes spaces to hyphens."""
|
||||||
|
filename = suggest_schema_filename("API Documentation", "1.0")
|
||||||
|
assert filename == "api-documentation-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_with_underscores(self):
|
||||||
|
"""Test suggestion normalizes underscores to hyphens."""
|
||||||
|
filename = suggest_schema_filename("my_custom_type", "1.0")
|
||||||
|
assert filename == "my-custom-type-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_with_uppercase(self):
|
||||||
|
"""Test suggestion converts to lowercase."""
|
||||||
|
filename = suggest_schema_filename("MyCustomType", "1.0")
|
||||||
|
assert filename == "mycustomtype-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_mixed_normalization(self):
|
||||||
|
"""Test suggestion with mixed case and separators."""
|
||||||
|
filename = suggest_schema_filename("My_Custom Type", "1.0")
|
||||||
|
assert filename == "my-custom-type-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_higher_version(self):
|
||||||
|
"""Test suggestion with version > 1.0."""
|
||||||
|
filename = suggest_schema_filename("manpage", "2.5")
|
||||||
|
assert filename == "manpage-schema-v2.5.md"
|
||||||
|
|
||||||
|
def test_suggest_double_digit_version(self):
|
||||||
|
"""Test suggestion with double-digit version."""
|
||||||
|
filename = suggest_schema_filename("manpage", "10.25")
|
||||||
|
assert filename == "manpage-schema-v10.25.md"
|
||||||
|
|
||||||
|
def test_suggest_consecutive_hyphens(self):
|
||||||
|
"""Test suggestion removes consecutive hyphens."""
|
||||||
|
filename = suggest_schema_filename("my--custom---type", "1.0")
|
||||||
|
assert filename == "my-custom-type-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_leading_trailing_hyphens(self):
|
||||||
|
"""Test suggestion removes leading/trailing hyphens."""
|
||||||
|
filename = suggest_schema_filename("-my-type-", "1.0")
|
||||||
|
assert filename == "my-type-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_default_version(self):
|
||||||
|
"""Test suggestion uses default version 1.0."""
|
||||||
|
filename = suggest_schema_filename("manpage")
|
||||||
|
assert filename == "manpage-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_empty_domain_raises_error(self):
|
||||||
|
"""Test that empty domain raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="Domain cannot be empty"):
|
||||||
|
suggest_schema_filename("", "1.0")
|
||||||
|
|
||||||
|
def test_suggest_invalid_version_format_raises_error(self):
|
||||||
|
"""Test that invalid version format raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="must be in format 'major.minor'"):
|
||||||
|
suggest_schema_filename("manpage", "1")
|
||||||
|
|
||||||
|
def test_suggest_invalid_version_parts_raises_error(self):
|
||||||
|
"""Test that non-integer version parts raise ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="major and minor must be integers"):
|
||||||
|
suggest_schema_filename("manpage", "1.x")
|
||||||
|
|
||||||
|
def test_suggest_negative_version_raises_error(self):
|
||||||
|
"""Test that negative version numbers raise ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="must be non-negative"):
|
||||||
|
suggest_schema_filename("manpage", "-1.0")
|
||||||
|
|
||||||
|
def test_suggest_without_normalization(self):
|
||||||
|
"""Test suggestion without normalization (must already be valid)."""
|
||||||
|
filename = suggest_schema_filename("manpage", "1.0", normalize=False)
|
||||||
|
assert filename == "manpage-schema-v1.0.md"
|
||||||
|
|
||||||
|
def test_suggest_without_normalization_invalid_raises_error(self):
|
||||||
|
"""Test that invalid domain without normalization raises ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="Invalid domain"):
|
||||||
|
suggest_schema_filename("My Custom Type", "1.0", normalize=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractSchemaMetadata:
|
||||||
|
"""Tests for extract_schema_metadata function."""
|
||||||
|
|
||||||
|
def test_extract_valid_metadata(self):
|
||||||
|
"""Test metadata extraction from valid filename."""
|
||||||
|
metadata = extract_schema_metadata("manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert metadata['domain'] == 'manpage'
|
||||||
|
assert metadata['version'] == '1.0'
|
||||||
|
assert metadata['major'] == 1
|
||||||
|
assert metadata['minor'] == 0
|
||||||
|
|
||||||
|
def test_extract_invalid_raises_error(self):
|
||||||
|
"""Test that invalid filename raises SchemaFilenameError."""
|
||||||
|
with pytest.raises(SchemaFilenameError, match="Invalid schema filename"):
|
||||||
|
extract_schema_metadata("invalid.json")
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetValidationErrors:
|
||||||
|
"""Tests for get_validation_errors function."""
|
||||||
|
|
||||||
|
def test_valid_filename_no_errors(self):
|
||||||
|
"""Test that valid filename returns empty error list."""
|
||||||
|
errors = get_validation_errors("manpage-schema-v1.0.md")
|
||||||
|
assert errors == []
|
||||||
|
|
||||||
|
def test_wrong_extension_error(self):
|
||||||
|
"""Test error for wrong file extension."""
|
||||||
|
errors = get_validation_errors("manpage-schema-v1.0.json")
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
assert any("Extension must be '.md'" in e for e in errors)
|
||||||
|
|
||||||
|
def test_missing_version_error(self):
|
||||||
|
"""Test error for missing version."""
|
||||||
|
errors = get_validation_errors("manpage-schema.md")
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
assert any("Missing version" in e for e in errors)
|
||||||
|
|
||||||
|
def test_missing_schema_keyword_error(self):
|
||||||
|
"""Test error for missing schema keyword."""
|
||||||
|
errors = get_validation_errors("manpage-v1.0.md")
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
assert any("Missing '-schema-'" in e for e in errors)
|
||||||
|
|
||||||
|
def test_uppercase_letters_error(self):
|
||||||
|
"""Test error for uppercase letters."""
|
||||||
|
errors = get_validation_errors("ManPage-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
assert any("must be lowercase" in e for e in errors)
|
||||||
|
|
||||||
|
def test_invalid_domain_error(self):
|
||||||
|
"""Test error for invalid domain format."""
|
||||||
|
errors = get_validation_errors("42answer-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert len(errors) > 0
|
||||||
|
# Should detect that domain doesn't start with letter
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsValidSchemaFilename:
|
||||||
|
"""Tests for is_valid_schema_filename convenience function."""
|
||||||
|
|
||||||
|
def test_is_valid_returns_true(self):
|
||||||
|
"""Test that valid filename returns True."""
|
||||||
|
assert is_valid_schema_filename("manpage-schema-v1.0.md") is True
|
||||||
|
|
||||||
|
def test_is_valid_returns_false(self):
|
||||||
|
"""Test that invalid filename returns False."""
|
||||||
|
assert is_valid_schema_filename("invalid.json") is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatValidationMessage:
|
||||||
|
"""Tests for format_validation_message function."""
|
||||||
|
|
||||||
|
def test_format_message_valid_filename(self):
|
||||||
|
"""Test formatting message for valid filename."""
|
||||||
|
message = format_validation_message("manpage-schema-v1.0.md")
|
||||||
|
|
||||||
|
assert "✅ Valid" in message
|
||||||
|
assert "manpage-schema-v1.0.md" in message
|
||||||
|
|
||||||
|
def test_format_message_invalid_filename(self):
|
||||||
|
"""Test formatting message for invalid filename."""
|
||||||
|
message = format_validation_message("invalid.json")
|
||||||
|
|
||||||
|
assert "❌ Invalid" in message
|
||||||
|
assert "Errors:" in message
|
||||||
|
assert "Expected format:" in message
|
||||||
|
assert "Example:" in message
|
||||||
|
|
||||||
|
def test_format_message_includes_suggestion(self):
|
||||||
|
"""Test that message includes filename suggestion."""
|
||||||
|
message = format_validation_message("manpage.json")
|
||||||
|
|
||||||
|
assert "Suggested filename:" in message
|
||||||
|
# Should suggest something like manpage-schema-v1.0.md
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchemaFilenamePattern:
|
||||||
|
"""Tests for the regex pattern itself."""
|
||||||
|
|
||||||
|
def test_pattern_matches_valid_filenames(self):
|
||||||
|
"""Test that pattern matches all valid filename variations."""
|
||||||
|
valid_filenames = [
|
||||||
|
"manpage-schema-v1.0.md",
|
||||||
|
"api-documentation-schema-v1.0.md",
|
||||||
|
"arc42-schema-v1.0.md",
|
||||||
|
"a-schema-v1.0.md", # Single letter domain
|
||||||
|
"my-long-domain-name-schema-v1.0.md",
|
||||||
|
"manpage-schema-v10.25.md", # Double digit versions
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename in valid_filenames:
|
||||||
|
match = SCHEMA_FILENAME_PATTERN.match(filename)
|
||||||
|
assert match is not None, f"Pattern should match {filename}"
|
||||||
|
|
||||||
|
def test_pattern_rejects_invalid_filenames(self):
|
||||||
|
"""Test that pattern rejects invalid filenames."""
|
||||||
|
invalid_filenames = [
|
||||||
|
"manpage-schema-v1.0.json", # Wrong extension
|
||||||
|
"manpage-v1.0.md", # Missing schema keyword
|
||||||
|
"manpage-schema.md", # Missing version
|
||||||
|
"ManPage-schema-v1.0.md", # Uppercase
|
||||||
|
"42answer-schema-v1.0.md", # Starts with number
|
||||||
|
"-manpage-schema-v1.0.md", # Starts with hyphen
|
||||||
|
"man_page-schema-v1.0.md", # Underscore in domain
|
||||||
|
"manpage-schema-1.0.md", # Missing 'v' prefix
|
||||||
|
"manpage-schema-v1.md", # Missing minor version
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename in invalid_filenames:
|
||||||
|
match = SCHEMA_FILENAME_PATTERN.match(filename)
|
||||||
|
assert match is None, f"Pattern should not match {filename}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Tests for edge cases and boundary conditions."""
|
||||||
|
|
||||||
|
def test_very_long_domain_name(self):
|
||||||
|
"""Test with very long domain name."""
|
||||||
|
long_domain = "a" * 100
|
||||||
|
filename = suggest_schema_filename(long_domain, "1.0")
|
||||||
|
assert is_valid_schema_filename(filename)
|
||||||
|
|
||||||
|
def test_domain_with_many_hyphens(self):
|
||||||
|
"""Test domain with multiple hyphens."""
|
||||||
|
filename = suggest_schema_filename("my-very-long-domain-name", "1.0")
|
||||||
|
assert filename == "my-very-long-domain-name-schema-v1.0.md"
|
||||||
|
assert is_valid_schema_filename(filename)
|
||||||
|
|
||||||
|
def test_version_zero_zero(self):
|
||||||
|
"""Test with version 0.0."""
|
||||||
|
filename = suggest_schema_filename("manpage", "0.0")
|
||||||
|
assert filename == "manpage-schema-v0.0.md"
|
||||||
|
assert is_valid_schema_filename(filename)
|
||||||
|
|
||||||
|
def test_large_version_numbers(self):
|
||||||
|
"""Test with large version numbers."""
|
||||||
|
filename = suggest_schema_filename("manpage", "999.999")
|
||||||
|
assert filename == "manpage-schema-v999.999.md"
|
||||||
|
assert is_valid_schema_filename(filename)
|
||||||
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
|
||||||
@@ -38,7 +38,7 @@ def create_test_workspace(prefix: str = "test") -> Path:
|
|||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def test_workspace(prefix: str = "test"):
|
def workspace_context(prefix: str = "test"):
|
||||||
"""Context manager for test workspace that auto-cleans up.
|
"""Context manager for test workspace that auto-cleans up.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
Reference in New Issue
Block a user