# 框架规则(`docs/FRAMEWORK_RULES.md`) > **这份文档给两类读者看:** > - **改这个仓库的 AI**:开工前先读,逐条遵守。这些不是风格建议,是**硬规矩** —— > 违反它们的改动会在真机上以"不报错但结果是错的"方式失败,而这正是本仓库 > 花最多代价在防的事。 > - **review 人**:每条例规都有编号(`R1`…),可以直接引用,例如 > "这条违反 R7.2"。 > > 配套:`.cursor/rules/*.mdc` 是给 Cursor 的常驻规则(自动注入),内容是本文件的 > 压缩版;本文件是**唯一权威**,两者冲突时以本文件为准。 > > 规则是可以改的 —— 但必须改这里,而不是在代码里偷偷绕开。改规则时写清**为什么**。 --- ## 0. 三条元规则 其余所有规则都是从这三条推出来的。拿不准某件事该怎么做时,回到这三条。 ### M1. 静默失效是头号敌人 本仓库遇到的大部分问题都不报错:**话题在、图像有、TF 查得到、日志干净,结果是错的。** 所以: - 任何"检查 / 校验 / 验收"都必须**能真的失败**。写完先问一句: "我故意把它弄坏,这条检查会红吗?" 不会红 = 这条检查等于没有(见 `R5.2`)。 - 宁可**当场报错**,也不要"宽容降级"。降级会把"没检查"伪装成"检查通过"。 真实案例:位姿解析函数格式对不上,于是永远返回空、调用方按设计"跳过位姿核对", 结果位姿检查从来没跑过,而日志只说"跳过"(见 `R5.4`)。 - 校验代码自己也会静默失效。凡是"检查 X 是否为空"的地方,都要问: "它会不会被填成一个**假值**?"(真实案例:`sdformat` 用 `__default__` / `__default_topic__` 填缺失字段,见 `R4.6`) ### M2. 单一数据源 同一个事实**只允许写一处**,其余全部派生。 - 能算出来的不要抄(内参由视场与图幅算、相机位姿从 world 文件解析)。 - 必须抄的(如仿真侧要提前知道像素门槛)**必须有人守着**: 用脚本交叉核对真机配置,不一致就判失败(`R2.6`)。 - 同一份事实出现两处时,一定有一处会先过期。过期的那处不会报错。 ### M3. 口径必须显式 **单位、坐标系、符号、参考基准**必须写下来,并且**只在一个地方换算**。 - 毫米还是米?相对零还是相对基线?压下去为正还是为负? 这些都不写清就会"数值看起来合理、语义是错的"。 - 换算只做一次,放在适配层(相机适配器、触觉适配器),下游不许再猜。 --- ## 1. 配置与数据 ### R1.1 分层优先级固定:命令行 > `site.yaml` > 包内默认 `config/site.yaml` 是**站点数据**(SN/IP/相机序列号/锁高/权重路径),**不进 git**; 包内 `config/*.yaml` 是默认值,被 site 覆盖。 **禁止**在代码里硬编码站点值。真实反例:15 处硬编码的 `Rizon4-063989` (见 `R8` 的清单),换一台臂就会静默连错机器。 ### R1.2 新增 `site.yaml` 键必须二选一:**接线** 或 **明确告警** 声明了却没接线的键是最难查的一类问题:用户按文档改了 `site.yaml`,行为毫无变化, 也不报错。所以每个新键都要么真的接线,要么在启动时**告警**(`wired: false` + `unwired_warnings()` 那套做法)。 > ⚠️ 现状:`wired:` 目前只是 `manifest.yaml` 里的**注释性约定**, > `arm_interfaces.manifest` 并不解析它。所以"未接线"的保障实际来自 > `sim_site.unwired_warnings()` 这类**代码里的告警**,不是清单。 > 加键时不要指望清单替你守法。 ### R1.3 设备一律走 `devices///`,并带 `manifest.yaml` ``` devices/// ├── manifest.yaml # id/label/kind/vendor/version + assets + driver + capabilities + models + config_schema ├── driver/ # 与厂商 SDK 对接的实现(可替换) └── models/<型号>/ # 该品牌的型号专属文件 ``` - `kind` 用固定的那几个(`robots` / `grippers` / `cameras` / `sensors` / `sim`), 目录名要与 `arm_interfaces.KIND_FOLDERS` 一致。 - **换品牌 = 新增一个目录**,不是改现有目录。上层按 `manifest.yaml` 选型, 不认具体品牌名。 - `assets[].required: true` 的物料缺失时,框架会报错并给出**格式与命名要求** (不只是"文件不存在")。 ### R1.4 权重一律走 `models/`,规则在 `models/README.md` - 二进制(`.pt/.onnx/.engine`)**不进 git**;`.labels.yaml`(文本标签)**要进**。 - 转换链:`models/staging/` 放待转换的原始文件 → `weights.sh convert` → 自动改名归位到 `models/<分组>/` 并固化标签。 - `models/**/best.pt` 这类**悬空引用**(配置指向不存在的文件)必须修掉或改成实际存在的, 查法:`weights.sh doctor`。 ### R1.5 什么入库、什么不入库 | 入库 | 不入库 | 理由 | |------|--------|------| | `src/` `scripts/` `docs/` `devices/` `config/site.example.yaml` | `config/site.yaml` | 站点数据含本机 IP/序列号 | | `models/README.md`、`models/**/*.labels.yaml` | `models/**/*.{pt,onnx,engine}` 等 | 二进制体积大、每台机器重转 | | `handeye/poses/` `handeye/results/` | `handeye/tmp/` | 示教点与标定结果是用真机时间换来的**资产**,要能回滚 | `config/site.yaml` 改了以后**必须**重跑 `python3 scripts/gen_site_example.py`, 否则模板会漂移(有一条 parity 测试会红)。 --- ## 2. 几何、单位与坐标系 ### R2.1 单位一律米与弧度 SDF / URDF / 几何计算里不允许出现毫米。用毫米建模的 mesh 必须先缩放。 **例外且必须显式**:真机 RealSense 的深度图是 `16UC1` **毫米**(见 `R2.3`)。 ### R2.2 相机的 `frame_id` 必须是**光学系** ROS 的 `image` / `camera_info` 必须用光学约定(Z 朝前 / X 朝右 / Y 朝下,REP-103/105), 不能用相机本体系(X 朝前 / Y 朝左 / Z 朝上)。两者差 `rpy=(-90°, 0, -90°)`。 盖错 frame 的后果:`tf2` 查询**能查到、不报错**,但反投影出的三维点整体转错, 而单目预览完全看不出来。 ### R2.3 深度:真机是毫米,Gazebo 是米 —— 必须真换算 | 来源 | 编码 | 单位 | |------|------|------| | 真机 RealSense aligned_depth | `16UC1` | 毫米,`0` = 无读数 | | Gazebo 深度相机 | `32FC1` | 米 | 直接改个名字转发,应用侧按毫米解释会整体差 1000 倍。换算只做一次, 放在 `sim_camera_adapter`。 ### R2.4 `rpy` 的复合顺序是 `Rz·Ry·Rx` 不是 `Ry·Rz·Rx`。单轴旋转看不出来,多轴组合才暴露。 **URDF 相机的视线是本体 X 轴**("X 朝前"约定)。所以"先俯 50° 再偏航 180°" 会把视线翻到**背面**。D435 就这么翻过:背对台面只看得见地面,什么都不报, **而且几何自洽**(点确实落在真实地面上)。 ### R2.5 Gazebo 发的 `camera_info` 不可信 Fortress 恒发 `fx=fy=277, cx=160, cy=120`(它的默认 60° 视场 + 半分辨率主点), 与真正渲染用的视场无关。内参必须由 `cameras.yaml` 的 `horizontal_fov` 与图幅**算出**, 并覆盖 gz 发的那份。 ### R2.6 像素门槛与**图幅**绑定 `hole_detect.yaml` 的门槛(`hough_min_radius`、`hole_distance_min`)是**像素**量, 只在特定分辨率下有意义。改图幅要同时改三处(`cameras.yaml`、SDF、 必要时重算真机门槛),启动时会比对并告警。 ### R2.7 位姿检查要有容差,且容差要说明理由 物体的落座微动是正常的。卡到 `1e-6` 只会得到永远为真的告警,人就会把检查关掉。 默认 5mm 起步(见 `sim_scene.pose_mismatches`)。 --- ## 3. 触觉与力 ### R3.1 力的语义是"相对静止基线的增量" 真机 Orisys 输出的是形变任意单位(非牛顿),且基线会漂移,所以全链路 (含下游阈值)都以**相对基线的增量**为语义。仿真必须做同样的事: 不减基线,下游阈值就对不上。 基线要慢跟踪(只在确认无接触时更新),否则换姿态后的残余会被当成接触。 ### R3.2 `fnormal` 压下去为正,且 clamp 到非负 两指符号一致。这条是**实测**结论(`config/tactile.yaml` 里有实测记录), 不是从 URDF 推导出来的。 ### R3.3 轴向映射必须实测,不许推导 哪个轴是"指腹法向"要用实验确定(夹一块大平面薄板等稳态)。猜错不会报错, 只会让所有力的语义静默错位。实测方法与结果记在 `config/tactile.yaml`。 用小方块做这个实验是错的:点/边接触会让法向偏、摩擦混进来。 ### R3.4 接触布尔必须用**超时**判据 Gazebo 的接触消息是**事件式**的:接触时发,离开时**一条都不发**。 写成"收到=接触 / 没收到=不管",离开工件后 `contact` 会永远停在 `True`。 判据 = "最近 `contact_timeout_s` 内收到过消息"。 ### R3.5 仿真与真机的力的量级不同,`force_scale` 是必须确认的口径 仿真输出 = Gazebo 牛顿 × `force_scale`。`force_scale = 1.0` 时输出牛顿, **不能**直接套用真机那套阈值(250/600)。位置控制器是"刚性地压进去"的, 实测能到几百。 选错口径不报任何错:阈值只是"不触发"或"一碰就炸",看起来像算法问题。 ### R3.6 视触觉的**图像**在仿真里只能是占位 仿真没有弹性体成像,深度图/力矢量图只能是空/NaN。话题与消息结构要保留 (下游不用改代码),但要打一次明确日志说明它不是真实数据。 --- ## 4. 仿真(Gazebo) ### R4.1 仿真与真机**共用同一套 `site.yaml` 参数** 任何"仿真专用参数"都是未来"仿真里调好的值搬到真机不生效"的隐患。 只允许**物理引擎自身的**参数(重力、步长)放在 `sim.*` 下。 ### R4.2 `world` 里声明任何一个 `` 都会挤掉默认系统 声明了任何插件后,Gazebo **不再**加载默认的 Physics / UserCommands / SceneBroadcaster。 少了 UserCommands 的后果最阴:`/world/<名>/create` 服务不存在 → 模型根本没进 Gazebo, 而 `create` **仍然打印** `OK creation of entity`。 所以这些系统必须在 world 里**显式声明**(含 `contact-system`、`forcetorque-system`)。 ### R4.3 插件与资源的搜索路径必须在 `ros2 launch` **之前**设好 conda 激活不会往 `LD_LIBRARY_PATH` 加东西,而 gz 的插件搜索路径需要 `$CONDA_PREFIX/lib`。症状是 `couldn't find shared library` 而库其实就在那儿。 **不要绕过 `scripts/sim_env.sh` 直接 `ros2 launch`。** ### R4.4 上游描述(`flexiv_description` 等)**不许直接改** 第三方包一律在**启动时归一化**,不在上游文件里下手: - 缺 `` / 零质量 link → NaN 惯性、物体消失 - `` 标签被 `sdformat` **丢弃** → 夹爪"控制器 active 但手指不动" - 无质量的根 link(world link)会**连整棵子树被静默丢掉** 改上游还有个额外代价:下次 `fetch_flexiv.sh` 会把你的改动覆盖掉。 ### R4.5 场景物体走 `scene.yaml`,不进 world `world` 是"环境"(地面/台面/相机/系统插件),`scene.yaml` 是"这一批活做什么"。 动 world 的风险面太大(`R4.2`)。 模型约定:**原点在底面中心**、单位米、目录名 = `` = `model.config` 里的 `` = 推理类别名。 ### R4.6 模型/场景校验必须逐**碰撞体**、三维判 - 不能拿总包围盒判重叠:正常装配里本来就有嵌套(规立在规座方孔里), 粗判会把正常嵌套报成碰撞,逼着人把检查关掉。 - 恰好贴合不算重叠(规的台肩正好坐在规座顶面上)。 - 变换必须用"中心 + 半宽":对 min/max 取绝对值再对称化会把偏移量吃掉、 盒子凭空变大。 - 校验要用**从 SDF 解析出来的**几何,不能在 `scene.yaml` 里再抄一份尺寸。 ### R4.7 建模:碰撞体用简单几何,视觉可以加"只给算法看"的细节 `` 决定物理,用 box/cylinder;`` 决定相机看见什么, 可以加亮环、色块这类只服务于检测的东西。 工件的碰撞体是方孔、视觉是圆环,**这是刻意的**:真机的 HoughCircles 靠亮环找孔, 只按碰撞体建视觉会让孔检测在仿真里永远失败,而那看起来像算法问题。 > 由此产生的约定:`visual` 的**名字有意义**(`insert_*_ring` / `center_bore`), > `sim_scene.features_from_workpiece` 按名字前缀读特征。改名字要同步改那个函数。 ### R4.8 夹爪/被夹物体的接触刚度要按"位置控制"的实际行为调 位置控制器会"刚性地压进去",默认刚性接触会把物体弹飞。需要放软时要**写明理由** (照抄现成参数前先想清楚你的物体是不是也要被夹)。 ### R4.9 不用"有图像 / 有话题 / TF 查得到"验收 这些只能证明"链路通"。验收必须是对**物理量**的断言: | 工具 | 判据 | |------|------| | `scripts/check_sim_cameras.py` | 深度反投影必须命中已知平面(地面 z=0 / 台面 z=0.40) | | `scripts/check_sim_scene.py` | 摆放自洽 + 像素门槛与真机一致 + 成像几何达标 + 手外相机**完整**看得见工件 | ### R4.10 停仿真用 `scripts/stop_sim_gz.sh`,不要 `pkill -f` `pkill -f` 会先匹配到**调用它的那个 shell**(它的命令行里含同样的字符串), 表现是"命令秒返回、什么都没停"。脚本按进程树精确识别,并在杀完之后**复查残留**。 --- ## 5. 代码与测试 ### R5.1 纯逻辑要抽成纯函数,才可能被钉住 把"数据变换"从 ROS 节点里抽出来(`sim_cameras`、`sim_tactile`、`sim_scene`), 因为静默失效点的错误**不会在运行时暴露**:轴向映射反了照样"有力、会响应、 松开归零"。抽出来才有单测可写。 ### R5.2 每条"检查/校验"都要有能证明它有效的测试 尤其是**负向**测试:故意造出坏数据,断言检查会报错。 只测"好数据通过"的测试,无法区分"检查有效"和"检查恒真"。 ### R5.3 不许把"降级跳过"写成"通过" 调用方必须把降级当作**告警**打印出来并说明原因。真实反例见 `R5.4`。 ### R5.4 解析外部输出:先确认真实格式,再写解析器 外部工具(`ign`、`sdformat`、厂商 SDK)的文本输出**没有稳定契约**, 而且格式与直觉不一致是常态。 先用真实输出跑一遍看清楚了再写解析;解析失败要返回"未知"而不是"零"。 真实反例:按 `x: N` 键值对写解析器,而 Fortress 实际输出是方括号两行, 于是位姿检查从来没跑过。 ### R5.5 删除 / 重构前先做全仓库引用扫描 不靠印象。反例:一个 `assets_root()` 函数指向从未存在的 `assets/` 目录、 零调用者,长期没人发现(见 `R8`)。 ### R5.6 不要在中文路径下 `colcon build` ROS 构建对非 ASCII 路径会出各种问题。工作目录保持 ASCII(推荐 `~/ros2`)。 ### R5.7 真机与仿真的构建产物必须隔离 `build/ install/ log/` 是真机;`build_sim/ install_sim/ log_sim/` 是仿真 (`scripts/build_sim.sh`)。两套并存是刻意的:同一份源码,两个环境的依赖路径不同, 共用 `install/` 会互相覆盖。 --- ## 6. 文档与提交 ### R6.1 文档与实现不一致时:以实现为准,**立刻**修文档 文档里"已就绪 / 未接线"必须**如实**标注。看到 ✘ 不要凭印象改成 ✔。 ### R6.2 每份文档要有"读法"和"这份文档记录什么事实"的说明 本仓库的文档面向"明天就要动手的人",所以要写清:先读哪节、哪些是硬事实、 哪些是踩过的坑、怎么验证。 ### R6.3 结论性数字必须能复现 写进文档的性能/几何数字(如"台面命中 11.9%")要么给出命令,要么给出脚本名。 ### R6.4 提交信息:中文,讲**为什么**,附实测验收数据 格式:`<阶段号> <一句话结果>` + 正文说明"问题是什么、为什么这么改、怎么验证的"。 修静默失效时,把"症状"写进去 —— 那是后来人搜索时唯一能匹配上的东西。 ### R6.5 注释、docstring、提示信息一律用中文 现场操作的人读中文。所以本仓库(`src/arm_*/**`、`scripts/**`)里: - **注释、docstring、`print`/日志/异常里给人看的句子 = 中文** - **标识符、API 名、话题名、参数名、字段名、文件名、命令名 = 保留英文原文**, 写在中文句子里时用双反引号包起来,例如 ``SITE_ROS2_ROOT``、``devices///`` - 单位、十六进制、协议报文、CRC 值、寄存器地址、命令示例**原样保留** **例外:`src/flexiv_*/` 是厂商上游代码,不许改、也不许翻译**(见 R4.4)。 只改文本的提交,必须能自证"没碰到逻辑"。做法:把新旧两份源码都解析成 AST, 删掉 docstring、把字符串常量抹平、把 f-string 只留"插值了几次 + 插值是什么", 再比对 —— 必须完全相同。这条检查会抓出"顺手改名""顺手调整语句顺序""漏掉一个 f-string 插值"这类问题;而"为了中文语序把两个插值换个显示顺序"属于允许的改动。 工具:`python3 scripts/check_text_only.py`(基准是 `git HEAD`,所以要在提交前跑)。 ### R6.6 行尾一律 LF;验脚本要**真跑**,不能只看 `bash -n` 行尾由 `.gitattributes` 兜住(`* text=auto eol=lf`,厂商上游 `-text` 不动)。 为什么专门立一条:`scripts/run_hole_detect.sh` 和 `scripts/run_dual_gripper.sh` 曾经是 CRLF,于是 bash 把行尾的 `\r` 当成标识符的一部分, `case "$1" in\r`、`set -eo pipefail\r` 全部变成非法记号 —— **文件在、五处文档说能用、命令一敲就死**,而报错信息里的 `\r` 根本看不出来。 更坑的是:`bash -n` 对其中一个还返回 OK,实际执行立刻死在第 6 行。 所以"静态检查通过"不等于"脚本能用": - 验语法:`bash -n`(必要但不充分,CRLF 会漏报) - 验能用:`bash <脚本> --help` - 验行尾:`file <文件>`,或看 `.gitattributes` 有没有覆盖到 ### R6.7 验收命令必须是**真跑过**的 写进文档的任何命令,都要当场执行一遍再写。理由是这个仓库已经踩过两次: - `run_hole_detect.sh` / `run_dual_gripper.sh` 是 CRLF,五处文档让用户跑, 一敲就死(R6.6); - `bash -n` 对其中一个还报 OK。 推论: - 文档里的命令**复制粘贴要能跑**,别凭记忆写; - 外链先确认目标文件存在(改完文档跑一次链接检查); - 文档说"已就绪"的能力,要能指出**用哪条命令**证明它已就绪(R6.1)。 指不出来的,就老实标 ❌。 ### R6.8 真机与仿真的 `install/` 产物会互相污染(已发现,待修) `colcon build` 会把**当时激活的那个 python 的绝对路径**写进每个 console script 的 shebang。于是: ``` $ head -1 install/arm_interfaces/lib/arm_interfaces/device_check #!/home/cc/anaconda3/envs/ros_gz_sim/bin/python3.11 ← 仿真环境的 python ``` `.gitattributes` 管不到这件事,`install/` 也不入库,所以它只能靠**规矩**和**检查**: ```bash # 真机 install/ 里有多少入口脚本的 shebang 指向仿真环境(正常应当是 0) grep -l ros_gz_sim install/*/lib/*/* | wc -l ``` 当前实测:`install/` 是 **31 / 59**,`install_sim/` 是 31 / 55(后者是对的)。 即真机产物是在仿真 conda 环境里构建出来的,违反 R5.7。 **为什么现在没炸**:两个 conda 环境的 Python 依赖当前**完全相同** (`pyrealsense2` / `serial` / `cv2` / `numpy` / `torch` / `ultralytics` / `tensorrt` 都在), 所以指错了也照样 import 成功。这是一颗**哑弹**:哪天某个环境多装/少装一个包, 真机栈就会以"模块找不到"的形式炸,而人是不会想到去查 shebang 的。 **修法**:在真机 conda 环境里重建 `install/`: ```bash source scripts/env.sh # 确认 python3 指向 ros_flexiv ./scripts/build.sh # 只重建 arm_* 与 flexiv_description grep -l ros_gz_sim install/*/lib/*/* | wc -l # 应当变成 0 ``` **预防**:构建脚本开头应当**断言**当前 conda 环境与目标一致(待补)。 --- ## 7. 改动前的检查清单 改任何东西之前/之后,逐条过一遍: | # | 检查 | 对应规则 | |---|------|----------| | 1 | 我要改的这个事实,仓库里有几处在写?其余的是派生的吗? | M2 | | 2 | 我加/改的配置键,真的接线了吗?没接线会告警吗? | R1.2 | | 3 | 涉及单位/坐标系/符号吗?换算只做了一次吗?写下来了吗? | M3、R2 | | 4 | 我加的检查,故意弄坏数据它会红吗?有负向测试吗? | M1、R5.2 | | 5 | 解析外部输出前,我看过真实输出了吗? | R5.4 | | 6 | 我改的是上游第三方文件吗?(应该走归一化/适配层) | R4.4 | | 7 | 改完跑了验收工具吗(`check_sim_cameras` / `check_sim_scene`)? | R4.9 | | 8 | 文档里的状态标注与实现还一致吗? | R6.1 | | 9 | 提交信息里有"为什么"和实测数据吗? | R6.4 | | 10 | 我写进文档的命令,自己跑过一遍吗? | R6.7 | | 11 | 这次构建是在**目标环境**里做的吗?(`head -1 install/*/lib/*/*`) | R6.8、R5.7 | --- ## 8. 历史反例清单(别再犯) 按"症状"排序 —— 因为出问题时你是先看到症状,再回来查这张表。 | 症状 | 真实原因 | 规则 | |------|----------|------| | 三维点整体偏半米,但单目预览正常 | 用了 gz 发的 `camera_info`(恒为 60° 视场的默认值) | R2.5 | | 深度差 1000 倍 / `frombuffer` 读出垃圾 | 把 gz 的 `32FC1` 米直接当毫米用 | R2.3 | | 反投影的点转到别处,`tf2` 却能查到 | `frame_id` 盖成了相机本体系 | R2.2 | | 相机背对目标,只看得见地面 | `rpy` 复合顺序理解错 / 视线是本体 X 轴 | R2.4 | | 模型"根本没进 Gazebo",但 `create` 打印成功 | world 里声明了插件,挤掉了 UserCommands | R4.2 | | `gz_ros2_control` 报找不到共享库,但 `ldd` 全齐 | 插件搜索路径没含 `$CONDA_PREFIX/lib` | R4.3 | | 物体一 spawn 就消失 | 缺 `` / 零质量 link → 惯性 NaN | R4.4 | | 夹爪控制器 active 但手指不动 | `` 被 `sdformat` 丢弃 | R4.4 | | 世界相机与它的话题一起消失 | 挂在无质量的 world link 上,整棵子树被丢 | R4.4 | | `contact` 离开工件后永远是 `True` | 接触消息是事件式的,没有"离开"事件 | R3.4 | | 力的语义全错,但"有力、会响应、松开归零" | 轴向映射猜的,没实测 | R3.3 | | 明明夹住了,`fnormal` 恒为 0 | 基线把真实接触吃掉了(基线更新条件写错) | R3.1 | | 阈值永不触发或一碰就炸 | 仿真牛顿与真机任意单位混用(`force_scale` 没标定) | R3.5 | | 位姿检查从来没跑过,日志只说"跳过" | 解析器格式对不上,降级把"没检查"伪装成通过 | M1、R5.4 | | 校验代码永远通过 | `sdformat` 用 `__default__` / `__default_topic__` 填了缺失字段 | M1 | | 报出根本不存在的碰撞 | 包围盒变换对 min/max 取了绝对值、把偏移吃掉 | R4.6 | | 正常嵌套被判成重叠,只好把检查关掉 | 拿总包围盒判重叠,没逐碰撞体三维判 | R4.6 | | 孔检测在仿真里根本不可能通过 | 仿真分辨率与真机不同,像素门槛失效 | R2.6 | | 仿真"通过"、真机不认 | 像素门槛抄了两份,漂移了 | M2、R2.6 | | 脚本说"已停止",进程还在跑 | 进程模式表漏项(漏过 `robot_state_publisher`) | M1、R4.10 | | `pkill -f gz` 秒返回、什么都没停 | 它先匹配到调用它的 shell | R4.10 | | 换了台臂却连错机器 | 硬编码的 `Rizon4-063989` 默认值 | R1.1 | | `assets_root()` 指向从未存在的 `assets/` | 零调用者的死代码,没人发现 | R5.5 | | 脚本报 `$'\r': 未找到命令`、`set: pipefail: 无效的选项名` | 脚本是 CRLF 行尾,`\r` 被当成标识符的一部分 | R6.6 | | `bash -n` 说没问题,真跑立刻死 | `bash -n` 对 CRLF 会漏报 —— 静态检查≠能用 | R6.6 | | 代码里一半英文 docstring 一半中文注释 | 没有统一口径,谁写谁定 | R6.5 | | 真机栈哪天突然报"模块找不到",明明装过 | `install/` 的入口脚本 shebang 指向**仿真环境**的 python(哑弹) | R6.8 | | 照着文档敲命令,命令根本不存在 | 写文档时没真跑过 | R6.7 | --- ## 9. 这份文档怎么用(给 AI 的具体指令) 改这个仓库时: 1. 先读第 0 节,记住三条元规则。 2. 按第 7 节的清单逐条过。 3. 出问题时先查第 8 节(按症状查)。 4. 要在代码里"绕开"某条规则时,**先改这份文档**并写清理由 —— 绕开而不改文档,下一个人会按文档改回去。