来源：https://ui.docs.xihanfun.com/guide/diagnostics

# 诊断通道

组件在运行期发现契约问题时不直接 `console.warn`，而是向诊断通道投递一条结构化记录。宿主订阅后自行决定打印、收集还是上报。

## 不直接打日志的原因

契约违约需要被处理，不只是被看见。`wc.missing-part` 这类问题在开发时应明显报出，在测试中应使用例失败，在生产环境应静默：同一条记录，三种归宿。写死 `console.warn` 只满足第一种。

## 记录的形状

```ts
interface DiagnosticRecord {
  code: string; // 稳定标识，如 'wc.missing-part'
  level: "error" | "warn";
  message: string; // 面向开发者的说明，文案可变
  scope?: string; // 组件名，取解剖名
  instanceId?: string; // 区分同一组件的多个实例
  part?: string;
  node?: Element;
  detail?: Record<string, unknown>;
}
```

`code` 稳定，`message` 不稳定。订阅方按码分流，不匹配文案。

## 现有的码

```ts
export const DIAGNOSTIC_CODES = {
  invariant: "core.invariant", // 断言不成立
  warn: "core.warn", // 条件告警
  layerDisposeNotTop: "core.layer.dispose-not-top", // dispose 的层不是栈顶
  machineError: "machine.error", // 机器抛出 MachineError
  wcMissingPart: "wc.missing-part", // 作者未渲染必需的角色节点
  wcUnknownPart: "wc.unknown-part", // 角色节点的 part 名不在组件解剖内
  wcWrongPartTag: "wc.wrong-part-tag", // 角色节点的标签不满足要求，原生语义会静默失效
  matrixCodeLogoDamage: "matrix-code.logo-damage", // 中心 logo 挖掉的码字超出纠错级别能恢复的量
  matrixCodeOptionIgnored: "matrix-code.option-ignored", // 二维码收到一个对当前码制没有意义的选项，按没给处理
  barCodeOptionIgnored: "bar-code.option-ignored", // 条形码收到一个对当前码制没有意义的选项，按没给处理
  stylesMissingSkin: "styles.missing-skin", // 页面上出现了组件，但它那份皮肤没被引入
  versionMismatch: "core.version-mismatch", // 适配器与 core 的版本不一致，锁步发版被打破
  ignoredSlot: "core.ignored-slot", // 作者给了默认插槽，但该组件不渲染插槽内容
  overlayStackingTrap: "overlay.stacking-trap", // 浮层的祖先建了层叠上下文，浮层的层号被困在其中
  scrollbarMissingScrollable: "scrollbar.missing-scrollable", // 滚动条挂载时找不到它要管的滚动容器
  overlayMissingAnchor: "overlay.missing-anchor", // 浮层展开了却没有锚点，位置无从算起
  chartMissingName: "chart.missing-name", // 图表没有可及名：caption 部件、aria-label、aria-labelledby 都没有
  chartUnknownField: "chart.unknown-field", // 系列引用的字段在数据里不存在
  chartDuplicateSeries: "chart.duplicate-series", // 两个系列的 id 相同
  chartTooManySeries: "chart.too-many-series", // 分类系列超过 8 个：没有第 9 色
  chartMixedColorRoles: "chart.mixed-color-roles", // 同一张图混用分类色与语气色
  chartInvalidSlot: "chart.invalid-slot", // 固定色槽越界，或两个系列固定到同一槽
  chartStackOffsetConflict: "chart.stack-offset-conflict", // 同一堆叠组的 stackOffset 不一致
  chartLogDomain: "chart.log-domain", // 对数轴的定义域含 0 或跨越正负
  chartBarBaseline: "chart.bar-baseline", // 柱系列所在的值轴不含 0
  chartNegativeShare: "chart.negative-share", // 占比类图表出现负值
};
```

这份清单与 `@xihan-ui/core` 中的码表逐条对账，不会遗漏，可以直接据此编写分流。

`machine.error` 的 `detail.machineCode` 是状态机错误码。机器崩溃或正常停机清理失败时，`detail.reason` 保留实际上报的原始或聚合异常对象；同时发生 cleanup 与 exit 异常时，它与调用方捕获的 `AggregateError` 是同一个对象，可以继续读取 `errors` 与 `cause`。

状态机实现引用属于执行前置条件。`UNKNOWN_ACTION`、`UNKNOWN_GUARD`、`UNKNOWN_EFFECT` 在创建机器时拒绝静态缺项；动态列表或实现内部引用缺项时使用 `MISSING_ACTION`、`MISSING_GUARD`、`MISSING_EFFECT`。后三种错误在开发与生产都会先投递 `machine.error`；没有其他停机异常时，其 `detail.reason` 与随后抛出的 `MachineError` 是同一个对象，存在其他停机异常时则由后续崩溃记录携带聚合结果。服务同时进入 `Stopped`。诊断阈值只控制记录是否送达，不会把错误改成继续执行、guard 的 `false` 或部分 effect。

三条 `wc.*` 是 Web Components 适配器的部件契约校验，也是日常最常遇到的三条：手写 DOM 时遗漏一个 `data-xh-part` 或写错名字，通道会明确指出节点与部件。

## 用法

```ts
import { onDiagnostic, setDiagnosticsConsoleOutput, setDiagnosticsLevel } from "@xihan-ui/core";

// 订阅
const off = onDiagnostic((record) => {
  if (record.code === "wc.missing-part")
    reportToSentry(record);
});

// 调阈值：'error' | 'warn' | 'silent'
setDiagnosticsLevel("warn");

// 关闭内建 console 输出，只使用自己的订阅
setDiagnosticsConsoleOutput(false);
```

其余可用的接口：

| 函数 | 作用 |
| --- | --- |
| `getDiagnostics()` | 获取通道对象本身 |
| `reportDiagnostic(record)` | 投递一条（自定义组件中使用） |
| `setDiagnosticsDedupe(on)` | 同一 `code + scope + instanceId + part + message` 是否只报一次 |
| `resetDiagnostics()` | 清空订阅者、去重记录与全部开关 |

## 默认行为

| 环境 | 阈值 | console 输出 | 去重 |
| --- | --- | --- | --- |
| 开发 | `warn` | 开 | 开 |
| 生产 | `silent` | 关 | 开 |

生产默认 `silent`，投递直接被丢弃，不产生任何开销。需要在生产收集时，显式调用 `setDiagnosticsLevel('error')` 并挂载自己的订阅。

去重键包含 message：同一个码下不同文案是不同的问题，只有逐帧重复的同一条才应被压缩。去重集有容量上限，超出即整体清空，长时间运行的进程不会无界增长。

## 隔离性

- 通道挂在全局，同一页面中多份 `@xihan-ui/core` 副本共用一条通道；
- 订阅方抛错不会回流进组件：上报逻辑出错不影响界面。

## 在测试中使用

把阈值调到 `warn` 并订阅，即可把契约违约变为用例失败：

```ts
import { onDiagnostic, resetDiagnostics, setDiagnosticsLevel } from "@xihan-ui/core";

beforeEach(() => {
  resetDiagnostics();
  setDiagnosticsLevel("warn");
});

it("不应有契约违约", () => {
  const records: DiagnosticRecord[] = [];
  onDiagnostic(r => records.push(r));
  render();
  expect(records).toEqual([]);
});
```

## 相关

- [解剖与部件契约](./anatomy)：部件契约校验的内容
- [Web Components 适配器](../adapters/web-components)：三条 `wc.*` 码的来源
- [版本与兼容性政策](./versioning)：名字的变更与移除规则
