From d1e42c430ada4a791aa206ef40afdced968b5300 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Wed, 29 Jul 2026 12:30:52 +0530 Subject: [PATCH 1/5] fix: add lint rule to detect bare pip install and fix beval.yml violation --- .../powerpoint/scripts/export_slides.py | 2 +- .../powerpoint/scripts/export_svg.py | 2 +- .../powerpoint/scripts/render_pdf_images.py | 2 +- .github/workflows/beval.yml | 5 +- .github/workflows/pip-install-lint.yml | 20 +++++ .github/workflows/pr-validation.yml | 4 + scripts/lint_pip_install.py | 82 +++++++++++++++++++ 7 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pip-install-lint.yml create mode 100644 scripts/lint_pip_install.py diff --git a/.github/skills/experimental/powerpoint/scripts/export_slides.py b/.github/skills/experimental/powerpoint/scripts/export_slides.py index a97c55c06..0a70bb0a8 100644 --- a/.github/skills/experimental/powerpoint/scripts/export_slides.py +++ b/.github/skills/experimental/powerpoint/scripts/export_slides.py @@ -161,7 +161,7 @@ def filter_pdf_pages(pdf_path: Path, pages: list[int], output_path: Path) -> Pat import fitz # noqa: PLC0415 — PyMuPDF except ImportError: logger.error( - "PyMuPDF is required for slide filtering. Install via: pip install pymupdf" + "PyMuPDF is required for slide filtering. Install via: uv pip install pymupdf" ) sys.exit(EXIT_FAILURE) diff --git a/.github/skills/experimental/powerpoint/scripts/export_svg.py b/.github/skills/experimental/powerpoint/scripts/export_svg.py index 1e0533563..672243024 100644 --- a/.github/skills/experimental/powerpoint/scripts/export_svg.py +++ b/.github/skills/experimental/powerpoint/scripts/export_svg.py @@ -173,7 +173,7 @@ def export_pdf_to_svg( import fitz # noqa: F401, PLC0415 — PyMuPDF availability check except ImportError as e: raise PyMuPDFError( - "PyMuPDF is required for SVG export. Install via: pip install pymupdf" + "PyMuPDF is required for SVG export. Install via: uv pip install pymupdf" ) from e try: diff --git a/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py b/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py index d1ebef071..6b3167dfa 100644 --- a/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py +++ b/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py @@ -98,7 +98,7 @@ def render_pages( try: import fitz # noqa: F401, PLC0415 — PyMuPDF availability check except ImportError: - logger.error("PyMuPDF is required. Install via: pip install pymupdf") + logger.error("PyMuPDF is required. Install via: uv pip install pymupdf") sys.exit(EXIT_FAILURE) output_dir.mkdir(parents=True, exist_ok=True) diff --git a/.github/workflows/beval.yml b/.github/workflows/beval.yml index 01e0a2e6a..d712490f9 100644 --- a/.github/workflows/beval.yml +++ b/.github/workflows/beval.yml @@ -38,11 +38,14 @@ jobs: npm ci --prefix evals/beval echo "${{ github.workspace }}/evals/beval/node_modules/.bin" >> "$GITHUB_PATH" + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 + - name: Install beval # beval is hosted under a personal account (vyta) while an org-owned # home is evaluated. The install is pinned to a specific commit SHA to # mitigate supply-chain risk in the interim. - run: pip install --no-cache-dir "beval[all] @ git+https://github.com/vyta/beval.git@d9f46c24f03b0b806d928a8a8ce2fc66d8e470fb#subdirectory=python" + run: uv pip install --system --no-cache-dir "beval[all] @ git+https://github.com/vyta/beval.git@d9f46c24f03b0b806d928a8a8ce2fc66d8e470fb#subdirectory=python" - name: Start agent (TCP) env: diff --git a/.github/workflows/pip-install-lint.yml b/.github/workflows/pip-install-lint.yml new file mode 100644 index 000000000..66e9d2185 --- /dev/null +++ b/.github/workflows/pip-install-lint.yml @@ -0,0 +1,20 @@ +name: Pip Install Lint + +on: + workflow_call: + +jobs: + check-bare-pip-install: + name: Check for bare pip install + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: '3.x' + + - name: Run bare pip install lint check + run: python scripts/lint_pip_install.py diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 398209132..9af250c18 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -27,6 +27,10 @@ jobs: with: soft-fail: false + pip-install-lint: + name: Pip Install Lint + uses: ./.github/workflows/pip-install-lint.yml + markdown-lint: name: Markdown Lint uses: ./.github/workflows/markdown-lint.yml diff --git a/scripts/lint_pip_install.py b/scripts/lint_pip_install.py new file mode 100644 index 000000000..6003caebb --- /dev/null +++ b/scripts/lint_pip_install.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026 Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Lint script to detect bare 'pip install' calls. +The repository follows a 'uv-first' Python convention. +""" +import os +import re +import sys + + +EXCLUDE_DIRS = {".git", "evals", ".venv", "venv", "env", "node_modules", "__pycache__"} +EXCLUDE_FILES = {"THIRD-PARTY-NOTICES", "lint_pip_install.py"} +TARGET_DIRS = [".github/workflows", "scripts"] + +bare_pip_pattern = re.compile(r'\bpip3?\s+install\b') +uv_pip_pattern = re.compile(r'\buv\s+pip3?\s+install\b') + +violations = [] +scanned_files = set() + +def should_exclude(filepath): + norm_path = filepath.replace("\\", "/") + + parts = set(norm_path.split("/")) + if parts & EXCLUDE_DIRS: + return True + + for ex_file in EXCLUDE_FILES: + if ex_file in norm_path: + return True + + return False + +def scan_file(filepath): + if filepath in scanned_files: + return + scanned_files.add(filepath) + + if should_exclude(filepath): + return + + try: + with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + for i, line in enumerate(f, 1): + stripped_line = line.strip() + + if not stripped_line or stripped_line.startswith("#"): + continue + + if stripped_line.startswith("name:") or stripped_line.startswith("- name:"): + continue + + if bare_pip_pattern.search(line) and not uv_pip_pattern.search(line): + violations.append(f"{filepath}:{i}: {stripped_line}") + except Exception as e: + print(f"Warning: Could not read {filepath}: {e}") + +def main(): + for target_dir in TARGET_DIRS: + if os.path.isdir(target_dir): + for root, _, files in os.walk(target_dir): + for file in files: + scan_file(os.path.join(root, file)) + + for root, _, files in os.walk("."): + for file in files: + if file.endswith(".py"): + scan_file(os.path.join(root, file)) + + if violations: + print("ERROR: Found bare 'pip install' calls. Use 'uv pip install' instead.") + print("The repo follows a uv-first Python convention.\n") + for v in sorted(set(violations)): + print(f" - {v}") + sys.exit(1) + else: + print("Success: No bare 'pip install' calls found.") + +if __name__ == "__main__": + main() From f7d072081e7192338c9a16a0413d5693b2c8bd54 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Fri, 31 Jul 2026 13:01:25 +0530 Subject: [PATCH 2/5] fix: add pip-install-lint to pr-validation-success needs --- .../skills/experimental/powerpoint/SKILL.md | 4 +- .github/workflows/beval.yml | 2 +- .github/workflows/pip-install-lint.yml | 12 +- .github/workflows/pr-validation.yml | 1 + scripts/lint_pip_install.py | 82 ------------- scripts/linting/Invoke-PipInstallLint.ps1 | 114 ++++++++++++++++++ .../linting/Invoke-PipInstallLint.Tests.ps1 | 78 ++++++++++++ 7 files changed, 201 insertions(+), 92 deletions(-) delete mode 100644 scripts/lint_pip_install.py create mode 100644 scripts/linting/Invoke-PipInstallLint.ps1 create mode 100644 scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 diff --git a/.github/skills/experimental/powerpoint/SKILL.md b/.github/skills/experimental/powerpoint/SKILL.md index 2c53ffa6a..b65b37f2a 100644 --- a/.github/skills/experimental/powerpoint/SKILL.md +++ b/.github/skills/experimental/powerpoint/SKILL.md @@ -6,7 +6,7 @@ compatibility: 'Requires uv, Python 3.11+, PowerShell 7+, and LibreOffice' metadata: authors: "microsoft/hve-core" spec_version: "1.0" - last_updated: "2026-03-18" + last_updated: "2026-07-31" --- # PowerPoint Skill @@ -452,7 +452,7 @@ python scripts/embed_audio.py \ Embeds WAV audio files into PPTX slides. Audio files are matched to slides by naming convention (`slide-001.wav`, `slide-002.wav`, etc.). The audio icon is placed off-screen (below the slide boundary) to keep it hidden during presentation. Pass `--slides` to embed audio on specific slides only. -**Dependencies**: Requires `pillow` (`pip install pillow`) for poster frame generation. +**Dependencies**: Requires `pillow` (`uv pip install pillow`) for poster frame generation. > [!NOTE] > WAV files are embedded uncompressed. For large narrated decks, consider pre-compressing audio before embedding to manage PPTX file size. diff --git a/.github/workflows/beval.yml b/.github/workflows/beval.yml index d712490f9..06fbabbce 100644 --- a/.github/workflows/beval.yml +++ b/.github/workflows/beval.yml @@ -39,7 +39,7 @@ jobs: echo "${{ github.workspace }}/evals/beval/node_modules/.bin" >> "$GITHUB_PATH" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Install beval # beval is hosted under a personal account (vyta) while an org-owned diff --git a/.github/workflows/pip-install-lint.yml b/.github/workflows/pip-install-lint.yml index 66e9d2185..ab18ea216 100644 --- a/.github/workflows/pip-install-lint.yml +++ b/.github/workflows/pip-install-lint.yml @@ -3,18 +3,16 @@ name: Pip Install Lint on: workflow_call: +permissions: + contents: read + jobs: check-bare-pip-install: name: Check for bare pip install runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 - with: - python-version: '3.x' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run bare pip install lint check - run: python scripts/lint_pip_install.py + run: pwsh -File scripts/linting/Invoke-PipInstallLint.ps1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 9af250c18..c667d0896 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -489,6 +489,7 @@ jobs: - node-tests - fuzz-tests - pip-audit + - pip-install-lint - docusaurus-tests - frontmatter-validation - adr-consistency-validation diff --git a/scripts/lint_pip_install.py b/scripts/lint_pip_install.py deleted file mode 100644 index 6003caebb..000000000 --- a/scripts/lint_pip_install.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2026 Microsoft Corporation. All rights reserved. -# SPDX-License-Identifier: MIT - -""" -Lint script to detect bare 'pip install' calls. -The repository follows a 'uv-first' Python convention. -""" -import os -import re -import sys - - -EXCLUDE_DIRS = {".git", "evals", ".venv", "venv", "env", "node_modules", "__pycache__"} -EXCLUDE_FILES = {"THIRD-PARTY-NOTICES", "lint_pip_install.py"} -TARGET_DIRS = [".github/workflows", "scripts"] - -bare_pip_pattern = re.compile(r'\bpip3?\s+install\b') -uv_pip_pattern = re.compile(r'\buv\s+pip3?\s+install\b') - -violations = [] -scanned_files = set() - -def should_exclude(filepath): - norm_path = filepath.replace("\\", "/") - - parts = set(norm_path.split("/")) - if parts & EXCLUDE_DIRS: - return True - - for ex_file in EXCLUDE_FILES: - if ex_file in norm_path: - return True - - return False - -def scan_file(filepath): - if filepath in scanned_files: - return - scanned_files.add(filepath) - - if should_exclude(filepath): - return - - try: - with open(filepath, "r", encoding="utf-8", errors="ignore") as f: - for i, line in enumerate(f, 1): - stripped_line = line.strip() - - if not stripped_line or stripped_line.startswith("#"): - continue - - if stripped_line.startswith("name:") or stripped_line.startswith("- name:"): - continue - - if bare_pip_pattern.search(line) and not uv_pip_pattern.search(line): - violations.append(f"{filepath}:{i}: {stripped_line}") - except Exception as e: - print(f"Warning: Could not read {filepath}: {e}") - -def main(): - for target_dir in TARGET_DIRS: - if os.path.isdir(target_dir): - for root, _, files in os.walk(target_dir): - for file in files: - scan_file(os.path.join(root, file)) - - for root, _, files in os.walk("."): - for file in files: - if file.endswith(".py"): - scan_file(os.path.join(root, file)) - - if violations: - print("ERROR: Found bare 'pip install' calls. Use 'uv pip install' instead.") - print("The repo follows a uv-first Python convention.\n") - for v in sorted(set(violations)): - print(f" - {v}") - sys.exit(1) - else: - print("Success: No bare 'pip install' calls found.") - -if __name__ == "__main__": - main() diff --git a/scripts/linting/Invoke-PipInstallLint.ps1 b/scripts/linting/Invoke-PipInstallLint.ps1 new file mode 100644 index 000000000..c26eee252 --- /dev/null +++ b/scripts/linting/Invoke-PipInstallLint.ps1 @@ -0,0 +1,114 @@ +# Copyright (c) 2026 Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +<# +.SYNOPSIS +Lint script to detect bare 'pip install' calls. +The repository follows a 'uv-first' Python convention. +#> + +param( + [string]$TestDirectory = "" +) + +$ErrorActionPreference = "Stop" + +$script:ExcludeDirs = @(".git", "evals", ".venv", "venv", "env", "node_modules", "__pycache__") +$script:ExcludeFiles = @("THIRD-PARTY-NOTICES", "Invoke-PipInstallLint.ps1", "Invoke-PipInstallLint.Tests.ps1") +$script:Violations = @() +$script:ScannedFiles = @{} + +function script:Should-Exclude { + param([string]$Path) + $normalizedPath = $Path.Replace("\", "/").ToLowerInvariant() + + foreach ($dir in $script:ExcludeDirs) { + if ($normalizedPath -match "(^|/)$dir(/|$)") { return $true } + } + foreach ($file in $script:ExcludeFiles) { + if ($normalizedPath -match "(^|/)$file(/|$)") { return $true } + } + return $false +} + +function script:Scan-File { + param([string]$FilePath) + + $normalizedPath = $FilePath.Replace("\", "/") + if ($script:ScannedFiles.ContainsKey($normalizedPath)) { return } + $script:ScannedFiles[$normalizedPath] = $true + + if (script:Should-Exclude -Path $FilePath) { return } + + $ext = [System.IO.Path]::GetExtension($FilePath).ToLowerInvariant() + if ($ext -notin @(".py", ".ps1", ".yml", ".yaml", ".md", "")) { return } + + try { + $lines = Get-Content -Path $FilePath -Raw -ErrorAction SilentlyContinue + if (-not $lines) { return } + + $lineNumber = 1 + foreach ($line in $lines -split "`r?`n") { + $strippedLine = $line.Trim() + + if ([string]::IsNullOrWhiteSpace($strippedLine)) { + $lineNumber++ + continue + } + + if ($line -match "#\s*pip-install-ok\b" -or $line -match "") { + $lineNumber++ + continue + } + + if ($line -match "\bpip3?\s+install\b" -and $line -notmatch "\buv\s+pip3?\s+install\b") { + if ($strippedLine -notmatch "^(name:|- name:)") { + $script:Violations += "$FilePath`:$lineNumber`: $strippedLine" + } + } + $lineNumber++ + } + } + catch { + Write-Warning "Could not read $FilePath`: $_" + } +} + +function script:Invoke-Lint { + param([string]$TargetDir = ".") + + $script:Violations = @() + $script:ScannedFiles = @{} + + if ($TargetDir -eq ".") { + foreach ($dir in @(".github/workflows", "scripts")) { + if (Test-Path $dir) { + Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { script:Scan-File -FilePath $_.FullName } + } + } + Get-ChildItem -Path "." -Recurse -Include *.py, *.ps1, *.yml, *.yaml, *.md -File -ErrorAction SilentlyContinue | ForEach-Object { + script:Scan-File -FilePath $_.FullName + } + } else { + Get-ChildItem -Path $TargetDir -Recurse -Include *.py, *.ps1, *.yml, *.yaml, *.md -File -ErrorAction SilentlyContinue | ForEach-Object { + script:Scan-File -FilePath $_.FullName + } + } + + if ($script:Violations.Count -gt 0) { + Write-Error "ERROR: Found bare 'pip install' calls. Use 'uv pip install' instead." + Write-Host "The repo follows a uv-first Python convention.`n" -ForegroundColor Yellow + foreach ($v in ($script:Violations | Sort-Object -Unique)) { + Write-Host " - $v" -ForegroundColor Red + } + return $false + } else { + Write-Host "Success: No bare 'pip install' calls found." -ForegroundColor Green + return $true + } +} + +if ($MyInvocation.InvocationName -ne '.') { + $success = script:Invoke-Lint -TargetDir $TestDirectory + if (-not $success) { exit 1 } +} diff --git a/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 new file mode 100644 index 000000000..607a2950d --- /dev/null +++ b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 @@ -0,0 +1,78 @@ +# Copyright (c) 2026 Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +Describe "Invoke-PipInstallLint.ps1" { + BeforeAll { + $scriptPath = "$PSScriptRoot/../../linting/Invoke-PipInstallLint.ps1" + . $scriptPath + $testDir = "$PSScriptRoot/TestLintDir" + if (Test-Path $testDir) { Remove-Item -Recurse -Force $testDir } + New-Item -ItemType Directory -Path $testDir | Out-Null + } + + AfterAll { + if (Test-Path $testDir) { Remove-Item -Recurse -Force $testDir } + } + + AfterEach { + if (Test-Path $testDir) { Remove-Item -Recurse -Force $testDir } + New-Item -ItemType Directory -Path $testDir | Out-Null + } + + It "Should pass on clean state" { + $testFile = Join-Path $testDir "clean.py" + Set-Content -Path $testFile -Value "print('hello world')" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } + + It "Should detect bare pip install violation" { + $testFile = Join-Path $testDir "violation.yml" + Set-Content -Path $testFile -Value "run: pip install malicious-package" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $false + $script:Violations.Count | Should -BeGreaterThan 0 + $script:Violations[0] | Should -Match "malicious-package" + } + + It "Should respect exclusion logic (evals directory)" { + $evalsDir = Join-Path $testDir "evals" + New-Item -ItemType Directory -Path $evalsDir | Out-Null + $testFile = Join-Path $evalsDir "fake_eval_test.py" + Set-Content -Path $testFile -Value "run: pip install mock-package" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } + + It "Should allow uv pip install" { + $testFile = Join-Path $testDir "uv_allowed.py" + Set-Content -Path $testFile -Value "run: uv pip install fastapi" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } + + It "Should respect inline ignore marker for Python/YAML" { + $testFile = Join-Path $testDir "ignored.py" + Set-Content -Path $testFile -Value "run: pip install legacy-package # pip-install-ok" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } + + It "Should respect inline ignore marker for Markdown" { + $testFile = Join-Path $testDir "ignored.md" + Set-Content -Path $testFile -Value "run: pip install legacy-package " + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } +} From f96c66c1ecd2a03e93fdddbef8787c7cfebd4dc5 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Sun, 2 Aug 2026 10:58:55 +0530 Subject: [PATCH 3/5] fix: enforce uv-first convention with bare pip install lint rule and CI fails --- .../gen-jupyter-notebook.agent.md | 14 +++--- .../test-streamlit-dashboard.agent.md | 12 +++-- .../project-planning/prd-builder.agent.md | 24 +++++----- .../python-script.instructions.md | 12 +++-- .../python-tests.instructions.md | 20 ++++++-- .../uv-projects.instructions.md | 2 +- .../experimental/pptx.instructions.md | 2 +- .../synth-data-generate.prompt.md | 10 ++-- .../dt-canonical-deck.prompt.md | 2 +- .../references/code-style-patterns.md | 17 ++++--- .../references/design-principles.md | 6 +++ .../customer-card-render/SKILL.md | 16 +++---- .../skills/experimental/powerpoint/SKILL.md | 30 ++++++------ .../powerpoint/content-extra-py-template.md | 6 ++- .github/workflows/beval.yml | 5 +- .github/workflows/pip-install-lint.yml | 10 +++- .github/workflows/pr-validation.yml | 4 ++ scripts/linting/Invoke-PipInstallLint.ps1 | 20 ++++---- .../linting/Invoke-PipInstallLint.Tests.ps1 | 48 +++++++++++++++---- 19 files changed, 172 insertions(+), 88 deletions(-) diff --git a/.github/agents/data-science/gen-jupyter-notebook.agent.md b/.github/agents/data-science/gen-jupyter-notebook.agent.md index e821afb38..6f774e427 100644 --- a/.github/agents/data-science/gen-jupyter-notebook.agent.md +++ b/.github/agents/data-science/gen-jupyter-notebook.agent.md @@ -74,8 +74,8 @@ Principles: Standard pattern: ```python -fig = px.bar(df_grouped, x='room', y='count', color='room', title='Records by Room') -fig.update_layout(xaxis_title='Room', yaxis_title='Count') +fig = px.bar(df_grouped, x="room", y="count", color="room", title="Records by Room") +fig.update_layout(xaxis_title="Room", yaxis_title="Count") fig.show() ``` @@ -148,11 +148,13 @@ Path resolution (include in Configuration & Imports): ```python from pathlib import Path -NOTEBOOK_DIR = Path(__file__).resolve().parent if '__file__' in globals() else Path.cwd() +NOTEBOOK_DIR = ( + Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd() +) PROJECT_ROOT = NOTEBOOK_DIR.parent -DATA_DIR = PROJECT_ROOT / 'data' -OUTPUTS_DIR = PROJECT_ROOT / 'outputs' -PROCESSED_DIR = DATA_DIR / 'processed' +DATA_DIR = PROJECT_ROOT / "data" +OUTPUTS_DIR = PROJECT_ROOT / "outputs" +PROCESSED_DIR = DATA_DIR / "processed" PROCESSED_DIR.mkdir(parents=True, exist_ok=True) ``` diff --git a/.github/agents/data-science/test-streamlit-dashboard.agent.md b/.github/agents/data-science/test-streamlit-dashboard.agent.md index 13b0b37c4..1874e1925 100644 --- a/.github/agents/data-science/test-streamlit-dashboard.agent.md +++ b/.github/agents/data-science/test-streamlit-dashboard.agent.md @@ -17,7 +17,7 @@ Confirm prerequisites and prepare the test environment. 2. Verify Playwright and pytest-playwright are installed. Install if missing: ```bash - pip install playwright pytest-playwright pytest-asyncio + uv pip install playwright pytest-playwright pytest-asyncio playwright install chromium ``` @@ -107,9 +107,13 @@ async def test_page_navigation(page): """Test sidebar navigation functionality""" await page.goto("http://localhost:8501") - pages = ["📊 Summary Statistics", "📈 Univariate Analysis", - "🔗 Multivariate Analysis", "⏰ Time Series Analysis", - "💬 Chat Interface"] + pages = [ + "📊 Summary Statistics", + "📈 Univariate Analysis", + "🔗 Multivariate Analysis", + "⏰ Time Series Analysis", + "💬 Chat Interface", + ] for page_name in pages: await page.select_option("select", page_name) diff --git a/.github/agents/project-planning/prd-builder.agent.md b/.github/agents/project-planning/prd-builder.agent.md index ca579c4ca..a55985837 100644 --- a/.github/agents/project-planning/prd-builder.agent.md +++ b/.github/agents/project-planning/prd-builder.agent.md @@ -240,12 +240,12 @@ When conversation context has been summarized, implement robust recovery: 5. State reconstruction algorithm: ```python if state_file_missing or state_file_corrupted: - analyze_prd_content() - extract_completed_sections() - infer_answered_questions() - identify_next_logical_steps() - create_new_state_file() - confirm_assumptions_with_user() + analyze_prd_content() + extract_completed_sections() + infer_answered_questions() + identify_next_logical_steps() + create_new_state_file() + confirm_assumptions_with_user() ``` ## Questioning Strategy @@ -453,12 +453,12 @@ Before asking any question, check state file: 1. Question history check: ```python if question_key in state.questionsAsked: - if question_key in state.answeredQuestions: - # Use existing answer, don't re-ask - use_existing_answer(state.answeredQuestions[question_key]) - else: - # Question was asked but not answered, ask again with context - ask_with_context("Previously asked but not answered...") + if question_key in state.answeredQuestions: + # Use existing answer, don't re-ask + use_existing_answer(state.answeredQuestions[question_key]) + else: + # Question was asked but not answered, ask again with context + ask_with_context("Previously asked but not answered...") ``` 2. Dynamic question generation: diff --git a/.github/instructions/coding-standards/python-script.instructions.md b/.github/instructions/coding-standards/python-script.instructions.md index 8f46132bf..95609d6dd 100644 --- a/.github/instructions/coding-standards/python-script.instructions.md +++ b/.github/instructions/coding-standards/python-script.instructions.md @@ -9,7 +9,7 @@ Conventions for Python 3.11+ scripts used in automation, tooling, and CLI applic ## Environment and Dependency Management -1. **Never use `pip install` directly.** All package management must be handled using `uv` (e.g., `uv add `). +1. **Never use `pip install` directly.** All package management must be handled using `uv` (e.g., `uv add `). 2. **Never run Python scripts or tools outside a virtual environment.** Always execute scripts via `uv run ` or ensure the `.venv` is activated before running. 3. Ensure a `.venv` exists in the project root before executing any Python code. If starting from scratch, refer to the `uv-projects.instructions.md` file for environment setup. @@ -21,7 +21,7 @@ import sys EXIT_SUCCESS = 0 # Successful execution EXIT_FAILURE = 1 # General failure -EXIT_ERROR = 2 # Arguments or configuration error +EXIT_ERROR = 2 # Arguments or configuration error def main() -> int: @@ -121,13 +121,17 @@ import os from pathlib import Path -def run_command(cmd: list[str], cwd: Path | None = None, extra_env: dict[str, str] | None = None) -> str: +def run_command( + cmd: list[str], cwd: Path | None = None, extra_env: dict[str, str] | None = None +) -> str: """Run command and return stdout, raising on failure.""" env = os.environ.copy() if extra_env: env.update(extra_env) try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=cwd, env=env) + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, cwd=cwd, env=env + ) return result.stdout except subprocess.CalledProcessError as e: logger.error("Command failed: %s\nstderr: %s", e.returncode, e.stderr) diff --git a/.github/instructions/coding-standards/python-tests.instructions.md b/.github/instructions/coding-standards/python-tests.instructions.md index 72a621260..b7a554b17 100644 --- a/.github/instructions/coding-standards/python-tests.instructions.md +++ b/.github/instructions/coding-standards/python-tests.instructions.md @@ -196,13 +196,17 @@ from myapp.service import DataService class TestDataProcessor: @pytest.fixture() def mock_service(self, mocker): - return mocker.patch.object(DataService, "fetch", return_value={"status": "ok", "value": 42}) + return mocker.patch.object( + DataService, "fetch", return_value={"status": "ok", "value": 42} + ) @pytest.fixture() def processor(self): return DataProcessor(service=DataService()) - def test_given_valid_response_when_process_then_returns_value(self, processor, mock_service): + def test_given_valid_response_when_process_then_returns_value( + self, processor, mock_service + ): # Act result = processor.process() @@ -212,7 +216,9 @@ class TestDataProcessor: def test_given_error_response_when_process_then_raises(self, processor, mocker): # Arrange - mocker.patch.object(DataService, "fetch", side_effect=ConnectionError("timeout")) + mocker.patch.object( + DataService, "fetch", side_effect=ConnectionError("timeout") + ) # Act & Assert with pytest.raises(ConnectionError, match="timeout"): @@ -225,9 +231,13 @@ class TestDataProcessor: ("pending", 0), ], ) - def test_given_status_when_process_then_returns_expected(self, mocker, status, expected): + def test_given_status_when_process_then_returns_expected( + self, mocker, status, expected + ): # Arrange - mocker.patch.object(DataService, "fetch", return_value={"status": status, "value": expected}) + mocker.patch.object( + DataService, "fetch", return_value={"status": status, "value": expected} + ) processor = DataProcessor(service=DataService()) # Act diff --git a/.github/instructions/coding-standards/uv-projects.instructions.md b/.github/instructions/coding-standards/uv-projects.instructions.md index 3ae1f0dc6..d1736f19d 100644 --- a/.github/instructions/coding-standards/uv-projects.instructions.md +++ b/.github/instructions/coding-standards/uv-projects.instructions.md @@ -9,7 +9,7 @@ You are a Python environment specialist focused on uv virtual environment manage ## Strict Constraints -1. **Never use `pip install` directly.** Always use `uv add ` for all package management (adding, removing, locking, and syncing dependencies). +1. **Never use `pip install` directly.** Always use `uv add ` for all package management (adding, removing, locking, and syncing dependencies). 2. **Never run Python scripts or tools outside a virtual environment.** Always execute scripts via `uv run ` or ensure the `.venv` is activated first. 3. **Verify `.venv` existence:** Before any Python work, verify a `.venv` exists in the project root. If not, create one with `uv init` and `uv sync`. 4. **Migration Path:** If a `requirements.txt` exists but no `pyproject.toml`, migrate it by running `uv init` and then `uv add -r requirements.txt`. diff --git a/.github/instructions/experimental/pptx.instructions.md b/.github/instructions/experimental/pptx.instructions.md index e06a365c5..4a34aed30 100644 --- a/.github/instructions/experimental/pptx.instructions.md +++ b/.github/instructions/experimental/pptx.instructions.md @@ -60,7 +60,7 @@ Include `` at the top of all markdown files cr * For update and cleanup workflows, preserve existing masters and layouts from the source deck. * When updating an existing deck, always regenerate from content YAML rather than modifying the PPTX directly; update content files first, then regenerate into `slide-deck/`. * Follow the repo's Python environment conventions (`uv-projects.instructions.md`) for virtual environment and dependency management. -* All dependencies are declared in `pyproject.toml` at the skill root. The `Invoke-PptxPipeline.ps1` orchestrator manages the virtual environment automatically. Never install packages with `pip install` directly. +* All dependencies are declared in `pyproject.toml` at the skill root. The `Invoke-PptxPipeline.ps1` orchestrator manages the virtual environment automatically. Never install packages with `pip install` directly. * When scripts fail due to missing modules or import errors, follow the Environment Recovery steps in the `powerpoint` skill instructions. ### Build Mode: `--template` vs `--source` diff --git a/.github/prompts/data-science/synth-data-generate.prompt.md b/.github/prompts/data-science/synth-data-generate.prompt.md index 1dda22d88..bad64747c 100644 --- a/.github/prompts/data-science/synth-data-generate.prompt.md +++ b/.github/prompts/data-science/synth-data-generate.prompt.md @@ -100,7 +100,7 @@ All files for the synthetic data project should be organized in a dedicated fold Create a well-structured notebook with the following cells: 1. Title Cell (Markdown): Clear title with the subject -2. Package Installation Cell (Python): Install required packages using `%pip install pandas numpy matplotlib seaborn scipy` +2. Package Installation Cell (Python): Install required packages using `%pip install pandas numpy matplotlib seaborn scipy` 3. Library Import Cell (Python): Import all required libraries 4. Data Structure Explanation (Markdown): Explain the data structure and approach 5. Backup Creation (Python): If updating existing data source, create backup in notebook directory with `.bak` extension @@ -138,7 +138,9 @@ day = np.random.choice(pd.date_range(start=start_date, end=end_date)) day = pd.Timestamp(day).date() # Ensures Python datetime.date hour = int(np.random.choice(range(8, 19))) minute = int(np.random.randint(0, 60)) -start_time = datetime.combine(day, datetime.min.time()) + timedelta(hours=hour, minutes=minute) +start_time = datetime.combine(day, datetime.min.time()) + timedelta( + hours=hour, minutes=minute +) ``` ### Data Types & Ranges @@ -237,7 +239,7 @@ start_time = datetime.combine(day, datetime.min.time()) + timedelta(hours=hour, ```python # Cell 1: Package Installation (Python) -%pip install pandas numpy matplotlib seaborn scipy +%pip install pandas numpy matplotlib seaborn scipy # Cell 2: Library Imports (Python) import pandas as pd @@ -352,4 +354,4 @@ print(f"\\nGeneration timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" weather_12_states_12_months/ ├── synth_weather_12_states_12_months.ipynb └── synthetic_weather_12_states_12_months_data.csv -``` \ No newline at end of file +``` diff --git a/.github/prompts/design-thinking/dt-canonical-deck.prompt.md b/.github/prompts/design-thinking/dt-canonical-deck.prompt.md index 0518c9a3e..71c12c107 100644 --- a/.github/prompts/design-thinking/dt-canonical-deck.prompt.md +++ b/.github/prompts/design-thinking/dt-canonical-deck.prompt.md @@ -108,7 +108,7 @@ Use the bash script instead. Verify the bash script flags by sending `invoke-ppt - Use `send_to_terminal` to send commands to the active terminal - Use `get_terminal_output` to poll for completion -- Do not run `pip install` or manual dependency installation +- Do not run `pip install` or manual dependency installation - Rely on PowerPoint skill environment setup (`uv sync`) and documented prerequisites - Keep output under the project slug render directory diff --git a/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md b/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md index 32201e182..817ce7329 100644 --- a/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md +++ b/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md @@ -24,18 +24,22 @@ Python naming conventions remove ambiguity. class ModelTrainer: pass + # Functions and variables: snake_case def train_model(): training_data = [] + # Constants: UPPER_SNAKE_CASE MAX_SEQUENCE_LENGTH = 2048 DEFAULT_LEARNING_RATE = 1e-4 + # Private members: leading underscore def _internal_helper(): pass + _internal_cache = {} ``` @@ -68,6 +72,7 @@ The `*` separator forces callers to name optional parameters, preventing silent def train(data: list, learning_rate: float = 1e-4, batch_size: int = 32): pass + # Caller can silently swap learning_rate and batch_size train(data, 32, 1e-4) ``` @@ -83,6 +88,7 @@ def train( ) -> None: pass + # Caller must name each optional parameter train(data, learning_rate=1e-3, batch_size=64) ``` @@ -209,10 +215,7 @@ def load_config(path: Path) -> dict: with open(path) as f: return yaml.safe_load(f) except yaml.YAMLError as e: - raise ValueError( - f"Invalid YAML in config file: {path}\n" - f"Error: {e}" - ) from e + raise ValueError(f"Invalid YAML in config file: {path}\nError: {e}") from e ``` ## Custom Exception Hierarchies @@ -223,20 +226,22 @@ In applications with multiple error categories, a base application exception ena class AppError(Exception): """Base exception for the application.""" + class ConfigError(AppError): """Configuration error.""" + class ValidationError(AppError): """Validation error.""" + def validate_config(config: dict) -> None: """Validate configuration.""" required = ["database", "api_key", "settings"] missing = [k for k in required if k not in config] if missing: raise ConfigError( - f"Missing required config keys: {missing}\n" - f"Required: {required}" + f"Missing required config keys: {missing}\nRequired: {required}" ) ``` diff --git a/.github/skills/coding-standards/python-foundational/references/design-principles.md b/.github/skills/coding-standards/python-foundational/references/design-principles.md index f9e62438b..cd180aaa5 100644 --- a/.github/skills/coding-standards/python-foundational/references/design-principles.md +++ b/.github/skills/coding-standards/python-foundational/references/design-principles.md @@ -29,6 +29,7 @@ def create_user(data: dict) -> User: raise ValueError("Invalid name") return User(**data) + def update_user(user: User, data: dict) -> User: if not data.get("email") or "@" not in data["email"]: raise ValueError("Invalid email") @@ -48,10 +49,12 @@ def _validate_user_fields(data: dict) -> None: if not data.get("name") or len(data["name"]) < 2: raise ValueError("Invalid name") + def create_user(data: dict) -> User: _validate_user_fields(data) return User(**data) + def update_user(user: User, data: dict) -> User: _validate_user_fields(data) user.email = data["email"] @@ -71,6 +74,7 @@ Introduce abstractions only when multiple implementations actually exist. Avoid class NotificationStrategy(Protocol): def send(self, message: str, recipient: str) -> None: ... + class EmailNotifier: def __init__(self, strategy: NotificationStrategy) -> None: self.strategy = strategy @@ -78,10 +82,12 @@ class EmailNotifier: def notify(self, message: str, recipient: str) -> None: self.strategy.send(message, recipient) + class SmtpStrategy: def send(self, message: str, recipient: str) -> None: smtp_client.send_email(recipient, message) + # Usage notifier = EmailNotifier(SmtpStrategy()) notifier.notify("Hello", "user@example.com") diff --git a/.github/skills/experimental/customer-card-render/SKILL.md b/.github/skills/experimental/customer-card-render/SKILL.md index 52ecbe284..1c626dd6c 100644 --- a/.github/skills/experimental/customer-card-render/SKILL.md +++ b/.github/skills/experimental/customer-card-render/SKILL.md @@ -38,7 +38,7 @@ For full PowerPoint pipeline documentation, see [powerpoint/SKILL.md](../powerpo powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Via pip (fallback) - pip install uv + pip install uv ``` * The experimental `powerpoint` skill at `.github/skills/experimental/powerpoint/` for the `Invoke-PptxPipeline.ps1` build step @@ -152,11 +152,11 @@ For complete mapping details, see [references/mapping-spec.md](references/mappin ## Troubleshooting -| Issue | Cause | Solution | -|---------------------------------|--------------------------------------------|------------------------------------------------------------------------------------------| -| `uv` not found | uv not installed | Run `curl -LsSf https://astral.sh/uv/install.sh \| sh` (macOS/Linux) or `pip install uv` | -| Python not found by uv | No Python 3.11+ on PATH | Run `uv python install 3.11` | -| Template not found | `--canonical-dir` contains unknown type | Check frontmatter `type:` field against supported artifact types | -| Empty output directory | No canonical markdown files found | Confirm `--canonical-dir` path and that files have `---` frontmatter | -| PPTX build fails after generate | PowerPoint skill missing or path incorrect | Confirm `powerpoint/` skill exists at `.github/skills/experimental/powerpoint/` | +| Issue | Cause | Solution | +|---------------------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| `uv` not found | uv not installed | Run `curl -LsSf https://astral.sh/uv/install.sh \| sh` (macOS/Linux) or `pip install uv` | +| Python not found by uv | No Python 3.11+ on PATH | Run `uv python install 3.11` | +| Template not found | `--canonical-dir` contains unknown type | Check frontmatter `type:` field against supported artifact types | +| Empty output directory | No canonical markdown files found | Confirm `--canonical-dir` path and that files have `---` frontmatter | +| PPTX build fails after generate | PowerPoint skill missing or path incorrect | Confirm `powerpoint/` skill exists at `.github/skills/experimental/powerpoint/` | diff --git a/.github/skills/experimental/powerpoint/SKILL.md b/.github/skills/experimental/powerpoint/SKILL.md index b65b37f2a..9a284ff35 100644 --- a/.github/skills/experimental/powerpoint/SKILL.md +++ b/.github/skills/experimental/powerpoint/SKILL.md @@ -37,7 +37,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Via pip (fallback) -pip install uv +pip install uv ``` ### System Dependencies (Export and Validation) @@ -452,7 +452,7 @@ python scripts/embed_audio.py \ Embeds WAV audio files into PPTX slides. Audio files are matched to slides by naming convention (`slide-001.wav`, `slide-002.wav`, etc.). The audio icon is placed off-screen (below the slide boundary) to keep it hidden during presentation. Pass `--slides` to embed audio on specific slides only. -**Dependencies**: Requires `pillow` (`uv pip install pillow`) for poster frame generation. +**Dependencies**: Requires `pillow` (`uv pip install pillow`) for poster frame generation. > [!NOTE] > WAV files are embedded uncompressed. For large narrated decks, consider pre-compressing audio before embedding to manage PPTX file size. @@ -529,19 +529,19 @@ Re-check [NVD](https://nvd.nist.gov) and [OSV](https://osv.dev) advisories for M ## Troubleshooting -| Issue | Cause | Solution | -|----------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------------------------------| -| SVG runtime error | python-pptx cannot embed SVG | Convert to PNG via `cairosvg` before adding | -| Text overlay between elements | Insufficient vertical spacing | Follow element positioning conventions in `pptx.instructions.md` | -| Width overflow off-slide | Element extends beyond slide boundary | Follow element positioning conventions in `pptx.instructions.md` | -| Bright accent color unreadable as fill | White text on bright background | Darken accent to ~60% saturation for box fills | -| Background fill replaced with NoFill | Accessed `background.fill` on inherited background | Check `slide.follow_master_background` before accessing | -| Missing speaker notes | Notes not specified in `content.yaml` | Add `speaker_notes` field to every content slide | -| LibreOffice not found during Validate | Validate exports slides to images first | Install LibreOffice: `brew install --cask libreoffice` (macOS) | -| `uv` not found | uv package manager not installed | Install uv: `curl -LsSf https://astral.sh/uv/install.sh \| sh` (macOS/Linux) or `pip install uv` | -| Python not found by uv | No Python 3.11+ on PATH | Install via `uv python install 3.11` or `pyenv install 3.11` | -| `uv sync` fails | Missing or corrupt `.venv` | Delete `.venv/` at the skill root and re-run `uv sync` | -| Import errors in scripts | Dependencies not installed or stale venv | Run `uv sync` from the skill root to recreate the environment | +| Issue | Cause | Solution | | +|----------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------------------------------|-------------------------| +| SVG runtime error | python-pptx cannot embed SVG | Convert to PNG via `cairosvg` before adding | | +| Text overlay between elements | Insufficient vertical spacing | Follow element positioning conventions in `pptx.instructions.md` | | +| Width overflow off-slide | Element extends beyond slide boundary | Follow element positioning conventions in `pptx.instructions.md` | | +| Bright accent color unreadable as fill | White text on bright background | Darken accent to ~60% saturation for box fills | | +| Background fill replaced with NoFill | Accessed `background.fill` on inherited background | Check `slide.follow_master_background` before accessing | | +| Missing speaker notes | Notes not specified in `content.yaml` | Add `speaker_notes` field to every content slide | | +| LibreOffice not found during Validate | Validate exports slides to images first | Install LibreOffice: `brew install --cask libreoffice` (macOS) | | +| `uv` not found | uv package manager not installed | Install uv: `curl -LsSf https://astral.sh/uv/install.sh \| sh` (macOS/Linux) or `pip install uv` | | +| Python not found by uv | No Python 3.11+ on PATH | Install via `uv python install 3.11` or `pyenv install 3.11` | | +| `uv sync` fails | Missing or corrupt `.venv` | Delete `.venv/` at the skill root and re-run `uv sync` | | +| Import errors in scripts | Dependencies not installed or stale venv | Run `uv sync` from the skill root to recreate the environment | | ## Environment Recovery diff --git a/.github/skills/experimental/powerpoint/content-extra-py-template.md b/.github/skills/experimental/powerpoint/content-extra-py-template.md index 0d6c677df..ea1420c3d 100644 --- a/.github/skills/experimental/powerpoint/content-extra-py-template.md +++ b/.github/skills/experimental/powerpoint/content-extra-py-template.md @@ -19,6 +19,7 @@ Use this template when a slide requires complex drawings that cannot be expresse ```python """Custom drawing for slide NNN — description of what this draws.""" + from pptx.util import Inches, Pt from pptx.dml.color import RGBColor @@ -41,7 +42,10 @@ def render(slide, style, content_dir): for label, color, top in layers: shape = slide.shapes.add_shape( 1, # MSO_SHAPE.RECTANGLE - Inches(2.0), Inches(top), Inches(9.0), Inches(1.2) + Inches(2.0), + Inches(top), + Inches(9.0), + Inches(1.2), ) shape.fill.solid() shape.fill.fore_color.rgb = RGBColor.from_string(color.lstrip("#")) diff --git a/.github/workflows/beval.yml b/.github/workflows/beval.yml index 06fbabbce..fb69f4d67 100644 --- a/.github/workflows/beval.yml +++ b/.github/workflows/beval.yml @@ -39,7 +39,10 @@ jobs: echo "${{ github.workspace }}/evals/beval/node_modules/.bin" >> "$GITHUB_PATH" - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install beval # beval is hosted under a personal account (vyta) while an org-owned diff --git a/.github/workflows/pip-install-lint.yml b/.github/workflows/pip-install-lint.yml index ab18ea216..e6aca34ac 100644 --- a/.github/workflows/pip-install-lint.yml +++ b/.github/workflows/pip-install-lint.yml @@ -2,6 +2,12 @@ name: Pip Install Lint on: workflow_call: + inputs: + soft-fail: + description: 'Whether to continue on bare pip install violations' # pip-install-ok + required: false + type: boolean + default: false permissions: contents: read @@ -10,9 +16,11 @@ jobs: check-bare-pip-install: name: Check for bare pip install runs-on: ubuntu-latest + permissions: + contents: read + continue-on-error: ${{ inputs.soft-fail }} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Run bare pip install lint check run: pwsh -File scripts/linting/Invoke-PipInstallLint.ps1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index c667d0896..c311edc13 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -30,6 +30,10 @@ jobs: pip-install-lint: name: Pip Install Lint uses: ./.github/workflows/pip-install-lint.yml + permissions: + contents: read + with: + soft-fail: false markdown-lint: name: Markdown Lint diff --git a/scripts/linting/Invoke-PipInstallLint.ps1 b/scripts/linting/Invoke-PipInstallLint.ps1 index c26eee252..d2615d6c0 100644 --- a/scripts/linting/Invoke-PipInstallLint.ps1 +++ b/scripts/linting/Invoke-PipInstallLint.ps1 @@ -8,7 +8,7 @@ The repository follows a 'uv-first' Python convention. #> param( - [string]$TestDirectory = "" + [string]$TestDirectory = "." ) $ErrorActionPreference = "Stop" @@ -18,27 +18,27 @@ $script:ExcludeFiles = @("THIRD-PARTY-NOTICES", "Invoke-PipInstallLint.ps1", "In $script:Violations = @() $script:ScannedFiles = @{} -function script:Should-Exclude { +function script:Test-ExcludedPath { param([string]$Path) $normalizedPath = $Path.Replace("\", "/").ToLowerInvariant() - + foreach ($dir in $script:ExcludeDirs) { - if ($normalizedPath -match "(^|/)$dir(/|$)") { return $true } + if ($normalizedPath -match "(^|/)$([regex]::Escape($dir))(/|$)") { return $true } } foreach ($file in $script:ExcludeFiles) { - if ($normalizedPath -match "(^|/)$file(/|$)") { return $true } + if ($normalizedPath -match "(^|/)$([regex]::Escape($file))(/|$)") { return $true } } return $false } -function script:Scan-File { +function script:Invoke-FileScan { param([string]$FilePath) $normalizedPath = $FilePath.Replace("\", "/") if ($script:ScannedFiles.ContainsKey($normalizedPath)) { return } $script:ScannedFiles[$normalizedPath] = $true - if (script:Should-Exclude -Path $FilePath) { return } + if (script:Test-ExcludedPath -Path $FilePath) { return } $ext = [System.IO.Path]::GetExtension($FilePath).ToLowerInvariant() if ($ext -notin @(".py", ".ps1", ".yml", ".yaml", ".md", "")) { return } @@ -83,15 +83,15 @@ function script:Invoke-Lint { if ($TargetDir -eq ".") { foreach ($dir in @(".github/workflows", "scripts")) { if (Test-Path $dir) { - Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { script:Scan-File -FilePath $_.FullName } + Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { script:Invoke-FileScan -FilePath $_.FullName } } } Get-ChildItem -Path "." -Recurse -Include *.py, *.ps1, *.yml, *.yaml, *.md -File -ErrorAction SilentlyContinue | ForEach-Object { - script:Scan-File -FilePath $_.FullName + script:Invoke-FileScan -FilePath $_.FullName } } else { Get-ChildItem -Path $TargetDir -Recurse -Include *.py, *.ps1, *.yml, *.yaml, *.md -File -ErrorAction SilentlyContinue | ForEach-Object { - script:Scan-File -FilePath $_.FullName + script:Invoke-FileScan -FilePath $_.FullName } } diff --git a/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 index 607a2950d..f802e9046 100644 --- a/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 +++ b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 @@ -22,7 +22,7 @@ Describe "Invoke-PipInstallLint.ps1" { It "Should pass on clean state" { $testFile = Join-Path $testDir "clean.py" Set-Content -Path $testFile -Value "print('hello world')" - + $result = script:Invoke-Lint -TargetDir $testDir $result | Should -Be $true $script:Violations.Count | Should -Be 0 @@ -31,9 +31,9 @@ Describe "Invoke-PipInstallLint.ps1" { It "Should detect bare pip install violation" { $testFile = Join-Path $testDir "violation.yml" Set-Content -Path $testFile -Value "run: pip install malicious-package" - - $result = script:Invoke-Lint -TargetDir $testDir - $result | Should -Be $false + + # Invoke-Lint calls Write-Error on violations, which throws under $ErrorActionPreference=Stop + { script:Invoke-Lint -TargetDir $testDir } | Should -Throw -ExpectedMessage "*bare 'pip install'*" $script:Violations.Count | Should -BeGreaterThan 0 $script:Violations[0] | Should -Match "malicious-package" } @@ -43,7 +43,7 @@ Describe "Invoke-PipInstallLint.ps1" { New-Item -ItemType Directory -Path $evalsDir | Out-Null $testFile = Join-Path $evalsDir "fake_eval_test.py" Set-Content -Path $testFile -Value "run: pip install mock-package" - + $result = script:Invoke-Lint -TargetDir $testDir $result | Should -Be $true $script:Violations.Count | Should -Be 0 @@ -52,7 +52,7 @@ Describe "Invoke-PipInstallLint.ps1" { It "Should allow uv pip install" { $testFile = Join-Path $testDir "uv_allowed.py" Set-Content -Path $testFile -Value "run: uv pip install fastapi" - + $result = script:Invoke-Lint -TargetDir $testDir $result | Should -Be $true $script:Violations.Count | Should -Be 0 @@ -61,7 +61,7 @@ Describe "Invoke-PipInstallLint.ps1" { It "Should respect inline ignore marker for Python/YAML" { $testFile = Join-Path $testDir "ignored.py" Set-Content -Path $testFile -Value "run: pip install legacy-package # pip-install-ok" - + $result = script:Invoke-Lint -TargetDir $testDir $result | Should -Be $true $script:Violations.Count | Should -Be 0 @@ -70,7 +70,39 @@ Describe "Invoke-PipInstallLint.ps1" { It "Should respect inline ignore marker for Markdown" { $testFile = Join-Path $testDir "ignored.md" Set-Content -Path $testFile -Value "run: pip install legacy-package " - + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } + + It "Should scan correctly when TestDirectory uses default value" { + $testFile = Join-Path $testDir "default_param_test.py" + Set-Content -Path $testFile -Value "run: pip install default-violation" + + Push-Location $testDir + try { + { script:Invoke-Lint } | Should -Throw -ExpectedMessage "*bare 'pip install'*" + $script:Violations.Count | Should -BeGreaterThan 0 + $script:Violations[0] | Should -Match "default-violation" + } finally { + Pop-Location + } + } + + It "Should NOT exclude files with similar names (regex escape regression)" { + $testFile = Join-Path $testDir "Invoke-PipInstallLintXps1.py" + Set-Content -Path $testFile -Value "run: pip install wildcard-false-negative" + + { script:Invoke-Lint -TargetDir $testDir } | Should -Throw -ExpectedMessage "*bare 'pip install'*" + $script:Violations.Count | Should -BeGreaterThan 0 + $script:Violations[0] | Should -Match "wildcard-false-negative" + } + + It "Should still exclude exact filename match after regex escaping" { + $testFile = Join-Path $testDir "Invoke-PipInstallLint.ps1" + Set-Content -Path $testFile -Value "run: pip install should-be-excluded" + $result = script:Invoke-Lint -TargetDir $testDir $result | Should -Be $true $script:Violations.Count | Should -Be 0 From 653da419edf94518671a6ec58cd2f96d149b94e4 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Sun, 9 Aug 2026 10:48:36 +0530 Subject: [PATCH 4/5] revert: remove accidental IDE auto-formatting in markdown files --- .../gen-jupyter-notebook.agent.md | 14 +++++------ .../project-planning/prd-builder.agent.md | 24 +++++++++---------- .../python-tests.instructions.md | 20 ++++------------ .../references/code-style-patterns.md | 17 +++++-------- .../references/design-principles.md | 6 ----- .../powerpoint/content-extra-py-template.md | 6 +---- 6 files changed, 30 insertions(+), 57 deletions(-) diff --git a/.github/agents/data-science/gen-jupyter-notebook.agent.md b/.github/agents/data-science/gen-jupyter-notebook.agent.md index 6f774e427..e821afb38 100644 --- a/.github/agents/data-science/gen-jupyter-notebook.agent.md +++ b/.github/agents/data-science/gen-jupyter-notebook.agent.md @@ -74,8 +74,8 @@ Principles: Standard pattern: ```python -fig = px.bar(df_grouped, x="room", y="count", color="room", title="Records by Room") -fig.update_layout(xaxis_title="Room", yaxis_title="Count") +fig = px.bar(df_grouped, x='room', y='count', color='room', title='Records by Room') +fig.update_layout(xaxis_title='Room', yaxis_title='Count') fig.show() ``` @@ -148,13 +148,11 @@ Path resolution (include in Configuration & Imports): ```python from pathlib import Path -NOTEBOOK_DIR = ( - Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd() -) +NOTEBOOK_DIR = Path(__file__).resolve().parent if '__file__' in globals() else Path.cwd() PROJECT_ROOT = NOTEBOOK_DIR.parent -DATA_DIR = PROJECT_ROOT / "data" -OUTPUTS_DIR = PROJECT_ROOT / "outputs" -PROCESSED_DIR = DATA_DIR / "processed" +DATA_DIR = PROJECT_ROOT / 'data' +OUTPUTS_DIR = PROJECT_ROOT / 'outputs' +PROCESSED_DIR = DATA_DIR / 'processed' PROCESSED_DIR.mkdir(parents=True, exist_ok=True) ``` diff --git a/.github/agents/project-planning/prd-builder.agent.md b/.github/agents/project-planning/prd-builder.agent.md index a55985837..ca579c4ca 100644 --- a/.github/agents/project-planning/prd-builder.agent.md +++ b/.github/agents/project-planning/prd-builder.agent.md @@ -240,12 +240,12 @@ When conversation context has been summarized, implement robust recovery: 5. State reconstruction algorithm: ```python if state_file_missing or state_file_corrupted: - analyze_prd_content() - extract_completed_sections() - infer_answered_questions() - identify_next_logical_steps() - create_new_state_file() - confirm_assumptions_with_user() + analyze_prd_content() + extract_completed_sections() + infer_answered_questions() + identify_next_logical_steps() + create_new_state_file() + confirm_assumptions_with_user() ``` ## Questioning Strategy @@ -453,12 +453,12 @@ Before asking any question, check state file: 1. Question history check: ```python if question_key in state.questionsAsked: - if question_key in state.answeredQuestions: - # Use existing answer, don't re-ask - use_existing_answer(state.answeredQuestions[question_key]) - else: - # Question was asked but not answered, ask again with context - ask_with_context("Previously asked but not answered...") + if question_key in state.answeredQuestions: + # Use existing answer, don't re-ask + use_existing_answer(state.answeredQuestions[question_key]) + else: + # Question was asked but not answered, ask again with context + ask_with_context("Previously asked but not answered...") ``` 2. Dynamic question generation: diff --git a/.github/instructions/coding-standards/python-tests.instructions.md b/.github/instructions/coding-standards/python-tests.instructions.md index b7a554b17..72a621260 100644 --- a/.github/instructions/coding-standards/python-tests.instructions.md +++ b/.github/instructions/coding-standards/python-tests.instructions.md @@ -196,17 +196,13 @@ from myapp.service import DataService class TestDataProcessor: @pytest.fixture() def mock_service(self, mocker): - return mocker.patch.object( - DataService, "fetch", return_value={"status": "ok", "value": 42} - ) + return mocker.patch.object(DataService, "fetch", return_value={"status": "ok", "value": 42}) @pytest.fixture() def processor(self): return DataProcessor(service=DataService()) - def test_given_valid_response_when_process_then_returns_value( - self, processor, mock_service - ): + def test_given_valid_response_when_process_then_returns_value(self, processor, mock_service): # Act result = processor.process() @@ -216,9 +212,7 @@ class TestDataProcessor: def test_given_error_response_when_process_then_raises(self, processor, mocker): # Arrange - mocker.patch.object( - DataService, "fetch", side_effect=ConnectionError("timeout") - ) + mocker.patch.object(DataService, "fetch", side_effect=ConnectionError("timeout")) # Act & Assert with pytest.raises(ConnectionError, match="timeout"): @@ -231,13 +225,9 @@ class TestDataProcessor: ("pending", 0), ], ) - def test_given_status_when_process_then_returns_expected( - self, mocker, status, expected - ): + def test_given_status_when_process_then_returns_expected(self, mocker, status, expected): # Arrange - mocker.patch.object( - DataService, "fetch", return_value={"status": status, "value": expected} - ) + mocker.patch.object(DataService, "fetch", return_value={"status": status, "value": expected}) processor = DataProcessor(service=DataService()) # Act diff --git a/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md b/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md index 817ce7329..32201e182 100644 --- a/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md +++ b/.github/skills/coding-standards/python-foundational/references/code-style-patterns.md @@ -24,22 +24,18 @@ Python naming conventions remove ambiguity. class ModelTrainer: pass - # Functions and variables: snake_case def train_model(): training_data = [] - # Constants: UPPER_SNAKE_CASE MAX_SEQUENCE_LENGTH = 2048 DEFAULT_LEARNING_RATE = 1e-4 - # Private members: leading underscore def _internal_helper(): pass - _internal_cache = {} ``` @@ -72,7 +68,6 @@ The `*` separator forces callers to name optional parameters, preventing silent def train(data: list, learning_rate: float = 1e-4, batch_size: int = 32): pass - # Caller can silently swap learning_rate and batch_size train(data, 32, 1e-4) ``` @@ -88,7 +83,6 @@ def train( ) -> None: pass - # Caller must name each optional parameter train(data, learning_rate=1e-3, batch_size=64) ``` @@ -215,7 +209,10 @@ def load_config(path: Path) -> dict: with open(path) as f: return yaml.safe_load(f) except yaml.YAMLError as e: - raise ValueError(f"Invalid YAML in config file: {path}\nError: {e}") from e + raise ValueError( + f"Invalid YAML in config file: {path}\n" + f"Error: {e}" + ) from e ``` ## Custom Exception Hierarchies @@ -226,22 +223,20 @@ In applications with multiple error categories, a base application exception ena class AppError(Exception): """Base exception for the application.""" - class ConfigError(AppError): """Configuration error.""" - class ValidationError(AppError): """Validation error.""" - def validate_config(config: dict) -> None: """Validate configuration.""" required = ["database", "api_key", "settings"] missing = [k for k in required if k not in config] if missing: raise ConfigError( - f"Missing required config keys: {missing}\nRequired: {required}" + f"Missing required config keys: {missing}\n" + f"Required: {required}" ) ``` diff --git a/.github/skills/coding-standards/python-foundational/references/design-principles.md b/.github/skills/coding-standards/python-foundational/references/design-principles.md index cd180aaa5..f9e62438b 100644 --- a/.github/skills/coding-standards/python-foundational/references/design-principles.md +++ b/.github/skills/coding-standards/python-foundational/references/design-principles.md @@ -29,7 +29,6 @@ def create_user(data: dict) -> User: raise ValueError("Invalid name") return User(**data) - def update_user(user: User, data: dict) -> User: if not data.get("email") or "@" not in data["email"]: raise ValueError("Invalid email") @@ -49,12 +48,10 @@ def _validate_user_fields(data: dict) -> None: if not data.get("name") or len(data["name"]) < 2: raise ValueError("Invalid name") - def create_user(data: dict) -> User: _validate_user_fields(data) return User(**data) - def update_user(user: User, data: dict) -> User: _validate_user_fields(data) user.email = data["email"] @@ -74,7 +71,6 @@ Introduce abstractions only when multiple implementations actually exist. Avoid class NotificationStrategy(Protocol): def send(self, message: str, recipient: str) -> None: ... - class EmailNotifier: def __init__(self, strategy: NotificationStrategy) -> None: self.strategy = strategy @@ -82,12 +78,10 @@ class EmailNotifier: def notify(self, message: str, recipient: str) -> None: self.strategy.send(message, recipient) - class SmtpStrategy: def send(self, message: str, recipient: str) -> None: smtp_client.send_email(recipient, message) - # Usage notifier = EmailNotifier(SmtpStrategy()) notifier.notify("Hello", "user@example.com") diff --git a/.github/skills/experimental/powerpoint/content-extra-py-template.md b/.github/skills/experimental/powerpoint/content-extra-py-template.md index ea1420c3d..0d6c677df 100644 --- a/.github/skills/experimental/powerpoint/content-extra-py-template.md +++ b/.github/skills/experimental/powerpoint/content-extra-py-template.md @@ -19,7 +19,6 @@ Use this template when a slide requires complex drawings that cannot be expresse ```python """Custom drawing for slide NNN — description of what this draws.""" - from pptx.util import Inches, Pt from pptx.dml.color import RGBColor @@ -42,10 +41,7 @@ def render(slide, style, content_dir): for label, color, top in layers: shape = slide.shapes.add_shape( 1, # MSO_SHAPE.RECTANGLE - Inches(2.0), - Inches(top), - Inches(9.0), - Inches(1.2), + Inches(2.0), Inches(top), Inches(9.0), Inches(1.2) ) shape.fill.solid() shape.fill.fore_color.rgb = RGBColor.from_string(color.lstrip("#")) From d0147ec0306f9ef3a5a0181a3af0b22b6e0f4b51 Mon Sep 17 00:00:00 2001 From: pratikwayase Date: Sun, 9 Aug 2026 11:04:37 +0530 Subject: [PATCH 5/5] fix: address review comments (pin uv action, exempt %pip, remove invalid html comments) --- .../prompts/data-science/synth-data-generate.prompt.md | 4 ++-- .github/workflows/beval.yml | 6 ++---- scripts/linting/Invoke-PipInstallLint.ps1 | 2 +- scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 | 9 +++++++++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/prompts/data-science/synth-data-generate.prompt.md b/.github/prompts/data-science/synth-data-generate.prompt.md index bad64747c..7ea85dd4e 100644 --- a/.github/prompts/data-science/synth-data-generate.prompt.md +++ b/.github/prompts/data-science/synth-data-generate.prompt.md @@ -100,7 +100,7 @@ All files for the synthetic data project should be organized in a dedicated fold Create a well-structured notebook with the following cells: 1. Title Cell (Markdown): Clear title with the subject -2. Package Installation Cell (Python): Install required packages using `%pip install pandas numpy matplotlib seaborn scipy` +2. Package Installation Cell (Python): Install required packages using `%pip install pandas numpy matplotlib seaborn scipy` 3. Library Import Cell (Python): Import all required libraries 4. Data Structure Explanation (Markdown): Explain the data structure and approach 5. Backup Creation (Python): If updating existing data source, create backup in notebook directory with `.bak` extension @@ -239,7 +239,7 @@ start_time = datetime.combine(day, datetime.min.time()) + timedelta( ```python # Cell 1: Package Installation (Python) -%pip install pandas numpy matplotlib seaborn scipy +%pip install pandas numpy matplotlib seaborn scipy # Cell 2: Library Imports (Python) import pandas as pd diff --git a/.github/workflows/beval.yml b/.github/workflows/beval.yml index cda7b32ac..b3096740a 100644 --- a/.github/workflows/beval.yml +++ b/.github/workflows/beval.yml @@ -41,10 +41,8 @@ jobs: npm ci --prefix evals/beval echo "${{ github.workspace }}/evals/beval/node_modules/.bin" >> "$GITHUB_PATH" - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Set up uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Install beval diff --git a/scripts/linting/Invoke-PipInstallLint.ps1 b/scripts/linting/Invoke-PipInstallLint.ps1 index d2615d6c0..4c19eff83 100644 --- a/scripts/linting/Invoke-PipInstallLint.ps1 +++ b/scripts/linting/Invoke-PipInstallLint.ps1 @@ -61,7 +61,7 @@ function script:Invoke-FileScan { continue } - if ($line -match "\bpip3?\s+install\b" -and $line -notmatch "\buv\s+pip3?\s+install\b") { + if ($line -match "\bpip3?\s+install\b" -and $line -notmatch "\buv\s+pip3?\s+install\b" -and $line -notmatch "%pip\s+install") { if ($strippedLine -notmatch "^(name:|- name:)") { $script:Violations += "$FilePath`:$lineNumber`: $strippedLine" } diff --git a/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 index f802e9046..5a5aaa362 100644 --- a/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 +++ b/scripts/tests/linting/Invoke-PipInstallLint.Tests.ps1 @@ -107,4 +107,13 @@ Describe "Invoke-PipInstallLint.ps1" { $result | Should -Be $true $script:Violations.Count | Should -Be 0 } + + It "Should ignore %pip install (Jupyter magic command)" { + $testFile = Join-Path $testDir "jupyter_allowed.py" + Set-Content -Path $testFile -Value "%pip install pandas numpy" + + $result = script:Invoke-Lint -TargetDir $testDir + $result | Should -Be $true + $script:Violations.Count | Should -Be 0 + } }