docs: 添加文档大纲及 superpowers/outline 目录

Co-Authored-By: deepseek-v4-pro <deepseek-ai@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-15 16:17:03 +08:00
parent 37dac682b9
commit 178868175e
39 changed files with 9972 additions and 0 deletions

View File

@@ -0,0 +1,265 @@
# 第五章Feature Flag 系统的三个硬约束
> `feature()` 不是普通函数,它是 Bun 编译器用来做死代码消除的语法标记。
打开 `src/types/internal-modules.d.ts:10`,你会看到这样一行声明:
```ts
declare module 'bun:bundle' {
export function feature(name: string): boolean
}
```
这是一个虚假的模块声明 -- `bun:bundle` 不存在于文件系统上,也不是 npm 包。它是 Bun 编译器在打包(`Bun.build()`)时内建的编译期原语。当 `Bun.build()` 看到 `feature('X')` 时,它会根据构建配置中的 `features` 列表决定把调用点替换为 `true``false`然后对所有不可达分支执行死代码消除Dead Code EliminationDCE
反编译重建之后,这个原语不再由编译器直接提供,必须通过类型声明 + 双构建管线各自模拟。这带来了三个硬约束,贯穿了整个代码库的每一个 feature-gated 代码块。
## 约束一:`feature()` 只能出现在 `if` 条件或三元表达式的位置
CLAUDE.md 里有一条铁律:
> `feature()` 只能直接用在 `if` 语句或三元表达式的条件位置,不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。
打开 `src/hooks/useReplBridge.tsx:117`,你能看到一段注释精确解释了为什么:
```ts
// feature() check must use positive pattern for dead code elimination —
// negative pattern (if (!feature(...)) return) does NOT eliminate
// dynamic imports below.
if (feature('BRIDGE_MODE')) {
```
这个约束的根源是 Bun 编译器 AST 模式匹配的局限性。编译器只识别两种模式:
1. `if (feature('X')) { ... }` -- 把 `feature('X')` 替换为 `false` 后,整个代码块变成 `if (false) { ... }`DCE 可以整块删除。
2. `feature('X') ? a : b` -- 替换后变成 `false ? a : b``true ? a : b`DCE 可以删掉不会走的分支。
如果你写成 `const enabled = feature('X'); if (enabled) { ... }`,编译器看到的是对变量 `enabled` 的判断,无法确定其值为常量,整个 feature-gated 代码块都会保留在产物里。
**反事实推演**:如果 `feature()` 能赋值给变量,整个 `tools.ts` 的条件导入模式就不需要那么别扭的 `feature('X') ? require(...) : null` 三元表达式了。你可以写 `const enabled = feature('X'); const tool = enabled ? require(...) : null;`,代码可读性会好很多。但代价是:所有被 gate 的代码(包括 `require()` 引用不存在的文件)都会被打进产物,运行时可能触发 `MODULE_NOT_FOUND` 崩溃。
### 正面模式与负面模式的陷阱
`src/hooks/useReplBridge.tsx:117` 提到了另一个细微之处:**正面模式**`if (feature('X'))`)才能触发 DCE**负面模式**`if (!feature('X')) return`)不行。
打开 `src/entrypoints/cli.tsx:165` 看一个正面模式的例子:
```ts
if (!feature('DAEMON')) {
console.error('Error: --daemon-worker requires DAEMON feature...');
process.exitCode = 1;
return;
}
```
这里用了 `!feature('DAEMON')`,但注意后面的 `return` 是从 `main()` 函数退出的,不是 return 从一个 require 块。DCE 只需要把 `feature('DAEMON')` 替换为 `false` 后变成 `if (!false)``if (true)`,保留这个检查分支没问题。真正的问题是当 feature 为 true 时Bun 需要把 `require('../daemon/workerRegistry.js')` 打进产物 -- 这要求文件存在。如果 DAEMON 在构建 features 列表里,一切正常;如果不在,那 `require()` 所在的分支因为 `!feature()``false` 会被 DCE 删掉。
关键区别在于:**`if (feature('X'))` 包裹的 `require()` 路径在 `X=false` 时被 DCE 删除**,所以文件可以不存在。但 **`if (!feature('X'))` 包裹的 `require()` 路径在 `X=true` 时必须存在**,因为 DCE 保留的是 `else` 分支。
## 约束二:`if (false)` 必须在 parse 阶段可见,否则 bundler 会崩溃
这是 Vite/Rollup 构建管线独有的约束。打开 `scripts/vite-plugin-feature-flags.ts:29`,你会看到注释:
```ts
/**
* Vite/Rollup plugin that replaces `feature('X')` calls with boolean literals
* at the transform stage, BEFORE the bundler resolves imports.
*
* This approach is necessary because some feature-gated code blocks contain
* require() calls to files that don't exist (e.g. hunter.js inside
* feature('REVIEW_ARTIFACT')). The bundler must see these as dead code
* (`if (false) { ... }`) before attempting import resolution.
*/
```
打开 `src/skills/bundled/index.ts:44`,看这个致命的模式:
```ts
if (feature('REVIEW_ARTIFACT')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerHunterSkill } = require('./hunter.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerHunterSkill()
}
```
文件 `src/skills/bundled/hunter.js` **不存在**。你可以在终端里验证:`ls src/skills/bundled/hunter.js` 返回 "No such file or directory"。代码库中完全找不到任何名为 `hunter*` 的文件。
这在 `Bun.build()` 管线下不是问题 -- Bun 的打包器知道 `feature('REVIEW_ARTIFACT')` 返回 `false`(因为它不在 `DEFAULT_BUILD_FEATURES` 列表里,见 `scripts/defines.ts:72` 的注释),直接 DCE 掉整个 `if` 块,从来不会尝试解析 `./hunter.js`
但 Vite/Rollup 不同。Rollup 的处理管线是resolve imports -> transform -> bundle。如果 Vite 在 transform 之前尝试 resolve imports它会看到 `require('./hunter.js')` 然后 `MODULE_NOT_FOUND` 崩溃。
这就是为什么 `vite-plugin-feature-flags.ts` 必须在 `transform` 阶段(而非 `load``resolveId` 阶段)替换 `feature('X')` 调用。打开 `scripts/vite-plugin-feature-flags.ts:54``transform` 函数用正则匹配替换:
```ts
transform(code, id) {
if (id.includes('node_modules')) return null
let transformed = code.replace(FEATURE_CALL_RE, (match, flagName) => {
return features.has(flagName) ? 'true' : 'false'
})
// ...
}
```
替换发生在 `resolveId` 之后、bundle 之前。这样 Rollup 看到 `if (false) { require('./hunter.js') }` 就知道整个分支不可达,不会尝试解析 `./hunter.js`
插件还提供了一个虚拟模块解决 `import { feature } from 'bun:bundle'` 的 "module not found" 错误(`scripts/vite-plugin-feature-flags.ts:47`
```ts
load(id) {
if (id === resolvedVirtualModuleId) {
return 'export function feature(name) { return false; }'
}
}
```
这个 stub 的 `return false` 在运行时永远不会被调用,因为所有 `feature()` 调用都在 `transform` 阶段被替换成了字面量。它存在的唯一意义是让 Rollup 不报 unresolved import 错误。
**反事实推演**:如果 `transform` 替换不够早Vite 构建管线在遇到任何引用不存在文件的 feature-gated `require()` 时都会崩溃。这意味着所有被注释掉的 feature`CONTEXT_COLLAPSE``UDS_INBOX``REVIEW_ARTIFACT` 等)在 Vite 管线下都是"定时炸弹" -- 只要它们的代码块里有 `require()` 指向不存在的文件,替换时机不对就会炸。
## 约束三Vite 的 `using` 声明必须 transpile否则 Node.js 崩溃
`vite-plugin-feature-flags.ts` 在 feature flag 替换之外还承担了一项额外职责。打开 `scripts/vite-plugin-feature-flags.ts:68`
```ts
// 2. Transpile `using _ = expr;` to `const _ = expr;` for Node.js compat.
// Node.js v22 does not support `using` declarations (Explicit Resource Management).
// Safe because: SLOW_OPERATION_LOGGING is not enabled, so slowLogging returns
// a no-op disposable whose [Symbol.dispose]() is empty.
if (transformed.includes('using _')) {
transformed = transformed.replace(/\busing\s+(_\w*)\s*=/g, 'const $1 =')
modified = true
}
```
这段正则把所有 `using _x = expr` 替换成 `const _x = expr`。注释解释了安全性前提:`SLOW_OPERATION_LOGGING` 未启用时,`slowLogging` 返回的 disposable 的 `[Symbol.dispose]()` 是空操作,所以 `using``const` 行为等价。
但这里有一条脆弱的依赖链:如果有人启用了 `SLOW_OPERATION_LOGGING` 并在 Vite 构建产物上用 Node.js 运行,资源清理就不会执行 -- `using``Symbol.dispose` 语义被丢弃了。
**反事实推演**:如果不做这个 transpileVite 构建的产物在 Node.js v22 上会直接 `SyntaxError: Unexpected token 'using'`。这意味着整个 "产物兼容 bun/node" 的承诺(`build.ts` 的 post-build `import.meta.require` 补丁)在 Vite 管线上多了一个前提条件。
## 三层切换机制Build 默认、Dev 全开、运行时环境变量
打开 `scripts/defines.ts:39`,你会看到 `DEFAULT_BUILD_FEATURES` 列表65+ 个 feature flag 中大约有 40 个默认启用,其余被注释掉。打开 `scripts/dev.ts:39`dev 模式使用同一个列表:
```ts
const allFeatures = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])]
const featureArgs = allFeatures.flatMap(name => ['--feature', name])
```
但 dev 模式可以通过 `FEATURE_<NAME>=1` 环境变量额外启用。例如 `FEATURE_REVIEW_ARTIFACT=1 bun run dev` 会尝试启用 `REVIEW_ARTIFACT`,然后代码会尝试 `require('./hunter.js')`,由于文件不存在而崩溃。
三层机制的行为差异:
| 层级 | 何时生效 | feature() 的值 | DCE 是否生效 |
|------|----------|---------------|-------------|
| `Bun.build()` | 构建时 | 编译期常量 | 是 -- 不可达代码被删除 |
| `vite build` | 构建时(通过 transform 插件) | transform 后的字面量 | 是 -- Rollup 删除不可达分支 |
| `bun run dev` | 运行时(通过 `--feature` flag | 运行时布尔值 | 否 -- 所有分支都在内存中 |
这意味着 dev 模式下所有 feature-gated 的 `require()` 路径都必须实际存在,否则运行时会崩溃。对 Bun 原生 dev 来说 `--feature` flag 是 Bun 运行时提供的;对 Vite dev 来说 `feature()` 被 transform 插件替换为字面量,运行时不存在 `bun:bundle` 模块。
## 反编译产物的 stub 陷阱:两类禁用,一个混淆
`DEFAULT_BUILD_FEATURES` 中被注释掉的 feature 可以分为两类。打开 `scripts/defines.ts:62-72`,看注释中的措辞差异:
**第一类:反编译丢失导致的空壳 stub**
```ts
// 'CONTEXT_COLLAPSE', // 已禁用:实现是空壳 stub启用后会抑制 auto compact 导致上下文管理完全失效
// 'HISTORY_SNIP', // 已禁用snip 功能暂时关闭
```
这些 feature 在原始 Claude Code 中是完整功能,反编译过程中逻辑丢失,留下的实现要么是空壳(`CONTEXT_COLLAPSE`),要么会破坏核心功能(`HISTORY_SNIP` 启用后 `SnipTool` 出现但上下文管理不正常)。启用它们不是"多了一个功能",而是"引入了一个损坏的功能"。
**第二类:功能原本就 stubbed 或已废弃**
```ts
// 'SKILL_LEARNING', // 已禁用
// 'TEAMMEM', // 已禁用:依赖 COORDINATOR_MODE邮箱文件无限增长
// 'REVIEW_ARTIFACT', // 已禁用代码审查产物API 请求无响应,待排查 schema 兼容性)
```
`SKILL_LEARNING``TEAMMEM` 在原始版本中也是 stubbed 或内部工具,并非完整的对外功能。`REVIEW_ARTIFACT` 更有趣 -- 它的 `hunter.js` 根本不存在于反编译产物中,说明要么原始代码中也是动态加载的(但反编译时丢失了),要么是整个 hunter 子系统在某个版本中被删除但 feature gate 的引用没清理干净。
打开 `src/tools.ts:148``ReviewArtifactTool` 的条件加载用的是标准的三元模式:
```ts
const ReviewArtifactTool = feature('REVIEW_ARTIFACT')
? require('@claude-code-best/builtin-tools/tools/ReviewArtifactTool/ReviewArtifactTool.js')
.ReviewArtifactTool
: null
```
打开 `packages/builtin-tools/src/tools/ReviewArtifactTool/` 验证一下 -- 这个目录是存在的,工具实现也完整。但 `hunter.js`(注册 hunter skill 的模块)不存在。这意味着 `REVIEW_ARTIFACT` 是"工具存在但 skill 不存在"的半死状态。
**如果不区分这两类**,有人可能觉得"注释掉的 feature 只要改一行配置就能启用"。对第二类也许可以,但对第一类,启用 `CONTEXT_COLLAPSE` 会让 auto compact 失效、启用 `UDS_INBOX` 会让 Node.js 构建卡住(`scripts/defines.ts:68` 的注释明确说了)。
## `const x = feature()` 为什么到处存在
CLAUDE.md 说 "不能赋值给变量",但你打开 `src/main.tsx:119` 就能看到违反这条规则的代码:
```ts
const coordinatorModeModule = feature('COORDINATOR_MODE')
? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
: null;
```
这不矛盾。CLAUDE.md 说的"不能赋值给变量"指的是你不能把 `feature()` 的返回值单独赋给变量然后在 `if` 里用那个变量。但 `feature() ? a : null` 是三元表达式 -- `feature()` 在条件位置。Bun 编译器的 DCE 看到的是 `feature('X')` 这个 AST 节点在三元条件的根,它知道可以替换。
同样的模式在 `src/tools.ts:140-158` 中大量出现:
```ts
const SnipTool = feature('HISTORY_SNIP')
? require('@claude-code-best/builtin-tools/tools/SnipTool/SnipTool.js').SnipTool
: null
const ReviewArtifactTool = feature('REVIEW_ARTIFACT')
? require('@claude-code-best/builtin-tools/tools/ReviewArtifactTool/ReviewArtifactTool.js').ReviewArtifactTool
: null
```
这是 "feature gate + 条件 require + null fallback" 三合一模式。如果 `feature()` 在条件位置DCE 生效,`require()` 路径在 false 时不会被解析。如果写成 `const enabled = feature('X'); const tool = enabled ? require(...) : null;`,第二行的 require 不在 `feature()` 的 AST 子树里DCE 无法保证它在 false 时被消除。
打开 `src/main.tsx:703`,看一个更微妙的三元用法:
```ts
const _pendingConnect: PendingConnect | undefined = feature('DIRECT_CONNECT')
? {
url: undefined,
authToken: undefined,
dangerouslySkipPermissions: false,
}
: undefined;
```
这里不是 require而是一个对象字面量。`feature('DIRECT_CONNECT')` 在三元条件位置DCE 可以把 false 分支(对象字面量)消除。如果不这么做,`PendingConnect` 类型可能引用的内部模块会被全量引入。
## feature 字符串本身的 DCE
还有一个容易被忽略的 DCE 细节。打开 `src/components/TokenWarning.tsx:87`
```ts
// Each feature() block stands alone so the flag strings DCE from
// external builds independently.
if (feature('REACTIVE_COMPACT')) {
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
reactiveOnlyMode = true;
}
}
if (feature('CONTEXT_COLLAPSE')) {
const { isContextCollapseEnabled } =
require('../services/contextCollapse/index.js');
// ...
}
```
注释说 "each feature() block stands alone"。为什么不合并成一个 `if (feature('A') || feature('B'))` 块?因为合并后,即使 `A``B` 都为 false`else` 分支中的 feature flag 字符串 `'REACTIVE_COMPACT'``'CONTEXT_COLLAPSE'` 可能不会从产物中消除。独立的 `if` 块让每个 flag 字符串在自己的 DCE 作用域里 -- `feature('X')` 替换为 `false` 后,整个 `if (false) { ... }` 块包括其中的字符串字面量都会被删除。
这对内部工具来说很重要feature flag 的名称(如 `CONTEXT_COLLAPSE`)本身可能泄露内部项目代号或功能名称。独立 DCE 确保外部构建的产物里找不到任何被注释掉的 feature 名称。
## 延伸阅读
- 想看 feature flag 如何与代码分割交互(为什么 600+ chunks 中的某些 chunks 只在特定 feature 启用时加载),见 [第一章Code Splitting 不是优化,是生存需求](./01-code-splitting.md)
- 想看入口函数如何用 feature gate 实现零模块加载的快速路径,见 [第二章:入口的 Fast-Path 优先级链](./02-fast-path.md)
- 想看工具系统如何用 feature gate 实现延迟加载与白名单过滤,见 [第六章:工具系统的延迟加载与 CORE_TOOLS 白名单](./06-tools-deferred.md)
- 想看 biome.json 关闭 42 条规则背后的反编译痕迹,见 [第十五章biome.json 的 42 条规则关闭](./15-biome-42-rules.md)