Loose Ends
为 AI 编写的代码提供由图驱动的活性检查。
每一次变更都应该通向某处。
编码智能体写代码很快,并且会留下一些东西:一个从未被调用的辅助函数,一个紧挨着它所替换的 UserService 的 UserServiceV2
check 在通过时以 0 退出,在有阻断性发现时以 1 退出,在无法运行时以 2 退出。
发现
| 类型 | 严重性 | 含义 |
|---|---|---|
| orphaned-addition | error | 新增的代码,没有任何生产根能到达。 |
| newly-orphaned | error | 在变更前是活跃的,变更后不再活跃的代码。 |
| superseded | error | 已被替换但未删除的实现。 |
| invalid-suppression | error | 没有理由、已过期或匹配不到任何内容的抑制或休眠标记。 |
| test-only-addition | warning | 新增的代码,只有测试能到达。 |
| newly-test-only | warning | 生产环境已不再使用此代码,只有测试仍在使用。 |
| duplicate | warning | 新增的代码与现有的活跃实现几乎相同。 |
| unused-dependency | warning | 没有任何活跃代码导入的依赖。 |
| uncertain | info | 可达性取决于依赖图无法解析的动态导入。 |
scan 报告的是现有代码而不是变更,因此它使用 orphaned(没有任何内容到达它)和 test-only(只有测试到达它),外加 unused-dependency 和 uncertain。除非传入 --fail,否则它以 0 退出。
参见 docs/findings.md,了解每种类型的含义以及如何处理。
配置
每个选项都是可选的。npx loose-ends init 会写入一个起始的 loose-ends.config.json:
{
"$schema": "https://raw.githubusercontent.com/sid732/loose-ends/main/schema/config.schema.json",
"entries": ["src/jobs/*.ts", "src/events/handlers.ts#onInvoicePaid"],
"extendExclude": ["src/generated/**"],
"languages": ["javascript", "python"],
"failOn": ["orphaned-addition", "newly-orphaned", "superseded", "invalid-suppression"],
"checks": ["npm run typecheck", "npm test"]
}
- entries 添加依赖图看不到的根,例如作业运行器按名称加载的文件。
- extendExclude 添加到默认排除项(node_modules、构建输出、virtualenvs 等)。
- languages 将分析限制为 javascript、python 和 java 中的一部分。
- failOn 选择会使 check 失败的发现类型。
- checks 是 fix --verify 运行的命令。
所有选项、休眠规则和抑制项都在 docs/configuration.md 中。
有意未使用的代码
// loose-ends: dormant v1 webhooks still arrive until the March cutover
export function handleV1Webhook() {}
# loose-ends: dormant v1 webhooks still arrive until the March cutover
def handle_v1_webhook(): ...
Java 像 TypeScript 一样使用 //。loose-ends-file: 注释作用于整个文件,而不是它下面的声明。
休眠标记和抑制项需要理由。没有理由时,它们自身就会成为发现。参见 docs/configuration.md。
在 CI 和拉取请求中
# .github/workflows/loose-ends.yml
name: Loose Ends
on: pull_request
permissions:
contents: read
pull-requests: write # for the PR comment
jobs:
liveness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # the base commit must exist locally
- uses: sid732/loose-ends@v1
- GitHub Action(无需服务器,在公共仓库上免费):在 diff 上添加内联注释,保持一条 PR 评论是最新的,将报告写入作业摘要,并在有阻断性发现时使作业失败。它也适用于 Python 和 Java 仓库,无需 Node 设置步骤。
- GitHub App:以 check runs 形式提供相同的报告,适合更愿意托管服务器并在多个仓库中一次性安装的人。
输入项(working-directory、fail-on、comment、sarif 等)和 App 设置都在 docs/github.md 中。
面向编码代理
- MCP server:npx -y @loose-ends/mcp 暴露 check_changes、scan_project、explain、list_roots 和 plan_cleanup。它们全部都是只读的。
- Skills:skills/ 中有两个简短的 SKILL.md 文件,告诉代理何时运行 check 以及如何响应每种发现。
{ "mcpServers": { "loose-ends": { "command": "npx", "args": ["-y", "@loose-ends/mcp"] } } }
清理
npx loose-ends fix --verify --patch cleanup.diff
fix 会把阻断性发现转换为文件删除和声明移除,并将它们应用到项目的临时副本。然后它在那里运行你配置的检查(npm test、npm run build、…)并写入一个补丁。任何会导致悬空导入的内容都会被跳过,并列出原因。--apply 会把更改写入你的工作树,并且只在验证通过后才会写入。
覆盖率作为证据
npx loose-ends check --coverage coverage/coverage-final.json
来自 istanbul 或 lcov 报告的运行时命中次数会附加到发现项上。覆盖率绝不会取代依赖图。它用于解释依赖图:依赖图认为不可达但仍然运行过的代码,指向依赖图看不到的加载器。
范围
- TypeScript 和 JavaScript:Node.js、React、Next.js、Vite、Express、NestJS、React Router framework mode、Vue、Svelte 和 SvelteKit、Astro、Nuxt、VitePress、npm/yarn/pnpm workspaces、浏览器扩展,以及常见测试运行器。
- Python:包和 src 布局、相对导入和命名空间导入、__init__.py 重新导出和 __all__、脚本、notebook、pyproject/setup.py 入口点、Django、Flask、FastAPI、Celery、Alembic、Airflow、Streamlit、pytest,以及 requirements*.txt、pyproject.toml、setup.py、setup.cfg 和 Pipfile 中的依赖。
- Java:顶层类、接口、枚举和 record;导入、同包引用和全限定引用;main 方法;Spring、Jakarta EE、CDI、JAX-RS、Quarkus、Micronaut、MyBatis、MapStruct、Dagger/Hilt 和 Android;资源文件中的类名;Maven 和 Gradle(包括版本目录)依赖;以及从 Kotlin 使用的 Java 代码。
一个仓库可以混合使用所有这些。其工作方式在 docs/architecture.md 中描述。
限制
- 跟踪止于顶层声明。已使用类中的未使用方法,或已使用对象上的未使用属性,不会被报告。
- 仅通过按名称依赖注入、基于字符串的插件注册表或反射到达的代码,对依赖图不可见。将其添加到 entries,或用 loose-ends: entry 标记。
- 带有框架注解(@Service、@Controller、@Entity、…)的 Java 类会被视为入口点,因此永远不会被报告为未使用。
- Kotlin 和 Groovy 只会扫描它们使用的 Java 名称。它们自身的代码不会被分析。
- 使用计算名称的 Python getattr,以及通过 sys.path 变更找到的模块,不会被跟踪。
- 其他语言不会被分析。
贡献
设置、拉取请求规则和发布流程见 CONTRIBUTING.md。要报告漏洞,请参阅 SECURITY.md。