Compare commits
26 Commits
testdrive-
...
82c1a3ab65
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
url = http://92.205.130.254:32166/coulomb/markitect_project.wiki.git
|
||||
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"]
|
||||
path = capabilities/kaizen-agentic
|
||||
url = http://92.205.130.254:32166/coulomb/kaizen-agentic.git
|
||||
[submodule "capabilities/testdrive-jsui"]
|
||||
path = capabilities/testdrive-jsui
|
||||
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
|
||||
|
||||
86
TODO.md
86
TODO.md
@@ -12,10 +12,92 @@ 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.
|
||||
|
||||
*No active tasks at this time.*
|
||||
0. the file TODO.html is legacy i think and can be removed
|
||||
|
||||
### Extract Capability-Capability from Issue-Facade
|
||||
|
||||
**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
|
||||
|
||||
*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
|
||||
- ✅ Implemented schema-analyze command to detect rigidity issues
|
||||
- ✅ Implemented schema-refine command with automatic loosening logic
|
||||
- ✅ Added interactive mode to schema-refine for fine-grained control
|
||||
- ✅ Created comprehensive test suite (33 unit tests, 100% passing)
|
||||
- ✅ Wrote user guide documentation with examples and workflows
|
||||
- ✅ Successfully tested on example schemas (reduced rigidity from 60/100 to 24/100)
|
||||
- ✅ Integrated into CLI with proper exit codes and error handling
|
||||
|
||||
**Key Features Delivered:**
|
||||
- Rigidity score calculation (0-100 scale)
|
||||
- Automatic detection of exact counts, const values, overly specific numbers
|
||||
- Path navigation for nested schema properties
|
||||
- Dry-run mode for previewing changes
|
||||
- Interactive approval workflow
|
||||
- Comprehensive reporting (normal and verbose modes)
|
||||
|
||||
### 2025-12-17 - Architecture Refactoring
|
||||
- ✅ Implemented ReusableCapabilitiesArchitecture v0.1
|
||||
- ✅ Added feedback capability to issue-facade
|
||||
- ✅ 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.
|
||||
787
examples/manpages/SCHEMA_EVOLUTION_WORKPLAN.md
Normal file
787
examples/manpages/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
|
||||
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.
|
||||
@@ -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 = 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
|
||||
# This avoids conflicts with CSS curly braces
|
||||
html_template = template_content.replace('{title}', title)
|
||||
html_template = html_template.replace('{version}', version_str)
|
||||
html_template = html_template.replace('{content}', html_content)
|
||||
html_template = html_template.replace('{css_content}', css_content)
|
||||
|
||||
return html_template
|
||||
|
||||
@@ -1302,8 +1316,18 @@ MISSING: {len(missing_components)} components
|
||||
|
||||
template_content = template_path.read_text(encoding='utf-8')
|
||||
|
||||
# Generate CSS
|
||||
css_content = self._get_template_css(template, image_max_width, image_max_height) if not css else css
|
||||
# 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)
|
||||
|
||||
# Create configuration object - ONLY dynamic data interface
|
||||
config = {
|
||||
|
||||
@@ -1872,6 +1872,95 @@ def schema_delete(config, schema_name, confirm):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command('schema-analyze')
|
||||
@click.argument('schema_file', type=click.Path(exists=True))
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Show detailed analysis')
|
||||
@pass_config
|
||||
def schema_analyze_cmd(config, schema_file, verbose):
|
||||
"""
|
||||
Analyze a schema for rigidity issues and suggest improvements.
|
||||
|
||||
Examines JSON schemas to detect:
|
||||
- Exact counts that should be ranges
|
||||
- Missing classification system
|
||||
- Deprecated extensions
|
||||
- Overly specific constraints
|
||||
|
||||
Returns exit code 0 for flexible schemas, 1 for rigid schemas, 2 for errors.
|
||||
|
||||
Examples:
|
||||
markitect schema-analyze schema.json
|
||||
markitect schema-analyze schema.json --verbose
|
||||
"""
|
||||
from .schema_analyzer import analyze_schema_cli
|
||||
sys.exit(analyze_schema_cli(schema_file, verbose=verbose))
|
||||
|
||||
|
||||
@cli.command('schema-refine')
|
||||
@click.argument('schema_file', type=click.Path(exists=True))
|
||||
@click.option('--output', '-o', type=click.Path(),
|
||||
help='Output file (default: overwrite input file)')
|
||||
@click.option('--loosen-counts', is_flag=True, default=True,
|
||||
help='Convert exact counts to flexible ranges (default: enabled)')
|
||||
@click.option('--no-loosen-counts', is_flag=True,
|
||||
help='Disable count loosening')
|
||||
@click.option('--round-numbers', is_flag=True, default=True,
|
||||
help='Round overly specific numbers (default: enabled)')
|
||||
@click.option('--no-round-numbers', is_flag=True,
|
||||
help='Disable number rounding')
|
||||
@click.option('--migrate-deprecated', is_flag=True, default=False,
|
||||
help='Migrate deprecated extensions (requires manual review)')
|
||||
@click.option('--dry-run', is_flag=True,
|
||||
help='Show changes without applying them')
|
||||
@click.option('--interactive', '-i', is_flag=True,
|
||||
help='Prompt for each refinement interactively')
|
||||
@pass_config
|
||||
def schema_refine_cmd(config, schema_file, output, loosen_counts, no_loosen_counts,
|
||||
round_numbers, no_round_numbers, migrate_deprecated, dry_run, interactive):
|
||||
"""
|
||||
Refine a schema by automatically applying fixes for rigidity issues.
|
||||
|
||||
This command analyzes the schema and applies automatic fixes:
|
||||
- Converts exact counts to flexible ranges
|
||||
- Rounds overly specific numbers
|
||||
- Widens narrow integer constraints
|
||||
- Documents deprecated extension usage
|
||||
|
||||
By default, the input file is overwritten. Use --output to save to a different file.
|
||||
|
||||
Examples:
|
||||
# Refine schema in place
|
||||
markitect schema-refine schema.json
|
||||
|
||||
# Preview changes without applying
|
||||
markitect schema-refine schema.json --dry-run
|
||||
|
||||
# Review each fix interactively
|
||||
markitect schema-refine schema.json --interactive
|
||||
|
||||
# Save refined schema to new file
|
||||
markitect schema-refine schema.json --output refined-schema.json
|
||||
|
||||
# Disable specific refinements
|
||||
markitect schema-refine schema.json --no-loosen-counts
|
||||
"""
|
||||
from .schema_refiner import refine_schema_cli
|
||||
|
||||
# Handle flag conflicts
|
||||
loosen = loosen_counts and not no_loosen_counts
|
||||
round_nums = round_numbers and not no_round_numbers
|
||||
|
||||
sys.exit(refine_schema_cli(
|
||||
schema_file,
|
||||
output=output,
|
||||
loosen_counts=loosen,
|
||||
migrate_deprecated=migrate_deprecated,
|
||||
round_numbers=round_nums,
|
||||
dry_run=dry_run,
|
||||
interactive=interactive
|
||||
))
|
||||
|
||||
|
||||
@cli.command('generate-stub')
|
||||
@click.argument('schema_file', type=click.Path(exists=True, path_type=Path))
|
||||
@click.option('--output', '-o', type=click.Path(path_type=Path),
|
||||
|
||||
@@ -112,6 +112,8 @@ class MetaschemaValidator:
|
||||
"x-markitect-instruction-type": self._validate_instruction_type,
|
||||
"x-markitect-generation-mode": self._validate_generation_mode,
|
||||
"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
|
||||
@@ -193,4 +195,190 @@ class MetaschemaValidator:
|
||||
"x-markitect-generated-from must be a string",
|
||||
property_name
|
||||
)
|
||||
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
|
||||
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
|
||||
@@ -40,6 +40,163 @@
|
||||
"type": "string",
|
||||
"enum": ["outline", "full"],
|
||||
"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": {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<meta name="generator" content="Markitect {version}">
|
||||
<title>{title}</title>
|
||||
|
||||
{css_content}
|
||||
|
||||
<!-- Base styling for document content -->
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
try:
|
||||
from .test_utils import test_workspace
|
||||
from .test_utils import workspace_context
|
||||
except ImportError:
|
||||
# Fallback for missing test utilities
|
||||
import tempfile
|
||||
@@ -29,16 +29,13 @@ except ImportError:
|
||||
import shutil
|
||||
|
||||
@contextmanager
|
||||
def _test_workspace_fallback(name=None):
|
||||
def workspace_context(name=None):
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=f"{name}_" if name else "test_"))
|
||||
try:
|
||||
yield temp_dir
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
# Assign to expected name
|
||||
test_workspace = _test_workspace_fallback
|
||||
|
||||
|
||||
class TestProductionErrorHandler:
|
||||
"""Test production error handling and recovery capabilities."""
|
||||
@@ -46,7 +43,7 @@ class TestProductionErrorHandler:
|
||||
@pytest.fixture
|
||||
def temp_workspace(self):
|
||||
"""Create temporary workspace for testing."""
|
||||
with test_workspace("error_handler") as temp_dir:
|
||||
with workspace_context("error_handler") as temp_dir:
|
||||
yield temp_dir
|
||||
|
||||
@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
|
||||
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
|
||||
def test_workspace(prefix: str = "test"):
|
||||
def workspace_context(prefix: str = "test"):
|
||||
"""Context manager for test workspace that auto-cleans up.
|
||||
|
||||
Args:
|
||||
|
||||
Reference in New Issue
Block a user