Skip to content

Commit f0802b8

Browse files
authored
chore(ci): enforce unused imports/vars + advisory dead-code scan (#2144)
* chore(ci): enforce unused imports/vars + advisory dead-code scan Enable ruff F401 (unused imports) and F841 (unused variables) -- previously ignored as "too noisy" -- across hindsight-api-slim, hindsight-dev, and hindsight-embed, and clean up the resulting violations. These are now blocking: lint.sh auto-removes them and the verify-generated-files CI job fails on any leftover diff. Add an advisory dead-code scan for what the linter cannot see -- whole unused Python functions (vulture) and orphaned files/exports/dependencies in the control plane (knip): - scripts/hooks/check-unused.sh runs both locally - new non-blocking check-unused-code CI job surfaces findings on PRs - hindsight-control-plane/knip.json tunes out toolchain false positives vulture stays advisory because its function/argument heuristics false-positive on FastAPI/SQLAlchemy/Pydantic patterns; knip can be flipped to blocking once the control-plane dead code (PR #2135) lands. * chore(ci): make knip blocking on unused files/deps; remove dead deps #2135 deleted tooltip.tsx but left @radix-ui/react-tooltip in package.json, and react-chrono / three were never imported. Remove all three, and declare @radix-ui/react-visually-hidden (used in directive-detail-modal but unlisted). With the control-plane tree now clean, the check-unused-code job runs `knip --include files,dependencies,unlisted` as a BLOCKING step. vulture and knip's unused-exports check (the shadcn/ui surface is kept intentionally) stay advisory.
1 parent 87448b1 commit f0802b8

15 files changed

Lines changed: 217 additions & 160 deletions

File tree

.github/workflows/test.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4311,6 +4311,60 @@ jobs:
43114311
fi
43124312
done
43134313
4314+
# Dead-code detection beyond what ruff's F401/F841 catch (those are already
4315+
# BLOCKING via the ruff config + the verify-generated-files job).
4316+
#
4317+
# - knip (control plane): BLOCKING on unused files / dependencies / unlisted
4318+
# dependencies. These are unambiguous — an orphaned file or a dead
4319+
# package.json entry — so they fail the build.
4320+
# - vulture (Python) + knip unused *exports*: ADVISORY only. vulture's
4321+
# function/argument heuristics false-positive on FastAPI/SQLAlchemy/Pydantic
4322+
# patterns, and the control plane intentionally keeps an unused shadcn/ui
4323+
# component surface, so these are surfaced in the step summary, not gated.
4324+
check-unused-code:
4325+
needs: [detect-changes]
4326+
if: >-
4327+
(github.event_name == 'workflow_dispatch' ||
4328+
needs.detect-changes.outputs.core == 'true' ||
4329+
needs.detect-changes.outputs.control-plane == 'true' ||
4330+
needs.detect-changes.outputs.ci == 'true')
4331+
runs-on: ubuntu-latest
4332+
timeout-minutes: 15
4333+
steps:
4334+
- uses: actions/checkout@v6
4335+
with:
4336+
ref: ${{ github.event.pull_request.head.sha || '' }}
4337+
4338+
- name: Install uv
4339+
uses: astral-sh/setup-uv@v7
4340+
with:
4341+
enable-cache: true
4342+
4343+
- name: Set up Node.js
4344+
uses: actions/setup-node@v6
4345+
with:
4346+
node-version: '20'
4347+
cache: 'npm'
4348+
cache-dependency-path: package-lock.json
4349+
4350+
- name: Install Control Plane dependencies
4351+
run: npm install --workspace=hindsight-control-plane
4352+
4353+
- name: knip — unused files / dependencies (blocking)
4354+
working-directory: hindsight-control-plane
4355+
run: npx --yes knip@5 --no-progress --include files,dependencies,unlisted
4356+
4357+
- name: Advisory scan — vulture + knip exports
4358+
continue-on-error: true
4359+
run: |
4360+
{
4361+
echo '## Dead-code scan (advisory)'
4362+
echo ''
4363+
echo '```'
4364+
./scripts/hooks/check-unused.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
4365+
echo '```'
4366+
} | tee -a "$GITHUB_STEP_SUMMARY"
4367+
43144368
verify-generated-files:
43154369
runs-on: ubuntu-latest
43164370
timeout-minutes: 30

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ migration file dispatches through `run_for_dialect`, which calls either
216216
./scripts/hooks/lint.sh
217217
```
218218

219+
Dead-code detection runs in CI (the `check-unused-code` job) at two levels:
220+
- **Blocking:** unused imports (ruff `F401`) and variables (`F841`) — `lint.sh` auto-removes
221+
them and `verify-generated-files` fails on any leftover diff; and **knip** for orphaned
222+
control-plane files / unused (or unlisted) `package.json` dependencies.
223+
- **Advisory:** whole unused Python functions (vulture) and unused control-plane *exports*
224+
(the shadcn/ui surface is kept on purpose) — surfaced, not gated.
225+
226+
Run both locally with:
227+
```bash
228+
./scripts/hooks/check-unused.sh
229+
```
230+
219231
**After completing any implementation work, run `/code-review`** to verify your changes against project standards (missing tests, dead code, type safety, etc.). Fix any "must fix" issues before considering the task done.
220232

221233
**MANDATORY: Run `/code-review` before pushing code or creating a pull request.** Do not push or create a PR until all "must fix" issues are resolved.

hindsight-api-slim/pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,6 @@ select = [
204204
ignore = [
205205
"E501", # line too long (handled by formatter)
206206
"E402", # module import not at top of file
207-
"F401", # unused import (too noisy during development)
208-
"F841", # unused variable (too noisy during development)
209207
"F811", # redefined while unused
210208
"F821", # undefined name (forward references in type hints)
211209
]

hindsight-control-plane/knip.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://unpkg.com/knip@5/schema.json",
3+
"entry": [
4+
"src/app/**/{page,layout,route,loading,error,not-found,template,default,global-error,sitemap,robots,manifest,opengraph-image,icon}.{ts,tsx}",
5+
"next.config.{js,mjs,ts}",
6+
"tailwind.config.{js,ts}",
7+
"postcss.config.{js,mjs}"
8+
],
9+
"project": ["src/**/*.{ts,tsx}"],
10+
"ignoreDependencies": [
11+
"eslint",
12+
"eslint-config-next",
13+
"@eslint/eslintrc",
14+
"autoprefixer",
15+
"tailwindcss-animate",
16+
"postcss-load-config",
17+
"react-is",
18+
"@vectorize-io/hindsight-client",
19+
"prettier",
20+
"tsx"
21+
]
22+
}

hindsight-control-plane/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@radix-ui/react-slot": "^1.2.4",
4343
"@radix-ui/react-switch": "^1.2.6",
4444
"@radix-ui/react-tabs": "^1.1.13",
45-
"@radix-ui/react-tooltip": "^1.2.8",
45+
"@radix-ui/react-visually-hidden": "^1.2.5",
4646
"@tailwindcss/postcss": "^4.1.17",
4747
"@tailwindcss/typography": "^0.5.19",
4848
"@types/cytoscape": "^3.21.9",
@@ -63,7 +63,6 @@
6363
"next-themes": "^0.4.6",
6464
"postcss": "^8.5.6",
6565
"react": "^19.2.0",
66-
"react-chrono": "^2.9.1",
6766
"react-dom": "^19.2.0",
6867
"react-is": "^19.2.4",
6968
"react-markdown": "^10.1.0",
@@ -74,7 +73,6 @@
7473
"tailwind-merge": "^3.4.0",
7574
"tailwindcss": "^4.1.17",
7675
"tailwindcss-animate": "^1.0.7",
77-
"three": "^0.182.0",
7876
"typescript": "^5.9.3"
7977
},
8078
"devDependencies": {

hindsight-dev/benchmarks/common/benchmark_runner.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import logging
2323
import os
2424
from abc import ABC, abstractmethod
25-
from datetime import datetime, timezone
25+
from datetime import datetime
2626
from pathlib import Path
2727
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
2828

@@ -34,7 +34,6 @@
3434
get_config().configure_logging()
3535
from hindsight_api.engine.memory_engine import Budget
3636
from hindsight_api.models import RequestContext
37-
from openai import AsyncOpenAI
3837
from rich import box
3938
from rich.console import Console
4039
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
@@ -596,9 +595,6 @@ async def answer_question(
596595
# Map thinking_budget to budget level
597596
budget = Budget.LOW if thinking_budget <= 30 else Budget.MID if thinking_budget <= 70 else Budget.HIGH
598597

599-
import time
600-
601-
recall_start_time = time.time()
602598
# Use default fact types (no filtering)
603599
search_result = await self.memory.recall_async(
604600
bank_id=agent_id,
@@ -611,12 +607,6 @@ async def answer_question(
611607
include_chunks=True,
612608
request_context=RequestContext(),
613609
)
614-
recall_time = time.time() - recall_start_time
615-
616-
# Log recall stats
617-
num_results = len(search_result.results) if search_result.results else 0
618-
num_chunks = len(search_result.chunks) if search_result.chunks else 0
619-
num_entities = len(search_result.entities) if search_result.entities else 0
620610

621611
# Convert entire RecallResult to dictionary for answer generation
622612
recall_result_dict = search_result.model_dump()

hindsight-dev/benchmarks/consolidation/consolidation_benchmark.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from hindsight_api.engine.memory_engine import MemoryEngine
2424
from hindsight_api.models import RequestContext
2525
from rich.console import Console
26-
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
2726
from rich.table import Table
2827

2928
console = Console()

hindsight-dev/benchmarks/locomo/locomo_benchmark.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
import asyncio
88
import json
99
import os
10-
import sys
1110
from datetime import datetime, timezone
1211
from pathlib import Path
1312
from typing import Any, Dict, List, Optional, Tuple
1413

1514
import pydantic
1615
from hindsight_api.engine.llm_wrapper import LLMConfig
17-
from openai import AsyncOpenAI
1816

1917
from benchmarks.common.benchmark_runner import (
2018
BenchmarkDataset,
@@ -414,7 +412,6 @@ async def run_benchmark(
414412
console.print(f"[green]Found {len(filtered_items)} conversations to re-evaluate[/green]")
415413

416414
# Temporarily replace dataset's load method
417-
original_load = dataset.load
418415

419416
def filtered_load(path: Path, max_items: Optional[int] = None):
420417
return filtered_items[:max_items] if max_items else filtered_items
@@ -501,8 +498,6 @@ def generate_markdown_table(results: dict, use_reflect: bool = False):
501498

502499
console = Console()
503500

504-
category_names = {"1": "Multi-hop", "2": "Single-hop", "3": "Temporal", "4": "Open-domain"}
505-
506501
# Build markdown content
507502
lines = []
508503
mode_str = " (Reflect Mode)" if use_reflect else ""

hindsight-dev/benchmarks/longmemeval/longmemeval_benchmark.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
import asyncio
88
import json
99
import os
10-
import sys
1110
from datetime import datetime, timezone
1211
from pathlib import Path
1312
from typing import Any, Dict, List, Optional, Tuple
1413

1514
import pydantic
1615
from hindsight_api.engine.llm_wrapper import LLMConfig
17-
from openai import AsyncOpenAI
1816

1917
from benchmarks.common.benchmark_runner import (
2018
BenchmarkDataset,
@@ -200,7 +198,6 @@ def _format_context_structured(self, recall_result: Dict[str, Any]) -> str:
200198

201199
# Extract temporal information
202200
occurred_start = fact.get("occurred_start")
203-
occurred_end = fact.get("occurred_end")
204201
mentioned_at = fact.get("mentioned_at")
205202

206203
# Build temporal string
@@ -622,8 +619,6 @@ async def run_benchmark(
622619
console.print(f"[green]Found {total_found} {filter_type} items to re-evaluate[/green]")
623620

624621
# Create local memory engine
625-
from hindsight_api.engine.memory_engine import Budget
626-
from hindsight_api.models import RequestContext
627622

628623
from benchmarks.common.benchmark_runner import create_memory_engine
629624

@@ -677,7 +672,6 @@ async def run_benchmark(
677672
# If filtering by category, failed, invalid, only_ingested, or max_instances_per_category, we need to use a custom dataset that only returns those items
678673
# We'll temporarily replace the dataset's load method
679674
if filtered_items is not None:
680-
original_load = dataset.load
681675

682676
def filtered_load(path: Path, max_items: Optional[int] = None):
683677
return filtered_items[:max_items] if max_items else filtered_items

hindsight-dev/benchmarks/perf/recall_perf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939

4040
import argparse
4141
import asyncio
42-
import json
4342
import os
4443
import statistics
4544
import time

0 commit comments

Comments
 (0)