---
title: "规则"
description: "每次 evlog map 检查的内容：六项会影响分数的要求、四项永远不会影响分数的建议，以及满足每项规则的条件"
canonical_url: "https://evlog.zhcndoc.com/cli/rules"
---
# 规则

> 每次 evlog map 检查的内容：六项会影响分数的要求、四项永远不会影响分数的建议，以及满足每项规则的条件

Coverage 由规则引擎检查，而不是一堆启发式规则。每条规则都有稳定的 id、记录在文档中的权重、文档链接，以及它所适用的入口点类型集合；每条发现还会带有其来源文件和行号，因此你始终可以去查看一个裁决的具体依据。

<warning icon="i-lucide-flask-conical">

**此列表尚未最终确定。** 十条规则是引擎的起点，而不是终点。规则会不断增加，现有规则对判定标准会变得更加精确，而更严格的规则可能会改变你没有修改过的代码的裁决。Id 和权重都有文档记录，因此变更始终可以解释，但不要将不同 CLI 版本之间的分数视为可比较。

</warning>

规则分为两类，这一区别很重要：

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      要求
    </th>
    
    <th>
      建议
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      对分数的影响
    </td>
    
    <td>
      失败时扣分
    </td>
    
    <td>
      从不影响
    </td>
  </tr>
  
  <tr>
    <td>
      出现时机
    </td>
    
    <td>
      只要适用就出现
    </td>
    
    <td>
      仅当项目已经使用该功能时出现
    </td>
  </tr>
  
  <tr>
    <td>
      在报告中
    </td>
    
    <td>
      <code>
        FIX FIRST
      </code>
      
       和 <code>
        THEN
      </code>
    </td>
    
    <td>
      <code>
        GOING FURTHER
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      在 JSON 中
    </td>
    
    <td>
      <code>
        checks
      </code>
    </td>
    
    <td>
      <code>
        suggestions
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      能否作为 CI 门禁失败
    </td>
    
    <td>
      是
    </td>
    
    <td>
      否
    </td>
  </tr>
</tbody>
</table>

## 三种状态

每条规则都会对一个入口点返回三种裁决之一。

<table>
<thead>
  <tr>
    <th>
      状态
    </th>
    
    <th>
      含义
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        pass
      </code>
    </td>
    
    <td>
      规则查看后找到了它想要的内容
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fail
      </code>
    </td>
    
    <td>
      规则查看后没有找到——如果它是必需项，则会扣除权重
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        n/a
      </code>
    </td>
    
    <td>
      这个问题在这里没有意义，或者你已<a href="#disabling-a-check">
        禁用它
      </a>
    </td>
  </tr>
</tbody>
</table>

`n/a` 正在发挥实际作用。一个从不抛出异常的处理器，永远不会被问及其错误是否包含 `why` 和 `fix`；一个没有 `catch` 的处理器，永远不会被问及其捕获是否会记录日志。过去这些都会被默认算作通过，这会导致报告声称某个文件处理了它实际上并没有处理的错误。现在它们在矩阵中只是一个个点，分数则根据实际适用的规则来计算。

<map-rules-matrix>



</map-rules-matrix>

## 要求

六条规则会影响分数。它们适用于类似 handler 的入口点（API handlers、middleware、scheduled jobs 和 server actions），但 `fetch` 除外，它是唯一一条仅适用于页面的规则。

<table>
<thead>
  <tr>
    <th>
      列
    </th>
    
    <th>
      Id
    </th>
    
    <th>
      权重
    </th>
    
    <th>
      期望
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        log
      </code>
    </td>
    
    <td>
      <code>
        wide-event
      </code>
    </td>
    
    <td>
      40
    </td>
    
    <td>
      <code>
        useLogger()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        audit
      </code>
    </td>
    
    <td>
      <code>
        audit
      </code>
    </td>
    
    <td>
      25
    </td>
    
    <td>
      <code>
        log.audit()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        err
      </code>
    </td>
    
    <td>
      <code>
        structured-errors
      </code>
    </td>
    
    <td>
      20
    </td>
    
    <td>
      <code>
        createError({ why, fix })
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fetch
      </code>
    </td>
    
    <td>
      <code>
        page-error-handling
      </code>
    </td>
    
    <td>
      20
    </td>
    
    <td>
      fetch 错误处理
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ctx
      </code>
    </td>
    
    <td>
      <code>
        context
      </code>
    </td>
    
    <td>
      15
    </td>
    
    <td>
      <code>
        log.set()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        catch
      </code>
    </td>
    
    <td>
      <code>
        error-handling
      </code>
    </td>
    
    <td>
      15
    </td>
    
    <td>
      在 <code>
        catch
      </code>
      
       中记录日志或重新抛出
    </td>
  </tr>
</tbody>
</table>

### log：这个入口点是否会发出宽事件？

这是最重要的规则，因为其他一切都依赖它。没有 logger 的入口点不会产生任何事件，而再好的错误处理也无法告诉你内部发生了什么。

通过已解析的 evlog logger（`useLogger`、`createLogger`、`createRequestLogger`、`initLogger`）或 evlog 包装器（`withEvlog`、`withAudit`）即可满足。

消息取决于你的框架。使用 evlog 的 Nitro 插件时，每个请求都会发出一个事件，无论 handler 是否主动请求，所以那里失败的不是“沉默”，而是“空洞”：

```text [Nuxt / Nitro]
handler 未向其请求事件添加任何内容——只记录了方法、路径和状态
```

```text [Next.js / TanStack Start]
没有 useLogger() — handler 是一个黑暗事件
```

<framework-tabs>

```ts [Nuxt]
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
})
```

```ts [Next.js]
export async function POST(request: Request) {
  const log = useLogger()
}
```

</framework-tabs>

[宽事件 →](https://evlog.zhcndoc.com/learn/wide-events)

### ctx：是否附加了请求上下文？

没有 `log.set()` 的 logger 产生的事件在技术上是有效的，但它对所描述的请求却什么也没说。这是宽事件在生产中变得无用最常见的方式：你每个请求得到一行日志，但没有一行能回答问题。

只有在已解析的 evlog logger 上调用 `set()` 才算。无关的 `Map.set()` 不算。

```ts
log.set({ user: { id }, order: { id, total } })
```

[宽事件 →](https://evlog.zhcndoc.com/learn/wide-events)

### err：抛出的错误是否携带 why 和 fix？

`throw new Error('failed')` 到达客户端时只是一串字符串，没有原因也没有补救措施。`createError({ why, fix })` 才能让错误对凌晨 3 点读它的人来说具有可操作性。

仅当 handler 抛出某些内容时适用：`throw` 或 `createError()` 调用。消息会准确指出缺少的内容：

```text
throw new Error() — 使用 createError({ why, fix })
createError() 缺少 why 和 fix
createError() 有 why 但缺少 fix
```

`createError()` 如果是返回而不是抛出，也会被检查，因为它仍然会塑造响应。

```ts
throw createError({
  status: 400,
  message: '调用方看到的内容',
  why: '实际出了什么问题',
  fix: '该怎么处理',
})
```

[结构化错误 →](https://evlog.zhcndoc.com/learn/structured-errors)

### audit：这个敏感入口点是否留下了痕迹？

仅当[敏感性分类器](https://evlog.zhcndoc.com/cli/scoring#mark-what-touches-money-auth-or-pii)发现涉及资金或认证时适用。在其他地方，审计记录只会造成噪音，因此规则会报告 `n/a`，而不是无条件通过。

`log.audit()` 可以满足该规则，包括通过可选链调用，因此 `log.audit?.deny()` 也算。

建议的操作会根据路由自动推断，因此 `/api/auth/login` 会建议 `auth.login`，而 `/api/orders/[id]/refund` 会建议 `orders.refund`：

```ts
log.audit({
  action: 'auth.login',
  actor: { type: 'user', id: user.id },
})
```

[审计日志 →](https://evlog.zhcndoc.com/use-cases/audit/overview)

### catch：每个捕获的错误是否都被记录日志或重新抛出？

被吞掉的错误比未处理的错误更糟：请求看起来成功了，事件里什么也没说，直到用户报告时故障才会暴露。

当 `catch` 满足以下任一情况时，就算已处理：记录日志（`log.error`、`log.warn`、`log.set`、`log.audit`、`captureException`，或任何 `console.*`）、重新抛出，或返回。会分别报告两种失败：

```text
empty catch block swallows errors
catch block swallows error without logging or rethrow
```

完全没有 `catch` 的 handler 会被标记为 `n/a`，而不是缺失。evlog 的框架集成会挂接运行时的错误通道，因此逃出你 handler 的异常仍会随着其状态记录在事件中。

```ts
catch (error) {
  log.error(error)
}
```

[结构化错误 →](https://evlog.zhcndoc.com/learn/structured-errors)

### fetch：这个页面在其数据获取失败时还能正常工作吗？

唯一一条页面规则。仅适用于确实在服务端获取数据的页面。纯展示页面没有会失败的内容。

通过 `catch`、`.catch()`、`onError` 处理器，或从 fetch 本身解构出的 `error` 绑定即可满足。

<framework-tabs>

```vue [Nuxt]
<script setup lang="ts">
const { data, error } = await useFetch('/api/orders')
if (error.value) log.error(error.value)
</script>
```

```tsx [Next.js]
try {
  const orders = await getOrders()
} catch (error) {
  log.error(error)
}
```

</framework-tabs>

[生命周期 →](https://evlog.zhcndoc.com/learn/lifecycle)

## 机会

四条规则建议进一步改进。它们永远不会影响分数，并且只有当项目**已经在某处采用**了该功能时才会触发。重点是让你选择的功能发挥更大价值，而不是向你推销某些东西。

是否采用是基于 AST 确认的，而不是通过搜索文本，所以在注释里提到的功能不算。

<table>
<thead>
  <tr>
    <th>
      列
    </th>
    
    <th>
      Id
    </th>
    
    <th>
      触发条件
    </th>
    
    <th>
      范围
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        catalog
      </code>
    </td>
    
    <td>
      <code>
        error-catalog
      </code>
    </td>
    
    <td>
      项目声明了一个 catalog，且同一个内联错误出现在两个或更多文件中
    </td>
    
    <td>
      每个入口点
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        audit+
      </code>
    </td>
    
    <td>
      <code>
        audit-coverage
      </code>
    </td>
    
    <td>
      项目记录审计事件，而此处理程序在没有审计记录的情况下更改了状态
    </td>
    
    <td>
      每个入口点
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai
      </code>
    </td>
    
    <td>
      <code>
        ai-logging
      </code>
    </td>
    
    <td>
      <code>
        ai
      </code>
      
       是一个依赖项，但调用 AI SDK 时没有 <code>
        evlog/ai
      </code>
    </td>
    
    <td>
      每个项目一次
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        identity
      </code>
    </td>
    
    <td>
      <code>
        auth-identity
      </code>
    </td>
    
    <td>
      <code>
        better-auth
      </code>
      
       是一个依赖项，但未安装 <code>
        evlog/better-auth
      </code>
    </td>
    
    <td>
      每个项目一次
    </td>
  </tr>
</tbody>
</table>

### catalog：这些重复的错误是否应该成为 catalog 条目？

刻意保持狭窄。早期版本会对任何内联 `createError()` 触发，这导致本来就有合理错误的处理程序也被提示。重复才是能自证其说的信号：同一个状态和消息如果在三个地方都写着，迟早会产生偏差。

```text
"402 Card declined" 在这里以及另外 2 个文件中都写了出来 — 一个 catalog 条目就能覆盖它们
```

这个建议会提议使用你已经有的 catalog，而不是凭空新建一个。[Catalogs →](https://evlog.zhcndoc.com/learn/catalogs)

### audit+：这种状态变更是否也应该记录在审计轨迹中？

这是对 `audit` 要求的补充，后者只会在资金和认证相关场景触发。这个规则更温和、覆盖面更广：一旦你有了审计轨迹，任何状态变更都值得关注，而你才是最清楚哪些重要的人。它会在没有审计记录的处理程序中寻找 `create`、`update`、`insert`、`upsert`、`delete` 或 `destroy` 调用。已经被 `audit` 要求覆盖的入口点会被跳过，因此同一个缺口不会被报告两次。[记录审计事件 →](https://evlog.zhcndoc.com/use-cases/audit/recording)

### ai：模型调用、token 和延迟是否记录在事件中？

当未导入 `evlog/ai` 时，在 `generateText`、`streamText`、`generateObject`、`streamObject`、`embed` 和 `embedMany` 上触发。如果没有它，事件只会记录请求发生了，却不会记录模型成本，而这通常是请求中最昂贵、变化也最大的部分。整个项目只报告一次，因为一个包装后的模型会服务于所有 handler。[AI SDK →](https://evlog.zhcndoc.com/use-cases/ai-sdk/overview)

### identity：事件是否携带已认证用户的信息？

在实际使用认证的入口点上触发：认证路由本身，或读取 session 的 handler。`evlog/better-auth` 会将用户和 session 附加到每个事件上，这会把“一个请求失败了”变成“这个用户的请求失败了”。整个项目只报告一次，因为它是一个只需安装一次的插件。[Better Auth →](https://evlog.zhcndoc.com/use-cases/better-auth/overview)

## evlog 辅助函数如何被识别

每一条查找 logger 的规则都会问同一个问题：*这是 evlog 的 logger 吗？* 这个问题是从 AST 中判断的，因此，本地定义的 stub 不会为你加分。

- **从 evlog 导入**：`import { useLogger } from 'evlog'`，包括子路径导入
- **自动导入**：在 Nuxt 和 Nitro 中，evlog 模块会注入 `useLogger` 和 `createEvlogError`，因此未导入的调用也算。除非文件声明了自己的同名内容，在这种情况下以本地声明为准
- **从本地模块重新导出**：当该模块转发 evlog 的导出时，`import { useLogger } from '@/lib/evlog'` 也算，这正是[Next.js 指南](https://evlog.zhcndoc.com/integrate/frameworks/nextjs)推荐的形式。从工厂函数中解构导出的内容（`export const { useLogger } = createEvlog(...)`）也会被解析
- **通过 evlog 包装器**：`withEvlog` 和 `withAudit` 可以为 handler 添加埋点，而无需 handler 自身命名 logger

有两种情况明确**不**算数：

- **本地 stub**。你自己的文件中的 `function useLogger() { ... }`，或从不转发 evlog 导出的 `./my-logger` 导入，都不是 evlog 的 logger
- **evlog 的 log 导出**。`log.info()` 是[简单日志 API](https://evlog.zhcndoc.com/learn/simple-logging)。它会发出自己的日志行，而不是为请求的宽事件做贡献，并且没有 `set` 或 `audit`。它不满足 `log` 列

## 禁用检查

一个你无法争辩的静态分析器，最终会变成一个你不再运行的静态分析器。当规则错误地判断了你的代码，或者它判断正确但你已经决定不在意时，用注释将它关闭，并放在相关代码旁边：

```ts [server/api/health.get.ts]
// evlog-map-disable-next-line wide-event, context -- liveness probe, deliberately silent
export default defineEventHandler(() => ({ ok: true }))
```

三种形式，与你对 lint 工具的预期一致：

<table>
<thead>
  <tr>
    <th>
      指令
    </th>
    
    <th>
      覆盖范围
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        // evlog-map-disable-next-line <ids>
      </code>
    </td>
    
    <td>
      注释下方的那一行
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        // evlog-map-disable-line <ids>
      </code>
    </td>
    
    <td>
      注释所在的那一行，作为行尾注释
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        // evlog-map-disable <ids>
      </code>
    </td>
    
    <td>
      整个文件，无论它出现在哪里
    </td>
  </tr>
</tbody>
</table>

这些 id 就是上方表格中的 id：`wide-event`、`audit`、`structured-errors`、`context`、`error-handling`、`page-error-handling`，以及任何机会 id。多个 id 用逗号或空格分隔。**不指定 id 时，指令会覆盖所有规则**，这对生成文件或 vendored 文件来说是正确形式，但对于你只是尚未处理的 handler 来说则不合适。块注释的工作方式相同，这正适合放在文件顶部：`/* evlog-map-disable -- generated by the SDK */`。

`--` 后面的内容就是你的理由。它是可选的，但无论如何都值得写：报告里会显示它，因此下一个人读到的是决策，而不是猜测。

### 被禁用的检查如何影响分数

它会变成附带你所写理由的 `n/a`，与从未适用的规则状态相同，因此不会扣分，`--min-score` 门禁也不会再因它失败。

规则仍然会运行。指令豁免的是 *发现结果*，而不是问题本身：原本会通过的检查仍会以 `pass` 报告，而从未适用于该 handler 的规则则保持为普通的 `n/a`。因此，对一个已经埋点的 handler 使用整文件级指令并不会禁用任何东西，而被禁用的检查数量，就是你实际上选择不去看的判定数量。

这意味着，被禁用的检查必须保持可见，否则这个逃生阀就会悄悄变成一种让“什么都不记录”的应用拿到 100 分的方法。它会出现在三个地方：

```text [evlog map]
○ 2 checks disabled by comment in 1 entry point
```

```text [evlog map <file>]
CHECKS
✓ log.set     context attached with log.set()
○ useLogger   disabled at line 1 — liveness probe, deliberately silent
```

在 `--all` 中，被禁用的单元格显示为 `○`，而不是规则不适用时显示的 `·`。在 JSON 中，该检查会在其 `n/a` 旁携带 `"suppressed": true`，并且 `evidence` 会指向该注释，因此 CI 作业可以报告绿色分数中有多少被抑制，`summary.suppressedChecks` 则提供项目总数。

### 拼写错误会被明确提示

没有任何规则响应的 id 不会静默失效。扫描会在报告上方发出警告，并且该检查会继续失败：

```text
⚠ server/api/probe.get.ts:1 disables "wide-evnt", which is not a check evlog map runs
```

以为检查已经关闭、但它实际上仍在失败，这比任何一种状态都更糟，因此这一种情况绝不会安静处理。

## 免除的入口点

evlog 自身的客户端日志采集端点（`/api/evlog/ingest` 及其相关端点）属于基础设施，而不是应用代码。每条规则都会针对它们返回带有原因说明的 `n/a`，它们绝不会被列为需要修复的内容，并且在汇总中会将它们计为 `exempt`，而不是你自己的已埋点入口点。即使文件解析失败，这一点也成立：如果某个豁免只适用于可读文件，那就称不上什么保障。

静态页面出于同样的原因会被豁免。它不获取任何数据，因此 `page-error-handling` 没有什么需要向它询问，并会报告 `n/a`。将其计为未观测的入口点，会把营销页面显示为可观测性缺口。

## 报告错误判定

规则每条都各自对应一个文件，并拥有自己的测试台，因此错误裁决可以在本地修复，而不必进行考古式排查。如果 `evlog map <file>` 声称某件你能看到的事情并不属实，那就是一个值得[提交 issue](https://github.com/evloghq/evlog/issues) 的 bug。请附上入口点的输出和 handler，修复就能落在单条规则中。

与此同时，先在那一行[禁用检查](#disabling-a-check)。一次误报最多只该让你多写一条注释，而不该影响你的 CI 门禁。

## 下一步

- [计分](https://evlog.zhcndoc.com/cli/scoring)：权重如何变成分数，以及如何确定敏感性
- [CI](https://evlog.zhcndoc.com/cli/ci)：在要求项上为 pull request 设置门禁，而不让建议项造成干扰

---

- [计分](https://evlog.zhcndoc.com/cli/scoring)
- [evlog map](https://evlog.zhcndoc.com/cli/map)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
