"最危险的依赖是曾经存在过的那个。"
它做什么
Phantom Limb 检测幽灵引用——代码试图获取不再存在的东西。不是那种你的 linter 能捕获的导入错误。而是更微妙的那种:没人设置的环境变量、三个 sprint 前被重命名的配置键、已弃用但从未从客户端移除的 API 端点、只存在于原始开发者机器上的目录路径。
每个代码库都会积累幽灵。它们不会导致错误——它们导致神秘感。它们就是某个功能"除了生产环境外 everywhere 都正常"的原因。它们就是入职需要两周而不是两天的原因。
为什么存在
静态分析捕捉错误。Linter 捕捉丑陋。Phantom Limb 捕捉缺失——你的代码和现实之间的负空间。
| 传统工具发现 | Phantom Limb 发现 |
|---|
| 破损的导入 | 能解析但引用死代码路径的导入 |
| 语法错误 | 语义有效但指向已删除概念的引用 |
| 未使用的变量 | 引用幽灵状态的已使用变量 |
| 缺失的文件 | 存在但包含先前架构假设的文件 |
| 类型不匹配 | 类型匹配但描述已不存在事物的类型 |
幽灵的六种类型
1. 环境幽灵
引用不再被任何进程设置的环境变量、配置文件或系统状态。
// This worked when we ran Redis locally
const cache = process.env.REDIS_URL || 'redis://localhost:6379';
// Redis was replaced with Memcached 8 months ago.
// Nobody removed this. The fallback silently runs. Against nothing.
检测方法: 交叉引用每个 process.env、os.environ、ENV[] 读取与实际的 .env、.env.example、CI/CD 配置和部署清单。
2. 引用幽灵
代码引用被移动、重命名或删除的函数、类或模块——但由于垫片、重导出或回退机制,引用仍然"有效"。
# utils.py re-exports calculate_tax for "backwards compatibility"
# Nobody imports calculate_tax from the original location anymore
# But nobody removed the re-export either
# And the original calculate_tax was rewritten. The re-export points to the old version.
from legacy.tax import calculate_tax # pragma: no cover
检测方法: 追踪每个导入链到其终端定义。标记超过 2 跳的链。标记路径中包含 "legacy"、"compat"、"old" 或 "deprecated" 但没有弃用截止日期的任何内容。
3. 时间幽灵
代码依赖于在先前架构下为真但不再保证的时序、排序或顺序。
// This worked when auth was synchronous middleware
// After the async rewrite, user might not be populated yet
app.get('/dashboard', (req, res) => {
const name = req.user.displayName; // Sometimes undefined. Sometimes not.
});
检测方法: 映射所有隐式排序假设。标记任何假设先前的中间件/钩子/生命周期事件已完成但没有显式 await/guard 的数据访问。
4. 契约幽灵
代码期望但另一方不再遵守的 API 契约、数据库模式或线格式。
# The payments API v2 removed the 'discount_code' field
# Our code still sends it. The API silently ignores it.
# Nobody knows the discount feature has been broken for 3 months.
payload = {
"amount": total,
"discount_code": user.discount, # Phantom. Silently ignored.
}
检测方法: 将每个出站 payload 构建与最新的 API 架构/文档进行比较。将每个数据库查询与当前模式进行比较。标记构造但从未消费的字段。
5. 意图幽灵
描述代码不再展现的行为的注释、TODO 和文档。规范变成了鬼故事。
/*
Retries up to 3 times with exponential backoff.
Falls back to cache on failure.
/
// Retry logic was removed in PR #847. Cache fallback was never implemented.
public Response fetchData() {
return client.get(url); // One shot. No retry. No fallback.
}
检测方法: 解析文档注释并将声称的行为与实际实现进行比较。标记文档字符串中提到但方法体中不存在的模式(重试、缓存、回退、队列、批处理)。
6. 身份幽灵
名称描述的内容与实际行为不符的变量、函数或模块。名称是它们原始目的的幽灵。
// This was a temporary cache. Three years ago.
func getTempCache() PermanentStore {
return &PermanentStore{ttl: 0} // TTL of zero = lives forever
}
检测方法: 对标识符名称与其实际行为进行语义分析。标记名称语义和实现语义之间的矛盾(例如 temp + 无过期、async + 同步执行、safe + 无错误处理)。
工作原理
Phase 1: EXCAVATION(挖掘)
├── 扫描所有源文件以查找外部引用
├── 构建引用图(什么指向什么)
├── 映射所有环境读取、配置查找、API 调用
└── 编目所有导入链及其终端定义Phase 2: REALITY CHECK(现实检查)
├── 交叉引用实际环境状态
├── 将 API 契约与当前模式进行比较
├── 追踪导入链以检测幽灵重导出
└── 将文档声明与实现进行比较
Phase 3: PHANTOM CLASSIFICATION(幽灵分类)
├── 按类型(上述 1-6)对每个幽灵进行分类
├── 评分严重程度(静默失败 vs 响亮失败 vs 潜在)
├── 估算爆炸半径(多少代码路径受影响)
└── 计算持续时间(幽灵存在了多久)
Phase 4: EXORCISM REPORT(驱魔报告)
├── 按严重程度 × 爆炸半径排序的幽灵列表
├── 对于每个幽灵:它引用什么、实际存在什么、该怎么办
├── 每个类别的快速修复建议
└── 依赖现实图(你的代码认为存在什么 vs 实际存在什么)
严重程度评分
| 严重程度 | 描述 | 示例 |
|---|
| Critical(严重) | 幽灵导致静默数据丢失或损坏 | API 字段被静默忽略,数据从未保存 |
| High(高) | 幽灵导致间歇性故障 | 时间幽灵,与幽灵状态的竞态条件 |
| Medium(中) | 幽灵导致混淆但无运行时错误 | 身份幽灵,误导性名称 |
| Low(低) | 幽灵是惰性的但增加认知负担 | 死重导出,孤立配置 |
| Vestigial(残留) | 幽灵无害但表明架构腐朽 | 2 年多前的 TODO 注释 |
输出格式
╔══════════════════════════════════════════════════════════════╗
║ PHANTOM LIMB SCAN ║
║ 12 phantoms detected ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ CRITICAL (2) ║
║ ├── [Contractual] POST /api/payments sends 'discount_code'║
║ │ → Field removed in API v2 (2024-11-03) ║
║ │ → 3 months of silent discount failures ║
║ │ → Fix: Remove field from payload builder ║
║ │ ║
║ ├── [Environmental] REDIS_URL referenced in 4 files ║
║ │ → No process sets this variable ║
║ │ → Fallback to localhost:6379 connects to nothing ║
║ │ → Fix: Remove Redis references, use Memcached client ║
║ │ ║
║ HIGH (3) ║
║ ├── [Temporal] req.user accessed before auth middleware ║
║ │ ... ║
╚══════════════════════════════════════════════════════════════╝
集成
在以下情况下调用:
- 开发者入职时(向他们展示幽灵在哪里)
- 主要重构后(找出重构留下了什么)
- 生产部署前(在用户之前捕获幽灵)
- 架构审查期间(映射意图与现实之间的差距)
为什么重要
每个代码库都有一个幽灵架构——它认为的系统,叠加在它实际的系统之上。这两个架构之间的差距是 bug 藏身、入职停滞和技术债务悄然累积的地方。
Phantom Limb 不找 bug。它找让 bug 必然出现的条件*。
零外部依赖。零 API 调用。纯粹的结构分析。
"The most dangerous dependency is the one that used to exist."
What It Does
Phantom Limb detects ghost references — code that reaches for things that aren't there anymore. Not broken imports (your linter catches those). The subtle kind: environment variables nobody sets, config keys that were renamed three sprints ago, API endpoints that were deprecated but never removed from the client, file paths that point to directories that exist only on the original developer's machine.
Every codebase accumulates phantoms. They don't cause errors — they cause mystery. They're the reason a feature "works everywhere except production." They're the reason onboarding takes two weeks instead of two days.
Why This Exists
Static analysis catches what's wrong. Linters catch what's ugly. Phantom Limb catches what's missing — the negative space between your code and reality.
| Traditional Tools Find | Phantom Limb Finds |
|---|
| Broken imports | Imports that resolve but reference dead code paths |
| Syntax errors | Semantically valid references to deleted concepts |
| Unused variables | Used variables that reference phantom state |
| Missing files | Files that exist but contain assumptions from a previous architecture |
| Type mismatches | Types that match but describe something that no longer exists |
The Six Classes of Phantoms
1. Environmental Phantoms
References to environment variables, config files, or system state that no process ever sets.
// This worked when we ran Redis locally
const cache = process.env.REDIS_URL || 'redis://localhost:6379';
// Redis was replaced with Memcached 8 months ago.
// Nobody removed this. The fallback silently runs. Against nothing.
Detection method: Cross-reference every process.env, os.environ, ENV[] read against actual .env, .env.example, CI/CD configs, and deployment manifests.
2. Referential Phantoms
Code that references functions, classes, or modules that were moved, renamed, or deleted — but the reference still "works" because a shim, re-export, or fallback catches it.
# utils.py re-exports calculate_tax for "backwards compatibility"
# Nobody imports calculate_tax from the original location anymore
# But nobody removed the re-export either
# And the original calculate_tax was rewritten. The re-export points to the old version.
from legacy.tax import calculate_tax # pragma: no cover
Detection method: Trace every import chain to its terminal definition. Flag chains longer than 2 hops. Flag anything with "legacy", "compat", "old", or "deprecated" in the path that has no deprecation deadline.
3. Temporal Phantoms
Code that depends on timing, ordering, or sequencing that was true under a previous architecture but is no longer guaranteed.
// This worked when auth was synchronous middleware
// After the async rewrite, user might not be populated yet
app.get('/dashboard', (req, res) => {
const name = req.user.displayName; // Sometimes undefined. Sometimes not.
});
Detection method: Map all implicit ordering assumptions. Flag any data access that assumes a prior middleware/hook/lifecycle event has already completed without explicit await/guard.
4. Contractual Phantoms
API contracts, database schemas, or wire formats that the code expects but the other side no longer honors.
# The payments API v2 removed the 'discount_code' field
# Our code still sends it. The API silently ignores it.
# Nobody knows the discount feature has been broken for 3 months.
payload = {
"amount": total,
"discount_code": user.discount, # Phantom. Silently ignored.
}
Detection method: Compare every outbound payload construction against the latest API schema/docs. Compare every database query against the current schema. Flag fields that are constructed but never consumed.
5. Intentional Phantoms
Comments, TODOs, and documentation that describe behavior the code no longer exhibits. The specification has become a ghost story.
/*
Retries up to 3 times with exponential backoff.
Falls back to cache on failure.
/
// Retry logic was removed in PR #847. Cache fallback was never implemented.
public Response fetchData() {
return client.get(url); // One shot. No retry. No fallback.
}
Detection method: Parse doc comments and compare claimed behavior against actual implementation. Flag docstrings that mention patterns (retry, cache, fallback, queue, batch) that don't appear in the method body.
6. Identity Phantoms
Variables, functions, or modules whose names describe something they no longer do. The name is a phantom of their original purpose.
// This was a temporary cache. Three years ago.
func getTempCache() PermanentStore {
return &PermanentStore{ttl: 0} // TTL of zero = lives forever
}
Detection method: Semantic analysis of identifier names vs. their actual behavior. Flag contradictions between name semantics and implementation semantics (e.g., temp + no expiry, async + synchronous execution, safe + no error handling).
How It Works
Phase 1: EXCAVATION
├── Scan all source files for external references
├── Build a reference graph (what reaches for what)
├── Map all environment reads, config lookups, API calls
└── Catalog all import chains and their terminal definitionsPhase 2: REALITY CHECK
├── Cross-reference against actual environment state
├── Compare API contracts against current schemas
├── Trace import chains to detect phantom re-exports
└── Compare documentation claims against implementation
Phase 3: PHANTOM CLASSIFICATION
├── Classify each phantom by type (1-6 above)
├── Score severity (silent failure vs. loud failure vs. latent)
├── Estimate blast radius (how many codepaths are affected)
└── Calculate haunting duration (how long has this been phantom)
Phase 4: EXORCISM REPORT
├── Prioritized list of phantoms by severity × blast radius
├── For each phantom: what it references, what's actually there, and what to do
├── Quick-fix suggestions for each class
└── Dependency reality map (what your code thinks exists vs. what does)
Severity Scoring
| Severity | Description | Example |
|---|
| Critical | Phantom causes silent data loss or corruption | API field silently ignored, data never saved |
| High | Phantom causes intermittent failures | Temporal phantom, race condition with ghost state |
| Medium | Phantom causes confusion but no runtime errors | Identity phantom, misleading names |
| Low | Phantom is inert but adds cognitive load | Dead re-exports, orphaned configs |
| Vestigial | Phantom is harmless but indicates architectural rot | TODO comments from 2+ years ago |
Output Format
╔══════════════════════════════════════════════════════════════╗
║ PHANTOM LIMB SCAN ║
║ 12 phantoms detected ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ CRITICAL (2) ║
║ ├── [Contractual] POST /api/payments sends 'discount_code' ║
║ │ → Field removed in API v2 (2024-11-03) ║
║ │ → 3 months of silent discount failures ║
║ │ → Fix: Remove field from payload builder ║
║ │ ║
║ ├── [Environmental] REDIS_URL referenced in 4 files ║
║ │ → No process sets this variable ║
║ │ → Fallback to localhost:6379 connects to nothing ║
║ │ → Fix: Remove Redis references, use Memcached client ║
║ │ ║
║ HIGH (3) ║
║ ├── [Temporal] req.user accessed before auth middleware ║
║ │ ... ║
╚══════════════════════════════════════════════════════════════╝
Integration
Invoke when:
- Onboarding a new developer (show them where the ghosts live)
- After a major refactor (find what the refactor left behind)
- Before a production deploy (catch phantoms before users do)
- During architecture review (map the gap between intent and reality)
Why It Matters
Every codebase has a phantom architecture — the system it thinks it is, layered on top of the system it actually is. The gap between these two architectures is where bugs hide, onboarding stalls, and technical debt compounds silently.
Phantom Limb doesn't find bugs. It finds the conditions* that make bugs inevitable.
Zero external dependencies. Zero API calls. Pure structural analysis.