Compare commits
4 Commits
65afc43d6b
...
818d8346ad
| Author | SHA1 | Date | |
|---|---|---|---|
| 818d8346ad | |||
| 9270a2e353 | |||
| 8e6ba272ca | |||
| 3231bd291a |
279
.clinerules
Normal file
279
.clinerules
Normal file
@@ -0,0 +1,279 @@
|
||||
# MarkiTect Project - Claude Code Rules
|
||||
# =====================================
|
||||
# Guidelines for Claude Code when working with the MarkiTect project
|
||||
# This project follows TDD8 methodology with Clean Architecture
|
||||
|
||||
## Project Overview
|
||||
This is a high-performance markdown processing engine with database integration,
|
||||
AST-based parsing, and sophisticated caching. The project follows Clean Architecture
|
||||
principles with strict separation of concerns.
|
||||
|
||||
## Directory Structure & Clean Architecture
|
||||
```
|
||||
markitect_project/
|
||||
├── domain/ # Business logic (innermost layer)
|
||||
├── application/ # Use cases and workflows
|
||||
├── infrastructure/ # External interfaces (database, file system)
|
||||
├── cli/ # Presentation layer (CLI interface)
|
||||
├── markitect/ # Core markdown processing engine
|
||||
├── tests/ # Comprehensive test suite (TDD8 methodology)
|
||||
├── docs/ # Architecture and user documentation
|
||||
└── tddai/ # TDD workflow tools and utilities
|
||||
```
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. TDD8 Methodology - ALWAYS FOLLOW
|
||||
1. **ISSUE**: Analyze GitHub issue and extract requirements
|
||||
2. **TEST**: Write comprehensive tests BEFORE implementation
|
||||
3. **RED**: Ensure tests fail initially (validate test correctness)
|
||||
4. **GREEN**: Implement minimum viable solution to pass tests
|
||||
5. **REFACTOR**: Improve code quality and design
|
||||
6. **DOCUMENT**: Update documentation and examples
|
||||
7. **REFINE**: Performance optimization and edge cases
|
||||
8. **PUBLISH**: Integration validation and delivery
|
||||
|
||||
### 2. Clean Architecture Dependency Rules
|
||||
- **NEVER violate dependency inversion**: Outer layers depend on inner layers, never reverse
|
||||
- **Domain layer**: Pure business logic, no external dependencies
|
||||
- **Application layer**: Use cases, may depend only on domain
|
||||
- **Infrastructure layer**: External concerns (database, CLI, API)
|
||||
- **Presentation layer**: User interfaces (CLI commands)
|
||||
|
||||
### 3. Testing Requirements
|
||||
- **Minimum 80% test coverage** - Use `pytest --cov=markitect --cov-report=html`
|
||||
- **Test naming**: `test_issue_{issue_num}_{scenario}.py` pattern
|
||||
- **Architectural testing**: Run tests by layer (`make test-domain`, `make test-infrastructure`)
|
||||
- **Performance validation**: All cache operations must be <50% of parsing time
|
||||
- **TDD workspace**: Use `.tddai_workspace/` for issue-specific development
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Starting Work on an Issue
|
||||
```bash
|
||||
# Always start with TDD workspace
|
||||
make tdd-start NUM=<issue_number>
|
||||
|
||||
# Analyze requirements first
|
||||
make validate-requirements
|
||||
|
||||
# Create tests before implementation
|
||||
make tdd-add-test
|
||||
```
|
||||
|
||||
### Code Quality Gates
|
||||
```bash
|
||||
# Run before any commit
|
||||
make test # All tests must pass
|
||||
make lint # Code style compliance
|
||||
make test-coverage NUM=X # Verify coverage targets
|
||||
make validate-mocks # Mock compatibility
|
||||
```
|
||||
|
||||
### Performance Requirements
|
||||
- **Cache operations**: <50% of initial parsing time (enforced by tests)
|
||||
- **Memory usage**: <50MB baseline for normal operations
|
||||
- **Database queries**: Sub-millisecond metadata retrieval
|
||||
- **Bulk operations**: Linear scaling with document count
|
||||
|
||||
## Technology Stack & Dependencies
|
||||
|
||||
### Core Technologies
|
||||
- **Python 3.8+** with type hints (gradual mypy adoption)
|
||||
- **SQLite** for database operations (ACID compliance required)
|
||||
- **markdown-it-py** for AST processing
|
||||
- **pytest** for testing with comprehensive fixtures
|
||||
- **Click** for CLI framework
|
||||
|
||||
### Key Libraries
|
||||
- `PyYAML` - Front matter processing
|
||||
- `jsonpath-ng` - AST querying
|
||||
- `tabulate` - Output formatting
|
||||
- `aiohttp` - Async HTTP operations
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Python Code Style
|
||||
- **Type hints**: Use where possible (gradual mypy adoption)
|
||||
- **Docstrings**: Required for all public methods
|
||||
- **Error handling**: Comprehensive exception handling and validation
|
||||
- **Security**: Never log secrets, validate all inputs, prevent SQL injection
|
||||
|
||||
### File Organization
|
||||
- **One concept per file**: Clear separation of responsibilities
|
||||
- **Interface segregation**: Clean interfaces between layers
|
||||
- **Plugin architecture**: Support modular extensions
|
||||
|
||||
### Database Operations
|
||||
- **Read-only queries**: Default to safe operations
|
||||
- **Transaction safety**: Use ACID compliance for batch operations
|
||||
- **Performance optimization**: Leverage SQLite capabilities
|
||||
- **Migration support**: Schema versioning and updates
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### CLI Command Structure
|
||||
```python
|
||||
@click.command()
|
||||
@click.option('--format', type=click.Choice(['table', 'json', 'yaml']))
|
||||
def command_name(format):
|
||||
"""Command description with clear purpose."""
|
||||
try:
|
||||
# Implementation with proper error handling
|
||||
pass
|
||||
except SpecificException as e:
|
||||
# Provide helpful error messages
|
||||
pass
|
||||
```
|
||||
|
||||
### Test Structure (TDD8 Pattern)
|
||||
```python
|
||||
class TestIssue{N}_{Description}:
|
||||
"""Test suite for issue #{N}: {description}"""
|
||||
|
||||
def test_{scenario}_success(self):
|
||||
"""Test successful operation scenario."""
|
||||
# Arrange
|
||||
# Act
|
||||
# Assert
|
||||
|
||||
def test_{scenario}_error_handling(self):
|
||||
"""Test error handling scenario."""
|
||||
# Test edge cases and error conditions
|
||||
```
|
||||
|
||||
### Domain Model Pattern
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class DomainEntity:
|
||||
"""Domain entity with business logic."""
|
||||
id: str
|
||||
name: str
|
||||
|
||||
def business_method(self) -> bool:
|
||||
"""Business logic belongs in domain layer."""
|
||||
return True
|
||||
```
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
### AST Caching System
|
||||
- **Cache validation**: Automatic timestamp-based invalidation
|
||||
- **Serialization**: Optimized JSON format for AST storage
|
||||
- **Memory management**: Careful resource cleanup
|
||||
- **Performance contracts**: <50% of parsing time (tested)
|
||||
|
||||
### Database Optimization
|
||||
- **Query optimization**: Use appropriate indexes
|
||||
- **Batch operations**: Minimize database round trips
|
||||
- **Connection management**: Proper connection lifecycle
|
||||
- **Read-only defaults**: Safety-first approach
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Input Validation
|
||||
- **SQL injection prevention**: Use parameterized queries
|
||||
- **Path traversal protection**: Validate file paths
|
||||
- **Command injection**: Sanitize shell command inputs
|
||||
- **YAML safety**: Safe loading of front matter
|
||||
|
||||
### Secrets Management
|
||||
- **Never log secrets**: Authentication tokens, passwords
|
||||
- **Environment variables**: Use for sensitive configuration
|
||||
- **Git repository**: Never commit credentials
|
||||
- **Error messages**: Don't expose sensitive information
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Code Documentation
|
||||
- **API documentation**: Clear method signatures and purposes
|
||||
- **Architecture decisions**: Document in docs/architecture/
|
||||
- **Usage examples**: Include practical examples
|
||||
- **Performance notes**: Document performance characteristics
|
||||
|
||||
### User Documentation
|
||||
- **CLI help**: Comprehensive command documentation
|
||||
- **Configuration**: Clear setup instructions
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
- **Performance**: Usage optimization guidelines
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Git Platform Integration
|
||||
- **Gitea API**: Primary integration for issue management
|
||||
- **GitHub compatibility**: Support multiple platforms
|
||||
- **Authentication**: Token-based with multiple sources
|
||||
- **Error handling**: Robust network failure handling
|
||||
|
||||
### Development Tools
|
||||
- **Makefile integration**: Standard development commands
|
||||
- **pytest integration**: Comprehensive test framework
|
||||
- **mypy integration**: Gradual type checking adoption
|
||||
- **CLI tools**: Complete command-line interface
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Architecture Violations
|
||||
- ❌ **Domain depending on infrastructure**: Never import database in domain
|
||||
- ❌ **Skipping tests**: Never implement without tests first (TDD8)
|
||||
- ❌ **Performance assumptions**: Always validate cache performance
|
||||
- ❌ **Direct database access**: Use repository pattern
|
||||
|
||||
### Security Issues
|
||||
- ❌ **SQL injection**: Always use parameterized queries
|
||||
- ❌ **Logging secrets**: Never log authentication tokens
|
||||
- ❌ **Unsafe YAML**: Use yaml.safe_load() not yaml.load()
|
||||
- ❌ **Path injection**: Validate and sanitize file paths
|
||||
|
||||
### Testing Issues
|
||||
- ❌ **Insufficient coverage**: Maintain >80% test coverage
|
||||
- ❌ **Missing edge cases**: Test error conditions thoroughly
|
||||
- ❌ **Test dependencies**: Tests must be independent
|
||||
- ❌ **Performance tests**: Validate cache performance contracts
|
||||
|
||||
## When Making Changes
|
||||
|
||||
### Before Implementation
|
||||
1. **Read the issue**: Understand requirements completely
|
||||
2. **TDD workspace**: Use `make tdd-start NUM=X`
|
||||
3. **Write tests first**: Follow TDD8 methodology strictly
|
||||
4. **Validate architecture**: Ensure clean dependency flow
|
||||
|
||||
### During Implementation
|
||||
1. **Red-Green-Refactor**: Follow TDD cycle religiously
|
||||
2. **Performance validation**: Test cache performance contracts
|
||||
3. **Security review**: Validate input handling and safety
|
||||
4. **Documentation updates**: Keep docs current with changes
|
||||
|
||||
### Before Completion
|
||||
1. **Full test suite**: `make test` must pass completely
|
||||
2. **Performance benchmarks**: Validate performance requirements
|
||||
3. **Code quality**: `make lint` and type checking
|
||||
4. **Integration tests**: Verify end-to-end functionality
|
||||
|
||||
## Emergency Procedures
|
||||
|
||||
### If Tests Fail
|
||||
1. **Don't ignore**: Never commit with failing tests
|
||||
2. **Isolate issue**: Use `make test-module MODULE=name`
|
||||
3. **Check dependencies**: Verify layer boundary violations
|
||||
4. **Performance regression**: Check cache performance contracts
|
||||
|
||||
### If Performance Degrades
|
||||
1. **Run benchmarks**: Use performance test suite
|
||||
2. **Cache validation**: Verify cache hit rates and timing
|
||||
3. **Memory profiling**: Check for memory leaks
|
||||
4. **Database optimization**: Review query performance
|
||||
|
||||
### If Security Issues Found
|
||||
1. **Immediate assessment**: Evaluate impact and scope
|
||||
2. **Input validation**: Review all user input handling
|
||||
3. **Secrets audit**: Check for credential exposure
|
||||
4. **Dependency updates**: Update vulnerable dependencies
|
||||
|
||||
Remember: This project's success depends on maintaining architectural discipline,
|
||||
comprehensive testing, and performance contracts. When in doubt, ask for clarification
|
||||
and always prioritize correctness over speed of implementation.
|
||||
63
CHANGELOG.md
Normal file
63
CHANGELOG.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to MarkiTect will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Comprehensive installer system with Python and shell scripts
|
||||
- Version and release information commands (`markitect version`, `markitect release`)
|
||||
- Global `--version` flag for quick version checking
|
||||
- Git integration for version metadata (commit, branch, tag information)
|
||||
- Multiple output formats for release information (text, JSON, YAML)
|
||||
- Installation documentation and troubleshooting guides
|
||||
|
||||
### Fixed
|
||||
- All test failures resolved (800/800 tests passing)
|
||||
- Visualization schema tests updated for correct tool paths
|
||||
- Cache management test isolation issues
|
||||
- Missing dependencies documentation and installation
|
||||
|
||||
### Documentation
|
||||
- Added comprehensive INSTALL.md with installation instructions
|
||||
- Added DEPENDENCIES.md with dependency information
|
||||
- Created release process documentation
|
||||
|
||||
## [0.1.0] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- Initial MarkiTect implementation
|
||||
- Core markdown processing with AST caching
|
||||
- Front matter and content matter support
|
||||
- Database integration for document metadata
|
||||
- CLI interface with comprehensive commands
|
||||
- Schema generation and validation
|
||||
- Template rendering system
|
||||
- Issue management integration
|
||||
- TDD workflow tools (TDDAI)
|
||||
- Comprehensive test suite with architectural layers
|
||||
- Documentation and architectural guides
|
||||
|
||||
### Features
|
||||
- Document ingestion and processing
|
||||
- Metadata extraction and querying
|
||||
- AST analysis and caching
|
||||
- Content statistics and analysis
|
||||
- Template-based document generation
|
||||
- Associated file management
|
||||
- Database operations with multiple output formats
|
||||
- Performance monitoring and optimization
|
||||
- Legacy compatibility system
|
||||
|
||||
### Technical
|
||||
- Python 3.8+ support
|
||||
- Click-based CLI framework
|
||||
- SQLite database backend
|
||||
- Markdown-it-py parser integration
|
||||
- Comprehensive test coverage
|
||||
- Type checking with mypy
|
||||
- Code formatting with black
|
||||
- Project structure following clean architecture principles
|
||||
92
DEPENDENCIES.md
Normal file
92
DEPENDENCIES.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# MarkiTect Project Dependencies
|
||||
|
||||
## Overview
|
||||
This document lists all project dependencies for the MarkiTect project.
|
||||
|
||||
## Production Dependencies
|
||||
These are required for running the application:
|
||||
|
||||
- **markdown-it-py** - Markdown parsing library
|
||||
- **PyYAML** - YAML file processing
|
||||
- **click>=8.0.0** - Command-line interface framework
|
||||
- **tabulate>=0.9.0** - Table formatting for output
|
||||
- **jsonpath-ng>=1.5.0** - JSONPath query support
|
||||
- **aiohttp>=3.8.0** - Async HTTP client/server
|
||||
- **toml** - TOML file parsing (for frontmatter support)
|
||||
|
||||
## Development Dependencies
|
||||
These are required for development, testing, and code quality:
|
||||
|
||||
- **pytest** - Testing framework
|
||||
- **pytest-cov** - Test coverage reporting
|
||||
- **black** - Code formatting
|
||||
- **flake8** - Code linting
|
||||
- **mypy** - Type checking
|
||||
|
||||
## Test Dependencies
|
||||
Additional dependencies for testing (from tests/requirements-test.txt if present):
|
||||
- See `tests/requirements-test.txt` for any additional test-specific dependencies
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Setup
|
||||
```bash
|
||||
# Install production dependencies only
|
||||
pip install -e .
|
||||
|
||||
# Install with development dependencies
|
||||
make dev
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
```bash
|
||||
# Production dependencies
|
||||
pip install markdown-it-py PyYAML click>=8.0.0 tabulate>=0.9.0 jsonpath-ng>=1.5.0 aiohttp>=3.8.0 toml
|
||||
|
||||
# Development dependencies
|
||||
pip install pytest pytest-cov black flake8 mypy
|
||||
```
|
||||
|
||||
### Virtual Environment Setup
|
||||
```bash
|
||||
# Create and activate virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
make dev
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
After installing dependencies:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run tests with coverage
|
||||
pytest --cov
|
||||
|
||||
# Run specific test layers
|
||||
make test-foundation
|
||||
make test-infrastructure
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Code Quality Tools
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
make format
|
||||
|
||||
# Run linting
|
||||
make lint
|
||||
|
||||
# Type checking
|
||||
mypy markitect/
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Python 3.8+ is required
|
||||
- Virtual environment (.venv) is recommended
|
||||
- All dependencies are managed through pyproject.toml
|
||||
219
INSTALL.md
Normal file
219
INSTALL.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# MarkiTect Installation Guide
|
||||
|
||||
This document describes how to install MarkiTect and make it available system-wide.
|
||||
|
||||
## Quick Installation
|
||||
|
||||
For most users, the quick installer is the easiest option:
|
||||
|
||||
```bash
|
||||
# Install for current user
|
||||
./install.sh
|
||||
|
||||
# Install system-wide (requires sudo)
|
||||
./install.sh --system
|
||||
|
||||
# Install in development mode with test dependencies
|
||||
./install.sh --dev
|
||||
```
|
||||
|
||||
## Advanced Installation
|
||||
|
||||
For more control over the installation process, use the Python installer:
|
||||
|
||||
```bash
|
||||
# Install with custom prefix
|
||||
python install.py --prefix /opt/markitect
|
||||
|
||||
# Install with custom virtual environment location
|
||||
python install.py --venv-dir /path/to/custom/venv
|
||||
|
||||
# Install without creating symbolic links (manual PATH setup)
|
||||
python install.py --no-symlinks
|
||||
|
||||
# Force reinstallation over existing installation
|
||||
python install.py --force
|
||||
```
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Installation Types
|
||||
|
||||
- **User Installation** (default): Installs to `~/.local/`
|
||||
- **System Installation** (`--system`): Installs to `/usr/local/` (requires sudo)
|
||||
- **Development Installation** (`--dev`): Installs in editable mode with test dependencies
|
||||
|
||||
### Installation Paths
|
||||
|
||||
By default, MarkiTect is installed to:
|
||||
|
||||
- **User installation**: `~/.local/lib/markitect/` (virtual environment)
|
||||
- **System installation**: `/usr/local/lib/markitect/` (virtual environment)
|
||||
- **Binaries**: `~/.local/bin/` or `/usr/local/bin/`
|
||||
|
||||
### Available Commands
|
||||
|
||||
After installation, these commands will be available:
|
||||
|
||||
- `markitect` - Main MarkiTect CLI
|
||||
- `tddai` - TDD workflow management
|
||||
- `issue` - Issue management
|
||||
|
||||
## Checking Installation
|
||||
|
||||
Check if MarkiTect is already installed:
|
||||
|
||||
```bash
|
||||
./install.sh --check
|
||||
# or
|
||||
python install.py --check
|
||||
```
|
||||
|
||||
Check version after installation:
|
||||
|
||||
```bash
|
||||
markitect version
|
||||
markitect version --short
|
||||
markitect release
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To remove MarkiTect:
|
||||
|
||||
```bash
|
||||
./install.sh --uninstall
|
||||
# or
|
||||
python install.py --uninstall
|
||||
```
|
||||
|
||||
## Manual Installation
|
||||
|
||||
If you prefer to install manually:
|
||||
|
||||
1. **Create virtual environment:**
|
||||
```bash
|
||||
python -m venv ~/.local/lib/markitect
|
||||
```
|
||||
|
||||
2. **Activate virtual environment:**
|
||||
```bash
|
||||
source ~/.local/lib/markitect/bin/activate
|
||||
```
|
||||
|
||||
3. **Install MarkiTect:**
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
4. **Create symbolic links:**
|
||||
```bash
|
||||
mkdir -p ~/.local/bin
|
||||
ln -sf ~/.local/lib/markitect/bin/markitect ~/.local/bin/markitect
|
||||
ln -sf ~/.local/lib/markitect/bin/tddai ~/.local/bin/tddai
|
||||
ln -sf ~/.local/lib/markitect/bin/issue ~/.local/bin/issue
|
||||
```
|
||||
|
||||
5. **Add to PATH** (add to `~/.bashrc` or `~/.zshrc`):
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
## Development Installation
|
||||
|
||||
For development work:
|
||||
|
||||
```bash
|
||||
# Install in development mode
|
||||
./install.sh --dev
|
||||
|
||||
# This includes:
|
||||
# - Editable installation (changes reflect immediately)
|
||||
# - Test dependencies (pytest, black, flake8, mypy)
|
||||
# - All development tools
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Command not found after installation:**
|
||||
- Make sure `~/.local/bin` is in your PATH
|
||||
- Run: `export PATH="$HOME/.local/bin:$PATH"`
|
||||
- Add the export to your shell profile
|
||||
|
||||
2. **Permission denied on system installation:**
|
||||
- Use `sudo ./install.sh --system`
|
||||
- Or install to user directory instead
|
||||
|
||||
3. **Python version error:**
|
||||
- MarkiTect requires Python 3.8 or higher
|
||||
- Check version: `python3 --version`
|
||||
|
||||
4. **Installation already exists:**
|
||||
- Use `--force` to overwrite: `./install.sh --force`
|
||||
- Or uninstall first: `./install.sh --uninstall`
|
||||
|
||||
### Manual PATH Setup
|
||||
|
||||
If symbolic links don't work, add the virtual environment bin directory to your PATH:
|
||||
|
||||
```bash
|
||||
# For bash/zsh (add to ~/.bashrc or ~/.zshrc)
|
||||
export PATH="$HOME/.local/lib/markitect/bin:$PATH"
|
||||
|
||||
# For fish (add to ~/.config/fish/config.fish)
|
||||
set -gx PATH $HOME/.local/lib/markitect/bin $PATH
|
||||
```
|
||||
|
||||
### Testing Installation
|
||||
|
||||
After installation, verify everything works:
|
||||
|
||||
```bash
|
||||
# Test basic functionality
|
||||
markitect --help
|
||||
markitect version
|
||||
|
||||
# Test TDD tools
|
||||
tddai --help
|
||||
|
||||
# Test issue management
|
||||
issue --help
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
MarkiTect automatically installs these dependencies:
|
||||
|
||||
### Production Dependencies
|
||||
- markdown-it-py - Markdown parsing
|
||||
- PyYAML - YAML processing
|
||||
- click>=8.0.0 - CLI framework
|
||||
- tabulate>=0.9.0 - Table formatting
|
||||
- jsonpath-ng>=1.5.0 - JSONPath queries
|
||||
- aiohttp>=3.8.0 - Async HTTP client
|
||||
- toml - TOML file parsing
|
||||
|
||||
### Development Dependencies (with --dev)
|
||||
- pytest - Testing framework
|
||||
- pytest-cov - Test coverage
|
||||
- black - Code formatting
|
||||
- flake8 - Code linting
|
||||
- mypy - Type checking
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Python 3.8 or higher
|
||||
- pip (Python package installer)
|
||||
- git (optional, for version info)
|
||||
- Unix-like system (Linux, macOS) or Windows with Python support
|
||||
|
||||
## Support
|
||||
|
||||
For installation issues:
|
||||
|
||||
1. Check this guide first
|
||||
2. Run `./install.sh --check` to diagnose problems
|
||||
3. See the main project documentation
|
||||
4. Report issues on the project issue tracker
|
||||
74
Makefile
74
Makefile
@@ -1,7 +1,7 @@
|
||||
# MarkiTect - Advanced Markdown Engine
|
||||
# Makefile for common development tasks
|
||||
|
||||
.PHONY: help setup install test build clean update status dev lint format check-deps venv-status update-digest add-diary-entry list-issues show-issue list-open-issues close-issue close-issue-enhanced close-issues-batch test-from-issue tdd-start tdd-add-test tdd-finish tdd-status test-status test-new test-coverage test-arch test-foundation test-infrastructure test-integration test-domain test-service test-application test-presentation test-quick test-layers test-random test-random-seed test-random-repeat test-install-randomly test-clean test-tdd test-changed test-module test-cache-clean test-efficient cli-help
|
||||
.PHONY: help setup install test build clean update status dev lint format check-deps venv-status update-digest add-diary-entry list-issues show-issue list-open-issues close-issue close-issue-enhanced close-issues-batch test-from-issue tdd-start tdd-add-test tdd-finish tdd-status test-status test-new test-coverage test-arch test-foundation test-infrastructure test-integration test-domain test-service test-application test-presentation test-quick test-layers test-random test-random-seed test-random-repeat test-install-randomly test-clean test-tdd test-changed test-module test-cache-clean test-efficient cli-help release-status release-validate release-prepare release-build release-publish release-dry-run chaos-validate chaos-matrix chaos-inject chaos-report
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -26,6 +26,20 @@ help:
|
||||
@echo " lint - Run code linting"
|
||||
@echo " format - Format code"
|
||||
@echo ""
|
||||
@echo "Release Management:"
|
||||
@echo " release-status - Show current release status"
|
||||
@echo " release-validate - Validate repository for release"
|
||||
@echo " release-prepare VERSION=x.y.z - Prepare new release"
|
||||
@echo " release-build - Build release packages"
|
||||
@echo " release-publish VERSION=x.y.z - Publish complete release"
|
||||
@echo " release-dry-run VERSION=x.y.z - Test release preparation"
|
||||
@echo ""
|
||||
@echo "Chaos Engineering:"
|
||||
@echo " chaos-validate - Run architectural independence validation"
|
||||
@echo " chaos-matrix - Show dependency matrix"
|
||||
@echo " chaos-inject LAYER=X TYPE=Y - Inject chaos into specific layer"
|
||||
@echo " chaos-report - Generate chaos engineering report"
|
||||
@echo ""
|
||||
@echo "Architectural Testing:"
|
||||
@echo " test-arch - Run all tests in architectural order"
|
||||
@echo " test-foundation - Run foundation layer tests only"
|
||||
@@ -200,6 +214,64 @@ build: $(VENV)/bin/activate
|
||||
$(VENV_PYTHON) -m build 2>/dev/null || \
|
||||
$(VENV_PIP) install build && $(VENV_PYTHON) -m build
|
||||
|
||||
# Release management
|
||||
release-status:
|
||||
@echo "🔍 Checking release status..."
|
||||
$(VENV_PYTHON) release.py status
|
||||
|
||||
release-validate:
|
||||
@echo "✅ Validating release readiness..."
|
||||
$(VENV_PYTHON) release.py validate
|
||||
|
||||
release-prepare:
|
||||
@echo "🚀 Preparing release..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-prepare VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py prepare --version $(VERSION)
|
||||
|
||||
release-build:
|
||||
@echo "📦 Building release packages..."
|
||||
$(VENV_PYTHON) release.py build $(if $(VERSION),--version $(VERSION))
|
||||
|
||||
release-publish:
|
||||
@echo "📢 Publishing release..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-publish VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py publish --version $(VERSION)
|
||||
|
||||
release-dry-run:
|
||||
@echo "🧪 Dry run release preparation..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-dry-run VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py prepare --version $(VERSION) --dry-run
|
||||
|
||||
# Chaos Engineering targets
|
||||
chaos-validate:
|
||||
@echo "🔥 Running architectural independence validation..."
|
||||
$(VENV_PYTHON) chaos_test_runner.py validate-independence
|
||||
|
||||
chaos-matrix:
|
||||
@echo "🏗️ Showing architectural dependency matrix..."
|
||||
$(VENV_PYTHON) chaos_test_runner.py dependency-matrix
|
||||
|
||||
chaos-inject:
|
||||
@echo "💥 Injecting chaos into layer..."
|
||||
@if [ -z "$(LAYER)" ]; then \
|
||||
echo "❌ Usage: make chaos-inject LAYER=L1_Presentation TYPE=import_failure"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) chaos_test_runner.py inject-layer-failure --layer $(LAYER) $(if $(TYPE),--injection-type $(TYPE))
|
||||
|
||||
chaos-report:
|
||||
@echo "📄 Generating chaos engineering report..."
|
||||
$(VENV_PYTHON) chaos_test_runner.py chaos-report
|
||||
|
||||
# Code linting
|
||||
lint: $(VENV)/bin/activate
|
||||
@echo "🔍 Running linting..."
|
||||
|
||||
332
RELEASE.md
Normal file
332
RELEASE.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# MarkiTect Release Process
|
||||
|
||||
This document describes the release process for MarkiTect, including versioning strategy, automation tools, and distribution guidelines.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The simplest way to create a release:
|
||||
|
||||
```bash
|
||||
# 1. Prepare the release
|
||||
make release-prepare VERSION=1.0.0
|
||||
|
||||
# 2. Review and commit changes
|
||||
git add -A && git commit -m "Prepare release 1.0.0"
|
||||
|
||||
# 3. Publish the release
|
||||
make release-publish VERSION=1.0.0
|
||||
```
|
||||
|
||||
## Release Commands
|
||||
|
||||
### Status and Validation
|
||||
|
||||
```bash
|
||||
# Check current release status
|
||||
make release-status
|
||||
|
||||
# Validate repository for release
|
||||
make release-validate
|
||||
```
|
||||
|
||||
### Release Preparation
|
||||
|
||||
```bash
|
||||
# Prepare a new release (updates version, changelog)
|
||||
make release-prepare VERSION=x.y.z
|
||||
|
||||
# Test preparation without making changes
|
||||
make release-dry-run VERSION=x.y.z
|
||||
```
|
||||
|
||||
### Building and Publishing
|
||||
|
||||
```bash
|
||||
# Build release packages only
|
||||
make release-build [VERSION=x.y.z]
|
||||
|
||||
# Complete release (build + tag + publish)
|
||||
make release-publish VERSION=x.y.z
|
||||
```
|
||||
|
||||
## Versioning Strategy
|
||||
|
||||
MarkiTect follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR.MINOR.PATCH** (e.g., 1.2.3)
|
||||
- **Pre-release**: MAJOR.MINOR.PATCH-{alpha|beta|rc}.N (e.g., 1.2.3-beta.1)
|
||||
|
||||
### Version Types
|
||||
|
||||
- **Major (X.0.0)**: Breaking changes, incompatible API changes
|
||||
- **Minor (x.Y.0)**: New features, backward compatible
|
||||
- **Patch (x.y.Z)**: Bug fixes, backward compatible
|
||||
- **Pre-release**: Alpha, beta, or release candidate versions
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Major release
|
||||
make release-prepare VERSION=2.0.0
|
||||
|
||||
# Minor release
|
||||
make release-prepare VERSION=1.1.0
|
||||
|
||||
# Patch release
|
||||
make release-prepare VERSION=1.0.1
|
||||
|
||||
# Pre-release
|
||||
make release-prepare VERSION=1.1.0-beta.1
|
||||
```
|
||||
|
||||
## Release Validation
|
||||
|
||||
Before a release can be created, the following validations are performed:
|
||||
|
||||
### Required Conditions
|
||||
|
||||
1. **Clean Repository**: No uncommitted changes
|
||||
2. **Main Branch**: Must be on the `main` branch
|
||||
3. **Passing Tests**: All tests must pass
|
||||
4. **Valid Version**: Version must follow semantic versioning
|
||||
5. **Version Increment**: New version must be greater than current
|
||||
|
||||
### Override Validation
|
||||
|
||||
Use `--force` to override validation warnings:
|
||||
|
||||
```bash
|
||||
python release.py prepare --version 1.0.1 --force
|
||||
```
|
||||
|
||||
## Automated Release Process
|
||||
|
||||
### What `release-prepare` Does
|
||||
|
||||
1. **Version Update**: Updates `pyproject.toml` and `markitect/__version__.py`
|
||||
2. **Changelog Generation**: Creates/updates `CHANGELOG.md` from git commits
|
||||
3. **Validation**: Ensures repository is ready for release
|
||||
|
||||
### What `release-publish` Does
|
||||
|
||||
1. **Package Building**: Creates source distribution and wheel
|
||||
2. **Git Tagging**: Creates annotated git tag (e.g., `v1.0.0`)
|
||||
3. **Tag Push**: Pushes tag to remote repository
|
||||
|
||||
## Manual Release Process
|
||||
|
||||
If you prefer manual control:
|
||||
|
||||
### 1. Update Version
|
||||
|
||||
```bash
|
||||
# Edit pyproject.toml
|
||||
version = "1.0.0"
|
||||
|
||||
# Edit markitect/__version__.py
|
||||
__version__ = "1.0.0"
|
||||
```
|
||||
|
||||
### 2. Update Changelog
|
||||
|
||||
Edit `CHANGELOG.md` to add release notes for the new version.
|
||||
|
||||
### 3. Commit Changes
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Prepare release 1.0.0"
|
||||
```
|
||||
|
||||
### 4. Build Packages
|
||||
|
||||
```bash
|
||||
make release-build
|
||||
```
|
||||
|
||||
### 5. Create Git Tag
|
||||
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release 1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
### Package Types
|
||||
|
||||
MarkiTect releases include:
|
||||
|
||||
- **Source Distribution** (`.tar.gz`): Full source code package
|
||||
- **Wheel** (`.whl`): Pre-built binary package for faster installation
|
||||
|
||||
### Installation Methods
|
||||
|
||||
Users can install MarkiTect in several ways:
|
||||
|
||||
```bash
|
||||
# From PyPI (when published)
|
||||
pip install markitect
|
||||
|
||||
# From wheel file
|
||||
pip install markitect-1.0.0-py3-none-any.whl
|
||||
|
||||
# From source
|
||||
pip install markitect-1.0.0.tar.gz
|
||||
|
||||
# Development installation
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Release Artifacts
|
||||
|
||||
Each release creates:
|
||||
|
||||
- Source and wheel packages in `dist/`
|
||||
- Git tag (e.g., `v1.0.0`)
|
||||
- Updated `CHANGELOG.md`
|
||||
- Updated version files
|
||||
|
||||
## Changelog Format
|
||||
|
||||
The automated changelog generation categorizes commits:
|
||||
|
||||
### Commit Prefixes
|
||||
|
||||
- `feat:` or `feature:` → **Added** section
|
||||
- `fix:` or `bugfix:` → **Fixed** section
|
||||
- `docs:` or `doc:` → **Documentation** section
|
||||
- Other commits → **Other** section
|
||||
|
||||
### Example Changelog Entry
|
||||
|
||||
```markdown
|
||||
## [1.0.0] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- feat: add template rendering system
|
||||
- feature: implement cache management commands
|
||||
|
||||
### Fixed
|
||||
- fix: resolve test isolation issues
|
||||
- bugfix: correct version information display
|
||||
|
||||
### Documentation
|
||||
- docs: add comprehensive installation guide
|
||||
- doc: update API documentation
|
||||
|
||||
### Other
|
||||
- chore: cleanup repository structure
|
||||
- refactor: improve code organization
|
||||
```
|
||||
|
||||
## Release Checklist
|
||||
|
||||
### Pre-Release
|
||||
|
||||
- [ ] All tests passing (`make test`)
|
||||
- [ ] No uncommitted changes
|
||||
- [ ] On `main` branch
|
||||
- [ ] Version number decided
|
||||
- [ ] Release notes ready
|
||||
|
||||
### Release Process
|
||||
|
||||
- [ ] Run `make release-prepare VERSION=x.y.z`
|
||||
- [ ] Review generated changelog
|
||||
- [ ] Commit changes
|
||||
- [ ] Run `make release-publish VERSION=x.y.z`
|
||||
- [ ] Verify packages created
|
||||
- [ ] Verify git tag created
|
||||
|
||||
### Post-Release
|
||||
|
||||
- [ ] Packages available in `dist/`
|
||||
- [ ] Git tag pushed to remote
|
||||
- [ ] Changelog updated
|
||||
- [ ] Version information correct
|
||||
- [ ] Installation tested
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Validation Failures**
|
||||
```bash
|
||||
# Check what's wrong
|
||||
make release-validate
|
||||
|
||||
# Force release if needed
|
||||
python release.py prepare --version 1.0.0 --force
|
||||
```
|
||||
|
||||
2. **Build Failures**
|
||||
```bash
|
||||
# Install build dependencies
|
||||
pip install build
|
||||
|
||||
# Clean and rebuild
|
||||
rm -rf dist/ build/
|
||||
make release-build
|
||||
```
|
||||
|
||||
3. **Git Issues**
|
||||
```bash
|
||||
# Check git status
|
||||
git status
|
||||
|
||||
# Commit changes
|
||||
git add -A && git commit -m "Prepare release"
|
||||
```
|
||||
|
||||
4. **Version Conflicts**
|
||||
```bash
|
||||
# Check current version
|
||||
make release-status
|
||||
|
||||
# Use correct version number
|
||||
make release-prepare VERSION=1.0.1 # Must be > current
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
```bash
|
||||
# Release tool help
|
||||
python release.py --help
|
||||
|
||||
# Makefile targets
|
||||
make help
|
||||
|
||||
# Command-specific help
|
||||
python release.py prepare --help
|
||||
```
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
The release tools are designed to work with automated CI/CD pipelines:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions workflow
|
||||
- name: Create Release
|
||||
run: |
|
||||
make release-prepare VERSION=${{ github.event.inputs.version }}
|
||||
git add -A
|
||||
git commit -m "Prepare release ${{ github.event.inputs.version }}"
|
||||
make release-publish VERSION=${{ github.event.inputs.version }}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Release artifacts should be signed
|
||||
- Use trusted publishing methods
|
||||
- Verify package contents before distribution
|
||||
- Keep release tools and dependencies updated
|
||||
|
||||
## Support
|
||||
|
||||
For release-related issues:
|
||||
|
||||
1. Check this documentation
|
||||
2. Run `make release-status` for diagnostics
|
||||
3. Use `--dry-run` to test changes
|
||||
4. Report issues on the project tracker
|
||||
748
chaos_test_runner.py
Executable file
748
chaos_test_runner.py
Executable file
@@ -0,0 +1,748 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Architectural Layer Independence Test Runner with Chaos Engineering
|
||||
|
||||
This module implements a sophisticated chaos engineering system that validates
|
||||
architectural layer independence by injecting controlled failures and monitoring
|
||||
the impact on dependent and independent layers.
|
||||
|
||||
The system systematically tests that:
|
||||
1. Failures in lower layers propagate only to dependent upper layers
|
||||
2. Independent layers remain unaffected by failures in unrelated layers
|
||||
3. The dependency matrix matches the intended architectural design
|
||||
|
||||
Usage:
|
||||
python chaos_test_runner.py [options]
|
||||
|
||||
Commands:
|
||||
validate-independence Run full architectural independence validation
|
||||
inject-layer-failure Inject failure into specific layer
|
||||
dependency-matrix Show architectural dependency matrix
|
||||
chaos-report Generate comprehensive chaos test report
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import pytest
|
||||
import tempfile
|
||||
import traceback
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Set, Optional, Tuple, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch, MagicMock
|
||||
import logging
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArchitecturalLayer:
|
||||
"""Represents an architectural layer with its properties."""
|
||||
name: str
|
||||
level: int
|
||||
description: str
|
||||
test_pattern: str
|
||||
dependencies: List[str] # Layers this layer depends on
|
||||
modules: List[str] # Python modules in this layer
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChaosInjectionResult:
|
||||
"""Results from a chaos injection test."""
|
||||
target_layer: str
|
||||
injection_type: str
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
affected_layers: List[str]
|
||||
expected_affected: List[str]
|
||||
dependency_violations: List[str]
|
||||
test_results: Dict[str, Any]
|
||||
success: bool
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DependencyViolation:
|
||||
"""Represents a detected architectural dependency violation."""
|
||||
violating_layer: str
|
||||
affected_layer: str
|
||||
violation_type: str
|
||||
expected_independence: bool
|
||||
actual_impact: bool
|
||||
severity: str
|
||||
description: str
|
||||
|
||||
|
||||
class ArchitecturalChaosEngine:
|
||||
"""
|
||||
Chaos engineering engine for validating architectural layer independence.
|
||||
|
||||
This engine systematically injects failures into each architectural layer
|
||||
and monitors the impact on other layers to validate the dependency matrix.
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = project_root or Path(__file__).parent
|
||||
self.test_dir = self.project_root / "tests"
|
||||
self.results_dir = self.project_root / "chaos_results"
|
||||
self.results_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Set up logging
|
||||
self.logger = self._setup_logging()
|
||||
|
||||
# Define architectural layers with their dependencies
|
||||
self.layers = self._define_architectural_layers()
|
||||
self.dependency_matrix = self._build_dependency_matrix()
|
||||
|
||||
# Chaos injection mechanisms
|
||||
self.injection_strategies = {
|
||||
'import_failure': self._inject_import_failure,
|
||||
'function_failure': self._inject_function_failure,
|
||||
'class_failure': self._inject_class_failure,
|
||||
'module_unavailable': self._inject_module_unavailable,
|
||||
'database_failure': self._inject_database_failure,
|
||||
'network_failure': self._inject_network_failure,
|
||||
'filesystem_failure': self._inject_filesystem_failure
|
||||
}
|
||||
|
||||
def _setup_logging(self) -> logging.Logger:
|
||||
"""Set up logging for chaos engineering operations."""
|
||||
logger = logging.getLogger('chaos_engine')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
def _define_architectural_layers(self) -> Dict[str, ArchitecturalLayer]:
|
||||
"""Define the architectural layers and their relationships."""
|
||||
return {
|
||||
'L1_Presentation': ArchitecturalLayer(
|
||||
name='L1_Presentation',
|
||||
level=1,
|
||||
description='CLI Interface and User Interaction',
|
||||
test_pattern='test_l1_*.py',
|
||||
dependencies=['L2_Application'],
|
||||
modules=['cli', 'markitect.cli']
|
||||
),
|
||||
'L2_Application': ArchitecturalLayer(
|
||||
name='L2_Application',
|
||||
level=2,
|
||||
description='Feature Workflows and Use Cases',
|
||||
test_pattern='test_l2_*.py',
|
||||
dependencies=['L3_Domain', 'L4_Service'],
|
||||
modules=['application', 'tddai', 'markitect.issues']
|
||||
),
|
||||
'L3_Domain': ArchitecturalLayer(
|
||||
name='L3_Domain',
|
||||
level=3,
|
||||
description='Business Logic and Domain Models',
|
||||
test_pattern='test_l3_*.py',
|
||||
dependencies=['L4_Service'],
|
||||
modules=['domain', 'markitect.schema_generator', 'markitect.metaschema']
|
||||
),
|
||||
'L4_Service': ArchitecturalLayer(
|
||||
name='L4_Service',
|
||||
level=4,
|
||||
description='Application Services and Orchestration',
|
||||
test_pattern='test_l4_*.py',
|
||||
dependencies=['L5_Infrastructure'],
|
||||
modules=['services', 'markitect.ast_service', 'markitect.document_manager']
|
||||
),
|
||||
'L5_Infrastructure': ArchitecturalLayer(
|
||||
name='L5_Infrastructure',
|
||||
level=5,
|
||||
description='Technical Infrastructure',
|
||||
test_pattern='test_l5_*.py',
|
||||
dependencies=['L6_Integration', 'L7_Foundation'],
|
||||
modules=['infrastructure', 'markitect.cache_service', 'markitect.ast_cache']
|
||||
),
|
||||
'L6_Integration': ArchitecturalLayer(
|
||||
name='L6_Integration',
|
||||
level=6,
|
||||
description='External API and System Integration',
|
||||
test_pattern='test_l6_*.py',
|
||||
dependencies=['L7_Foundation'],
|
||||
modules=['gitea', 'markitect.issues.plugins']
|
||||
),
|
||||
'L7_Foundation': ArchitecturalLayer(
|
||||
name='L7_Foundation',
|
||||
level=7,
|
||||
description='Core Components and Utilities',
|
||||
test_pattern='test_l7_*.py',
|
||||
dependencies=[], # Foundation depends on nothing
|
||||
modules=['markitect.database', 'markitect.parser', 'markitect.frontmatter']
|
||||
)
|
||||
}
|
||||
|
||||
def _build_dependency_matrix(self) -> Dict[str, Set[str]]:
|
||||
"""Build the complete dependency matrix including transitive dependencies."""
|
||||
matrix = {}
|
||||
|
||||
for layer_name, layer in self.layers.items():
|
||||
# Start with direct dependencies
|
||||
all_deps = set(layer.dependencies)
|
||||
|
||||
# Add transitive dependencies
|
||||
to_check = list(layer.dependencies)
|
||||
while to_check:
|
||||
dep = to_check.pop(0)
|
||||
if dep in self.layers:
|
||||
for transitive_dep in self.layers[dep].dependencies:
|
||||
if transitive_dep not in all_deps:
|
||||
all_deps.add(transitive_dep)
|
||||
to_check.append(transitive_dep)
|
||||
|
||||
matrix[layer_name] = all_deps
|
||||
|
||||
return matrix
|
||||
|
||||
def show_dependency_matrix(self):
|
||||
"""Display the architectural dependency matrix."""
|
||||
print("🏗️ Architectural Layer Dependency Matrix")
|
||||
print("=" * 50)
|
||||
|
||||
for layer_name in sorted(self.layers.keys()):
|
||||
layer = self.layers[layer_name]
|
||||
deps = self.dependency_matrix[layer_name]
|
||||
|
||||
print(f"\n{layer.name} (L{layer.level})")
|
||||
print(f" Description: {layer.description}")
|
||||
print(f" Direct Dependencies: {layer.dependencies}")
|
||||
print(f" All Dependencies: {sorted(deps)}")
|
||||
print(f" Test Pattern: {layer.test_pattern}")
|
||||
|
||||
@contextmanager
|
||||
def _chaos_injection_context(self, layer_name: str, injection_type: str):
|
||||
"""Context manager for safe chaos injection with cleanup."""
|
||||
self.logger.info(f"🔥 Starting chaos injection: {injection_type} on {layer_name}")
|
||||
|
||||
# Store original state for restoration
|
||||
original_modules = sys.modules.copy()
|
||||
import builtins
|
||||
original_import = builtins.__import__
|
||||
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Chaos injection failed: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Restore original state
|
||||
builtins.__import__ = original_import
|
||||
self._restore_system_state(original_modules, {})
|
||||
self.logger.info(f"✅ Chaos injection cleanup completed for {layer_name}")
|
||||
|
||||
def _restore_system_state(self, original_modules: Dict, original_builtins: Dict):
|
||||
"""Restore system state after chaos injection."""
|
||||
# Restore modules
|
||||
modules_to_remove = set(sys.modules.keys()) - set(original_modules.keys())
|
||||
for module in modules_to_remove:
|
||||
if module in sys.modules:
|
||||
del sys.modules[module]
|
||||
|
||||
# Restore modified modules
|
||||
for module_name, module in original_modules.items():
|
||||
sys.modules[module_name] = module
|
||||
|
||||
def _inject_import_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject import failure for modules in the specified layer."""
|
||||
layer = self.layers[layer_name]
|
||||
failed_modules = []
|
||||
|
||||
# Store original import function
|
||||
import builtins
|
||||
original_import = builtins.__import__
|
||||
|
||||
def patched_import(name, *args, **kwargs):
|
||||
# Check if this import should fail
|
||||
if any(name.startswith(mod) for mod in layer.modules):
|
||||
raise ImportError(f"Chaos injection: {name} module failure")
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
# Apply the patch
|
||||
builtins.__import__ = patched_import
|
||||
failed_modules.extend(layer.modules)
|
||||
|
||||
return {'failed_modules': failed_modules}
|
||||
|
||||
def _inject_function_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject function-level failures in the specified layer."""
|
||||
layer = self.layers[layer_name]
|
||||
patched_functions = []
|
||||
|
||||
# This would patch specific functions based on the layer
|
||||
# Implementation would depend on the specific layer's key functions
|
||||
return {'patched_functions': patched_functions}
|
||||
|
||||
def _inject_class_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject class-level failures in the specified layer."""
|
||||
layer = self.layers[layer_name]
|
||||
patched_classes = []
|
||||
|
||||
# This would patch specific classes based on the layer
|
||||
return {'patched_classes': patched_classes}
|
||||
|
||||
def _inject_module_unavailable(self, layer_name: str, **kwargs):
|
||||
"""Make entire modules unavailable for the specified layer."""
|
||||
layer = self.layers[layer_name]
|
||||
|
||||
for module_name in layer.modules:
|
||||
if module_name in sys.modules:
|
||||
# Temporarily remove the module
|
||||
del sys.modules[module_name]
|
||||
|
||||
return {'removed_modules': layer.modules}
|
||||
|
||||
def _inject_database_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject database failures for infrastructure layer."""
|
||||
if layer_name != 'L5_Infrastructure':
|
||||
return {'message': 'Database failure only applicable to Infrastructure layer'}
|
||||
|
||||
# Patch database operations to fail
|
||||
patches = []
|
||||
return {'database_patches': patches}
|
||||
|
||||
def _inject_network_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject network failures for integration layer."""
|
||||
if layer_name != 'L6_Integration':
|
||||
return {'message': 'Network failure only applicable to Integration layer'}
|
||||
|
||||
# Patch network operations to fail
|
||||
patches = []
|
||||
return {'network_patches': patches}
|
||||
|
||||
def _inject_filesystem_failure(self, layer_name: str, **kwargs):
|
||||
"""Inject filesystem failures."""
|
||||
patches = []
|
||||
return {'filesystem_patches': patches}
|
||||
|
||||
def run_layer_tests(self, layer_name: str) -> Dict[str, Any]:
|
||||
"""Run tests for a specific layer and return results."""
|
||||
layer = self.layers[layer_name]
|
||||
test_files = list(self.test_dir.glob(layer.test_pattern))
|
||||
|
||||
if not test_files:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'No test files found for pattern {layer.test_pattern}',
|
||||
'test_count': 0,
|
||||
'failures': 0
|
||||
}
|
||||
|
||||
# Run pytest for the layer
|
||||
cmd = ['python', '-m', 'pytest'] + [str(f) for f in test_files] + [
|
||||
'--tb=short', '--quiet', '--disable-warnings'
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=self.project_root,
|
||||
timeout=120 # 2 minute timeout per layer
|
||||
)
|
||||
|
||||
# Parse pytest output for test counts
|
||||
output = result.stdout + result.stderr
|
||||
test_count = self._extract_test_count(output)
|
||||
failures = self._extract_failure_count(output)
|
||||
|
||||
return {
|
||||
'success': result.returncode == 0,
|
||||
'test_count': test_count,
|
||||
'failures': failures,
|
||||
'output': output[:1000], # Truncate for storage
|
||||
'return_code': result.returncode
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Test execution timeout',
|
||||
'test_count': 0,
|
||||
'failures': 1
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'test_count': 0,
|
||||
'failures': 1
|
||||
}
|
||||
|
||||
def _extract_test_count(self, output: str) -> int:
|
||||
"""Extract total test count from pytest output."""
|
||||
import re
|
||||
patterns = [
|
||||
r'(\d+) passed',
|
||||
r'collected (\d+) items',
|
||||
r'(\d+) failed',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, output)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
return 0
|
||||
|
||||
def _extract_failure_count(self, output: str) -> int:
|
||||
"""Extract failure count from pytest output."""
|
||||
import re
|
||||
patterns = [
|
||||
r'(\d+) failed',
|
||||
r'FAILED.*::.*(\d+)',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, output)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
return 0
|
||||
|
||||
def inject_chaos_and_test(self, target_layer: str, injection_type: str) -> ChaosInjectionResult:
|
||||
"""Inject chaos into a layer and run all tests to measure impact."""
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
with self._chaos_injection_context(target_layer, injection_type):
|
||||
# Perform the chaos injection
|
||||
injection_result = self.injection_strategies[injection_type](target_layer)
|
||||
|
||||
# Run tests on all layers to see impact
|
||||
test_results = {}
|
||||
affected_layers = []
|
||||
|
||||
for layer_name in self.layers.keys():
|
||||
self.logger.info(f"🧪 Testing {layer_name} under chaos conditions")
|
||||
layer_result = self.run_layer_tests(layer_name)
|
||||
test_results[layer_name] = layer_result
|
||||
|
||||
# Consider layer affected if tests fail
|
||||
if not layer_result['success']:
|
||||
affected_layers.append(layer_name)
|
||||
|
||||
# Determine expected affected layers
|
||||
expected_affected = self._calculate_expected_impact(target_layer)
|
||||
|
||||
# Detect violations
|
||||
violations = self._detect_dependency_violations(
|
||||
target_layer, affected_layers, expected_affected
|
||||
)
|
||||
|
||||
end_time = datetime.now()
|
||||
|
||||
return ChaosInjectionResult(
|
||||
target_layer=target_layer,
|
||||
injection_type=injection_type,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
affected_layers=affected_layers,
|
||||
expected_affected=expected_affected,
|
||||
dependency_violations=[v.violating_layer for v in violations],
|
||||
test_results=test_results,
|
||||
success=len(violations) == 0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
end_time = datetime.now()
|
||||
return ChaosInjectionResult(
|
||||
target_layer=target_layer,
|
||||
injection_type=injection_type,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
affected_layers=[],
|
||||
expected_affected=[],
|
||||
dependency_violations=[],
|
||||
test_results={},
|
||||
success=False,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
def _calculate_expected_impact(self, target_layer: str) -> List[str]:
|
||||
"""Calculate which layers should be affected by failure in target layer."""
|
||||
expected_affected = [target_layer] # Target layer should always be affected
|
||||
|
||||
# Find all layers that depend on the target layer
|
||||
for layer_name, dependencies in self.dependency_matrix.items():
|
||||
if target_layer in dependencies:
|
||||
expected_affected.append(layer_name)
|
||||
|
||||
return expected_affected
|
||||
|
||||
def _detect_dependency_violations(self, target_layer: str,
|
||||
actual_affected: List[str],
|
||||
expected_affected: List[str]) -> List[DependencyViolation]:
|
||||
"""Detect violations of architectural dependencies."""
|
||||
violations = []
|
||||
|
||||
# Check for unexpected impacts (layers that shouldn't be affected but were)
|
||||
for layer in actual_affected:
|
||||
if layer not in expected_affected:
|
||||
violations.append(DependencyViolation(
|
||||
violating_layer=layer,
|
||||
affected_layer=target_layer,
|
||||
violation_type='unexpected_dependency',
|
||||
expected_independence=True,
|
||||
actual_impact=True,
|
||||
severity='HIGH',
|
||||
description=f'{layer} was affected by {target_layer} failure but should be independent'
|
||||
))
|
||||
|
||||
# Check for missing impacts (layers that should be affected but weren't)
|
||||
for layer in expected_affected:
|
||||
if layer not in actual_affected and layer != target_layer:
|
||||
violations.append(DependencyViolation(
|
||||
violating_layer=layer,
|
||||
affected_layer=target_layer,
|
||||
violation_type='missing_dependency',
|
||||
expected_independence=False,
|
||||
actual_impact=False,
|
||||
severity='MEDIUM',
|
||||
description=f'{layer} should be affected by {target_layer} failure but was not'
|
||||
))
|
||||
|
||||
return violations
|
||||
|
||||
def validate_architectural_independence(self) -> Dict[str, Any]:
|
||||
"""Run comprehensive architectural independence validation."""
|
||||
self.logger.info("🚀 Starting comprehensive architectural independence validation")
|
||||
|
||||
validation_results = {
|
||||
'start_time': datetime.now(),
|
||||
'layer_results': {},
|
||||
'violations': [],
|
||||
'summary': {
|
||||
'total_injections': 0,
|
||||
'successful_injections': 0,
|
||||
'total_violations': 0,
|
||||
'layers_tested': len(self.layers)
|
||||
}
|
||||
}
|
||||
|
||||
# Test each layer with different injection types
|
||||
injection_types = ['import_failure', 'module_unavailable']
|
||||
|
||||
for layer_name in self.layers.keys():
|
||||
layer_results = {}
|
||||
|
||||
for injection_type in injection_types:
|
||||
self.logger.info(f"🔥 Testing {layer_name} with {injection_type}")
|
||||
|
||||
result = self.inject_chaos_and_test(layer_name, injection_type)
|
||||
layer_results[injection_type] = result
|
||||
|
||||
validation_results['summary']['total_injections'] += 1
|
||||
if result.success:
|
||||
validation_results['summary']['successful_injections'] += 1
|
||||
|
||||
validation_results['summary']['total_violations'] += len(result.dependency_violations)
|
||||
|
||||
validation_results['layer_results'][layer_name] = layer_results
|
||||
|
||||
validation_results['end_time'] = datetime.now()
|
||||
validation_results['duration'] = (
|
||||
validation_results['end_time'] - validation_results['start_time']
|
||||
).total_seconds()
|
||||
|
||||
# Save results
|
||||
self._save_validation_results(validation_results)
|
||||
|
||||
return validation_results
|
||||
|
||||
def _save_validation_results(self, results: Dict[str, Any]):
|
||||
"""Save validation results to file."""
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"chaos_validation_{timestamp}.json"
|
||||
filepath = self.results_dir / filename
|
||||
|
||||
# Convert datetime objects to strings for JSON serialization
|
||||
serializable_results = self._make_json_serializable(results)
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(serializable_results, f, indent=2)
|
||||
|
||||
self.logger.info(f"📄 Results saved to {filepath}")
|
||||
|
||||
def _make_json_serializable(self, obj):
|
||||
"""Convert objects to JSON-serializable format."""
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, dict):
|
||||
return {k: self._make_json_serializable(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [self._make_json_serializable(item) for item in obj]
|
||||
elif hasattr(obj, '__dict__'):
|
||||
return self._make_json_serializable(asdict(obj))
|
||||
else:
|
||||
return obj
|
||||
|
||||
def generate_chaos_report(self, results_file: Optional[str] = None) -> str:
|
||||
"""Generate a comprehensive chaos engineering report."""
|
||||
if results_file:
|
||||
with open(results_file, 'r') as f:
|
||||
results = json.load(f)
|
||||
else:
|
||||
# Use the most recent results file
|
||||
result_files = sorted(self.results_dir.glob("chaos_validation_*.json"))
|
||||
if not result_files:
|
||||
return "No chaos validation results found"
|
||||
|
||||
with open(result_files[-1], 'r') as f:
|
||||
results = json.load(f)
|
||||
|
||||
report = self._build_text_report(results)
|
||||
|
||||
# Save report
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
report_file = self.results_dir / f"chaos_report_{timestamp}.md"
|
||||
with open(report_file, 'w') as f:
|
||||
f.write(report)
|
||||
|
||||
return report
|
||||
|
||||
def _build_text_report(self, results: Dict[str, Any]) -> str:
|
||||
"""Build a formatted text report from results."""
|
||||
summary = results['summary']
|
||||
|
||||
report = f"""# Architectural Independence Chaos Engineering Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Validation Date**: {results['start_time']}
|
||||
**Duration**: {results.get('duration', 0):.2f} seconds
|
||||
**Layers Tested**: {summary['layers_tested']}
|
||||
**Total Chaos Injections**: {summary['total_injections']}
|
||||
**Successful Injections**: {summary['successful_injections']}
|
||||
**Total Violations Detected**: {summary['total_violations']}
|
||||
|
||||
**Overall Health**: {'✅ PASS' if summary['total_violations'] == 0 else '❌ VIOLATIONS DETECTED'}
|
||||
|
||||
## Architectural Layer Overview
|
||||
|
||||
"""
|
||||
|
||||
for layer_name, layer in self.layers.items():
|
||||
dependencies = ', '.join(layer.dependencies) if layer.dependencies else 'None'
|
||||
report += f"- **{layer.name}**: {layer.description} (Dependencies: {dependencies})\n"
|
||||
|
||||
report += "\n## Dependency Matrix\n\n"
|
||||
for layer_name, deps in self.dependency_matrix.items():
|
||||
deps_str = ', '.join(sorted(deps)) if deps else 'None'
|
||||
report += f"- **{layer_name}**: {deps_str}\n"
|
||||
|
||||
report += "\n## Chaos Injection Results\n\n"
|
||||
|
||||
layer_results = results.get('layer_results', {})
|
||||
for layer_name, injections in layer_results.items():
|
||||
report += f"### {layer_name}\n\n"
|
||||
|
||||
for injection_type, result in injections.items():
|
||||
status = '✅ PASS' if result['success'] else '❌ FAIL'
|
||||
violations = len(result['dependency_violations'])
|
||||
|
||||
report += f"**{injection_type}**: {status}\n"
|
||||
report += f"- Affected Layers: {', '.join(result['affected_layers'])}\n"
|
||||
report += f"- Expected Affected: {', '.join(result['expected_affected'])}\n"
|
||||
report += f"- Violations: {violations}\n"
|
||||
|
||||
if result.get('error_message'):
|
||||
report += f"- Error: {result['error_message']}\n"
|
||||
|
||||
report += "\n"
|
||||
|
||||
report += "\n## Recommendations\n\n"
|
||||
|
||||
if summary['total_violations'] == 0:
|
||||
report += "✅ **Excellent**: All architectural boundaries are properly maintained.\n"
|
||||
report += "✅ **No violations detected**: The system demonstrates proper layer independence.\n"
|
||||
else:
|
||||
report += "❌ **Action Required**: Architectural violations detected that need attention.\n"
|
||||
report += "🔧 **Priority**: Review and refactor components with dependency violations.\n"
|
||||
report += "📊 **Monitor**: Run chaos tests regularly to prevent regression.\n"
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the chaos test runner."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Architectural Layer Independence Test Runner with Chaos Engineering"
|
||||
)
|
||||
|
||||
parser.add_argument('command', choices=[
|
||||
'validate-independence',
|
||||
'inject-layer-failure',
|
||||
'dependency-matrix',
|
||||
'chaos-report'
|
||||
], help='Command to execute')
|
||||
|
||||
parser.add_argument('--layer', type=str, help='Target layer for injection')
|
||||
parser.add_argument('--injection-type', type=str, default='import_failure',
|
||||
choices=['import_failure', 'module_unavailable', 'function_failure'],
|
||||
help='Type of chaos injection')
|
||||
parser.add_argument('--results-file', type=str, help='Results file for report generation')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Set up logging level
|
||||
if args.verbose:
|
||||
logging.getLogger('chaos_engine').setLevel(logging.DEBUG)
|
||||
|
||||
engine = ArchitecturalChaosEngine()
|
||||
|
||||
if args.command == 'dependency-matrix':
|
||||
engine.show_dependency_matrix()
|
||||
|
||||
elif args.command == 'inject-layer-failure':
|
||||
if not args.layer:
|
||||
print("❌ Error: --layer is required for inject-layer-failure")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🔥 Injecting {args.injection_type} into {args.layer}")
|
||||
result = engine.inject_chaos_and_test(args.layer, args.injection_type)
|
||||
|
||||
print(f"\n📊 Results:")
|
||||
print(f"Success: {result.success}")
|
||||
print(f"Affected Layers: {result.affected_layers}")
|
||||
print(f"Expected Affected: {result.expected_affected}")
|
||||
print(f"Violations: {result.dependency_violations}")
|
||||
|
||||
elif args.command == 'validate-independence':
|
||||
print("🚀 Starting comprehensive architectural independence validation")
|
||||
results = engine.validate_architectural_independence()
|
||||
|
||||
summary = results['summary']
|
||||
print(f"\n📊 Validation Complete!")
|
||||
print(f"Total Injections: {summary['total_injections']}")
|
||||
print(f"Successful: {summary['successful_injections']}")
|
||||
print(f"Violations: {summary['total_violations']}")
|
||||
|
||||
if summary['total_violations'] == 0:
|
||||
print("✅ All architectural boundaries properly maintained!")
|
||||
else:
|
||||
print("❌ Architectural violations detected - review results")
|
||||
|
||||
elif args.command == 'chaos-report':
|
||||
print("📄 Generating chaos engineering report")
|
||||
report = engine.generate_chaos_report(args.results_file)
|
||||
print(report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
388
install.py
Normal file
388
install.py
Normal file
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MarkiTect Installer
|
||||
|
||||
This script provides an easy way to install MarkiTect and make it available
|
||||
system-wide. It handles virtual environment creation, dependency installation,
|
||||
and creates symbolic links to make the commands available from anywhere.
|
||||
|
||||
Usage:
|
||||
python install.py [options]
|
||||
|
||||
Options:
|
||||
--prefix PATH Installation prefix (default: ~/.local)
|
||||
--system Install system-wide (requires sudo, uses /usr/local)
|
||||
--venv-dir PATH Custom virtual environment directory
|
||||
--no-symlinks Don't create symbolic links (manual PATH setup required)
|
||||
--force Force reinstallation over existing installation
|
||||
--dev Install in development mode with test dependencies
|
||||
--check Check if MarkiTect is already installed
|
||||
--uninstall Uninstall MarkiTect
|
||||
--help Show this help message
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
|
||||
class MarkiTectInstaller:
|
||||
"""MarkiTect installation manager."""
|
||||
|
||||
def __init__(self, prefix=None, system=False, venv_dir=None, force=False, dev=False):
|
||||
self.system = system
|
||||
self.force = force
|
||||
self.dev = dev
|
||||
|
||||
# Determine installation paths
|
||||
if system:
|
||||
self.prefix = Path("/usr/local")
|
||||
self.bin_dir = self.prefix / "bin"
|
||||
self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect"
|
||||
else:
|
||||
self.prefix = Path(prefix) if prefix else Path.home() / ".local"
|
||||
self.bin_dir = self.prefix / "bin"
|
||||
self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect"
|
||||
|
||||
self.project_dir = Path(__file__).parent.absolute()
|
||||
|
||||
def check_requirements(self):
|
||||
"""Check system requirements."""
|
||||
print("🔍 Checking system requirements...")
|
||||
|
||||
# Check Python version
|
||||
if sys.version_info < (3, 8):
|
||||
print("❌ Python 3.8 or higher is required")
|
||||
sys.exit(1)
|
||||
print(f"✅ Python {sys.version.split()[0]} found")
|
||||
|
||||
# Check if pip is available
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "pip", "--version"],
|
||||
check=True, capture_output=True)
|
||||
print("✅ pip is available")
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ pip is not available. Please install pip first.")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if git is available (optional)
|
||||
try:
|
||||
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
||||
print("✅ git is available")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("⚠️ git is not available (optional for version info)")
|
||||
|
||||
def check_existing_installation(self):
|
||||
"""Check if MarkiTect is already installed."""
|
||||
# Check for existing venv
|
||||
if self.venv_dir.exists():
|
||||
print(f"📁 Existing installation found at {self.venv_dir}")
|
||||
return True
|
||||
|
||||
# Check for existing binaries
|
||||
markitect_bin = self.bin_dir / "markitect"
|
||||
if markitect_bin.exists():
|
||||
print(f"📁 Existing binary found at {markitect_bin}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def create_directories(self):
|
||||
"""Create necessary directories."""
|
||||
print(f"📁 Creating directories...")
|
||||
|
||||
if self.system and not os.access(self.prefix, os.W_OK):
|
||||
print("❌ System installation requires sudo privileges")
|
||||
print(" Please run with sudo or choose a different installation prefix")
|
||||
sys.exit(1)
|
||||
|
||||
self.prefix.mkdir(parents=True, exist_ok=True)
|
||||
self.bin_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"✅ Created directories in {self.prefix}")
|
||||
|
||||
def create_virtual_environment(self):
|
||||
"""Create and set up virtual environment."""
|
||||
print(f"🐍 Creating virtual environment at {self.venv_dir}")
|
||||
|
||||
if self.venv_dir.exists():
|
||||
if self.force:
|
||||
print(f"🗑️ Removing existing installation...")
|
||||
shutil.rmtree(self.venv_dir)
|
||||
else:
|
||||
print("❌ Virtual environment already exists. Use --force to overwrite.")
|
||||
sys.exit(1)
|
||||
|
||||
# Create virtual environment
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "venv", str(self.venv_dir)
|
||||
], check=True)
|
||||
|
||||
# Get paths to venv executables
|
||||
if sys.platform == "win32":
|
||||
venv_python = self.venv_dir / "Scripts" / "python.exe"
|
||||
venv_pip = self.venv_dir / "Scripts" / "pip.exe"
|
||||
else:
|
||||
venv_python = self.venv_dir / "bin" / "python"
|
||||
venv_pip = self.venv_dir / "bin" / "pip"
|
||||
|
||||
# Upgrade pip
|
||||
print("📦 Upgrading pip...")
|
||||
subprocess.run([
|
||||
str(venv_pip), "install", "--upgrade", "pip", "setuptools", "wheel"
|
||||
], check=True)
|
||||
|
||||
return venv_python, venv_pip
|
||||
|
||||
def install_markitect(self, venv_python, venv_pip):
|
||||
"""Install MarkiTect in the virtual environment."""
|
||||
print("📦 Installing MarkiTect...")
|
||||
|
||||
install_cmd = [str(venv_pip), "install"]
|
||||
|
||||
if self.dev:
|
||||
print("🛠️ Installing in development mode with test dependencies...")
|
||||
# Install in editable mode from current directory
|
||||
install_cmd.extend(["-e", str(self.project_dir)])
|
||||
|
||||
# Install test dependencies
|
||||
subprocess.run(install_cmd, check=True)
|
||||
subprocess.run([
|
||||
str(venv_pip), "install", "pytest", "pytest-cov", "black", "flake8", "mypy"
|
||||
], check=True)
|
||||
else:
|
||||
# Install from current directory
|
||||
install_cmd.append(str(self.project_dir))
|
||||
subprocess.run(install_cmd, check=True)
|
||||
|
||||
print("✅ MarkiTect installed successfully")
|
||||
|
||||
def create_symlinks(self, no_symlinks=False):
|
||||
"""Create symbolic links for global access."""
|
||||
if no_symlinks:
|
||||
print("⚠️ Skipping symbolic link creation")
|
||||
self.show_manual_setup()
|
||||
return
|
||||
|
||||
print("🔗 Creating symbolic links...")
|
||||
|
||||
# Get venv bin directory
|
||||
if sys.platform == "win32":
|
||||
venv_bin = self.venv_dir / "Scripts"
|
||||
exe_suffix = ".exe"
|
||||
else:
|
||||
venv_bin = self.venv_dir / "bin"
|
||||
exe_suffix = ""
|
||||
|
||||
# Commands to link
|
||||
commands = ["markitect", "tddai", "issue"]
|
||||
|
||||
for cmd in commands:
|
||||
src = venv_bin / f"{cmd}{exe_suffix}"
|
||||
dst = self.bin_dir / cmd
|
||||
|
||||
if src.exists():
|
||||
# Remove existing symlink/file
|
||||
if dst.exists() or dst.is_symlink():
|
||||
dst.unlink()
|
||||
|
||||
# Create symlink
|
||||
try:
|
||||
dst.symlink_to(src)
|
||||
print(f"✅ Created symlink: {dst} -> {src}")
|
||||
except OSError:
|
||||
# Fallback: create wrapper script
|
||||
self.create_wrapper_script(dst, src)
|
||||
else:
|
||||
print(f"⚠️ Command {cmd} not found in virtual environment")
|
||||
|
||||
def create_wrapper_script(self, dst, src):
|
||||
"""Create a wrapper script when symlinks aren't available."""
|
||||
print(f"🔧 Creating wrapper script: {dst}")
|
||||
|
||||
if sys.platform == "win32":
|
||||
# Windows batch file
|
||||
dst = dst.with_suffix(".bat")
|
||||
content = f'@echo off\n"{src}" %*\n'
|
||||
else:
|
||||
# Unix shell script
|
||||
content = f'#!/bin/bash\nexec "{src}" "$@"\n'
|
||||
|
||||
dst.write_text(content)
|
||||
if sys.platform != "win32":
|
||||
os.chmod(dst, 0o755)
|
||||
|
||||
def show_manual_setup(self):
|
||||
"""Show manual PATH setup instructions."""
|
||||
print("\n📋 Manual Setup Instructions:")
|
||||
print("=" * 50)
|
||||
print(f"Add the following to your PATH environment variable:")
|
||||
print(f" {self.venv_dir / 'bin'}")
|
||||
print()
|
||||
print("For bash/zsh, add this line to ~/.bashrc or ~/.zshrc:")
|
||||
print(f' export PATH="{self.venv_dir / "bin"}:$PATH"')
|
||||
print()
|
||||
|
||||
def test_installation(self):
|
||||
"""Test the installation."""
|
||||
print("🧪 Testing installation...")
|
||||
|
||||
# Test markitect command
|
||||
try:
|
||||
markitect_bin = self.bin_dir / "markitect"
|
||||
if not markitect_bin.exists():
|
||||
# Try direct venv path
|
||||
if sys.platform == "win32":
|
||||
markitect_bin = self.venv_dir / "Scripts" / "markitect.exe"
|
||||
else:
|
||||
markitect_bin = self.venv_dir / "bin" / "markitect"
|
||||
|
||||
result = subprocess.run([
|
||||
str(markitect_bin), "version", "--short"
|
||||
], capture_output=True, text=True, check=True)
|
||||
|
||||
version = result.stdout.strip()
|
||||
print(f"✅ MarkiTect installed successfully - version {version}")
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"❌ Installation test failed: {e}")
|
||||
return False
|
||||
|
||||
def uninstall(self):
|
||||
"""Uninstall MarkiTect."""
|
||||
print("🗑️ Uninstalling MarkiTect...")
|
||||
|
||||
removed_something = False
|
||||
|
||||
# Remove virtual environment
|
||||
if self.venv_dir.exists():
|
||||
print(f"🗑️ Removing virtual environment: {self.venv_dir}")
|
||||
shutil.rmtree(self.venv_dir)
|
||||
removed_something = True
|
||||
|
||||
# Remove symlinks
|
||||
commands = ["markitect", "tddai", "issue"]
|
||||
for cmd in commands:
|
||||
for bin_path in [self.bin_dir / cmd, self.bin_dir / f"{cmd}.bat"]:
|
||||
if bin_path.exists() or bin_path.is_symlink():
|
||||
print(f"🗑️ Removing: {bin_path}")
|
||||
bin_path.unlink()
|
||||
removed_something = True
|
||||
|
||||
if removed_something:
|
||||
print("✅ MarkiTect uninstalled successfully")
|
||||
else:
|
||||
print("⚠️ No MarkiTect installation found")
|
||||
|
||||
def install(self, no_symlinks=False):
|
||||
"""Perform the complete installation."""
|
||||
print("🚀 Installing MarkiTect")
|
||||
print("=" * 50)
|
||||
|
||||
self.check_requirements()
|
||||
|
||||
if not self.force and self.check_existing_installation():
|
||||
print("❌ MarkiTect is already installed. Use --force to reinstall.")
|
||||
sys.exit(1)
|
||||
|
||||
self.create_directories()
|
||||
venv_python, venv_pip = self.create_virtual_environment()
|
||||
self.install_markitect(venv_python, venv_pip)
|
||||
self.create_symlinks(no_symlinks)
|
||||
|
||||
print()
|
||||
if self.test_installation():
|
||||
print("🎉 Installation completed successfully!")
|
||||
print()
|
||||
print("You can now use MarkiTect from anywhere:")
|
||||
print(" markitect --help")
|
||||
print(" markitect version")
|
||||
print(" tddai --help")
|
||||
print(" issue --help")
|
||||
else:
|
||||
print("⚠️ Installation completed but tests failed")
|
||||
self.show_manual_setup()
|
||||
|
||||
def check_installation_status(self):
|
||||
"""Check current installation status."""
|
||||
print("🔍 MarkiTect Installation Status")
|
||||
print("=" * 50)
|
||||
|
||||
# Check virtual environment
|
||||
if self.venv_dir.exists():
|
||||
print(f"✅ Virtual environment: {self.venv_dir}")
|
||||
else:
|
||||
print(f"❌ Virtual environment: Not found at {self.venv_dir}")
|
||||
|
||||
# Check binaries
|
||||
commands = ["markitect", "tddai", "issue"]
|
||||
for cmd in commands:
|
||||
bin_path = self.bin_dir / cmd
|
||||
if bin_path.exists():
|
||||
print(f"✅ {cmd}: {bin_path}")
|
||||
else:
|
||||
print(f"❌ {cmd}: Not found at {bin_path}")
|
||||
|
||||
# Try to get version
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"markitect", "version", "--short"
|
||||
], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
print(f"✅ Working installation: version {version}")
|
||||
else:
|
||||
print("❌ Installation found but not working")
|
||||
except FileNotFoundError:
|
||||
print("❌ markitect command not available in PATH")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="MarkiTect Installer",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__.split('\n\n')[1] # Show usage from docstring
|
||||
)
|
||||
|
||||
parser.add_argument("--prefix", type=Path,
|
||||
help="Installation prefix (default: ~/.local)")
|
||||
parser.add_argument("--system", action="store_true",
|
||||
help="Install system-wide (requires sudo)")
|
||||
parser.add_argument("--venv-dir", type=Path,
|
||||
help="Custom virtual environment directory")
|
||||
parser.add_argument("--no-symlinks", action="store_true",
|
||||
help="Don't create symbolic links")
|
||||
parser.add_argument("--force", action="store_true",
|
||||
help="Force reinstallation")
|
||||
parser.add_argument("--dev", action="store_true",
|
||||
help="Install in development mode")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="Check installation status")
|
||||
parser.add_argument("--uninstall", action="store_true",
|
||||
help="Uninstall MarkiTect")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create installer instance
|
||||
installer = MarkiTectInstaller(
|
||||
prefix=args.prefix,
|
||||
system=args.system,
|
||||
venv_dir=args.venv_dir,
|
||||
force=args.force,
|
||||
dev=args.dev
|
||||
)
|
||||
|
||||
# Handle different actions
|
||||
if args.check:
|
||||
installer.check_installation_status()
|
||||
elif args.uninstall:
|
||||
installer.uninstall()
|
||||
else:
|
||||
installer.install(no_symlinks=args.no_symlinks)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
160
install.sh
Executable file
160
install.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# MarkiTect Quick Installer
|
||||
#
|
||||
# This script provides a simple way to install MarkiTect.
|
||||
# It's a wrapper around the Python installer script.
|
||||
#
|
||||
# Usage:
|
||||
# ./install.sh [options]
|
||||
# curl -sSL https://raw.githubusercontent.com/example/markitect/main/install.sh | bash
|
||||
#
|
||||
# Options:
|
||||
# --system Install system-wide (requires sudo)
|
||||
# --dev Install in development mode
|
||||
# --check Check installation status
|
||||
# --uninstall Uninstall MarkiTect
|
||||
# --help Show help
|
||||
|
||||
set -e
|
||||
|
||||
# Default options
|
||||
SYSTEM=""
|
||||
DEV=""
|
||||
CHECK=""
|
||||
UNINSTALL=""
|
||||
HELP=""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--system)
|
||||
SYSTEM="--system"
|
||||
shift
|
||||
;;
|
||||
--dev)
|
||||
DEV="--dev"
|
||||
shift
|
||||
;;
|
||||
--check)
|
||||
CHECK="--check"
|
||||
shift
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL="--uninstall"
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
HELP="--help"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Show help if requested
|
||||
if [[ -n "$HELP" ]]; then
|
||||
cat << EOF
|
||||
MarkiTect Quick Installer
|
||||
|
||||
Usage: $0 [options]
|
||||
|
||||
Options:
|
||||
--system Install system-wide (requires sudo)
|
||||
--dev Install in development mode with test dependencies
|
||||
--check Check current installation status
|
||||
--uninstall Uninstall MarkiTect
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
$0 # Install for current user
|
||||
$0 --system # Install system-wide
|
||||
$0 --dev # Install in development mode
|
||||
$0 --check # Check installation status
|
||||
$0 --uninstall # Uninstall MarkiTect
|
||||
|
||||
For more advanced options, use the Python installer directly:
|
||||
python install.py --help
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if Python is available
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
print_error "Python 3 is required but not found"
|
||||
print_info "Please install Python 3.8 or higher and try again"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Python version
|
||||
python_version=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||||
required_version="3.8"
|
||||
|
||||
if ! python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)"; then
|
||||
print_error "Python $required_version or higher is required (found: $python_version)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Python $python_version found"
|
||||
|
||||
# Determine script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INSTALLER_SCRIPT="$SCRIPT_DIR/install.py"
|
||||
|
||||
# Check if installer script exists
|
||||
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
|
||||
print_error "Installer script not found: $INSTALLER_SCRIPT"
|
||||
print_info "Make sure you're running this from the MarkiTect project directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build command
|
||||
cmd="python3 $INSTALLER_SCRIPT"
|
||||
|
||||
if [[ -n "$SYSTEM" ]]; then
|
||||
cmd="$cmd $SYSTEM"
|
||||
print_warning "System installation requires sudo privileges"
|
||||
fi
|
||||
|
||||
if [[ -n "$DEV" ]]; then
|
||||
cmd="$cmd $DEV"
|
||||
fi
|
||||
|
||||
if [[ -n "$CHECK" ]]; then
|
||||
cmd="$cmd $CHECK"
|
||||
fi
|
||||
|
||||
if [[ -n "$UNINSTALL" ]]; then
|
||||
cmd="$cmd $UNINSTALL"
|
||||
fi
|
||||
|
||||
# Run the installer
|
||||
print_info "Running: $cmd"
|
||||
exec $cmd
|
||||
123
markitect/__version__.py
Normal file
123
markitect/__version__.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Version information for MarkiTect.
|
||||
|
||||
This module provides version and release information for the MarkiTect package.
|
||||
Version information is sourced from pyproject.toml and git metadata when available.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Base version from pyproject.toml
|
||||
__version__ = "0.1.0"
|
||||
|
||||
def get_git_commit_hash() -> Optional[str]:
|
||||
"""Get the current git commit hash if available."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=Path(__file__).parent.parent
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def get_git_branch() -> Optional[str]:
|
||||
"""Get the current git branch if available."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'branch', '--show-current'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=Path(__file__).parent.parent
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def get_git_tag() -> Optional[str]:
|
||||
"""Get the current git tag if available."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'describe', '--tags', '--exact-match'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=Path(__file__).parent.parent
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def is_development_version() -> bool:
|
||||
"""Check if this is a development version (has uncommitted changes)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'status', '--porcelain'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=Path(__file__).parent.parent
|
||||
)
|
||||
return bool(result.stdout.strip())
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def get_version_info() -> dict:
|
||||
"""Get comprehensive version information."""
|
||||
git_commit = get_git_commit_hash()
|
||||
git_branch = get_git_branch()
|
||||
git_tag = get_git_tag()
|
||||
is_dev = is_development_version()
|
||||
|
||||
# Build version string
|
||||
version_parts = [__version__]
|
||||
|
||||
if git_tag and git_tag != f"v{__version__}":
|
||||
# If we have a different tag, use it
|
||||
version_parts = [git_tag.lstrip('v')]
|
||||
|
||||
if git_commit:
|
||||
if is_dev:
|
||||
version_parts.append(f"dev+{git_commit}")
|
||||
elif not git_tag:
|
||||
version_parts.append(f"+{git_commit}")
|
||||
|
||||
if is_dev and not git_commit:
|
||||
version_parts.append("dev")
|
||||
|
||||
full_version = ".".join(version_parts)
|
||||
|
||||
return {
|
||||
"version": __version__,
|
||||
"full_version": full_version,
|
||||
"git_commit": git_commit,
|
||||
"git_branch": git_branch,
|
||||
"git_tag": git_tag,
|
||||
"is_development": is_dev,
|
||||
"is_git_repo": git_commit is not None
|
||||
}
|
||||
|
||||
def get_release_info() -> dict:
|
||||
"""Get release information."""
|
||||
version_info = get_version_info()
|
||||
|
||||
release_type = "development" if version_info["is_development"] else "release"
|
||||
if version_info["git_tag"]:
|
||||
release_type = "tagged-release"
|
||||
elif version_info["git_commit"] and not version_info["is_development"]:
|
||||
release_type = "commit-build"
|
||||
|
||||
return {
|
||||
"release_type": release_type,
|
||||
"build_from": version_info["git_branch"] or "unknown",
|
||||
"commit": version_info["git_commit"] or "unknown",
|
||||
"clean_build": not version_info["is_development"],
|
||||
**version_info
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import builtins
|
||||
|
||||
from .database import DatabaseManager
|
||||
from .legacy_compat import LegacyMode, emit_deprecation_warning, legacy_switch_option
|
||||
from .__version__ import get_version_info, get_release_info
|
||||
|
||||
# Import legacy system components for advanced management
|
||||
try:
|
||||
@@ -175,10 +176,20 @@ def format_output(data, output_format):
|
||||
return format_output(data, 'table')
|
||||
|
||||
|
||||
def print_version(ctx, param, value):
|
||||
"""Callback to print version and exit."""
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
version_info = get_version_info()
|
||||
click.echo(version_info['full_version'])
|
||||
ctx.exit()
|
||||
|
||||
@click.group()
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
|
||||
@click.option('--config', 'config_file', type=click.Path(exists=True), help='Configuration file path')
|
||||
@click.option('--database', type=click.Path(), help='Database file path')
|
||||
@click.option('--version', is_flag=True, expose_value=False, is_eager=True,
|
||||
callback=print_version, help='Show version and exit')
|
||||
@pass_config
|
||||
def cli(config, verbose, database, config_file):
|
||||
"""
|
||||
@@ -218,6 +229,63 @@ def cli(config, verbose, database, config_file):
|
||||
|
||||
# Issue management commands removed - use dedicated 'issue' CLI or 'tddai' CLI instead
|
||||
|
||||
# Version and release information commands
|
||||
|
||||
@cli.command()
|
||||
@click.option('--short', is_flag=True, help='Show only version number')
|
||||
def version(short):
|
||||
"""Show MarkiTect version information."""
|
||||
version_info = get_version_info()
|
||||
|
||||
if short:
|
||||
click.echo(version_info['full_version'])
|
||||
else:
|
||||
click.echo("MarkiTect Version Information")
|
||||
click.echo("============================")
|
||||
click.echo(f"Version: {version_info['full_version']}")
|
||||
click.echo(f"Base Version: {version_info['version']}")
|
||||
|
||||
if version_info['is_git_repo']:
|
||||
click.echo(f"Git Commit: {version_info['git_commit'] or 'N/A'}")
|
||||
click.echo(f"Git Branch: {version_info['git_branch'] or 'N/A'}")
|
||||
if version_info['git_tag']:
|
||||
click.echo(f"Git Tag: {version_info['git_tag']}")
|
||||
click.echo(f"Development Build: {'Yes' if version_info['is_development'] else 'No'}")
|
||||
else:
|
||||
click.echo("Git Repository: Not available")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--format', 'output_format', default='text',
|
||||
type=click.Choice(['text', 'json', 'yaml']),
|
||||
help='Output format (text, json, yaml)')
|
||||
def release(output_format):
|
||||
"""Show MarkiTect release information."""
|
||||
release_info = get_release_info()
|
||||
|
||||
if output_format == 'json':
|
||||
import json
|
||||
click.echo(json.dumps(release_info, indent=2))
|
||||
elif output_format == 'yaml':
|
||||
import yaml
|
||||
click.echo(yaml.dump(release_info, default_flow_style=False))
|
||||
else:
|
||||
# Text format
|
||||
click.echo("MarkiTect Release Information")
|
||||
click.echo("============================")
|
||||
click.echo(f"Version: {release_info['full_version']}")
|
||||
click.echo(f"Release Type: {release_info['release_type']}")
|
||||
click.echo(f"Build From: {release_info['build_from']}")
|
||||
click.echo(f"Commit: {release_info['commit']}")
|
||||
click.echo(f"Clean Build: {'Yes' if release_info['clean_build'] else 'No'}")
|
||||
|
||||
if release_info['is_git_repo']:
|
||||
click.echo(f"Git Repository: Available")
|
||||
if release_info['git_tag']:
|
||||
click.echo(f"Tagged Release: {release_info['git_tag']}")
|
||||
else:
|
||||
click.echo("Git Repository: Not available")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('file_path', type=click.Path(exists=True))
|
||||
|
||||
@@ -8,7 +8,7 @@ version = "0.1.0"
|
||||
description = "Advanced Markdown engine for structured content"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "jsonpath-ng>=1.5.0", "aiohttp>=3.8.0"]
|
||||
dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "jsonpath-ng>=1.5.0", "aiohttp>=3.8.0", "toml"]
|
||||
|
||||
[project.scripts]
|
||||
markitect = "markitect.cli:main"
|
||||
|
||||
492
release.py
Executable file
492
release.py
Executable file
@@ -0,0 +1,492 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MarkiTect Release Management Tool
|
||||
|
||||
This script automates the release process for MarkiTect, including:
|
||||
- Version management and validation
|
||||
- Changelog generation
|
||||
- Git tagging and repository management
|
||||
- Package building and distribution
|
||||
- Release artifact creation
|
||||
|
||||
Usage:
|
||||
python release.py [command] [options]
|
||||
|
||||
Commands:
|
||||
prepare Prepare a new release (bump version, update changelog)
|
||||
build Build release packages
|
||||
tag Create git tag for release
|
||||
publish Publish release (build + tag + distribute)
|
||||
status Show current release status
|
||||
validate Validate current state for release
|
||||
|
||||
Options:
|
||||
--version VERSION Target version (e.g., 1.0.0, 1.0.1-rc1)
|
||||
--pre-release Mark as pre-release
|
||||
--dry-run Show what would be done without making changes
|
||||
--force Force operation even with warnings
|
||||
--help Show help message
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import tempfile
|
||||
|
||||
|
||||
class ReleaseManager:
|
||||
"""Manages the MarkiTect release process."""
|
||||
|
||||
def __init__(self, dry_run=False, force=False):
|
||||
self.dry_run = dry_run
|
||||
self.force = force
|
||||
self.project_root = Path(__file__).parent.absolute()
|
||||
self.pyproject_toml = self.project_root / "pyproject.toml"
|
||||
self.version_file = self.project_root / "markitect" / "__version__.py"
|
||||
self.changelog_file = self.project_root / "CHANGELOG.md"
|
||||
|
||||
def run_command(self, cmd: List[str], capture=True, check=True, skip_dry_run=False) -> subprocess.CompletedProcess:
|
||||
"""Run a command with optional dry-run support."""
|
||||
if self.dry_run and not skip_dry_run:
|
||||
print(f"[DRY RUN] Would run: {' '.join(cmd)}")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
return subprocess.run(cmd, capture_output=capture, text=True, check=check)
|
||||
|
||||
def get_current_version(self) -> str:
|
||||
"""Get current version from pyproject.toml."""
|
||||
with open(self.pyproject_toml, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
match = re.search(r'version\s*=\s*"([^"]+)"', content)
|
||||
if not match:
|
||||
raise ValueError("Could not find version in pyproject.toml")
|
||||
|
||||
return match.group(1)
|
||||
|
||||
def validate_version(self, version: str) -> bool:
|
||||
"""Validate version format (semantic versioning)."""
|
||||
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.?(\d+))?$'
|
||||
return bool(re.match(pattern, version))
|
||||
|
||||
def compare_versions(self, v1: str, v2: str) -> int:
|
||||
"""Compare two versions. Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
|
||||
def version_tuple(v):
|
||||
parts = v.split('-')[0].split('.')
|
||||
main = tuple(int(x) for x in parts)
|
||||
|
||||
if '-' in v:
|
||||
pre = v.split('-')[1]
|
||||
if 'alpha' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (0, pre_num)
|
||||
elif 'beta' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (1, pre_num)
|
||||
elif 'rc' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (2, pre_num)
|
||||
|
||||
return main + (3, 0) # Release version
|
||||
|
||||
t1, t2 = version_tuple(v1), version_tuple(v2)
|
||||
if t1 < t2:
|
||||
return -1
|
||||
elif t1 > t2:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def update_version(self, new_version: str):
|
||||
"""Update version in pyproject.toml and __version__.py."""
|
||||
print(f"📝 Updating version to {new_version}")
|
||||
|
||||
# Update pyproject.toml
|
||||
with open(self.pyproject_toml, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
new_content = re.sub(
|
||||
r'version\s*=\s*"[^"]+"',
|
||||
f'version = "{new_version}"',
|
||||
content
|
||||
)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.pyproject_toml, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
# Update __version__.py
|
||||
with open(self.version_file, 'r') as f:
|
||||
version_content = f.read()
|
||||
|
||||
new_version_content = re.sub(
|
||||
r'__version__\s*=\s*"[^"]+"',
|
||||
f'__version__ = "{new_version}"',
|
||||
version_content
|
||||
)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.version_file, 'w') as f:
|
||||
f.write(new_version_content)
|
||||
|
||||
def get_git_status(self) -> Dict[str, any]:
|
||||
"""Get current git repository status."""
|
||||
try:
|
||||
# Check if in git repo
|
||||
result = self.run_command(['git', 'rev-parse', '--git-dir'], skip_dry_run=True)
|
||||
|
||||
# Get current branch
|
||||
branch_result = self.run_command(['git', 'branch', '--show-current'], skip_dry_run=True)
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Check for uncommitted changes
|
||||
status_result = self.run_command(['git', 'status', '--porcelain'], skip_dry_run=True)
|
||||
has_changes = bool(status_result.stdout.strip())
|
||||
|
||||
# Get latest commit
|
||||
commit_result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], skip_dry_run=True)
|
||||
latest_commit = commit_result.stdout.strip()
|
||||
|
||||
# Get latest tag
|
||||
try:
|
||||
tag_result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'], skip_dry_run=True)
|
||||
latest_tag = tag_result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
latest_tag = None
|
||||
|
||||
return {
|
||||
'is_repo': True,
|
||||
'branch': current_branch,
|
||||
'has_changes': has_changes,
|
||||
'latest_commit': latest_commit,
|
||||
'latest_tag': latest_tag
|
||||
}
|
||||
except subprocess.CalledProcessError:
|
||||
return {'is_repo': False}
|
||||
|
||||
def generate_changelog_entry(self, version: str, since_tag: str = None) -> str:
|
||||
"""Generate changelog entry from git commits."""
|
||||
print(f"📋 Generating changelog for {version}")
|
||||
|
||||
# Get commits since last tag or all commits
|
||||
if since_tag:
|
||||
cmd = ['git', 'log', f'{since_tag}..HEAD', '--oneline', '--no-merges']
|
||||
else:
|
||||
cmd = ['git', 'log', '--oneline', '--no-merges']
|
||||
|
||||
try:
|
||||
result = self.run_command(cmd)
|
||||
commits = result.stdout.strip().split('\n') if result.stdout.strip() else []
|
||||
except subprocess.CalledProcessError:
|
||||
commits = []
|
||||
|
||||
# Categorize commits
|
||||
features = []
|
||||
fixes = []
|
||||
docs = []
|
||||
other = []
|
||||
|
||||
for commit in commits:
|
||||
if not commit:
|
||||
continue
|
||||
|
||||
commit_msg = commit.split(' ', 1)[1] if ' ' in commit else commit
|
||||
|
||||
if commit_msg.startswith(('feat:', 'feature:')):
|
||||
features.append(commit_msg)
|
||||
elif commit_msg.startswith(('fix:', 'bugfix:')):
|
||||
fixes.append(commit_msg)
|
||||
elif commit_msg.startswith(('docs:', 'doc:')):
|
||||
docs.append(commit_msg)
|
||||
else:
|
||||
other.append(commit_msg)
|
||||
|
||||
# Generate changelog entry
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
entry = f"## [{version}] - {date}\n\n"
|
||||
|
||||
if features:
|
||||
entry += "### Added\n"
|
||||
for feat in features:
|
||||
entry += f"- {feat}\n"
|
||||
entry += "\n"
|
||||
|
||||
if fixes:
|
||||
entry += "### Fixed\n"
|
||||
for fix in fixes:
|
||||
entry += f"- {fix}\n"
|
||||
entry += "\n"
|
||||
|
||||
if docs:
|
||||
entry += "### Documentation\n"
|
||||
for doc in docs:
|
||||
entry += f"- {doc}\n"
|
||||
entry += "\n"
|
||||
|
||||
if other:
|
||||
entry += "### Other\n"
|
||||
for oth in other:
|
||||
entry += f"- {oth}\n"
|
||||
entry += "\n"
|
||||
|
||||
return entry
|
||||
|
||||
def update_changelog(self, version: str, since_tag: str = None):
|
||||
"""Update CHANGELOG.md with new version entry."""
|
||||
entry = self.generate_changelog_entry(version, since_tag)
|
||||
|
||||
# Read existing changelog or create new one
|
||||
if self.changelog_file.exists():
|
||||
with open(self.changelog_file, 'r') as f:
|
||||
existing_content = f.read()
|
||||
else:
|
||||
existing_content = "# Changelog\n\nAll notable changes to MarkiTect will be documented in this file.\n\n"
|
||||
|
||||
# Insert new entry after header
|
||||
lines = existing_content.split('\n')
|
||||
header_end = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('## [') or (i > 0 and not line.startswith('#')):
|
||||
header_end = i
|
||||
break
|
||||
|
||||
new_lines = lines[:header_end] + entry.split('\n') + lines[header_end:]
|
||||
new_content = '\n'.join(new_lines)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.changelog_file, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
def validate_release_state(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate that the repository is ready for release."""
|
||||
issues = []
|
||||
|
||||
git_status = self.get_git_status()
|
||||
|
||||
if not git_status['is_repo']:
|
||||
issues.append("Not in a git repository")
|
||||
else:
|
||||
if git_status['has_changes'] and not self.force:
|
||||
issues.append("Repository has uncommitted changes")
|
||||
|
||||
if git_status['branch'] != 'main' and not self.force:
|
||||
issues.append(f"Not on main branch (currently on {git_status['branch']})")
|
||||
|
||||
# Check if tests pass (skip for dry run)
|
||||
if not self.dry_run:
|
||||
try:
|
||||
print("🧪 Running tests...")
|
||||
test_result = self.run_command(['make', 'test'], capture=False)
|
||||
if test_result.returncode != 0:
|
||||
issues.append("Tests are failing")
|
||||
except subprocess.CalledProcessError:
|
||||
issues.append("Could not run tests (make test failed)")
|
||||
except FileNotFoundError:
|
||||
# Try pytest directly
|
||||
try:
|
||||
test_result = self.run_command(['python', '-m', 'pytest'])
|
||||
if test_result.returncode != 0:
|
||||
issues.append("Tests are failing")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
issues.append("Could not run tests")
|
||||
else:
|
||||
print("🧪 Skipping tests in dry run mode")
|
||||
|
||||
return len(issues) == 0, issues
|
||||
|
||||
def build_packages(self, version: str):
|
||||
"""Build release packages."""
|
||||
print(f"📦 Building packages for version {version}")
|
||||
|
||||
# Clean previous builds
|
||||
build_dirs = ['build', 'dist', '*.egg-info']
|
||||
for pattern in build_dirs:
|
||||
self.run_command(['rm', '-rf'] + [str(self.project_root / pattern)])
|
||||
|
||||
# Build source distribution
|
||||
print("Building source distribution...")
|
||||
self.run_command(['python', '-m', 'build', '--sdist'], capture=False)
|
||||
|
||||
# Build wheel
|
||||
print("Building wheel...")
|
||||
self.run_command(['python', '-m', 'build', '--wheel'], capture=False)
|
||||
|
||||
print("✅ Packages built successfully")
|
||||
|
||||
def create_git_tag(self, version: str, message: str = None):
|
||||
"""Create and push git tag."""
|
||||
tag_name = f"v{version}"
|
||||
tag_message = message or f"Release {version}"
|
||||
|
||||
print(f"🏷️ Creating git tag {tag_name}")
|
||||
|
||||
# Create annotated tag
|
||||
self.run_command(['git', 'tag', '-a', tag_name, '-m', tag_message])
|
||||
|
||||
# Push tag
|
||||
print(f"📤 Pushing tag to origin...")
|
||||
self.run_command(['git', 'push', 'origin', tag_name])
|
||||
|
||||
def show_status(self):
|
||||
"""Show current release status."""
|
||||
print("🔍 MarkiTect Release Status")
|
||||
print("=" * 50)
|
||||
|
||||
current_version = self.get_current_version()
|
||||
print(f"Current Version: {current_version}")
|
||||
|
||||
git_status = self.get_git_status()
|
||||
if git_status['is_repo']:
|
||||
print(f"Git Branch: {git_status['branch']}")
|
||||
print(f"Latest Commit: {git_status['latest_commit']}")
|
||||
print(f"Latest Tag: {git_status['latest_tag'] or 'None'}")
|
||||
print(f"Uncommitted Changes: {'Yes' if git_status['has_changes'] else 'No'}")
|
||||
else:
|
||||
print("Git Repository: Not available")
|
||||
|
||||
# Check build tools
|
||||
print("\nBuild Tools:")
|
||||
try:
|
||||
self.run_command(['python', '-m', 'build', '--help'])
|
||||
print("✅ build module available")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("❌ build module not available (pip install build)")
|
||||
|
||||
# Check if packages exist
|
||||
dist_dir = self.project_root / "dist"
|
||||
if dist_dir.exists():
|
||||
packages = list(dist_dir.glob("*"))
|
||||
print(f"\nExisting Packages: {len(packages)} files in dist/")
|
||||
for pkg in packages:
|
||||
print(f" - {pkg.name}")
|
||||
else:
|
||||
print("\nExisting Packages: None")
|
||||
|
||||
def prepare_release(self, version: str, pre_release: bool = False):
|
||||
"""Prepare a new release."""
|
||||
print(f"🚀 Preparing release {version}")
|
||||
|
||||
# Validate version format
|
||||
if not self.validate_version(version):
|
||||
raise ValueError(f"Invalid version format: {version}")
|
||||
|
||||
# Check if version is newer than current
|
||||
current_version = self.get_current_version()
|
||||
if self.compare_versions(version, current_version) <= 0 and not self.force:
|
||||
raise ValueError(f"New version {version} must be greater than current {current_version}")
|
||||
|
||||
# Validate release state
|
||||
is_valid, issues = self.validate_release_state()
|
||||
if not is_valid:
|
||||
print("❌ Release validation failed:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
if not self.force:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("⚠️ Continuing with --force flag")
|
||||
|
||||
# Update version
|
||||
self.update_version(version)
|
||||
|
||||
# Update changelog
|
||||
git_status = self.get_git_status()
|
||||
since_tag = git_status.get('latest_tag') if git_status['is_repo'] else None
|
||||
self.update_changelog(version, since_tag)
|
||||
|
||||
print(f"✅ Release {version} prepared successfully")
|
||||
print("Next steps:")
|
||||
print("1. Review and edit CHANGELOG.md if needed")
|
||||
print("2. Commit changes: git add -A && git commit -m 'Prepare release {version}'")
|
||||
print("3. Run: python release.py publish --version {version}")
|
||||
|
||||
def publish_release(self, version: str):
|
||||
"""Publish a complete release."""
|
||||
print(f"📢 Publishing release {version}")
|
||||
|
||||
# Validate state
|
||||
is_valid, issues = self.validate_release_state()
|
||||
if not is_valid and not self.force:
|
||||
print("❌ Cannot publish release due to validation issues:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
sys.exit(1)
|
||||
|
||||
# Build packages
|
||||
self.build_packages(version)
|
||||
|
||||
# Create git tag
|
||||
self.create_git_tag(version)
|
||||
|
||||
print(f"✅ Release {version} published successfully!")
|
||||
print(f"📦 Packages available in dist/")
|
||||
print(f"🏷️ Git tag v{version} created and pushed")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="MarkiTect Release Management Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__.split('\n\n')[1]
|
||||
)
|
||||
|
||||
parser.add_argument('command', choices=['prepare', 'build', 'tag', 'publish', 'status', 'validate'],
|
||||
help='Release command to execute')
|
||||
parser.add_argument('--version', type=str, help='Target version (e.g., 1.0.0)')
|
||||
parser.add_argument('--pre-release', action='store_true', help='Mark as pre-release')
|
||||
parser.add_argument('--dry-run', action='store_true', help='Show what would be done')
|
||||
parser.add_argument('--force', action='store_true', help='Force operation despite warnings')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
manager = ReleaseManager(dry_run=args.dry_run, force=args.force)
|
||||
|
||||
try:
|
||||
if args.command == 'status':
|
||||
manager.show_status()
|
||||
|
||||
elif args.command == 'validate':
|
||||
is_valid, issues = manager.validate_release_state()
|
||||
if is_valid:
|
||||
print("✅ Repository is ready for release")
|
||||
else:
|
||||
print("❌ Release validation failed:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == 'prepare':
|
||||
if not args.version:
|
||||
print("❌ --version is required for prepare command")
|
||||
sys.exit(1)
|
||||
manager.prepare_release(args.version, args.pre_release)
|
||||
|
||||
elif args.command == 'build':
|
||||
version = args.version or manager.get_current_version()
|
||||
manager.build_packages(version)
|
||||
|
||||
elif args.command == 'tag':
|
||||
if not args.version:
|
||||
print("❌ --version is required for tag command")
|
||||
sys.exit(1)
|
||||
manager.create_git_tag(args.version)
|
||||
|
||||
elif args.command == 'publish':
|
||||
if not args.version:
|
||||
print("❌ --version is required for publish command")
|
||||
sys.exit(1)
|
||||
manager.publish_release(args.version)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -67,8 +67,13 @@ This is test content.
|
||||
|
||||
def test_cache_stats_shows_total_files_count(self):
|
||||
"""RED: cache-stats should show count of cached files."""
|
||||
# Create cache with known files
|
||||
cache = ASTCache(self.cache_dir)
|
||||
# Clean existing cache first
|
||||
self.runner.invoke(cli, ['cache-clean'])
|
||||
|
||||
# Create cache with known files using the project's default cache location
|
||||
from pathlib import Path
|
||||
project_cache_dir = Path.cwd() / ".ast_cache"
|
||||
cache = ASTCache(project_cache_dir)
|
||||
cache.cache_file(self.test_file)
|
||||
|
||||
result = self.runner.invoke(cli, ['cache-stats'])
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestSchemaVisualization:
|
||||
"""Test that emoji mode produces expected output format."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(sample_markdown_file)
|
||||
sys.executable, 'tools/visualize_schema.py', str(sample_markdown_file)
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -106,7 +106,7 @@ class TestSchemaVisualization:
|
||||
"""Test that ASCII mode produces expected output format."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(sample_markdown_file), '--ascii'
|
||||
sys.executable, 'tools/visualize_schema.py', str(sample_markdown_file), '--ascii'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -166,7 +166,7 @@ class TestSchemaVisualization:
|
||||
"""Test that depth limitation works correctly."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py',
|
||||
sys.executable, 'tools/visualize_schema.py',
|
||||
str(sample_markdown_file), '--max-depth', '2'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
@@ -187,7 +187,7 @@ class TestSchemaVisualization:
|
||||
"""Test that schema summary emoji mode produces expected format."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'schema_summary.py', str(sample_markdown_file)
|
||||
sys.executable, 'tools/schema_summary.py', str(sample_markdown_file)
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -218,7 +218,7 @@ class TestSchemaVisualization:
|
||||
"""Test that schema summary ASCII mode produces expected format."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'schema_summary.py', str(sample_markdown_file), '--ascii'
|
||||
sys.executable, 'tools/schema_summary.py', str(sample_markdown_file), '--ascii'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -257,12 +257,12 @@ class TestSchemaVisualization:
|
||||
try:
|
||||
# Test emoji mode
|
||||
result_emoji = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(sample_markdown_file)
|
||||
sys.executable, 'tools/visualize_schema.py', str(sample_markdown_file)
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
# Test ASCII mode
|
||||
result_ascii = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(sample_markdown_file), '--ascii'
|
||||
sys.executable, 'tools/visualize_schema.py', str(sample_markdown_file), '--ascii'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result_emoji.returncode == 0
|
||||
@@ -289,7 +289,7 @@ class TestSchemaVisualization:
|
||||
# Test both modes
|
||||
for args in [[], ['--ascii']]:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(sample_markdown_file)
|
||||
sys.executable, 'tools/visualize_schema.py', str(sample_markdown_file)
|
||||
] + args, capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -318,7 +318,7 @@ class TestSchemaVisualization:
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(empty_file)
|
||||
sys.executable, 'tools/visualize_schema.py', str(empty_file)
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
# Should handle empty file gracefully
|
||||
@@ -335,7 +335,7 @@ class TestSchemaVisualization:
|
||||
def test_visualization_error_handling(self):
|
||||
"""Test error handling for non-existent files."""
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', 'nonexistent_file.md'
|
||||
sys.executable, 'tools/visualize_schema.py', 'nonexistent_file.md'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 1
|
||||
@@ -344,7 +344,7 @@ class TestSchemaVisualization:
|
||||
def test_help_output_format(self):
|
||||
"""Test help output contains expected information."""
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', '--help'
|
||||
sys.executable, 'tools/visualize_schema.py', '--help'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -369,7 +369,7 @@ class TestOutputConsistency:
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(test_file), '--ascii'
|
||||
sys.executable, 'tools/visualize_schema.py', str(test_file), '--ascii'
|
||||
], capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
@@ -393,7 +393,7 @@ class TestOutputConsistency:
|
||||
try:
|
||||
for mode_args in [[], ['--ascii']]:
|
||||
result = subprocess.run([
|
||||
sys.executable, 'visualize_schema.py', str(test_file)
|
||||
sys.executable, 'tools/visualize_schema.py', str(test_file)
|
||||
] + mode_args, capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
||||
|
||||
assert result.returncode == 0
|
||||
|
||||
Reference in New Issue
Block a user