# Project Agents.md Guide

This is a [MoonBit](https://docs.moonbitlang.com) project.

You can browse and install extra skills here:
<https://github.com/moonbitlang/skills>

## Project Structure

- MoonBit packages are organized per directory; each directory contains a
  `moon.pkg` file listing its dependencies. Each package has its files and
  blackbox test files (ending in `_test.mbt`) and whitebox test files (ending in
  `_wbtest.mbt`).

- In the toplevel directory, there is a `moon.mod` file listing module
  metadata.

## 传输层分层（`transport/`）

- `transport/` 只放跨 target 的东西：配置、metadata 组装、gRPC status。
  模块 `preferred_target = "wasm"`，这层在 wasm / wasm-gc / js / native 下
  都必须编得过，所以**不要**在这里 import `moonbitstack/moonrpc/net`
  或任何 native-only 的包。
- `transport/native/` 放真连 socket 的实现，`moon.pkg` 里声明
  `supported_targets = "native"`。新开的 native-only 包照此办理。
- 根包 `milvus_client.mbt` 用 `pub type` + 转发函数暴露公共 API，
  不直接暴露 `@transport` 的内部结构。
- 细节与理由见 `docs/DEVELOPMENT.md`。

## Development Environment

- CNB 云原生开发：在 `.cnb.yml` 的 `vscode` 事件中声明，与 CI 共用同一基础镜像
  `.cnb/Dockerfile`（基于 `cnbcool/default-dev-env`，MoonBit 工具链走中国站
  `cli.moonbitlang.cn`）。

## CI

- CNB 流水线：`.cnb.yml`，`push` / `pull_request` 跑两条 pipeline：
  - `check-and-test` —— `moon fmt`、`moon info`、`moon check --target all`、
    `moon test --target all`；
  - `integration` —— 起 Milvus 容器、跑 `cmd/integration`、收尾停容器。
- GitHub Actions：`.github/workflows/check.yml`（三平台 `build` + ubuntu 上的
  `integration`），`.github/workflows/publish.yml`（手动触发发布到 mooncakes.io）。
- 工具链下载源：CNB 侧统一 `cli.moonbitlang.cn`，GitHub 侧统一 `cli.moonbitlang.com`。
- 本地等价命令：`moon check --target all && moon test --target all`。

## 集成测试与 Milvus 容器（`scripts/`）

连真实服务端的那条链路：`scripts/milvus-start.sh` 起容器 →
`moon run --target native cmd/integration -- 127.0.0.1:19530` → `scripts/milvus-stop.sh` 收容器。

- 镜像锚定 **`docker.io/milvusdb/milvus:v3.0.2`**，容器参数照搬上游
  `milvus-io/milvus` 的 `scripts/standalone_embed.sh`：embedded etcd +
  `COMMON_STORAGETYPE=local`，一个容器自足。**不用 `latest`**，CI 要可复现。
  换版本时同步改 `.cnb.yml` 的 `MILVUS_IMAGE` 与本节。
- 就绪判据是容器内 `9091/healthz` 返回 200，而不是自己拨 19530 ——
  端口在数据面起来之前就监听了。
  **但判据要从宿主探，别读 docker 的 health 状态**：Moby 的重试一旦耗尽，
  `unhealthy` 是终态，后面服务真起来了也不会翻回 `healthy`。Milvus 冷启动
  在 CI 上耗时不定，固定 `start-period`/`retries` 迟早被穿破，一破就永久卡死
  （#33 的 CI 就是这么红了一整轮）。
- embedded etcd 的两份配置（`embedEtcd.yaml` / `user.yaml`）用 **`docker cp` 送进容器**，
  不要用 `-v` 挂单文件。CNB 的 `services: - docker` 是 dind，daemon 在另一个容器里，
  看不到本任务 `/tmp` 下的文件；挂载源在 daemon 侧不存在时，Docker 会在**目标路径建同名
  空目录**，于是 `/milvus/configs/embedEtcd.yaml` 变成目录。Milvus `v3.0.2` 的
  `InitEtcdServer` 在 `embed.ConfigFromFile` 失败时只记 `initError` 不返回，紧接着
  `cfg.Dir = dataDir` 解引用 nil 直接 SIGSEGV（`pkg/util/etcd/etcd_server.go:49`）——
  #33 的 CI 第一轮就是这么炸的。`docker cp` 走 daemon API 传 tar，跟 daemon 在不在
  同一文件系统无关。同理，别指望 `mktemp -d` 出的路径能被 daemon 挂进去。
- 容器配置顺序是 `create` → `cp` → `start`：Milvus 启动即读配置文件，先 `start`
  再 `cp` 会读到不存在的路径。落位后脚本会把文件 cp 回来比一次大小，配错时给明确原因，
  而不是甩一段 panic。
- CNB 侧收尾放 `endStages`（`stages` 成功失败都跑），GitHub 侧用 `if: always()`。
  stop 是幂等的，容器没起来时也只打个跳过。
- `cmd/integration` 是 native-only 的自检程序，与 `cmd/main`（只管传输层）分工：
  它走完整门面，验「建集合 → 写入 → 检索 → 查询 → 清理」在真服务端上成立，
  失败以非 0 退出码结束。
- **顺序是硬要求**：`create_collection` → `create_index` → `load_collection`
  （轮询到 `Loaded`）→ `search` / `query`。少一步的报错不同：没索引就 load
  是 `index not found`（`IndexNotExist`），建了不 load 就检索是
  `collection not loaded`（`CollectionNotLoaded`）。#33 的 CI 两种都踩过。
- 集成测试用 `new_flat_index(L2)`：10 行的集合上暴力检索就是最优解，
  也不用等索引构建。
- `Client::load_collection` 返回只表示请求被受理，数据面就绪是异步的，
  所以集成测试接着轮询 `Client::get_load_state` 到 `Loaded` 才检索。
- **`Loaded` 之后写进去的行还要再等一拍才可见**：第 6b 步显式轮询到自检索
  命中为止。不等的话第 7 / 8 步（紧接着写入跑）会稳定拿到 0 命中 / 0 行，
  报错文本与「过滤条件写错了」一模一样，指不到病根。`Loaded` 描述的是
  加载那一刻的状态，不是之后每次写入的结果。
  判据只能拿自己写的列：`auto_id` 的主键是 Snowflake 量级，没有可用起点。
  这条在真 Milvus 上必现，在 mock 上永远看不到（mock 立刻可见）——
  写这类「先写后读」的断言时，mock 通过不代表真服务端通过。

## entity 包

`entity/` 放协议无关的领域类型（`DataType` / `Field` / `CollectionSchema` / 各向量类型），
不 import `proto/milvus/proto/`，也不碰 wire 编解码。数值常量（`DataType::to_int`）与
`milvus.proto.schema` 逐条对齐，改动等于改协议，必须同步改 `entity/datatype_test.mbt`。

BF16 是手写实现，对 float32 做 bit 截断加 round-half-to-even，与上游 `ml_dtypes.bfloat16`
逐位一致；不要为了省事换成有损的近似算法。改这个函数要跑 `entity/bfloat16_test.mbt`。

## 许可与上游同步

本项目是上游 Milvus Go SDK 的移植/改写，Apache-2.0 → Apache-2.0。

- 基线：`milvus-io/milvus` commit `1bcc8cb1`（2026-09-30），`client/`。
  `.proto` 来自 `milvus-io/milvus-proto`，锚定 commit 见 `proto/upstream/PROVENANCE.md`。
- 归属声明：来源仓库 + commit + 改写定性写明在 README「来源与许可」一节。
  归属相关事实变更时，同时更新这一节与 `proto/upstream/PROVENANCE.md`。
- **许可头按 Apache-2.0 §4(b) 的要求给足**（保留 NOTICE、标明改动），
  这是硬性底线。写法有二，取其一即可：
  - 上游许可头（ASF 模板全文，含 `Copyright (c) LF AI & Data Foundation`）；
  - 或一份写明来源仓库 + commit + Apache-2.0 + 上游版权人的头，例如
    `// 移植自 github.com/milvus-io/milvus client/internal/merr (Apache-2.0)`
    加一行 `// Copyright (c) LF AI & Data Foundation`。
- 有对应的上游文件时，**推荐**在许可头里把那两行来源声明也写上 —— 它比模板
  全文多给一个「对着谁读」的坐标。没有对应上游文件时（本项目自己写的
  `*_native` 传输、`transport/*` 的配置与 metadata 组装、根包门面等）就不写，
  免得让读者去找一份并不存在的源文件。
- 现状对得上这条：`client/`（除 `paths.mbt`）与 `index/` 走第一种，
  `errors/` 走第二种，`column/`、`entity/`、`transport/`、`client/native/`
  下未被改写的文件不加来源声明。**新增移植文件时二选一即可**，不必为了
  统一格式去补文件头。
- 商标：不得在文档里把本项目写成 official Milvus SDK。统一措辞是
  "community-driven MoonBit client for Milvus"。

### 上游变更 → 手动评估（风险 R6）

上游 `client/` 是活跃代码，本移植是快照。**不做自动同步**，流程是：

1. 比对上游 `client/` 自基线 commit 以来的改动；
2. 判断每处改动是否落在已移植的包里——**按语义判断，不按目录名机械对照**，
   MoonBit 侧分包与上游并非一一对应；
3. 受影响则在对应包内手动回移，PR 里注明对齐的上游 commit；
4. 需要推进基线时，同步更新 README「来源与许可」、
   `proto/upstream/PROVENANCE.md` 与本文件。

## README 的「快速上手」是被真编译的

`moon fmt` 对 ```moonbit 代码块默认**只排版、不检查**，还会把围栏改写成
```moonbit nocheck —— 带了 `nocheck` 的块连 `moon check` 都跳过。所以
「示例能编过」这句话不靠围栏标记保证，靠 `cmd/quickstart`：

- `cmd/quickstart/quickstart.mbt` 是 README 那段示例的**逐字副本**，
  `supported_targets = "native"`（示例引了 native-only 的 `client/native`，
  挂到根包上整棵依赖图会编不过，所以单开一个包）。
  `moon check --target all` 会走到它，示例里的类型错误、函数名写错都跑不掉。
- `scripts/readme-quickstart-check.sh` 校验两份逐字一致，
  CI 的 `check-and-test` 与 GitHub Actions 都跑。改了一边就得改另一边。
- README 的围栏标记保持 `nocheck` 就行 —— `moon fmt` 每次都会把它改回这个词，
  跟它较劲没有意义。可编译性不走它这条路。

## 文档归属

- **README（`README.mbt.md`）面向使用者**，只写：项目定位、特性与能力边界、
  安装、快速上手、文档导航、贡献方式、许可证。按这个顺序排，读者的阅读路径
  是单向的。
  - **用法示例收在一个「快速上手」里**：一条能跑通的链路把主要 API 串起来即可，
    不要按包铺开成多节、也不要罗列全部 API。逐个函数的签名看 `.mbti`。
  - **特性与能力边界合成一节**，不拆成两节各说一遍。
  - **实现细节不进 README**：各包内部的约定（函数签名与默认值、键集合、
    边界条件、状态枚举的岔路、构造期校验）写在本文件或包内文档里。
    README 只说「有这个能力」和「用起来有什么不同」，不说它是怎么实现的。
  - 不写进度、不写「尚未发布」、不写「某任务已交付」这类状态信息 —— 状态会过期，
    README 不该跟着烂。能力清单跟着 API 走，只有 API 真增删才动它。
  - 不写开发流程与集成测试步骤，那是 `docs/DEVELOPMENT.md` 的事，README 只留链接。
    **具体到连构建命令都别写**：「从源码构建」`moon check && moon test`、改 `.proto`
    后跑 `gen.sh`、连真 Milvus 跑集成测试——这三条已经在 `DEVELOPMENT.md`
    「开发流程」里，README 复述一遍就多一份会漂的副本。**构建命令不是
    「贡献方式」**：贡献一节只留「欢迎提 Issue / PR」加两个指向链接，
    不要把命令清单当贡献流程塞回去。
  - 不写「改代码走 PR」这类贡献流程，那是 `docs/DEVELOPMENT.md` 与本文件的事。
  - **必须有一节「文档导航」**，登记仓库里的每一份文档（`docs/*`、`AGENTS.md`、
    `proto/REPORT.md`、`proto/upstream/PROVENANCE.md`、`LICENSE`、`.githooks/README.md` 等），
    只登记一份，不要把参赛用的 `docs/项目申报书.md` 单拎出来解释豁免 ——
    它不属于这套分工，提它反而把内部约定泄进 README。
    新增文档时必须同步登记，别让导航烂掉。
- **`docs/ROADMAP.md` 放「已规划未实现」**：列进规划但还没做的能力。
  「暂时没做」和「不打算做」是两回事 —— 前者写这里，后者写进同一份文件的
  「不做」一节并给理由，不要散落在 README 里当免责声明。
  不要用「不移植」这种终结性措辞挡掉一个有价值的特性。
- **README 里删掉的东西要有去处**：删一段之前先确认它的内容已经在
  `docs/DEVELOPMENT.md` 或本文件里有落点，别直接丢。
- **`docs/DEVELOPMENT.md` 放过程**：开发流程（构建 / 集成测试 / 生成）、上游
  同步策略、交付范围、验收标准、发布流程、被推翻的方案与理由。任务分解与讨论
  现场在 Issue / PR 里，结论沉淀到这份文档或本文件。
- **Issue / PR 是对话现场，不是档案室**：讨论完把结论写进文档，不要往 Issue 里
  回贴长总结。单个 Issue 只留「问题是什么、怎么解」的短结论，够下一个接手的人
  看懂就行。
- 对外文档统一措辞是 "community-driven MoonBit client for Milvus"，
  别写成 official Milvus SDK（见「许可与上游同步」）。
- **「来源与许可」只写事实，不写方法论**：上游仓库、基线 commit、许可与版权、
  协议来源。不要在里面论证「这是重新实现、不是逐行翻译、也不是 clean-room」
  这类定性 —— 那是给做评审的人看的辩护，读 README 的人用不上。归属与同步策略
  的正文在「许可与上游同步」，README 只留指针。

## Coding convention

- MoonBit code is organized in block style, each block is separated by `///|`,
  the order of each block is irrelevant. In some refactorings, you can process
  block by block independently.

- Try to keep deprecated blocks in file called `deprecated.mbt` in each
  directory.

## Tooling

- `moon fmt` is used to format your code properly.

- `moon ide` provides project navigation helpers like `peek-def`, `outline`, and
  `find-references`. See $moonbit-agent-guide for details.

- `moon info` is used to update the generated interface of the package, each
  package has a generated interface file `.mbti`, it is a brief formal
  description of the package. If nothing in `.mbti` changes, this means your
  change does not bring the visible changes to the external package users, it is
  typically a safe refactoring.

- In the last step, run `moon info && moon fmt` to update the interface and
  format the code. Check the diffs of `.mbti` file to see if the changes are
  expected.

- Run `moon test` to check tests pass. MoonBit supports snapshot testing; when
  changes affect outputs, run `moon test --update` to refresh snapshots.

- Prefer `assert_eq` or `assert_true(pattern is Pattern(...))` for results that
  are stable or very unlikely to change. For snapshot tests that record
  structured debugging output, derive `Debug` and use `debug_inspect`, rather
  than deriving `Show` for debugging. For solid, well-defined results (e.g.
  scientific computations), prefer assertion tests. You can use
  `moon coverage analyze > uncovered.log` to see which parts of your code are
  not covered by tests.

## Protobuf 生成代码（`proto/`）

`proto/` 下的 MoonBit 代码由 `protoc` + `moonbitlang/protoc-gen-mbt` **自动生成**，
**不要手工修改**。改了会在下次生成时被覆盖。

- `proto/upstream/` —— 上游 `milvus-io/milvus-proto` 的 `.proto` 只读快照，
  锚定 commit 见 `proto/upstream/PROVENANCE.md`。同样不手改。
- `proto/trimmed/` —— 裁剪到 P0 子集的 `.proto`。上游变更时需同步。
- `proto/milvus/proto/` —— 生成产物，**已入版本库**，是主模块里的普通包目录。
- `proto/tools/p0test/` —— 手写的 wire 往返测试模板，生成后由脚本叠加进产物。
- `proto/tools/gen.sh` —— 一键生成。`gen.sh upstream` 全量，`gen.sh trimmed` P0 集。
- `proto/REPORT.md` —— 生成器支持度探针报告（含已知缺陷与退路）。

要改生成结果，改 `.proto` 或 `gen.sh` 的参数，然后重新生成、把生成的 diff
一起提交。

### 单模块优先：没有第二个模块，也没有 moon.work

本仓库**只有一个模块**（`moon.mod` 那一份）。协议代码是它的普通包目录
`proto/milvus/proto/`，与 `errors` / `client` 同级，语法上是
`SundayT1998/milvus-client/proto/milvus/proto/<pkg>`。

发布包必须自带被依赖的协议代码。早先的写法是「生成物不入库 + 用 `moon.work`
把生成模块注册成第二个模块（`SundayT1998/proto`）」，本地面板能跑，但那个
`moon.work` 会跟着进发布 zip，里面的成员路径 `proto/gen/trimmed/proto`
在包里并不存在，装的人一解析工作区就挂；何况那个成员模块也不在 registry 上。
**别再引入第二个模块**，也别再加 `moon.work`。

生成后 `gen.sh` 做两件事把它接进主模块：

1. 把产物里的 import 前缀 `SundayT1998/proto/` 改写成
   `SundayT1998/milvus-client/proto/`（生成器只会写自己那个模块的路径）；
2. 从 `proto/tools/*/` 叠加四个 wire 往返测试包。

生成是**可复现**的：CI 每次重跑 `gen.sh` 后跟一句
`git diff --exit-code -- proto/milvus/proto`，有 diff 就红。所以手改生成物
一定会在 CI 上暴露，别手改。

「可复现」的边界要说清：可复现的是**同一套工具链**下的重复生成。
`.cnb/Dockerfile` 装的是 `latest`，工具链升级会同时改变生成器的产物与
`moon fmt` 的排版 —— 这时 diff 不是「有人手改了」，是工具链换底。
对策是显式对齐：重跑 `gen.sh` + 全树 `moon fmt` + `moon info`，把这段
diff 单独成一个 commit 说明来源。详细判据与 10-12 版踩过的两类差异见
`docs/DEVELOPMENT.md`「工具链漂移」一节。

一并记住：本工具链下 **`Int32` 与 `Int` 是同一个 32 位整型**，可互相赋值
与传参。生成器写 `Int32`、`moon info` 统一写回 `Int`，所以 `.mbt` 与
`.mbti` 的拼写不一致属正常，别去「修」它。

`proto/upstream/` 与 `proto/trimmed/` 仍是**源码侧输入**，不参与编译；
只有 `proto/milvus/` 是编译目标。

## `column/` 包

Milvus 响应 `FieldData` → 列容器，对应上游 `client/column/`
（`milvus-io/milvus` commit `1bcc8cb1`，Apache-2.0）。

- `types.mbt` —— `ColumnValue` / `Column`，各列类型（含向量）
- `decode.mbt` —— `from_field_data`，`FieldData` → `Column` 的唯一入口
- `valid_data.mbt` —— 有效性位图的双源读取与行区间归一
- `extract.mbt` / `arrays.mbt` / `vectors.mbt` —— 从生成的 proto 里取数
- `float16.mbt` —— 回读侧的 fp16/bf16 解码（写侧在 `entity/`）
- `access.mbt` / `get_as.mbt` —— 行访问接口

约定：
- 读错类型报 `ColumnError::DataTypeNotMatch`，null 行报 `NullValue`，
  **不做隐式转换**。
- 动态字段（JSON）以 JSON 字符串取回，不做路径查询（不引 `tidwall/gjson`）。
- 生成的 `@schema.DataType` 与 `@entity.DataType` 的对应写在
  `column/datatype.mbt`；新增类型时两边都要动，`from_field_data` 的
  `match` 会提醒你漏了哪一支。

## `errors/` 包

Milvus `common.Status` → MoonBit 错误模型。对应上游 `client/internal/merr/`
（`milvus-io/milvus` commit `1bcc8cb1`，Apache-2.0）。

- `rpc_error.mbt` —— `MerError` suberror，code / legacy_code / detail / retriable / input 五元组
- `code_mapping.mbt` —— 双轨错误码映射表，逐条照搬上游 `modernCodeFromLegacy` / `legacyCodeFromModern`
- `sentinels.mbt` —— 预定义错误值，对应上游 `ErrCollectionNotFound` 等命名变量
- `wrap.mbt` —— `WrapErr*` 包装族，保留 code 身份、只改写文本
- `status.mbt` —— `Status` ↔ `MerError` 双向转换
- `redact.mbt` —— 敏感信息脱敏（上游没有，本项目新增的验收要求）

约定：
- `is_retryable_err` 只看服务端下发的 `Status.retriable`，不做本地猜测，
  与上游 `IsRetryableErr` 一致。retry 归属在 transport 层（#11）。
- `same_code` 是 Go 版 `errors.Is` 的等价物：**比对 code，不比对文本**。
- `MerError?` 上的方法在包外用 UFCS 调用（`@errors.error_code(err)`），
  点调用只对非 Option 的 `MerError` 有效。

## `client/` 包

Milvus 客户端门面与核心 RPC 编排，对应上游 `client/milvusclient/`
（`milvus-io/milvus` commit `1bcc8cb1`，Apache-2.0）。

- `types.mbt` —— `Unary` 函数值、`Client`、`ClientError`、`call_service`、`check_status`
- `consistency.mbt` / `schema_convert.mbt` —— 一致性等级与 schema ↔ proto 的桥
- `collection.mbt` —— CreateCollection / DropCollection / HasCollection /
  DescribeCollection / ListCollections 及其 Option
- `index.mbt` —— CreateIndex / DescribeIndex / DropIndex 及其 Option，
  把 `@index` 装配好的参数塞进请求、响应翻回 `@index.IndexDescription`
- `partition.mbt` —— Create / Drop / Has / ListPartitions 及其 Option
- `load.mbt` —— Load / Release Collection & Partitions、`LoadTask`、`GetLoadState`
- `flush.mbt` —— `Flush`、`FlushTask`、`GetFlushState`
- `write_column.mbt` / `write.mbt` —— `WriteColumn` → `FieldData`，insert / upsert / delete
- `search.mbt` / `query.mbt` —— 占位符编码、search_params、结果反序列化
- `paths.mbt` —— gRPC 方法路径常量
- `client/native/connect.mbt` —— native 专属：把 `Unary` 接到 `@channel.Client::unary`

约定：
- 发字节抽成 `pub type Unary = async (String, Bytes) -> Bytes raise @transport.RpcError`，
  所以 `client/` 在 wasm / js 下也编得过；真连 socket 的只有 `client/native`。
- `ClientError` 是**单一** suberror：`Transport` / `Server` / `Schema` / `Encode` / `Decode`。
  `async fn` 只允许一个 `raise` 类型，多错误组合装不进签名，所以这里是刻意的收敛。
- Milvus 老式 RPC 把错误放在响应体的 `common.Status` 里，而不是 gRPC status。
  每个响应都要过一遍 `check_status`，OK 返回 `None`。
- `ClientError` 的四个分类处置不同：`Transport` 可原样重试，`Server` 看
  `@errors.is_retryable_err`（只看服务端下发的 `retriable`，不做本地猜测）。
- 不缓存集合 schema：insert / upsert 的 `schema_timestamp` 留 0，
  没有上游那套 schema-mismatch 自动重试。这是 R6 里写明的取舍，改它要连带
  补一个 schema 缓存。
- `WriteColumn` 用 `@column.ColumnValue` 而不是另造枚举，让回读的 `Column`
  能改个名字写回去。它推导不出 `Array` 列的元素类型，所以那一支直接报 `Encode`。
- 向量列的**每行宽度要一致**，本地校验（`check_row_width`）。服务端是按
  `dim` 这个列级宽度切值的，看不见客户端的行边界 —— 宽度对不上时它自己
  从总数反推行数，报出来的错（`num_rows (2) ... not equal to passed
  num_rows (1)`）跟「这一行有几列不对」毫无关系。判据是各行等宽，
  **不是等于 `dim`**：查询向量的 `dim` 是冗余的，建表 8 维、按 4 维写合法。
- `search_params` 的键集合与顺序照搬上游 `AnnRequest.searchRequest`：
  固定七个键写全，调用方的 `with_search_param` **就地覆盖**同名的键。
  这条曾经只写在注释里没兑现在代码里（`seen` 把调用方同名键全丢了，
  `with_search_param("topk", ...)` 静默无效），改动前先看 `search_params_of`
  的注释。
- **建索引 → 加载 → 检索的顺序不能倒**：Milvus 拒绝加载没有索引的集合
  （`index not found`），也拒绝对未加载的集合检索（`collection not loaded`）。
  加载是异步的，`load_collection` 返回不代表就绪，调用方要用 `LoadTask::wait`
  或 `get_load_state` 轮询到 `Loaded`。
- **`Loaded` 不等于「刚写的行可查」**：集合是第 4 步加载的，可第 5 步写进去
  的行要到第 5 步之后才进数据面。紧接着写完就检索 / 查询，会拿到 0 命中 /
  0 行，症状与「过滤条件被忽略」完全一样。写完要读就得自己轮询到看得见
  （如 `cmd/integration` 的第 6b 步），别把这次空结果当成表达式写错了。
- 状态枚举都保留生成物的 `Unknown` 岔路：`CollectionLoadState::Unknown` /
  `@index.IndexState::Unknown`。别把它们并进 `NotLoad` / `None`，那会把
  「没见过的状态」当成「什么都没发生」，然后无限等下去。
- `limit` 不预置，与上游 `NewQueryOption` 一致：不传就是服务端默认上限。
  想断言确定行数必须显式 `with_limit`。
- 写集成断言时别拿主键当行号：`auto_id` 发的是 Snowflake ID（18 位量级），
  `id >= 5` 这类条件等于全表通过。要按内容筛就用自己写的字段。
- `query` 的列名来自 `FieldData.field_name`。服务端在返回**全部字段**时
  （`with_output_fields(["*"])`，或调用方一个 `output_fields` 都没给）不填
  `field_name`，只有 `field_id`。这时客户端的 `output_fields` 会被逐个切掉、
  列名全空：`QueryResult::len` 读 `columns[0]` = 0，`column(name)` 也取不到，
  症状酷似「过滤条件被忽略、全量返回」——集成自检踩过。
  契约是：要看列名就必须在 `output_fields` 里逐点名，别用 `*`，也别留空。
- `LoadTask` / `FlushTask` 的 `wait` 照上游 `Await`：先等一个间隔再查第一次，
  默认 200ms。轮询用 `@async.sleep`（`moonbitlang/async` 是 `client` 的真依赖，
  不只是测试依赖），取消翻成 `Code::Cancelled` 的 `Transport` 错误。
- `has_partition` 用真 `HasPartition` RPC（`BoolResponse`），
  `has_collection` 用 `DescribeCollection`——两处不同是上游的选择，别顺手统一。

### 迭代器（`client/iterator.mbt`，对应上游 `iterator.go` / `iterator_option.go`）

- `QueryIterator` 是**客户端侧**游标：`DescribeCollection` 拿主键，之后每批
  把 `pk > last` 拼进 `expr` 往后挪。要求主键是 `Int64` / `VarChar`，
  否则建立时就报 `IteratorError::Setup`。
- `SearchIterator` 是**服务端侧** v2 游标：翻页凭据（`search_iter_id` /
  `search_iter_last_bound`）在响应 `SearchResultData.search_iterator_v2_results`
  上，跟命中一起回来。所以取数与挪游标在 `SearchIterator::fetch` 里一趟做完
  —— 分开做中间会有凭据是旧的窗口。
- 搜索迭代器的响应要拿整个 `SearchResultData`，`SearchResult` 装不下凭据，
  所以 `Client::search_call` 是共用的编排（校验 / 占位符 / 七个键 / status），
  `search` 与 `search_raw` 都从它过。改 `search_params` 的键集合只改这一处。
- 收尾走 `Client::cancel_search`：带续页凭据再发一发检索，
  `search_input` 用**一个真实维度的占位符**、`topk = 1`。
  **不能**发 `nq = 0` + `NotSet` —— v3.0.2 会拒（`nq [0] is invalid`），
  上游 v2.6 才收。走不了 `Client::search`：那条路要求至少一个查询向量、
  还会用 `search_params_of` 覆盖整套键。
- `SearchIterator` 走到末尾会自己发一次收尾请求。服务端侧游标不会自己过期，
  「不泄漏服务端资源」是 #18 的验收标准之一。
- `limit` 的判定是 `remaining <= 0` 而不是 `== 0`，而且越界的那一批**要切短**
  （`keep_within_limit`，两条迭代器都有）。原因写在 `iterator.mbt` 文件头
  第 2 条：`remaining` 按整批扣减，非整数倍上限会跨过零点，等号判定等于
  没有上限。改判定或改截断之前先读那段注释。
- 一处与上游刻意的差异，写在 `iterator.mbt` 文件头：nq 恒为 1（多给报错）。
- 游标状态（`last` / `cursor` / `remaining`）在 `pub struct` 里标了 `priv`，
  外部只能通过 `next` / `close` / `is_closed` 操作。

`entity/` 的 float16 写侧（`float16_from_float` / `float16_vector_bytes`）与
`column/float16.mbt` 的读侧是一对，逐位对齐 IEEE-754 binary16。
改任一侧都要跑 `entity/float16_test.mbt`（期望值由 Python `struct.pack('<e')` 生成）。
