# 工具模块与领域规则（UtilsSchema）

UtilsSchema 是**业务无关工具模块**（base utility modules）的声明：带完整签名的纯函数集合，在 service 层被调用（含 flow 的 `invoke`）。方法签名（args/result）由 schema 声明，方法体由用户实现。

**当 utils 名称与某张表同名时，它就是该实体的领域规则集**（DDD 领域服务落点）——命名即归属、位置即角色：`utils_schema/.../order.utils.ts` = order 表的领域规则（`OrderUtils`）。

## 定义

```ts
import { defineUtils, booleanField, decimalField, dtoField, intField, stringField } from '@pylonts/dsl';
import { api, miniuser } from '../../../../project.config';
import { order } from '../../../../schema/order.table';

const orderUtils = defineUtils({
  name: 'OrderUtils',
  api,
  app: miniuser,
  description: '订单领域规则',
  methods: {
    // 防御 guard：条件抛错，通过无返回值（void），失败即 throw
    assertCancelable: {
      args: { status: dtoField(order.columns.status) },
    },
    // 判断：返回 boolean 判定值，供 flow IF 条件位使用
    canCancel: {
      args: { status: dtoField(order.columns.status) },
      result: booleanField(),
    },
    // 计算：返回派生标量
    calcTotal: {
      args: { unitPrice: dtoField(decimalField({ precision: 10, scale: 2 })), qty: dtoField(intField({ min: 1 })) },
      result: decimalField({ precision: 10, scale: 2 }),
    },
  },
});

// 必须 default-export 方法引用对象（XXUtils 约定），loader 从 refs 反推 schema
export default {
  assertCancelable: orderUtils.methods.assertCancelable,
  canCancel: orderUtils.methods.canCancel,
  calcTotal: orderUtils.methods.calcTotal,
};
```

### 方法形态

- **args**：`Record<string, DtoField>`——用 `dtoField(...)` 包装。可包装**表列**（`dtoField(order.columns.status)`，领域规则引用表字段的标准方式）或内联字段（`dtoField(stringField(...))`）。包装层反写不污染共享表列实例（与 `buildMessage` 同规则：仅内联字段写回底层）。
- **result**：`Field | undefined`——输出是全新值（boolean 判断、decimal 计算、string 派生文案），**不是共享表列**；省略 = void 方法（防御 guard 通过时无返回值）。
- **throws**：`ExceptionSchema[] | undefined`——方法可抛的异常清单，**防御 guard 的失败契约**。flow 里 `invoke` 防御 guard 时，其 throws 自动进入流程逃逸集——调用方 service 方法的 throws 必须包含它（或 TRY 捕获）。谓词（can/is/has）不抛，无需声明。
- **失败语义**：规则失败直接 `throw`（配合 `throws` 声明的 `BusinessException`/`CodeException` 通道），**不返回错误码结构**。

## 文件与存储约定

- 一文件一 utils；文件必须 **default-export 方法引用对象**（`export default { m: xxUtils.methods.m }`），schema 变量模块私有不导出。
- 文件名 = 名字去 `Utils`/`Util` 后缀转 kebab：`OrderUtils → order.utils.ts`、`AmtUtils → amt.utils.ts`。
- 存储位置按绑定（机器校验，`lint utils`）：

| 绑定 | 目录（utils_schema/） | 生成物 |
|---|---|---|
| api + app | `{api}/{app}/utils/` | `{api}/src/modules/{app}/utils/{Name}.ts` |
| 仅 api | `{api}/common/utils/` | `{api}/src/modules/common/utils/`（后端公共） |
| 仅 app | `{app}/utils/` | `{app.dir}/src/utils/`（前端） |
| 无绑定 | `common/utils/` 或 `shared/utils/` | `{root}/shared/utils/`（前后端共享） |

## 领域规则三分类

从纯函数视角，方法的出口只有两种：**返回值**或**抛异常**。因此领域规则方法恰好三类：

| 分类 | 命名 | 本质 | result | throws | 失败语义 | 典型示例 |
|---|---|---|---|---|---|---|
| **1 防御 guard** | `assertXX` / `validateXX` / `ensureXX` | 条件抛错（前置校验） | `void` | **必填**（失败契约） | **失败必抛错**，通过无返回值 | `assertCancelable`——已完成/已取消订单 throw `BusinessException` |
| **2 判断** | `canXX` / `isXX` / `hasXX` | 返回判定值（谓词） | `boolean` | 无 | 返回 false，不抛 | `canCancel`、`isExpired`——供 flow `IF(...)` 条件位 |
| **3 计算** | `calcXX` / `formatXX` / 动词 | 派生值计算 | 标量（金额/数量/文案） | 一般不声明 | 一般不抛（入参合法前提） | `calcTotal`——金额计算 |

### 为什么这就是全集

- **判断 = 计算的特例**：boolean/枚举也是值，只是返回值窄化——单独分类是为 flow 谓词位（`IF(canCancel(slots.status))`）的语义清晰，不是本质区别。
- **防御 guard = 判断 + 抛错**：guard 内部必然先判断再 throw，是"判断"失败分支的显式化；通过时没有调用方需要的值，所以返回 `void`。
- **void 方法只能是防御 guard**：纯函数无副作用，不返回也不抛 = 什么都没做——void 方法若默认通过、特殊状态才抛，就是 guard 的语义。

### 命名与返回的一致性（机器校验，已落地）

- `assert/validate/ensure` 前缀 → **必须 void + 必须声明 throws**（guard：通过无值、失败抛契约异常）；
- `can/is/has` 前缀 → **必须 boolean**（判断谓词，进 flow `IF` 条件位）；
- 计算类 → 标量 result。

**双重校验**：

1. **声明侧**（`pylonts lint utils`）：命名与 result 形态不匹配（如 `canXX` 返回 void、`assertXX` 返回 boolean）即违规报错；防御 guard 缺 throws 声明（失败契约缺失）同样违规。
2. **使用侧**（flow 编译期，`defineFlow` 校验）：条件位谓词必须声明 boolean result——`IF(invoke(xxUtils.canCancel, ...))` 通过；`IF(invoke(xxUtils.assertCancelable, ...))`（void 守卫）和标量方法进 IF 直接报错，提示"守卫应 invoke 而非 IF"。

**逃逸集联动**：`invoke` 防御 guard 时，guard 的 throws 自动并入流程逃逸集（与 dao/third 的 throws 同规则）——调用方 service 方法契约因此必须声明该异常（或 TRY 捕获），守卫的失败成为可验证的契约，而不是隐式冒泡。

## 建议（best practices）

1. **命名即归属**：utils 名与表名一致 = 该实体领域规则集（`OrderUtils` ↔ `order`）。业务无关的通用工具（`AmtUtils`/`DateTimeUtils`）不绑定表，放共享目录。
2. **规则二分定性**（既定决策）：**不查库 → utils 谓词**（纯函数）；**查库 → service 断言方法**（`invoke(dao.findByName) → IF(isNotNull) → THROW`）；**硬不变量 → 表约束**（TableSchema unique/FK，DB 物理兜底）。不建 RuleSchema——规则已有 schema：表/谓词/守卫。
3. **失败抛错，不返回错误码结构**：校验失败 `throw new Error('ORDER_CANNOT_CANCEL: ...')` 或业务异常类；flow 的 TRY-CATCH 按异常名捕获。
4. **复用判据**：规则被 ≥2 处使用才提取为 utils；单处使用留在 flow 守卫片段（"三行相似胜过过早抽象"）。
5. **纯函数、无 I/O**：utils 不做数据库/网络访问；需要 I/O 的规则属于 service。
6. **表列引用用 `dtoField(table.columns.x)` 包装**：包装层反写安全，共享列实例永不被动；参数名可以与列名不同（如 `state` 包装 `status` 列）。
7. **result 选型与命名一致**：防御 guard → **`void`** + `assert/validate/ensure` 前缀（抛错即语义）；判断 → `booleanField()`（或枚举）+ `can/is/has` 前缀（供 flow 谓词位）；计算 → `decimalField`/`intField`/`stringField` 标量。
8. **flow 集成**：判断类可直接作 `IF(cond)` 条件位谓词（多入参靠 invoke 多槽位），`gen service --flow` 渲染为 `OrderUtils.canCancel(body.status)`；防御 guard 通过 `invoke` 在流程前置位调用，渲染为 `OrderUtils.assertCancelable(body.status);`（void、内部 throw）。**计算类绑定标量槽**：flow 声明 `slots: { total: decimalField(...) }`，`invoke(calcTotal, args, slots.total)` 渲染 `const total: string = OrderUtils.calcTotal(body.unitPrice);`，标量槽可直接进守卫比较（`IF(gt(slots.total, 100))` → `if (Number(total) > 100)`）或作后续调用的标量参数（jsType 匹配直传）。

## 生成与合并

`pylonts gen utils`：生成 `export class OrderUtils { static ... }` 骨架（每方法 `Not implemented` throw），签名由 schema 推导（enum → 枚举类型名 + 自动 import；date/datetime → string；无 result → `void`）。已存在文件**方法级 MERGE**（用户填充的方法体保留、私有 helper 不碰、契约删方法移除、新方法追加 stub）；`--force` 整文件覆盖。覆盖规范见 [gen-utils-overwrite.md](../../docs/gen-utils-overwrite.md)。

## 校验

`pylonts lint utils`：存储位置 + 绑定共享实例 + 文件名 + **命名↔result 一致性**（assert/validate/ensure → void，can/is/has → boolean）机器校验（`lint all` 已包含）。使用侧谓词 boolean 校验在 flow 编译期（`defineFlow`）执行。
