diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 412cef9e67..7ff2685a6b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,16 +19,16 @@ jobs: (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: - contents: read - pull-requests: read - issues: read + contents: write # Allow commits + pull-requests: write # Allow PR creation + issues: write # Allow issue comments id-token: write - actions: read # Required for Claude to read CI results on PRs + actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 1 + fetch-depth: 0 # Full history for better context - name: Run Claude Code id: claude diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml new file mode 100644 index 0000000000..ec01449950 --- /dev/null +++ b/.github/workflows/quality-check.yml @@ -0,0 +1,120 @@ +name: Quality Check + +on: + workflow_run: + workflows: ["Test and Generate Previews"] + types: [completed] + +jobs: + quality-check: + name: AI Quality Evaluation + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download preview metadata + uses: actions/download-artifact@v4 + with: + name: preview-metadata + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Load metadata + id: metadata + run: | + if [ ! -f preview_metadata.json ]; then + echo "No preview metadata found, skipping quality check" + exit 0 + fi + + PR_NUMBER=$(jq -r '.pr_number' preview_metadata.json) + BUCKET=$(jq -r '.bucket' preview_metadata.json) + BASE_PATH=$(jq -r '.base_path' preview_metadata.json) + CHANGED_FILES=$(jq -r '.changed_files[]' preview_metadata.json | tr '\n' ' ') + + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "bucket=$BUCKET" >> $GITHUB_OUTPUT + echo "base_path=$BASE_PATH" >> $GITHUB_OUTPUT + echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT + + - name: Setup Google Cloud authentication + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCS_CREDENTIALS }} + + - name: Download preview images + run: | + mkdir -p preview_images + gsutil -m cp -r "gs://${{ steps.metadata.outputs.bucket }}/${{ steps.metadata.outputs.base_path }}/*" preview_images/ || echo "No images to download" + + - name: Trigger quality check with @claude + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.metadata.outputs.pr_number }}; + const bucket = '${{ steps.metadata.outputs.bucket }}'; + const basePath = '${{ steps.metadata.outputs.base_path }}'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `@claude + + ## šŸŽÆ Task: Quality Evaluation + + Preview images have been generated and uploaded to GCS. Please evaluate the implementations. + + ### Instructions + + 1. **Preview images location** + - Already downloaded to \`preview_images/\` directory in this repository + - GCS Bucket: \`${bucket}\` + - GCS Path: \`${basePath}/\` + + 2. **For each preview image:** + - Parse filename to extract: spec_id, library, variant (format: \`{spec_id}_{library}_{variant}.png\`) + - Read corresponding spec file: \`specs/{spec_id}.md\` + - View the preview image carefully using your vision capabilities + - Evaluate against quality criteria listed in the spec + + 3. **Check each quality criterion:** + - Does it meet ALL quality criteria from the spec? + - Are visual elements clear and readable? + - Are colors appropriate and accessible? + - Is the layout well-structured? + - Score: 0-100 (≄85 to pass) + + 4. **Generate quality report with:** + - Overall verdict (PASS if all implementations score ≄85, FAIL otherwise) + - Score for each implementation (matplotlib and seaborn separately) + - Specific feedback for each quality criterion (met/failed) + - Strengths and improvements needed + - Concrete, actionable suggestions if FAIL + + 5. **Update this PR:** + - Post quality report as comment on this PR + - Use \`gh pr edit\` to add appropriate label: + - "quality-approved" if PASS + - "quality-check-failed" if FAIL + + ### Scoring Guidelines + - 90-100: Excellent - All criteria met, production ready + - 85-89: Good - Minor issues, acceptable + - 75-84: Needs improvement - regeneration recommended + - <75: Rejected - Major issues, regeneration required + + Be objective and constructive in your feedback. Focus on measurable criteria. + + Preview images are ready - please proceed with the evaluation!` + }); diff --git a/.github/workflows/spec-to-code.yml b/.github/workflows/spec-to-code.yml new file mode 100644 index 0000000000..d1b2d5cb25 --- /dev/null +++ b/.github/workflows/spec-to-code.yml @@ -0,0 +1,104 @@ +name: Generate Code from Spec + +on: + issues: + types: [labeled] + +jobs: + trigger-claude-code: + # Only run when "approved" label is added + if: github.event.label.name == 'approved' + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Extract spec ID from issue + id: extract_spec + env: + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_TITLE: ${{ github.event.issue.title }} + run: | + # Try to extract spec ID from title (format: "scatter-basic-001: Title" or just "scatter-basic-001") + # Case-insensitive, allows 3-4 digits, converts to lowercase + SPEC_ID=$(echo "$ISSUE_TITLE" | grep -oiP '^[a-z]+-[a-z]+-\d{3,4}' | tr '[:upper:]' '[:lower:]' || echo "") + + if [ -z "$SPEC_ID" ]; then + # Try to find spec ID in body (look for markdown heading like "# scatter-basic-001:") + SPEC_ID=$(echo "$ISSUE_BODY" | grep -oiP '^#\s*\K[a-z]+-[a-z]+-\d{3,4}' | tr '[:upper:]' '[:lower:]' || echo "") + fi + + if [ -z "$SPEC_ID" ]; then + echo "āŒ Could not extract spec ID from issue" + echo "Expected format: {type}-{variant}-{001-9999}" + exit 1 + fi + + # Validate spec file exists + if [ ! -f "specs/${SPEC_ID}.md" ]; then + echo "āš ļø Warning: Spec file specs/${SPEC_ID}.md does not exist" + echo "Please ensure the spec file is created before code generation" + fi + + echo "spec_id=$SPEC_ID" >> $GITHUB_OUTPUT + echo "āœ… Extracted spec ID: $SPEC_ID" + + - name: Trigger Claude Code with @claude comment + uses: actions/github-script@v7 + with: + script: | + const specId = '${{ steps.extract_spec.outputs.spec_id }}'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@claude + + ## šŸŽÆ Task: Generate Plot Implementations + + This issue has been approved! Please generate implementations for **${specId}**. + + ### Instructions + + 1. **Read the specification** + - File: \`specs/${specId}.md\` + - If file doesn't exist, create it from the issue body + + 2. **Read generation rules** + - \`rules/generation/v1.0.0-draft/code-generation-rules.md\` + - \`rules/generation/v1.0.0-draft/quality-criteria.md\` + - \`rules/generation/v1.0.0-draft/self-review-checklist.md\` + + 3. **Generate matplotlib implementation** + - Target: \`plots/matplotlib/scatter/${specId}/default.py\` + - Follow generation rules exactly + - Implement all spec requirements + - Use type hints, docstrings, validation + - Add standalone execution block (\`if __name__ == '__main__':\`) + - Self-review: Max 3 attempts to pass quality criteria + + 4. **Generate seaborn implementation** + - Target: \`plots/seaborn/scatterplot/${specId}/default.py\` + - Same requirements as matplotlib + + 5. **Create Pull Request** + - Branch: \`auto/${specId}\` + - Title: \`feat: implement ${specId}\` + - Include summary of generation results + - Link to this issue + + ### Requirements + - Each implementation must pass self-review before moving to next + - Use deterministic sample data (fixed seeds or hardcoded data) + - Code must work standalone (no external files) + - Follow Python 3.10+ syntax + - Commit with clear messages + + ### Expected Outcome + - PR created with matplotlib and seaborn implementations + - Both pass self-review + - Tests will run automatically on PR + + Please proceed with the generation!` + }); diff --git a/.github/workflows/test-and-preview.yml b/.github/workflows/test-and-preview.yml new file mode 100644 index 0000000000..346c8f26bb --- /dev/null +++ b/.github/workflows/test-and-preview.yml @@ -0,0 +1,216 @@ +name: Test and Generate Previews + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'plots/**/*.py' + - 'specs/**/*.md' + - 'tests/**/*.py' + +jobs: + test: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13'] + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync --all-extras + + - name: Run tests + run: | + uv run pytest tests/ -v --cov=plots --cov-report=xml --cov-report=term + + - name: Upload coverage + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + + generate-previews: + name: Generate Preview Images + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync --all-extras + + - name: Find changed plot files + id: changed_plots + run: | + # Get list of changed Python files in plots/ + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^plots/.*\.py$' || echo "") + + if [ -z "$CHANGED_FILES" ]; then + echo "No plot files changed" + echo "changed_files=" >> $GITHUB_OUTPUT + exit 0 + fi + + # Convert to JSON array + CHANGED_ARRAY=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "changed_files=$CHANGED_ARRAY" >> $GITHUB_OUTPUT + echo "Changed files: $CHANGED_ARRAY" + + - name: Generate preview images + if: steps.changed_plots.outputs.changed_files != '' + run: | + mkdir -p preview_outputs + + # Parse changed files and generate previews + echo '${{ steps.changed_plots.outputs.changed_files }}' | jq -r '.[]' | while read -r file; do + echo "šŸ“Š Generating preview for: $file" + + # Extract spec_id, library, variant from path + # Format: plots/{library}/{plot_type}/{spec_id}/{variant}.py + if [[ $file =~ ^plots/([^/]+)/([^/]+)/([^/]+)/([^/]+)\.py$ ]]; then + LIBRARY="${BASH_REMATCH[1]}" + PLOT_TYPE="${BASH_REMATCH[2]}" + SPEC_ID="${BASH_REMATCH[3]}" + VARIANT="${BASH_REMATCH[4]}" + else + echo "āš ļø Invalid file path format: $file" + echo "Expected: plots/{library}/{plot_type}/{spec_id}/{variant}.py" + continue + fi + + # Run the plot script to generate image + OUTPUT_FILE="preview_outputs/${SPEC_ID}_${LIBRARY}_${VARIANT}.png" + + # Execute with matplotlib backend set to Agg (non-interactive) + MPLBACKEND=Agg uv run python "$file" 2>&1 | tee "preview_outputs/${SPEC_ID}_${LIBRARY}_${VARIANT}.log" + + # Check if preview was generated (look for common output filenames) + if [ -f "test_output.png" ]; then + mv test_output.png "$OUTPUT_FILE" + echo "āœ… Preview generated: $OUTPUT_FILE" + elif [ -f "output.png" ]; then + mv output.png "$OUTPUT_FILE" + echo "āœ… Preview generated: $OUTPUT_FILE" + elif [ -f "${SPEC_ID}.png" ]; then + mv "${SPEC_ID}.png" "$OUTPUT_FILE" + echo "āœ… Preview generated: $OUTPUT_FILE" + else + echo "āš ļø No preview image found for $file" + fi + done + + - name: Setup Google Cloud authentication + if: steps.changed_plots.outputs.changed_files != '' + id: gcs_auth + continue-on-error: true + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCS_CREDENTIALS }} + + - name: Upload previews to GCS + if: steps.changed_plots.outputs.changed_files != '' && steps.gcs_auth.outcome == 'success' + continue-on-error: true + uses: google-github-actions/upload-cloud-storage@v2 + with: + path: preview_outputs + destination: ${{ secrets.GCS_BUCKET }}/previews/pr-${{ github.event.pull_request.number }} + process_gcloudignore: false + + - name: Generate preview URLs + if: steps.changed_plots.outputs.changed_files != '' && steps.gcs_auth.outcome == 'success' + id: preview_urls + run: | + BUCKET="${{ secrets.GCS_BUCKET }}" + PR_NUM="${{ github.event.pull_request.number }}" + + # Create markdown table with preview links + PREVIEW_TABLE="| Library | Spec | Variant | Preview |\n|---------|------|---------|----------|\n" + + for file in preview_outputs/*.png; do + if [ -f "$file" ]; then + BASENAME=$(basename "$file" .png) + SPEC_ID=$(echo "$BASENAME" | cut -d'_' -f1-3) + LIBRARY=$(echo "$BASENAME" | cut -d'_' -f4) + VARIANT=$(echo "$BASENAME" | cut -d'_' -f5) + + URL="https://storage.googleapis.com/${BUCKET}/previews/pr-${PR_NUM}/$(basename "$file")" + PREVIEW_TABLE+="| $LIBRARY | $SPEC_ID | $VARIANT | [View]($URL) |\n" + fi + done + + # Save to file for comment + echo -e "$PREVIEW_TABLE" > preview_table.md + + - name: Comment on PR with previews + if: steps.changed_plots.outputs.changed_files != '' && steps.gcs_auth.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const previewTable = fs.readFileSync('preview_table.md', 'utf8'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `## šŸŽØ Preview Images Generated + + ${previewTable} + + --- + + *Generated by [test-and-preview workflow](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*` + }); + + - name: Save preview metadata + if: steps.changed_plots.outputs.changed_files != '' + run: | + # Create metadata file for downstream workflows + cat > preview_metadata.json < str: + """ + Extract Python code from Claude response and validate syntax. + + Args: + response_text: Raw response from Claude API + + Returns: + Validated Python code + + Raises: + ValueError: If code cannot be extracted or has syntax errors + """ + code = response_text.strip() + + # Extract code if wrapped in markdown + if "```python" in code: + code = code.split("```python")[1].split("```")[0].strip() + elif "```" in code: + code = code.split("```")[1].split("```")[0].strip() + + if not code: + raise ValueError("No code could be extracted from response") + + # Validate Python syntax + try: + ast.parse(code) + except SyntaxError as e: + raise ValueError(f"Generated code has syntax errors: {e}") + + return code + + +T = TypeVar('T') + + +def retry_with_backoff( + func: Callable[[], T], + max_retries: int = 3, + initial_delay: float = 2.0, + backoff_factor: float = 2.0 +) -> T: + """ + Retry a function with exponential backoff. + + Args: + func: Function to retry + max_retries: Maximum number of retry attempts + initial_delay: Initial delay in seconds + backoff_factor: Multiplier for delay after each retry + + Returns: + Result from successful function call + + Raises: + Last exception if all retries fail + """ + delay = initial_delay + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func() + except (RateLimitError, APIConnectionError) as e: + last_exception = e + if attempt < max_retries: + print(f"āš ļø API error: {type(e).__name__}. Retrying in {delay}s... (attempt {attempt + 1}/{max_retries})") + time.sleep(delay) + delay *= backoff_factor + else: + print(f"āŒ Max retries ({max_retries}) exceeded") + raise + except APIError as e: + # For other API errors, don't retry + print(f"āŒ API error: {e}") + raise + + # Should never reach here, but for type checker + if last_exception: + raise last_exception + raise RuntimeError("Unexpected retry loop exit") + + +def load_spec(spec_id: str) -> str: + """Load specification from specs/ directory""" + spec_path = Path(f"specs/{spec_id}.md") + if not spec_path.exists(): + raise FileNotFoundError(f"Spec file not found: {spec_path}") + + content = spec_path.read_text() + + # Check spec version + import re + version_match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', content) + if version_match: + spec_version = version_match.group(1) + print(f"šŸ“‹ Spec version: {spec_version}") + else: + print(f"āš ļø Warning: Spec has no version marker. Consider upgrading with upgrade_specs.py") + + return content + + +def load_generation_rules(version: str = "v1.0.0-draft") -> str: + """Load code generation rules""" + rules_path = Path(f"rules/generation/{version}/code-generation-rules.md") + if not rules_path.exists(): + raise FileNotFoundError(f"Rules not found: {rules_path}") + return rules_path.read_text() + + +def load_quality_criteria(version: str = "v1.0.0-draft") -> str: + """Load quality criteria for self-review""" + criteria_path = Path(f"rules/generation/{version}/quality-criteria.md") + if not criteria_path.exists(): + raise FileNotFoundError(f"Quality criteria not found: {criteria_path}") + return criteria_path.read_text() + + +def load_self_review_checklist(version: str = "v1.0.0-draft") -> str: + """Load self-review checklist""" + checklist_path = Path(f"rules/generation/{version}/self-review-checklist.md") + if not checklist_path.exists(): + raise FileNotFoundError(f"Self-review checklist not found: {checklist_path}") + return checklist_path.read_text() + + +def generate_code( + spec_id: str, + library: LibraryType, + variant: str = "default", + rules_version: str = "v1.0.0-draft", + max_attempts: int = 3 +) -> dict: + """ + Generate plot implementation code using Claude + + Args: + spec_id: Specification ID (e.g., "scatter-basic-001") + library: Target library (matplotlib, seaborn, etc.) + variant: Implementation variant (default, style variant, etc.) + rules_version: Version of generation rules to use + max_attempts: Maximum number of self-review attempts + + Returns: + dict with 'code', 'file_path', 'attempt_count', 'passed_review' + """ + # Load inputs + spec_content = load_spec(spec_id) + generation_rules = load_generation_rules(rules_version) + quality_criteria = load_quality_criteria(rules_version) + self_review = load_self_review_checklist(rules_version) + + # Initialize Claude + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("ANTHROPIC_API_KEY environment variable not set") + + client = anthropic.Anthropic(api_key=api_key) + + # Determine output path + # Format: plots/{library}/{plot_type}/{spec_id}/{variant}.py + spec_type = spec_id.split('-')[0] # e.g., "scatter" from "scatter-basic-001" + + # Library-specific plot type mapping + plot_type_map = { + "matplotlib": {"scatter": "scatter", "bar": "bar", "line": "line", "heatmap": "imshow"}, + "seaborn": {"scatter": "scatterplot", "bar": "barplot", "line": "lineplot", "heatmap": "heatmap"}, + "plotly": {"scatter": "scatter", "bar": "bar", "line": "line", "heatmap": "heatmap"}, + } + + plot_type = plot_type_map.get(library, {}).get(spec_type, spec_type) + file_path = Path(f"plots/{library}/{plot_type}/{spec_id}/{variant}.py") + + # Self-review loop + for attempt in range(1, max_attempts + 1): + print(f"šŸ”„ Attempt {attempt}/{max_attempts} for {spec_id}/{library}/{variant}") + + # Build prompt + if attempt == 1: + prompt = f"""You are an expert Python data visualization developer. + +# Task +Generate a production-ready implementation of this plot specification for {library}. + +# Specification +{spec_content} + +# Generation Rules (Version: {rules_version}) +{generation_rules} + +# Quality Criteria +{quality_criteria} + +# Requirements +1. Follow the generation rules exactly +2. Implement all data requirements and optional parameters from the spec +3. Meet all quality criteria listed in the spec +4. Use type hints (Python 3.10+ syntax) +5. Include comprehensive docstring +6. Add input validation +7. Make the code deterministic (use fixed random seeds if needed) +8. Add a standalone execution block for testing + +# Output Format +Provide ONLY the Python code, no explanations. The code should be production-ready. + +# Target +- Library: {library} +- Variant: {variant} +- Python: 3.10+ +- File: {file_path} + +Generate the implementation now:""" + else: + # For subsequent attempts, include the previous code and ask for improvements + prompt = f"""Your previous implementation needs improvements. Please revise based on the feedback below. + +# Previous Code +```python +{code} +``` + +# Self-Review Feedback +{review_feedback} + +# Requirements +1. Fix all issues identified in the feedback +2. Ensure all quality criteria are met +3. Maintain the same structure and function signature + +Generate the improved implementation:""" + + # Call Claude with retry logic + response = retry_with_backoff( + lambda: client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=4000, + messages=[{"role": "user", "content": prompt}] + ) + ) + + # Extract and validate code + try: + code = extract_and_validate_code(response.content[0].text) + except ValueError as e: + print(f"āŒ Code extraction/validation failed: {e}") + if attempt < max_attempts: + print(f"šŸ”„ Retrying... ({attempt + 1}/{max_attempts})") + continue + raise + + # Self-review + print(f"šŸ” Running self-review...") + review_prompt = f"""Review this generated plot implementation against the specification and quality criteria. + +# Specification +{spec_content} + +# Generated Code +```python +{code} +``` + +# Self-Review Checklist +{self_review} + +# Task +Go through the self-review checklist and verify each item. For each item, respond with: +- āœ… PASS: [brief reason] +- āŒ FAIL: [specific issue and how to fix] + +After reviewing all items, provide: +- Overall verdict: PASS or FAIL +- If FAIL: Specific improvements needed + +Format your response as: +## Review Results +[checklist items with āœ…/āŒ] + +## Verdict +[PASS or FAIL] + +## Improvements Needed (if FAIL) +[specific actionable items] +""" + + review_response = retry_with_backoff( + lambda: client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=2000, + messages=[{"role": "user", "content": review_prompt}] + ) + ) + + review_feedback = review_response.content[0].text + + # Check if passed + if "## Verdict\nPASS" in review_feedback or "Verdict: PASS" in review_feedback: + print(f"āœ… Self-review passed on attempt {attempt}") + return { + "code": code, + "file_path": str(file_path), + "attempt_count": attempt, + "passed_review": True, + "review_feedback": review_feedback + } + else: + print(f"āŒ Self-review failed on attempt {attempt}") + if attempt < max_attempts: + print(f"šŸ”„ Regenerating with feedback...") + else: + print(f"āš ļø Max attempts reached. Returning best effort code.") + + # Max attempts reached without passing + return { + "code": code, + "file_path": str(file_path), + "attempt_count": max_attempts, + "passed_review": False, + "review_feedback": review_feedback + } + + +def save_implementation(result: dict) -> Path: + """Save generated code to file""" + file_path = Path(result["file_path"]) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(result["code"]) + print(f"šŸ’¾ Saved to: {file_path}") + return file_path + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Generate plot implementation from spec") + parser.add_argument("spec_id", help="Specification ID (e.g., scatter-basic-001)") + parser.add_argument("library", choices=["matplotlib", "seaborn", "plotly", "bokeh", "altair"], + help="Target library") + parser.add_argument("--variant", default="default", help="Variant name (default: default)") + parser.add_argument("--rules-version", default="v1.0.0-draft", help="Rules version") + parser.add_argument("--max-attempts", type=int, default=3, help="Max self-review attempts") + + args = parser.parse_args() + + try: + result = generate_code( + spec_id=args.spec_id, + library=args.library, + variant=args.variant, + rules_version=args.rules_version, + max_attempts=args.max_attempts + ) + + # Save code + file_path = save_implementation(result) + + # Print summary + print("\n" + "="*60) + print(f"āœ… Generation complete!") + print(f" Spec: {args.spec_id}") + print(f" Library: {args.library}") + print(f" Variant: {args.variant}") + print(f" File: {file_path}") + print(f" Attempts: {result['attempt_count']}") + print(f" Review: {'āœ… PASSED' if result['passed_review'] else 'āŒ FAILED'}") + print("="*60) + + # Exit with appropriate code + sys.exit(0 if result['passed_review'] else 1) + + except Exception as e: + print(f"āŒ Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/automation/scripts/upgrade_specs.py b/automation/scripts/upgrade_specs.py new file mode 100644 index 0000000000..833e92c812 --- /dev/null +++ b/automation/scripts/upgrade_specs.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Spec Version Upgrader + +Automatically upgrades spec files to the latest template version. +""" + +import re +from pathlib import Path +from typing import Dict, List, Tuple + + +def get_spec_version(spec_content: str) -> str: + """Extract spec version from content""" + match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', spec_content) + if match: + return match.group(1) + + # Check comment version (older format) + match = re.search(r'Spec Template Version:\s+(\d+\.\d+\.\d+)', spec_content) + if match: + return match.group(1) + + return "0.0.0" # Unknown/very old + + +def upgrade_to_1_0_0(spec_content: str, spec_id: str) -> str: + """Upgrade spec to version 1.0.0 format""" + # Check if already has version + if "**Spec Version:**" in spec_content: + return spec_content + + # Extract title + title_match = re.search(r'^#\s+(.+)$', spec_content, re.MULTILINE) + if not title_match: + raise ValueError(f"Could not find title in spec: {spec_id}") + + title_line = title_match.group(0) + + # Insert version info after title + version_block = f""" + + +**Spec Version:** 1.0.0 +""" + + # Replace title with title + version block + upgraded = spec_content.replace( + title_line, + title_line + "\n" + version_block, + 1 + ) + + return upgraded + + +def upgrade_spec_file(spec_path: Path, target_version: str = "1.0.0") -> Tuple[bool, str]: + """ + Upgrade a single spec file to target version + + Returns: + (success: bool, message: str) + """ + try: + content = spec_path.read_text() + current_version = get_spec_version(content) + + if current_version == target_version: + return True, f"Already at version {target_version}" + + # Apply upgrades based on version + if current_version < "1.0.0" and target_version >= "1.0.0": + content = upgrade_to_1_0_0(content, spec_path.stem) + + # Future upgrades would go here: + # if current_version < "1.1.0" and target_version >= "1.1.0": + # content = upgrade_to_1_1_0(content, spec_path.stem) + + # Write back + spec_path.write_text(content) + + return True, f"Upgraded from {current_version} to {target_version}" + + except Exception as e: + return False, f"Error: {str(e)}" + + +def find_all_specs() -> List[Path]: + """Find all spec files (excluding template)""" + specs_dir = Path("specs") + return [f for f in specs_dir.glob("*.md") if f.name != ".template.md"] + + +def upgrade_all_specs(target_version: str = "1.0.0", dry_run: bool = False) -> Dict[str, Tuple[bool, str]]: + """ + Upgrade all spec files to target version + + Args: + target_version: Target template version + dry_run: If True, don't write changes + + Returns: + Dict mapping spec_id to (success, message) + """ + results = {} + specs = find_all_specs() + + for spec_path in specs: + spec_id = spec_path.stem + + if dry_run: + content = spec_path.read_text() + current_version = get_spec_version(content) + results[spec_id] = (True, f"Would upgrade from {current_version} to {target_version}") + else: + success, message = upgrade_spec_file(spec_path, target_version) + results[spec_id] = (success, message) + + return results + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Upgrade spec files to latest template version") + parser.add_argument("--version", default="1.0.0", help="Target version (default: 1.0.0)") + parser.add_argument("--dry-run", action="store_true", help="Show what would be changed without modifying files") + parser.add_argument("--spec", help="Upgrade specific spec file (e.g., scatter-basic-001)") + + args = parser.parse_args() + + print(f"šŸ”„ Spec Version Upgrader") + print(f"Target version: {args.version}") + print(f"Dry run: {args.dry_run}") + print("=" * 60) + + if args.spec: + # Upgrade single spec + spec_path = Path(f"specs/{args.spec}.md") + if not spec_path.exists(): + print(f"āŒ Spec not found: {spec_path}") + exit(1) + + if args.dry_run: + content = spec_path.read_text() + current = get_spec_version(content) + print(f"šŸ“„ {args.spec}") + print(f" Current: {current}") + print(f" Would upgrade to: {args.version}") + else: + success, message = upgrade_spec_file(spec_path, args.version) + status = "āœ…" if success else "āŒ" + print(f"{status} {args.spec}: {message}") + else: + # Upgrade all specs + results = upgrade_all_specs(args.version, args.dry_run) + + total = len(results) + succeeded = sum(1 for success, _ in results.values() if success) + failed = total - succeeded + + for spec_id, (success, message) in results.items(): + status = "āœ…" if success else "āŒ" + print(f"{status} {spec_id}: {message}") + + print("=" * 60) + print(f"Total: {total} | Succeeded: {succeeded} | Failed: {failed}") + + if failed > 0: + exit(1) diff --git a/automation/scripts/upgrade_specs_ai.py b/automation/scripts/upgrade_specs_ai.py new file mode 100644 index 0000000000..5d70c6cd69 --- /dev/null +++ b/automation/scripts/upgrade_specs_ai.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +""" +AI-Powered Spec Upgrader + +Uses Claude to intelligently upgrade spec files with semantic improvements, +not just structural changes. +""" + +import os +import re +from pathlib import Path +from typing import Dict, Tuple +import anthropic + + +def get_spec_version(spec_content: str) -> str: + """Extract spec version from content""" + match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', spec_content) + if match: + return match.group(1) + return "0.0.0" + + +def load_template(version: str) -> str: + """Load template for target version""" + if version == "1.0.0": + template_path = Path("specs/.template.md") + else: + # Future: specs/.template-v{version}.md for historical templates + template_path = Path("specs/.template.md") + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + return template_path.read_text() + + +def load_upgrade_instructions(from_version: str, to_version: str) -> str: + """Load specific upgrade instructions for version transition""" + instructions_path = Path(f"specs/upgrades/{from_version}-to-{to_version}.md") + + if instructions_path.exists(): + return instructions_path.read_text() + + # Default generic instructions + return f""" +# Upgrade Instructions: {from_version} → {to_version} + +## Goals +- Maintain the core intent and requirements of the spec +- Improve clarity and specificity +- Ensure quality criteria are measurable and actionable +- Update to latest template structure + +## Specific Improvements +- Quality criteria should be concrete and verifiable (not vague like "looks good") +- Parameter descriptions should include types and ranges +- Use cases should be specific with domain examples +- Expected output should describe visual elements precisely + +## Preserve +- Spec ID and title +- Core functionality and requirements +- Existing optional parameters (unless deprecated) +""" + + +def upgrade_spec_with_ai( + spec_content: str, + spec_id: str, + target_version: str, + api_key: str, + dry_run: bool = False +) -> Tuple[bool, str, str]: + """ + Use Claude to upgrade a spec to target version + + Returns: + (success: bool, upgraded_content: str, message: str) + """ + current_version = get_spec_version(spec_content) + + if current_version == target_version: + return True, spec_content, f"Already at version {target_version}" + + # Load template and instructions + template = load_template(target_version) + upgrade_instructions = load_upgrade_instructions(current_version, target_version) + + # Build prompt for Claude + prompt = f"""You are a technical documentation expert specializing in data visualization specifications. + +# Task +Upgrade this plot specification from version {current_version} to version {target_version}. + +# Current Spec +{spec_content} + +# Target Template (v{target_version}) +{template} + +# Upgrade Instructions +{upgrade_instructions} + +# Requirements + +1. **Preserve Core Intent** + - Keep spec ID and title identical + - Maintain all required data parameters + - Preserve optional parameters (unless explicitly deprecated) + - Keep the fundamental purpose of the plot + +2. **Improve Quality Criteria** + - Make criteria specific and measurable + - Replace vague terms ("looks good") with concrete checks + - Ensure each criterion can be verified visually or programmatically + - Examples: + āŒ "Plot looks nice" + āœ… "Grid is visible but subtle with alpha=0.3 and dashed linestyle" + +3. **Enhance Parameter Descriptions** + - Add explicit types (numeric, categorical, datetime, etc.) + - Specify ranges where applicable (e.g., "0.0-1.0") + - Include examples in descriptions + +4. **Improve Use Cases** + - Make use cases domain-specific + - Include concrete examples with data types + - Example: "Correlation analysis between height and weight in healthcare data" + +5. **Update Metadata** + - Set Spec Version to {target_version} + - Set Template Version to {target_version} + - Keep Created date (if present) + - Update Last Updated to today (2025-01-24) + +6. **Maintain Structure** + - Follow the template structure exactly + - Keep all required sections + - Add any new sections from template + +# Output Format +Provide ONLY the upgraded spec content in Markdown format. +Do NOT include explanations, just the spec file content. + +Generate the upgraded spec now:""" + + if dry_run: + return True, spec_content, f"Would upgrade from {current_version} to {target_version} using AI" + + # Call Claude + client = anthropic.Anthropic(api_key=api_key) + + response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=4000, + messages=[{"role": "user", "content": prompt}] + ) + + upgraded_content = response.content[0].text + + # Clean markdown code blocks if present + if "```markdown" in upgraded_content: + upgraded_content = upgraded_content.split("```markdown")[1].split("```")[0].strip() + elif "```" in upgraded_content: + upgraded_content = upgraded_content.split("```")[1].split("```")[0].strip() + + # Verify upgrade was successful + new_version = get_spec_version(upgraded_content) + if new_version != target_version: + return False, spec_content, f"Upgrade failed: version is {new_version}, expected {target_version}" + + # Verify spec ID is preserved + spec_id_pattern = r'^#\s+([a-z]+-[a-z]+-\d{3}):' + old_match = re.search(spec_id_pattern, spec_content, re.MULTILINE) + new_match = re.search(spec_id_pattern, upgraded_content, re.MULTILINE) + + if old_match and new_match: + if old_match.group(1) != new_match.group(1): + return False, spec_content, f"Upgrade failed: spec ID changed from {old_match.group(1)} to {new_match.group(1)}" + elif old_match and not new_match: + return False, spec_content, "Upgrade failed: spec ID was removed" + + return True, upgraded_content, f"Successfully upgraded from {current_version} to {target_version}" + + +def upgrade_spec_file_ai( + spec_path: Path, + target_version: str, + api_key: str, + dry_run: bool = False, + backup: bool = True +) -> Tuple[bool, str]: + """ + Upgrade a single spec file using AI + + Args: + spec_path: Path to spec file + target_version: Target version + api_key: Anthropic API key + dry_run: Don't write changes + backup: Create .backup file before upgrading + + Returns: + (success: bool, message: str) + """ + try: + content = spec_path.read_text() + spec_id = spec_path.stem + + success, upgraded_content, message = upgrade_spec_with_ai( + content, spec_id, target_version, api_key, dry_run + ) + + if not success: + return False, message + + if dry_run: + return True, message + + # Create backup + if backup: + backup_path = spec_path.with_suffix(f".md.backup-{get_spec_version(content)}") + backup_path.write_text(content) + print(f" šŸ’¾ Backup saved: {backup_path.name}") + + # Write upgraded content + spec_path.write_text(upgraded_content) + + return True, message + + except Exception as e: + return False, f"Error: {str(e)}" + + +def upgrade_all_specs_ai( + target_version: str, + api_key: str, + dry_run: bool = False, + backup: bool = True +) -> Dict[str, Tuple[bool, str]]: + """ + Upgrade all spec files using AI + + Args: + target_version: Target version + api_key: Anthropic API key + dry_run: Don't write changes + backup: Create backup files + + Returns: + Dict mapping spec_id to (success, message) + """ + results = {} + specs_dir = Path("specs") + + # Get all spec files (exclude template and backups) + spec_files = [ + f for f in specs_dir.glob("*.md") + if f.name != ".template.md" and not f.name.endswith(".backup") + ] + + for spec_path in spec_files: + spec_id = spec_path.stem + print(f"\nšŸ”„ Processing: {spec_id}") + + success, message = upgrade_spec_file_ai( + spec_path, target_version, api_key, dry_run, backup + ) + + results[spec_id] = (success, message) + status = "āœ…" if success else "āŒ" + print(f"{status} {message}") + + return results + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="AI-powered spec upgrader using Claude" + ) + parser.add_argument( + "--version", + default="1.0.0", + help="Target version (default: 1.0.0)" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be changed without modifying files" + ) + parser.add_argument( + "--spec", + help="Upgrade specific spec file (e.g., scatter-basic-001)" + ) + parser.add_argument( + "--no-backup", + action="store_true", + help="Don't create backup files" + ) + + args = parser.parse_args() + + # Get API key + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + print("āŒ ANTHROPIC_API_KEY environment variable not set") + exit(1) + + print(f"šŸ¤– AI-Powered Spec Upgrader") + print(f"Target version: {args.version}") + print(f"Dry run: {args.dry_run}") + print(f"Backup: {not args.no_backup}") + print("=" * 60) + + if args.spec: + # Upgrade single spec + spec_path = Path(f"specs/{args.spec}.md") + if not spec_path.exists(): + print(f"āŒ Spec not found: {spec_path}") + exit(1) + + success, message = upgrade_spec_file_ai( + spec_path, + args.version, + api_key, + args.dry_run, + not args.no_backup + ) + + status = "āœ…" if success else "āŒ" + print(f"\n{status} {args.spec}: {message}") + + if not success: + exit(1) + else: + # Upgrade all specs + results = upgrade_all_specs_ai( + args.version, + api_key, + args.dry_run, + not args.no_backup + ) + + print("\n" + "=" * 60) + print("Summary") + print("=" * 60) + + total = len(results) + succeeded = sum(1 for success, _ in results.values() if success) + failed = total - succeeded + + print(f"Total: {total} | Succeeded: {succeeded} | Failed: {failed}") + + if failed > 0: + print("\nāŒ Failed specs:") + for spec_id, (success, message) in results.items(): + if not success: + print(f" - {spec_id}: {message}") + exit(1) + + print("\nāœ… Upgrade complete!") diff --git a/specs/.template.md b/specs/.template.md new file mode 100644 index 0000000000..6cd0766dd5 --- /dev/null +++ b/specs/.template.md @@ -0,0 +1,66 @@ +# {spec-id}: {Title} + + + +**Spec Version:** 1.0.0 + +## Description + +{2-3 sentences describing what this plot does and when to use it} + +## Data Requirements + +- **{param_name}**: {Description of required data - specify type: numeric, categorical, datetime, etc.} +- **{param_name}**: {Description of required data} + +## Optional Parameters + +- `{param}`: {Description} (type: {type}, default: {value}) +- `{param}`: {Description} + +## Quality Criteria + + +- [ ] {Specific criterion about labeling - e.g., "X and Y axes are labeled with column names"} +- [ ] {Specific criterion about readability - e.g., "No overlapping axis labels or tick marks"} +- [ ] {Specific criterion about visual quality - e.g., "Grid is visible but subtle (alpha=0.3)"} +- [ ] {Specific criterion about accessibility - e.g., "Colorblind-safe colors when color mapping is used"} +- [ ] {Specific criterion about correctness - e.g., "Data accurately represented without distortion"} + +## Expected Output + +{1-2 paragraphs describing what the final plot should look like. Be specific about visual elements, layout, and what makes it "good"} + +## Tags + +{tag1}, {tag2}, {tag3}, {tag4} + + + +## Use Cases + +- {Specific, realistic use case scenario - include domain and example} +- {Use case 2} +- {Use case 3} +- {Use case 4 - optional} +- {Use case 5 - optional} +- {Use case 6 - optional} + + diff --git a/specs/VERSIONING.md b/specs/VERSIONING.md new file mode 100644 index 0000000000..063c55dc86 --- /dev/null +++ b/specs/VERSIONING.md @@ -0,0 +1,257 @@ +# Spec Template Versioning + +## Overview + +All spec files follow a versioned template to ensure consistency and enable automated upgrades when the template is improved. + +## Current Version + +**Latest:** 1.0.0 (2025-01-24) + +## Version Format + +Each spec file contains: + +```markdown +# {spec-id}: {Title} + + + +**Spec Version:** 1.0.0 +``` + +## Version History + +### 1.0.0 (2025-01-24) + +**Initial standardized template** + +Sections: +- Description +- Data Requirements +- Optional Parameters +- Quality Criteria (5-10 items) +- Expected Output +- Tags +- Use Cases + +Features: +- Explicit version marker in spec file +- HTML comment with metadata +- Structured quality criteria as checklist +- Clear parameter type definitions + +--- + +## Upgrading Specs + +### Manual Upgrade + +When creating a new spec, always use the latest template: + +```bash +cp specs/.template.md specs/your-spec-id.md +``` + +Then fill in the placeholders. + +### Automatic Upgrade + +#### Structural Upgrades (Simple Script) + +For simple structural changes (adding version markers, new sections): + +```bash +# Dry run (see what would change) +python automation/scripts/upgrade_specs.py --dry-run + +# Upgrade all specs +python automation/scripts/upgrade_specs.py + +# Upgrade specific spec +python automation/scripts/upgrade_specs.py --spec scatter-basic-001 +``` + +#### Semantic Upgrades (AI-Powered) + +For semantic improvements (better wording, clearer criteria): + +```bash +# Requires ANTHROPIC_API_KEY environment variable +export ANTHROPIC_API_KEY=sk-ant-... + +# Dry run (see what would change) +python automation/scripts/upgrade_specs_ai.py --dry-run + +# Upgrade all specs (creates automatic backups) +python automation/scripts/upgrade_specs_ai.py + +# Upgrade specific spec +python automation/scripts/upgrade_specs_ai.py --spec scatter-basic-001 + +# Upgrade without backups +python automation/scripts/upgrade_specs_ai.py --no-backup +``` + +**Why AI-Powered Upgrades?** +- Improves quality criteria to be more specific and measurable +- Enhances parameter descriptions with types and ranges +- Makes use cases more concrete with domain examples +- Preserves spec ID and core intent +- Creates backups automatically + +### When to Upgrade + +Upgrade specs when: +- Template gets new required sections +- Quality criteria format changes +- Parameter documentation structure improves +- Metadata requirements change + +**Note:** Backward compatibility is maintained - old specs remain valid, but new features may be missing. + +--- + +## Template Changes + +### Adding a New Section (Minor Version) + +Example: 1.0.0 → 1.1.0 + +```markdown +## New Section (v1.1.0+) + +{New content} +``` + +**Migration:** +- Old specs (1.0.0) continue to work +- New specs include the section +- Upgrade script adds section with default content +- No breaking changes + +### Restructuring Sections (Major Version) + +Example: 1.x.x → 2.0.0 + +Breaking changes require: +1. Update template version to 2.0.0 +2. Create upgrade function in `upgrade_specs.py` +3. Document migration in this file +4. Run upgrade script on all specs + +**Migration:** +- Automated where possible +- Manual review recommended +- Clear changelog of changes + +--- + +## Best Practices + +### When Creating Specs + +āœ… **Always use the template:** +```bash +cp specs/.template.md specs/new-plot-001.md +``` + +āœ… **Keep version in sync:** +- Use the template version at time of creation +- Update `Created` date +- Update `Last Updated` date when modifying + +āœ… **Document changes:** +- Update `Last Updated` date +- Consider if spec version should increment + +### When Updating Template + +āœ… **Version bump rules:** +- **Patch (1.0.0 → 1.0.1):** Clarifications, typos, examples +- **Minor (1.0.0 → 1.1.0):** New optional sections, additional fields +- **Major (1.0.0 → 2.0.0):** Breaking changes, section renames/removals + +āœ… **Document changes:** +- Add entry to Version History section +- Update `upgrade_specs.py` if needed +- Test upgrade script with dry-run +- Commit template + upgrade script together + +āœ… **Communication:** +- Document breaking changes clearly +- Provide migration guide +- Consider automation for common upgrades + +--- + +## Checking Spec Versions + +### List all specs with versions + +```bash +grep -r "Spec Version:" specs/*.md +``` + +### Find outdated specs + +```python +from pathlib import Path +import re + +def check_versions(): + template_version = "1.0.0" + outdated = [] + + for spec_file in Path("specs").glob("*.md"): + if spec_file.name == ".template.md": + continue + + content = spec_file.read_text() + match = re.search(r'\*\*Spec Version:\*\*\s+(\d+\.\d+\.\d+)', content) + + if not match: + outdated.append((spec_file.stem, "no version")) + elif match.group(1) != template_version: + outdated.append((spec_file.stem, match.group(1))) + + return outdated + +# Run +outdated = check_versions() +for spec_id, version in outdated: + print(f"āš ļø {spec_id}: version {version}") +``` + +--- + +## Future Enhancements + +Potential future template improvements: + +### v1.1.0 (Planned) +- [ ] Add "Performance Considerations" section +- [ ] Add "Accessibility" as separate section (not just in criteria) +- [ ] Add "Interactive Features" for plotly/bokeh + +### v1.2.0 (Ideas) +- [ ] Add "Variants" section (list common style variants) +- [ ] Add "Related Plots" section (similar visualizations) +- [ ] Add "Common Pitfalls" section + +### v2.0.0 (Breaking Changes - Future) +- [ ] Structured YAML front matter for metadata +- [ ] Machine-readable quality criteria format +- [ ] Separate visual vs. code quality criteria + +--- + +## Questions? + +- Template unclear? Open an issue with `template-feedback` label +- Upgrade script broken? Open an issue with `bug` label +- Proposal for new section? Open an issue with `template-enhancement` label diff --git a/specs/scatter-basic-001.md b/specs/scatter-basic-001.md new file mode 100644 index 0000000000..cdaeb00238 --- /dev/null +++ b/specs/scatter-basic-001.md @@ -0,0 +1,61 @@ +# scatter-basic-001: Basic 2D Scatter Plot + + + +**Spec Version:** 1.0.0 + +## Description + +Create a simple scatter plot showing the relationship between two numeric variables. +Perfect for correlation analysis, outlier detection, and exploring bivariate relationships. +Works with any dataset containing two numeric columns. + +## Data Requirements + +- **x**: Numeric values for x-axis (continuous or discrete) +- **y**: Numeric values for y-axis (continuous or discrete) + +## Optional Parameters + +- `color`: Point color (type: string or column name, default: "steelblue") +- `size`: Point size in pixels (type: numeric or column name, default: 50) +- `alpha`: Transparency level (type: float 0.0-1.0, default: 0.8) +- `title`: Plot title (type: string, default: None) +- `xlabel`: Custom x-axis label (type: string, default: column name) +- `ylabel`: Custom y-axis label (type: string, default: column name) + +## Quality Criteria + +- [ ] X and Y axes are labeled with column names (or custom labels if provided) +- [ ] Grid is visible but subtle with alpha=0.3 and dashed linestyle +- [ ] Points are clearly distinguishable with appropriate size (50px default) +- [ ] No overlapping axis labels or tick marks +- [ ] Legend is shown if color or size mapping is used +- [ ] Colorblind-safe colors when color mapping is used (use colormap like 'viridis' or 'tab10') +- [ ] Appropriate figure size (10x6 inches default) for readability +- [ ] Title is centered and clearly readable if provided (fontsize 14, bold) + +## Expected Output + +A clean 2D scatter plot with clearly visible points showing the correlation or distribution +between x and y variables. The plot should be immediately understandable without +additional explanation. Grid lines should help readability without overpowering the data. +If color or size mapping is used, the legend should clearly indicate what each variation +means. All text elements (labels, title, legend) should be legible at standard display sizes. + +## Tags + +correlation, bivariate, basic, 2d, statistical, exploratory, scatter + +## Use Cases + +- Correlation analysis between two variables (e.g., height vs weight in healthcare) +- Outlier detection in bivariate data (e.g., unusual transactions in finance) +- Pattern recognition in data (linear, quadratic, clusters - e.g., customer segmentation) +- Relationship visualization (e.g., price vs demand in economics) +- Quality control charts (e.g., measurement vs target in manufacturing) +- Scientific data exploration (e.g., temperature vs pressure in physics experiments) diff --git a/specs/upgrades/README.md b/specs/upgrades/README.md new file mode 100644 index 0000000000..00b825884a --- /dev/null +++ b/specs/upgrades/README.md @@ -0,0 +1,175 @@ +# Spec Upgrade Instructions + +This directory contains version-specific upgrade instructions for AI-powered spec upgrades. + +## Format + +Each upgrade instruction file is named: `{from_version}-to-{to_version}.md` + +Example: `1.0.0-to-1.1.0.md` + +## Purpose + +These instructions tell Claude: +- What semantic improvements to make +- What new sections to add +- How to reformulate existing content +- What to preserve from the old version + +## Usage + +The `upgrade_specs_ai.py` script automatically loads these instructions when upgrading specs. + +```bash +# Upgrade all specs from 1.0.0 to 1.1.0 +# Uses specs/upgrades/1.0.0-to-1.1.0.md if it exists +python automation/scripts/upgrade_specs_ai.py --version 1.1.0 +``` + +## Example Upgrade Instruction File + +```markdown +# Upgrade Instructions: 1.0.0 → 1.1.0 + +## Major Changes +- Add "Performance Considerations" section +- Improve Quality Criteria specificity +- Add type annotations to all parameters + +## Quality Criteria Improvements +Replace vague criteria with specific, measurable ones: + +āŒ Before (vague): +- [ ] Plot looks good +- [ ] Colors are nice + +āœ… After (specific): +- [ ] Grid is visible but subtle with alpha=0.3 and dashed linestyle +- [ ] Colors use colorblind-safe palette (viridis, tab10, or similar) + +## Parameter Description Improvements +Add explicit types and ranges: + +āŒ Before: +- `alpha`: Transparency level (default: 0.8) + +āœ… After: +- `alpha`: Transparency level (type: float 0.0-1.0, default: 0.8) + +## Preserve +- Spec ID and title (must not change) +- Core data requirements +- Use cases (can enhance but don't remove) +``` + +## Writing Good Upgrade Instructions + +### 1. Be Specific About Changes +```markdown +## Changes +- Add "Performance Considerations" section after "Expected Output" +- Split "Quality Criteria" into "Visual Quality" and "Code Quality" +``` + +### 2. Show Before/After Examples +```markdown +## Example Transformation + +Before: +- `color`: Point color or column name + +After: +- `color`: Point color (type: string like "blue" or column name for color mapping, default: "steelblue") +``` + +### 3. List What Must Be Preserved +```markdown +## Preserve +- Spec ID (critical - must not change) +- Created date +- Core data requirements +- Existing optional parameters +``` + +### 4. Provide Rationale +```markdown +## Why This Change? +Quality criteria should be verifiable by AI without human interpretation. +Vague terms like "looks good" or "is readable" should be replaced with +measurable criteria like specific alpha values, font sizes, or color ranges. +``` + +## Future Upgrades + +When creating a new template version: + +1. **Create upgrade instructions** + ```bash + cat > specs/upgrades/1.0.0-to-1.1.0.md <