{"slug": "loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove", "title": "Loose Ends: A check for unused code AI wrote or forgot to remove", "summary": "A new open-source tool called Loose Ends, published to npm as loose-ends and requiring Node.js 20 or newer plus git, checks AI-written code changes before merge and reports whether added or touched code is still reachable from a real entry point. Loose Ends builds a Liveness Graph across TypeScript, JavaScript, Python and Java — covering imports, exports, re-exports, top-level references, dynamic imports and package dependencies — and flags two error classes, orphaned-addition and superseded, as shown in its express-api example where an agent added a v2 service, left the original behind and added an unused pagination helper, producing 2 errors and a failed check. The tool's bench/RESULTS.md reports results on public repositories, including about 700 Python and Java commits replayed through check and a sweep of 30 popular repositories, and it never deletes code without deterministic evidence and a passing verification run.", "body_md": "Graph-powered liveness checks for AI-written code.\n\nEvery change should lead somewhere.\n\nCoding agents write code fast, and they leave things behind: a helper that is never called, a `UserServiceV2` sitting next to the `UserService` it replaced, a route that was unregistered while its handler stayed. Loose Ends checks every change before merge and reports, with evidence, whether the code it added or touched is still reachable from a real entry point.\n\n``` bash\n$ npx loose-ends check\nLoose Ends · check main (dc9af08) → working tree\n4 files changed\n\n✖ error   orphaned-addition  src/lib/pagination.ts:1\n  File added in this change is not reachable from any entry point.\n  Import it from live code, add it to `entries`, or mark it with `// loose-ends-file: dormant <reason>`.\n\n✖ error   superseded  src/services/users.ts:1\n  src/services/users.ts appears to have been replaced by src/services/users-v2.ts#listUsersV2 but was not removed: its callers now use the new code (src/routes/users.ts); the names match; 100% structural similarity. Delete it, or mark it dormant with a reason.\n  edge removed: src/routes/users.ts → src/services/users.ts\n  was reached via: src/server.ts → src/routes/index.ts → src/routes/users.ts → src/services/users.ts\n  root: entry point \"src/server.ts\" of express-api-example\n  related: src/services/users-v2.ts#listUsersV2\n\nSummary: 2 errors, 0 warnings, 0 info\nFiles: 6 live, 0 test-only, 2 orphaned, 0 uncertain, 0 dormant\nResult: fail (2 findings in failOn: orphaned-addition, superseded)\n```\n\nThat is the [express-api example](https://github.com/sid732/loose-ends/blob/main/examples/express-api) after an agent-style change: a `v2` service was added and wired in, the original was left behind, and an unused pagination helper was added.\n\n1. **Added code must be reachable** from a recognized product entry point.\n2. **Changes must not orphan existing code** by removing the edges that kept it alive.\n3. **Replaced implementations must be removed** or explicitly justified.\n\nLoose Ends builds a Liveness Graph of TypeScript, JavaScript, Python, and Java code: imports, exports, re-exports, references between top-level declarations, dynamic imports, and package dependencies. It finds roots in package manifests, scripts, framework conventions (Next.js routes, Vite entries, browser extension manifests, serverless functions, tool configs, Django apps, pyproject entry points, server commands in Dockerfiles and Procfiles, Spring and Jakarta annotations, Android manifests and layouts), config, and tests. It analyzes the base and head revisions and reports only what the change did. Existing dead code never blocks a pull request.\n\nIt is **not** an LLM guessing whether code looks unused, a general code reviewer, a formatter, or a security scanner. It does not claim that unobserved code is safe to delete, and it never deletes anything without deterministic evidence and a passing verification run.\n\n[bench/RESULTS.md](https://github.com/sid732/loose-ends/blob/main/bench/RESULTS.md) has the results on public repositories, including about 700 Python and Java commits replayed through `check` and a sweep of 30 popular repositories.\n\n```\nnpm install --save-dev loose-ends\n```\n\nRequires Node.js 20 or newer and git.\n\nPython and Java projects need no `package.json`. With Node installed, run `npx loose-ends check` from the repository root.\n\nThe npm packages are published from GitHub Actions with [provenance](https://docs.npmjs.com/generating-provenance-statements), so each version links back to the commit it was built from.\n\n```\nnpx loose-ends check                     # working tree vs. merge base with the default branch\nnpx loose-ends check --base origin/main --head HEAD\nnpx loose-ends check --format markdown   # also: json, sarif\nnpx loose-ends scan                      # whole-project report of existing debt\nnpx loose-ends explain src/lib/format.ts#formatUser\nnpx loose-ends roots                     # what counts as an entry point, and why\nnpx loose-ends fix --verify --patch cleanup.diff\nnpx loose-ends init                      # starter loose-ends.config.json\n```\n\n`check` exits with 0 on pass, 1 on blocking findings, and 2 when it could not run.\n\n| Kind | Severity | Meaning | \n|---|---|---|\n| `orphaned-addition` | error | Added code that no production root reaches. | \n| `newly-orphaned` | error | Code that was live before the change and is not live after it. | \n| `superseded` | error | A replaced implementation that was not removed. | \n| `invalid-suppression` | error | A suppression or dormant marker that has no reason, has expired, or matches nothing. | \n| `test-only-addition` | warning | Added code that only tests reach. | \n| `newly-test-only` | warning | Production stopped using this code, and only tests still do. | \n| `duplicate` | warning | Added code that is near-identical to an existing live implementation. | \n| `unused-dependency` | warning | A dependency that no live code imports. | \n| `uncertain` | info | Reachability depends on a dynamic import the graph cannot resolve. | \n\n`scan` reports existing code instead of a change, so it uses `orphaned` (nothing reaches it) and `test-only` (only tests reach it), plus `unused-dependency` and `uncertain`. It exits 0 unless you pass `--fail`.\n\nSee [docs/findings.md](https://github.com/sid732/loose-ends/blob/main/docs/findings.md) for what each kind means and what to do about it.\n\nEvery option is optional. `npx loose-ends init` writes a starter `loose-ends.config.json`:\n\n```\n{\n  \"$schema\": \"https://raw.githubusercontent.com/sid732/loose-ends/main/schema/config.schema.json\",\n  \"entries\": [\"src/jobs/*.ts\", \"src/events/handlers.ts#onInvoicePaid\"],\n  \"extendExclude\": [\"src/generated/**\"],\n  \"languages\": [\"javascript\", \"python\"],\n  \"failOn\": [\"orphaned-addition\", \"newly-orphaned\", \"superseded\", \"invalid-suppression\"],\n  \"checks\": [\"npm run typecheck\", \"npm test\"]\n}\n```\n\n- `entries` adds roots the graph cannot see, such as files a job runner loads by name.\n- `extendExclude` adds to the default excludes (`node_modules` , build output, virtualenvs, …).\n- `languages` limits analysis to some of`javascript` ,`python` , and`java` .\n- `failOn` picks the finding kinds that fail`check` .\n- `checks` are the commands`fix --verify` runs.\n\nAll options, dormant rules, and suppressions are in [docs/configuration.md](https://github.com/sid732/loose-ends/blob/main/docs/configuration.md).\n\n```\n// loose-ends: dormant v1 webhooks still arrive until the March cutover\nexport function handleV1Webhook() {}\n# loose-ends: dormant v1 webhooks still arrive until the March cutover\ndef handle_v1_webhook(): ...\n```\n\nJava uses `//` like TypeScript. A `loose-ends-file:` comment applies to the whole file instead of the declaration below it.\n\nDormant markers and suppressions need a reason. Without one, they become findings themselves. See [docs/configuration.md](https://github.com/sid732/loose-ends/blob/main/docs/configuration.md).\n\n```\n# .github/workflows/loose-ends.yml\nname: Loose Ends\non: pull_request\npermissions:\n  contents: read\n  pull-requests: write # for the PR comment\njobs:\n  liveness:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0 # the base commit must exist locally\n      - uses: sid732/loose-ends@v1\n```\n\n- **GitHub Action (no server, free on public repos):** adds inline annotations on the diff, keeps one PR comment up to date, writes the report to the job summary, and fails the job on blocking findings. It works for Python and Java repositories too, with no Node setup step.\n- **GitHub App:** the same report as check runs, for people who prefer to host a server and install once across many repositories.\n\nInputs (`working-directory`, `fail-on`, `comment`, `sarif`, …) and App setup are in [docs/github.md](https://github.com/sid732/loose-ends/blob/main/docs/github.md).\n\n- **MCP server:**`npx -y @loose-ends/mcp` exposes`check_changes` ,`scan_project` ,`explain` ,`list_roots` , and`plan_cleanup` . All of them are read-only.\n- **Skills:**[skills/](https://github.com/sid732/loose-ends/blob/main/skills) has two short`SKILL.md` files that tell an agent when to run the check and how to respond to each finding.\n\n```\n{ \"mcpServers\": { \"loose-ends\": { \"command\": \"npx\", \"args\": [\"-y\", \"@loose-ends/mcp\"] } } }\nnpx loose-ends fix --verify --patch cleanup.diff\n```\n\n`fix` turns blocking findings into file deletions and declaration removals and applies them to a scratch copy of the project. Then it runs your configured `checks` (`npm test`, `npm run build`, …) there and writes a patch. Anything that would leave a dangling import is skipped, with the reason listed. `--apply` writes the change to your tree, and only after verification passes.\n\n```\nnpx loose-ends check --coverage coverage/coverage-final.json\n```\n\nRuntime hit counts from istanbul or lcov reports are attached to findings. Coverage never overrides the graph. It explains it: code the graph considers unreachable that still ran points to a loader the graph cannot see.\n\n- **TypeScript and JavaScript:** Node.js, React, Next.js, Vite, Express, NestJS, React Router framework mode, Vue, Svelte and SvelteKit, Astro, Nuxt, VitePress, npm/yarn/pnpm workspaces, browser extensions, and common test runners.\n- **Python:** packages and src layouts, relative and namespace imports,`__init__.py` re-exports and`__all__` , scripts, notebooks, pyproject/setup.py entry points, Django, Flask, FastAPI, Celery, Alembic, Airflow, Streamlit, pytest, and dependencies in`requirements*.txt` ,`pyproject.toml` ,`setup.py` ,`setup.cfg` , and`Pipfile` .\n- **Java:** top-level classes, interfaces, enums, and records; imports, same-package and fully qualified references;`main` methods; Spring, Jakarta EE, CDI, JAX-RS, Quarkus, Micronaut, MyBatis, MapStruct, Dagger/Hilt, and Android; class names in resource files; Maven and Gradle (including version catalogs) dependencies; and Java code used from Kotlin.\n\nA repository can mix all of them. How it works is described in [docs/architecture.md](https://github.com/sid732/loose-ends/blob/main/docs/architecture.md).\n\n- Tracking stops at top-level declarations. An unused method inside a used class, or an unused property on a used object, is not reported.\n- Code reached only through dependency injection by name, string-based plugin registries, or reflection is invisible to the graph. Add it to `entries` or mark it with`loose-ends: entry` .\n- Java classes with framework annotations (`@Service` ,`@Controller` ,`@Entity` , …) count as entry points, so they are never reported as unused.\n- Kotlin and Groovy are only scanned for the Java names they use. Their own code is not analyzed.\n- Python `getattr` with a computed name and modules found through`sys.path` changes are not followed.\n- Other languages are not analyzed.\n\nSetup, pull request rules, and the release process are in [CONTRIBUTING.md](https://github.com/sid732/loose-ends/blob/main/CONTRIBUTING.md). To report a vulnerability, see [SECURITY.md](https://github.com/sid732/loose-ends/blob/main/SECURITY.md).", "url": "https://wpnews.pro/news/loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove", "canonical_source": "https://github.com/sid732/loose-ends", "published_at": "2026-11-13 02:56:13+00:00", "updated_at": "2026-11-13 03:21:33.171098+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents", "artificial-intelligence"], "entities": ["Loose Ends", "npm", "Node.js", "GitHub Actions", "express-api", "TypeScript", "Python", "Java"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove", "markdown": "https://wpnews.pro/news/loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove.md", "text": "https://wpnews.pro/news/loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove.txt", "jsonld": "https://wpnews.pro/news/loose-ends-a-check-for-unused-code-ai-wrote-or-forgot-to-remove.jsonld"}}