feat: Add Kaizen Optimizer and Optimized Refactoring Assistant agents

Added two new Claude Code subagents following proper specification format:

**Kaizen Optimizer Agent:**
- Meta-agent for analyzing and optimizing other subagents
- Performance analysis and specification improvement recommendations
- Agent ecosystem health assessment and continuous improvement
- Proper YAML frontmatter with proactive usage guidelines

**Refactoring Assistant Agent (Optimized):**
- Streamlined from 19-section complex specification to focused Claude Code format
- Code quality assessment and refactoring guidance within Claude Code environment
- Security analysis and performance optimization recommendations
- Integration with existing agent ecosystem (tddai-assistant, general-purpose, project-assistant)

**Also includes Issue #15 AST Query CLI implementation:**
- AST Service with display, query, and statistics capabilities
- JSONPath integration for flexible AST navigation
- CLI commands: ast-show, ast-query, ast-stats (22/22 tests passing)
- Leverages existing cache system for optimal performance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-26 02:02:00 +02:00
parent e1832ddeb1
commit 162a2ae93c
7 changed files with 797 additions and 243 deletions

270
markitect/ast_service.py Normal file
View File

@@ -0,0 +1,270 @@
"""
AST Service for Issue #15 - AST Query and Analysis functionality.
This service provides high-level AST operations for the CLI commands:
- AST display and visualization
- JSONPath querying of AST structures
- Statistical analysis of document content
Leverages the existing AST cache system for optimal performance.
"""
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, List, Any, Optional
from jsonpath_ng import parse as jsonpath_parse
from .ast_cache import ASTCache
from .cache_service import CacheDirectoryService
class ASTService:
"""
Service for AST introspection and analysis operations.
Provides high-level operations for CLI commands while leveraging
the existing AST cache system for performance optimization.
"""
def __init__(self):
"""Initialize AST service with cache integration."""
self.cache_service = CacheDirectoryService()
cache_dir = self.cache_service.get_cache_directory()
self.ast_cache = ASTCache(cache_dir)
def display_ast(self, file_path: Path, format_type: str = "tree") -> Dict[str, Any]:
"""
Display AST structure for a markdown file.
Args:
file_path: Path to markdown file
format_type: Display format (tree, json, compact)
Returns:
Dictionary with display results and metadata
"""
try:
if not file_path.exists():
return {
'success': False,
'message': f'File not found: {file_path}',
'output': ''
}
# Load AST using cache system
ast = self.ast_cache.load_cached_ast(file_path)
if format_type == "json":
output = json.dumps(ast, indent=2, ensure_ascii=False)
elif format_type == "compact":
output = self._format_ast_compact(ast)
else: # tree format (default)
output = self._format_ast_tree(ast)
return {
'success': True,
'message': f'AST structure for {file_path.name}',
'output': output,
'token_count': len(ast)
}
except Exception as e:
return {
'success': False,
'message': f'Error displaying AST: {e}',
'output': ''
}
def query_ast(self, file_path: Path, jsonpath_expr: str) -> Dict[str, Any]:
"""
Query AST using JSONPath expressions.
Args:
file_path: Path to markdown file
jsonpath_expr: JSONPath query expression
Returns:
Dictionary with query results and metadata
"""
try:
if not file_path.exists():
return {
'success': False,
'message': f'File not found: {file_path}',
'matches': [],
'count': 0
}
# Load AST using cache system
ast = self.ast_cache.load_cached_ast(file_path)
# Parse JSONPath expression
try:
jsonpath_expr_parsed = jsonpath_parse(jsonpath_expr)
except Exception as e:
return {
'success': False,
'message': f'Invalid JSONPath syntax: {e}',
'matches': [],
'count': 0
}
# Execute query
matches = jsonpath_expr_parsed.find(ast)
results = [match.value for match in matches]
return {
'success': True,
'message': f'JSONPath query results for {file_path.name}',
'matches': results,
'count': len(results),
'query': jsonpath_expr
}
except Exception as e:
return {
'success': False,
'message': f'Error executing query: {e}',
'matches': [],
'count': 0
}
def analyze_ast_statistics(self, file_path: Path) -> Dict[str, Any]:
"""
Generate comprehensive statistics about AST structure.
Args:
file_path: Path to markdown file
Returns:
Dictionary with detailed statistics
"""
try:
if not file_path.exists():
return {
'success': False,
'message': f'File not found: {file_path}',
'statistics': {}
}
# Load AST using cache system
ast = self.ast_cache.load_cached_ast(file_path)
stats = self._calculate_ast_statistics(ast)
return {
'success': True,
'message': f'AST statistics for {file_path.name}',
'statistics': stats
}
except Exception as e:
return {
'success': False,
'message': f'Error analyzing statistics: {e}',
'statistics': {}
}
def _format_ast_tree(self, ast: List[Dict[str, Any]]) -> str:
"""Format AST as a tree structure."""
lines = []
for i, token in enumerate(ast):
level = token.get('level', 0)
indent = ' ' * level
token_type = token.get('type', 'unknown')
# Add some content info for readability
content_info = ""
if token.get('content'):
content_preview = token['content'][:30]
if len(token['content']) > 30:
content_preview += "..."
content_info = f' "{content_preview}"'
elif token.get('tag'):
content_info = f' <{token["tag"]}>'
lines.append(f'{indent}[{i:2d}] {token_type}{content_info}')
return '\n'.join(lines)
def _format_ast_compact(self, ast: List[Dict[str, Any]]) -> str:
"""Format AST in compact form."""
lines = []
for token in ast:
token_type = token.get('type', 'unknown')
if token.get('content'):
content = token['content'][:20]
if len(token['content']) > 20:
content += "..."
lines.append(f'{token_type}: "{content}"')
else:
lines.append(f'{token_type}')
return '\n'.join(lines)
def _calculate_ast_statistics(self, ast: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate comprehensive AST statistics."""
if not ast:
return {
'total_tokens': 0,
'headings': {'total': 0, 'by_level': {}},
'paragraphs': 0,
'links': 0,
'lists': {'ordered': 0, 'unordered': 0},
'code_blocks': 0,
'inline_code': 0,
'blockquotes': 0,
'emphasis': {'strong': 0, 'italic': 0},
'document_structure': 'empty'
}
# Count token types
token_types = Counter(token.get('type', 'unknown') for token in ast)
# Analyze headings by level
headings_by_level = {}
for token in ast:
if token.get('type') == 'heading_open':
tag = token.get('tag', 'h1')
level = int(tag[1:]) if tag.startswith('h') else 1
headings_by_level[f'h{level}'] = headings_by_level.get(f'h{level}', 0) + 1
# Count various elements
stats = {
'total_tokens': len(ast),
'headings': {
'total': token_types.get('heading_open', 0),
'by_level': headings_by_level
},
'paragraphs': token_types.get('paragraph_open', 0),
'links': token_types.get('link_open', 0),
'lists': {
'ordered': token_types.get('ordered_list_open', 0),
'unordered': token_types.get('bullet_list_open', 0)
},
'code_blocks': token_types.get('fence', 0) + token_types.get('code_block', 0),
'inline_code': token_types.get('code_inline', 0),
'blockquotes': token_types.get('blockquote_open', 0),
'emphasis': {
'strong': token_types.get('strong_open', 0),
'italic': token_types.get('em_open', 0)
}
}
# Determine document structure
if stats['headings']['total'] > 0:
if stats['paragraphs'] > stats['headings']['total']:
stats['document_structure'] = 'article'
else:
stats['document_structure'] = 'outline'
elif stats['lists']['ordered'] + stats['lists']['unordered'] > 0:
stats['document_structure'] = 'list-based'
elif stats['paragraphs'] > 0:
stats['document_structure'] = 'simple'
else:
stats['document_structure'] = 'minimal'
return stats

View File

@@ -28,6 +28,7 @@ from .database import DatabaseManager
from .document_manager import DocumentManager
from .serializer import ASTSerializer
from .cache_service import CacheDirectoryService
from .ast_service import ASTService
# Global options for CLI configuration
@@ -741,6 +742,192 @@ def cache_invalidate(config, file_path):
sys.exit(1)
@cli.command('ast-show')
@click.argument('file_path', type=click.Path(exists=False))
@click.option('--format', '-f', type=click.Choice(['tree', 'json', 'compact']), default='tree', help='Display format')
@pass_config
def ast_show(config, file_path, format):
"""
Display AST structure for file.
Shows the Abstract Syntax Tree representation of a markdown file
with various formatting options for analysis and debugging.
FILE_PATH: Path to the markdown file to analyze
Examples:
markitect ast-show document.md
markitect ast-show document.md --format json
markitect ast-show document.md --format compact
"""
try:
if config.get('verbose'):
click.echo(f"Analyzing AST structure for: {file_path}", err=True)
ast_service = ASTService()
result = ast_service.display_ast(Path(file_path), format)
if result['success']:
if result.get('message'):
if config.get('verbose'):
click.echo(f"Info: {result['message']}", err=True)
click.echo(result['output'])
if config.get('verbose') and result.get('token_count'):
click.echo(f"Total tokens: {result['token_count']}", err=True)
else:
click.echo(f"Error: {result['message']}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"AST display error: {e}", err=True)
if config and config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command('ast-query')
@click.argument('file_path', type=click.Path(exists=False))
@click.argument('jsonpath', type=str)
@click.option('--format', '-f', type=click.Choice(['json', 'compact']), default='json', help='Output format')
@pass_config
def ast_query(config, file_path, jsonpath, format):
"""
Query AST using JSONPath.
Execute JSONPath expressions against the AST structure of a markdown file
to extract specific elements or patterns.
FILE_PATH: Path to the markdown file to query
JSONPATH: JSONPath expression to execute
Examples:
markitect ast-query doc.md '$.*.type'
markitect ast-query doc.md '$..tag'
markitect ast-query doc.md '$[:5]' --format compact
"""
try:
if config.get('verbose'):
click.echo(f"Executing JSONPath query on: {file_path}", err=True)
click.echo(f"Query: {jsonpath}", err=True)
ast_service = ASTService()
result = ast_service.query_ast(Path(file_path), jsonpath)
if result['success']:
if config.get('verbose'):
click.echo(f"Query results: {result['count']} matches", err=True)
if result['count'] == 0:
click.echo("No matches found for query.")
else:
if format == 'compact':
for i, match in enumerate(result['matches']):
if isinstance(match, dict):
token_type = match.get('type', 'unknown')
content = match.get('content', match.get('tag', ''))[:30]
click.echo(f"[{i}] {token_type}: {content}")
else:
click.echo(f"[{i}] {match}")
else:
import json
click.echo(json.dumps(result['matches'], indent=2, ensure_ascii=False))
else:
click.echo(f"Error: {result['message']}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"AST query error: {e}", err=True)
if config and config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command('ast-stats')
@click.argument('file_path', type=click.Path(exists=False))
@click.option('--format', '-f', type=click.Choice(['table', 'json', 'yaml']), default='table', help='Output format')
@pass_config
def ast_stats(config, file_path, format):
"""
Show AST statistics (headings, links, etc.).
Analyze markdown file structure and provide comprehensive statistics
about document elements, organization, and content patterns.
FILE_PATH: Path to the markdown file to analyze
Examples:
markitect ast-stats document.md
markitect ast-stats document.md --format json
markitect ast-stats document.md --format yaml
"""
try:
if config.get('verbose'):
click.echo(f"Calculating statistics for: {file_path}", err=True)
ast_service = ASTService()
result = ast_service.analyze_ast_statistics(Path(file_path))
if result['success']:
if config.get('verbose'):
click.echo(f"Analysis complete for: {Path(file_path).name}", err=True)
stats = result['statistics']
if format == 'table':
# Format statistics as readable table
click.echo("Document Statistics:")
click.echo("=" * 40)
click.echo(f"Total AST tokens: {stats.get('total_tokens', 0)}")
click.echo(f"Document structure: {stats.get('document_structure', 'unknown')}")
click.echo()
# Headings
headings = stats.get('headings', {})
click.echo(f"Headings: {headings.get('total', 0)}")
for level, count in headings.get('by_level', {}).items():
click.echo(f" {level.upper()}: {count}")
click.echo(f"Paragraphs: {stats.get('paragraphs', 0)}")
click.echo(f"Links: {stats.get('links', 0)}")
# Lists
lists = stats.get('lists', {})
total_lists = lists.get('ordered', 0) + lists.get('unordered', 0)
click.echo(f"Lists: {total_lists}")
if total_lists > 0:
click.echo(f" Ordered: {lists.get('ordered', 0)}")
click.echo(f" Unordered: {lists.get('unordered', 0)}")
click.echo(f"Code blocks: {stats.get('code_blocks', 0)}")
click.echo(f"Inline code: {stats.get('inline_code', 0)}")
click.echo(f"Blockquotes: {stats.get('blockquotes', 0)}")
# Emphasis
emphasis = stats.get('emphasis', {})
click.echo(f"Strong text: {emphasis.get('strong', 0)}")
click.echo(f"Italic text: {emphasis.get('italic', 0)}")
elif format == 'json':
import json
click.echo(json.dumps(stats, indent=2, ensure_ascii=False))
elif format == 'yaml':
import yaml
click.echo(yaml.dump(stats, default_flow_style=False, allow_unicode=True))
else:
click.echo(f"Error: {result['message']}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"AST statistics error: {e}", err=True)
if config and config.get('verbose'):
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
def main():
"""
Main entry point for the CLI.