diff --git a/.github/workflows/test-sdlc-engine.yml b/.github/workflows/test-sdlc-engine.yml index 3724faa4..68c4040a 100644 --- a/.github/workflows/test-sdlc-engine.yml +++ b/.github/workflows/test-sdlc-engine.yml @@ -13,6 +13,9 @@ on: - 'scripts/run-test-suites.sh' - 'scripts/check-complexity.py' - 'scripts/test-check-complexity.sh' + - 'scripts/check-vulture.sh' + - 'scripts/test-check-vulture.sh' + - 'config/vulture-whitelist.py' - '.github/workflows/test-sdlc-engine.yml' push: branches: [main] @@ -27,6 +30,9 @@ on: - 'scripts/run-test-suites.sh' - 'scripts/check-complexity.py' - 'scripts/test-check-complexity.sh' + - 'scripts/check-vulture.sh' + - 'scripts/test-check-vulture.sh' + - 'config/vulture-whitelist.py' - '.github/workflows/test-sdlc-engine.yml' jobs: @@ -45,6 +51,10 @@ jobs: run: python3 -m ruff check --select F,E9 engine/src scripts - name: Complexity proving test run: bash scripts/test-check-complexity.sh + - name: Vulture proving test + run: bash scripts/test-check-vulture.sh + - name: Vulture dead code + run: bash scripts/check-vulture.sh engine/src scripts - name: Complexity gate on this diff (CCN>10 / NLOC>80 / CCN rise) if: github.event_name == 'pull_request' run: | diff --git a/config/vulture-whitelist.py b/config/vulture-whitelist.py new file mode 100644 index 00000000..4f640df9 --- /dev/null +++ b/config/vulture-whitelist.py @@ -0,0 +1,88 @@ +# First-bake vulture allowlist. Add unused names here; do not delete this file. +READINESS_CANONICAL # unused variable (engine/src/sdlc_engine/canvas.py:53) +REL_KEYWORD # unused variable (engine/src/sdlc_engine/context_model.py:34) +_.row_factory # unused attribute (engine/src/sdlc_engine/db.py:61) +_.upsert_edge # unused method (engine/src/sdlc_engine/db.py:255) +_.link_section_to_area # unused method (engine/src/sdlc_engine/db.py:430) +_.upsert_context_session # unused method (engine/src/sdlc_engine/db.py:545) +_.upsert_context_entry # unused method (engine/src/sdlc_engine/db.py:575) +_.lessons_for_area # unused method (engine/src/sdlc_engine/db_query.py:32) +_.lessons_for_work # unused method (engine/src/sdlc_engine/db_query.py:43) +REL_KEYWORD # unused variable (engine/src/sdlc_engine/db_schema.py:30) +_.project_load # unused method (engine/src/sdlc_engine/guide_client.py:99) +_requirements_issue_sections # unused function (engine/src/sdlc_engine/installer/app.py:114) +vue_assets # unused function (engine/src/sdlc_engine/installer/app.py:203) +api_health # unused function (engine/src/sdlc_engine/installer/app.py:207) +api_detect # unused function (engine/src/sdlc_engine/installer/app.py:219) +api_run # unused function (engine/src/sdlc_engine/installer/app.py:230) +api_sqlite_status # unused function (engine/src/sdlc_engine/installer/app.py:257) +api_sqlite_works # unused function (engine/src/sdlc_engine/installer/app.py:280) +api_sqlite_work # unused function (engine/src/sdlc_engine/installer/app.py:341) +api_sqlite_rebuild # unused function (engine/src/sdlc_engine/installer/app.py:377) +api_persistence_status # unused function (engine/src/sdlc_engine/installer/app.py:401) +api_persistence_save # unused function (engine/src/sdlc_engine/installer/app.py:414) +api_persistence_parity # unused function (engine/src/sdlc_engine/installer/app.py:444) +api_dashboard_status # unused function (engine/src/sdlc_engine/installer/app.py:463) +api_dashboard_activity # unused function (engine/src/sdlc_engine/installer/app.py:478) +api_dashboard_suggestions # unused function (engine/src/sdlc_engine/installer/app.py:497) +api_backups # unused function (engine/src/sdlc_engine/installer/app.py:512) +api_rollback # unused function (engine/src/sdlc_engine/installer/app.py:518) +api_guide_get # unused function (engine/src/sdlc_engine/installer/app.py:607) +api_guide_save # unused function (engine/src/sdlc_engine/installer/app.py:614) +api_guide_ensure # unused function (engine/src/sdlc_engine/installer/app.py:623) +api_neo4j_start # unused function (engine/src/sdlc_engine/installer/app.py:651) +api_neo4j_stop # unused function (engine/src/sdlc_engine/installer/app.py:667) +api_guide_start # unused function (engine/src/sdlc_engine/installer/app.py:680) +api_guide_stop # unused function (engine/src/sdlc_engine/installer/app.py:704) +api_guide_ensure_profile # unused function (engine/src/sdlc_engine/installer/app.py:717) +api_guide_projection_load # unused function (engine/src/sdlc_engine/installer/app.py:742) +api_guide_stats # unused function (engine/src/sdlc_engine/installer/app.py:761) +api_guide_ingest # unused function (engine/src/sdlc_engine/installer/app.py:775) +api_guide_purge_preview # unused function (engine/src/sdlc_engine/installer/app.py:790) +api_guide_purge # unused function (engine/src/sdlc_engine/installer/app.py:819) +api_guide_git_revision_reset # unused function (engine/src/sdlc_engine/installer/app.py:850) +api_guide_purge_all_rag # unused function (engine/src/sdlc_engine/installer/app.py:867) +api_adf_status # unused function (engine/src/sdlc_engine/installer/app.py:899) +api_adf_start # unused function (engine/src/sdlc_engine/installer/app.py:906) +api_adf_stop # unused function (engine/src/sdlc_engine/installer/app.py:919) +api_adf_restart # unused function (engine/src/sdlc_engine/installer/app.py:930) +api_adf_browse # unused function (engine/src/sdlc_engine/installer/app.py:943) +api_adf_init_work # unused function (engine/src/sdlc_engine/installer/app.py:964) +api_templates_list # unused function (engine/src/sdlc_engine/installer/app.py:1010) +api_templates_render # unused function (engine/src/sdlc_engine/installer/app.py:1020) +api_integrations_status # unused function (engine/src/sdlc_engine/installer/app.py:1066) +api_integrations_save # unused function (engine/src/sdlc_engine/installer/app.py:1078) +api_jira_status # unused function (engine/src/sdlc_engine/installer/app.py:1189) +api_jira_link # unused function (engine/src/sdlc_engine/installer/app.py:1195) +api_jira_sync # unused function (engine/src/sdlc_engine/installer/app.py:1203) +resolve_guide_home # unused function (engine/src/sdlc_engine/installer/guide.py:66) +_.close_github # unused method (engine/src/sdlc_engine/issues.py:1200) +_.discard_staged # unused method (engine/src/sdlc_engine/lessons_ledger.py:261) +jira_summary # unused variable (engine/src/sdlc_engine/links.py:23) +registry_note # unused variable (engine/src/sdlc_engine/links.py:33) +_.jira_summary # unused attribute (engine/src/sdlc_engine/links.py:254) +_.registry_note # unused attribute (engine/src/sdlc_engine/links.py:270) +_.is_empty # unused method (engine/src/sdlc_engine/metrics.py:87) +ENFORCED_GATES # unused variable (engine/src/sdlc_engine/phases.py:44) +_.init_from_env # unused method (engine/src/sdlc_engine/pointer.py:42) +_.run_against # unused method (engine/src/sdlc_engine/pointer.py:49) +_.registry_path # unused property (engine/src/sdlc_engine/project.py:97) +_.skills_dir # unused property (engine/src/sdlc_engine/project.py:123) +edit_query # unused function (engine/src/sdlc_engine/viewer/app.py:116) +edit_legacy # unused function (engine/src/sdlc_engine/viewer/app.py:142) +api_browse # unused function (engine/src/sdlc_engine/viewer/app.py:147) +api_create # unused function (engine/src/sdlc_engine/viewer/app.py:156) +api_get_adf # unused function (engine/src/sdlc_engine/viewer/app.py:176) +api_get_adf_legacy # unused function (engine/src/sdlc_engine/viewer/app.py:186) +api_render # unused function (engine/src/sdlc_engine/viewer/app.py:193) +api_html_to_adf # unused function (engine/src/sdlc_engine/viewer/app.py:205) +api_save # unused function (engine/src/sdlc_engine/viewer/app.py:219) +api_save_legacy # unused function (engine/src/sdlc_engine/viewer/app.py:241) +api_sync # unused function (engine/src/sdlc_engine/viewer/app.py:325) +api_sync_legacy # unused function (engine/src/sdlc_engine/viewer/app.py:329) +api_download # unused function (engine/src/sdlc_engine/viewer/app.py:414) +api_github_pull # unused function (engine/src/sdlc_engine/viewer/app.py:420) +api_github_push # unused function (engine/src/sdlc_engine/viewer/app.py:479) +_.handle_starttag # unused method (engine/src/sdlc_engine/viewer/html_adf.py:40) +_.handle_endtag # unused method (engine/src/sdlc_engine/viewer/html_adf.py:197) +_.handle_data # unused method (engine/src/sdlc_engine/viewer/html_adf.py:327) diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 5a1c3701..0e6c0d8a 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -13,7 +13,7 @@ authors = [{ name = "SDLC-SPDD contributors" }] dependencies = [] [project.optional-dependencies] -dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.11", "lizard==1.24.0"] +dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.11", "lizard==1.24.0", "vulture>=2.14"] viewer = ["flask>=3"] viewer-e2e = ["flask>=3", "pytest-playwright>=0.5.0"] diff --git a/scripts/check-vulture.sh b/scripts/check-vulture.sh new file mode 100755 index 00000000..c890b397 --- /dev/null +++ b/scripts/check-vulture.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Fail on dead code vulture reports outside config/vulture-whitelist.py. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WHITELIST="${VULTURE_WHITELIST:-$ROOT/config/vulture-whitelist.py}" +MIN="${VULTURE_MIN_CONFIDENCE:-60}" +if [[ $# -eq 0 ]]; then + echo "usage: check-vulture.sh ..." >&2 + exit 2 +fi +exec python3 -m vulture "$@" "$WHITELIST" --min-confidence "$MIN" diff --git a/scripts/test-check-vulture.sh b/scripts/test-check-vulture.sh new file mode 100755 index 00000000..d151fc45 --- /dev/null +++ b/scripts/test-check-vulture.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Proving test: an allowlisted unused name stays green; a new unused function fails. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CHECK="${SCRIPT_DIR}/check-vulture.sh" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +cat > "$TMP/known_dead.py" <<'PY' +def known_dead(): + return 1 +PY +cat > "$TMP/whitelist.py" <<'PY' +known_dead # unused function (vulture whitelist) +PY +if ! VULTURE_WHITELIST="$TMP/whitelist.py" bash "$CHECK" "$TMP/known_dead.py"; then + echo "expected PASS on allowlisted unused name" >&2 + exit 1 +fi +cat > "$TMP/new_dead.py" <<'PY' +def brand_new_unused(): + return 2 +PY +if VULTURE_WHITELIST="$TMP/whitelist.py" bash "$CHECK" "$TMP/known_dead.py" "$TMP/new_dead.py"; then + echo "expected FAIL on a new unused function" >&2 + exit 1 +fi +echo "test-check-vulture: PASS"