0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
414 lines
31 KiB
Markdown
414 lines
31 KiB
Markdown
# 并行任务工作树工作流(worktree + `task-worktree` skill)
|
||
|
||
> **这个文件是什么**:多任务并行开发的**机制规格**——如何为每个任务开一个互不干扰的工作空间、
|
||
> 干完如何合并回主干、哪些资源必须串行、冲突怎么治理。
|
||
>
|
||
> **状态**:**已落地并端到端实测**(2026-10-12)——skill 在 `.dsh/skills/task-worktree/`,
|
||
> 实测记录与暴露并修复的缺陷见 §11。
|
||
>
|
||
> **决策基线**(2026-10-12 用户确认):工作树放**仓库同级 `_wt/`**;集成分支**沿用 `v2-on-ui`**;
|
||
> `done` 回收**不自动 push**(沿用「push 需用户明确许可」)。
|
||
|
||
---
|
||
|
||
## 1. 目的与范围
|
||
|
||
**要解决的问题**:多个任务同时推进时,若共用同一个工作目录,会互相踩——构建产物、日志目录、
|
||
Python 环境、以及 `docs/任务规划/` 这类**全局唯一登记簿**都会被并发写坏。
|
||
|
||
**做法**:每个任务一棵 `git worktree` + 一条任务分支,任务之间物理隔离;干完由固定命令合回主干。
|
||
|
||
**范围**:只解决「开发期隔离 + 回收期合并」的机械流程。**不改变**既有的分支纪律
|
||
(push 需许可)、门禁口径(pre-commit 30 钩子 + `@build` + `@test`)与文档约定。
|
||
|
||
---
|
||
|
||
## 2. 方案选型
|
||
|
||
| 方案 | 为什么不用 |
|
||
| ---------------------- | ----------------------------------------------------------------------------------- |
|
||
| 同一目录切分支 | 构建产物 / `.venv` / 日志目录 / `.workbuddy-ai` 全共享,两任务互相踩;且本仓库明令禁止用 `git checkout` 丢弃未提交工作 |
|
||
| 每任务 `git clone` | 对象库 234 MB × N 份、Git LFS 全量重复;克隆之间无法安全共享分支,推送关系也更绕 |
|
||
| `git stash` / 打补丁倒换 | 不具备并发安全性,冲突只能靠人工记忆 |
|
||
| **`git worktree`(采用)** | 唯一同时满足「共享对象库 + 独立工作目录 + 独立 HEAD/index」的机制;git 原生,本仓库已有先例 |
|
||
|
||
**命名惯例来源**:`D:/Code/DTU Code/_v1pin-build`、`_v2oui-build` 曾是遗留 worktree(detached HEAD,
|
||
2026-09-19 建、已过期),本方案沿用其「仓库同级 + `_` 前缀」命名,但把散落的目录收敛到统一的
|
||
`_wt/` 容器下。**两者已于 2026-10-12 清理**:`_v1pin-build` 干净直接移除;`_v2oui-build` 有 2 个
|
||
未提交改动(uvprojx / freertos.c),已按字节留档于 `_wt/.archive/2026-10-12-legacy-worktrees/`
|
||
后才移除(两提交由两条 `wip/*` 保活分支持有,其一为 `wip/v2-pending-recv`;无提交丢失)。
|
||
|
||
---
|
||
|
||
## 3. 目录与资源分配
|
||
|
||
### 3.1 目录布局
|
||
|
||
```text
|
||
D:/Code/DTU Code/
|
||
├── STM32F4-Base/ # 主工作树 = 集成分支 v2-on-ui(唯一检出主干的地方)
|
||
│ ├── .git/ # 共享对象库 + hooks(worktree 自动共享,无需任何操作)
|
||
│ ├── .venv/ # 唯一实体(Python 环境)
|
||
│ ├── .workbuddy-ai/ # 唯一实体(记忆;**不**共享给任务树,见 3.2)
|
||
│ └── simulator/SDL2/ # 唯一实体(模拟器 SDK)
|
||
└── _wt/ # ★ 并行任务容器(不在任何 git 仓库内)
|
||
├── .state/<ID>.json # 任务台账:分支 / 联接 / 门禁与台架戳
|
||
├── .state/locks/ # 独占锁:build.lock(UV4)+ bench.lock(COM6/ST-LINK/设备 IP)
|
||
└── <ID>-<slug>/ # ← 一棵工作树(分支 task/<ID>-<slug>)
|
||
├── .git # 文件(指向共享对象库,git 自动创建)
|
||
├── .venv # → junction 到主工作树
|
||
├── simulator/SDL2 # → junction 到主工作树
|
||
├── .workbuddy-ai # 本任务自己的(清场前归并回主工作树)
|
||
└── MDK-ARM/… # 本任务独占的构建产物
|
||
```
|
||
|
||
**工作树绝不能建在仓库内部**(`STM32F4-Base/` 里):那会变成未跟踪目录,污染 `git status`、
|
||
`pre-commit --all-files` 与全库搜索。故统一放仓库同级的 `_wt/`。
|
||
|
||
### 3.2 共享 vs 独占
|
||
|
||
| 资源 | 归属 | 原因 |
|
||
| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------- |
|
||
| `.git/` 对象库 + hooks | **共享**(git 机制自动) | 工作树共享 common dir,提交门禁无需在新空间重装 |
|
||
| `.venv/` | **共享**(junction) | 体积大、内容与任务无关;每任务重建一份纯属浪费 |
|
||
| `simulator/SDL2/` | **共享**(junction) | 下载型 SDK,与任务无关 |
|
||
| `.workbuddy-ai/` | **独占**(刻意不共享) | ① 避免并发写同一份日志丢更新;② 不给联接递归删除的机会——它是本项目最不可再生的目录。任务侧记忆由清场前归并回主工作树 |
|
||
| `MDK-ARM/` 构建产物、`simulator/build/` | **独占** | 正是需要隔离的部分:两个任务可各自构建互不干扰 |
|
||
| `.serialtest/config.json`、`.dsh/skills/`、`.pre-commit-config.yaml` | **独占但内容一致** | 受版本控制,随检出白拿,无需 bootstrap |
|
||
| `.mnemon/`、`ci-artifacts/` | **独占** | 本地运行态,gitignored |
|
||
|
||
### 3.3 junction 规则(**不能用 symlink**)
|
||
|
||
本仓库 `core.symlinks=false`:git 里的符号链接会被检出成「内容是目标路径的普通文本文件」,
|
||
不是链接。因此**共享 `.venv` 只能靠 Windows 目录联接(junction)**,且 junction 必须创建在
|
||
**版本控制之外**(`_wt/<ID>-<slug>/` 里的那几个路径都不在 git 索引内,安全)。
|
||
|
||
创建方式用 `mklink /J`(**不需要管理员权限**,也不同于符号链接)。实测(2026-10-12):
|
||
用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)` 会被系统拒绝(Win32 4392),
|
||
缓冲区经 hexdump 逐字节核对无误仍失败,而 `mklink /J` 正常 → 采用后者;参数以**列表**
|
||
交给 subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
|
||
|
||
**摘除联接必须用 `os.rmdir`**(= `RemoveDirectoryW`,只摘链不递归)。实测 `.venv` 条目数
|
||
摘除前后一致(2809 → 2809)。**清场顺序铁律:先摘联接,再 `git worktree remove`**——
|
||
否则 git 可能递归进联接,把主工作树的 `.venv` / SDL2 一起删掉。
|
||
|
||
> ⚠ 不要在仓库里提交任何指向 `../` 的 symlink 来实现共享——在本机就是坏文件。
|
||
> ⚠ MSYS 的 `rmdir` 拒绝摘除联接(把它当符号链接),手工清场要用 Python 的 `os.rmdir`
|
||
> 或直接走本 skill 的 `abort` / `done`。
|
||
|
||
---
|
||
|
||
## 4. 分支模型
|
||
|
||
| 项 | 取值 |
|
||
| -------- | ---------------------------------------------------------------------------------------------------- |
|
||
| 集成分支(主干) | `v2-on-ui` |
|
||
| 任务分支 | `task/<ID>-<slug>`,从主干当前 HEAD 切出 |
|
||
| `<ID>` | **沿用 `docs/任务规划/README.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-<slug>` |
|
||
| 合并方式 | `git merge --no-ff` 合入主干 |
|
||
| 并发上限 | **同时最多 3 棵工作树** |
|
||
|
||
**`<ID>` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。
|
||
|
||
**并发上限 3** 的依据:`.git` 已 234 MB,每棵工作树要检出约 2400 个受控文件(含 Git LFS 图片),
|
||
并发太多磁盘与 I/O 都不划算,人工也管不过来。
|
||
|
||
---
|
||
|
||
## 5. 任务生命周期
|
||
|
||
| 阶段 | 动作 | 并行性 |
|
||
| ------ | -------------------------------------------- | ----------- |
|
||
| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 |
|
||
| 编码 | 在新空间里改代码 | 可并行 |
|
||
| 单测产物 | `%TEMP%\dtu_host\<工作树哈希>\` 分家 + 每跑独占 `runs\<pid>-<ns>\` | **不需要锁,但必须按树分家**(旧固定共享名跨树互踩:A 树可能执行 B 树编出的同名 exe,summary 相同不可辨;build/bench 是真独占,继续用锁) |
|
||
| 门禁 | `scripts/ci_local.py`(L0 + L0.5,纯软件) | **可并行** |
|
||
| 构建 | `@build`(UV4 增量,需持 `build` 锁) | **必须串行** |
|
||
| 台架 | `@test` / 板级套件(需先持台架锁,见 §7) | **必须串行** |
|
||
| 提交 | 本地 `git commit`(多行正文用 `-F <UTF-8 无 BOM 文件>`) | 可并行 |
|
||
| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 |
|
||
| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** |
|
||
| `push` | 推送主干/任务分支 | **需用户明确许可** |
|
||
|
||
**为什么 `done` 必须串行**:合并动作会移动主干引用,两处同时合入会互相覆盖;且主干只在主工作树检出。
|
||
|
||
---
|
||
|
||
## 6. 冲突治理
|
||
|
||
### 6.1 单写者文档
|
||
|
||
并行失败通常不是代码冲突,而是**多个任务同时改同一个「登记簿」**。以下文件在任务期间
|
||
**禁止在任务工作树里修改**,一律由 `done` 回收阶段在主工作树统一处理:
|
||
|
||
| 文件 | 冲突原因 | 回收时的处理 |
|
||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||
| `docs/任务规划/`(整个文件夹) | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 |
|
||
| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、变更记录里**各加一行** → 两任务必冲突(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**) | 由主工作树统一追加;提交时**只 `git add` 本任务的行**(`git apply --cached` 选择性入库),勿整文件 `git add` |
|
||
| `AGENTS.md` 的 `TASK_TABLE` 块 | 生成物,两个任务都抬栈就冲突 | 合并后重跑 `scripts/gen_task_table.py` **重新生成**,不手工并 |
|
||
| `MDK-ARM/STM32F407-Demo.uvprojx` | 新源文件注册 + UV4 自动改写 | 手工并 `<File>` 条目 → 重跑 `registration-check` → `@build` |
|
||
| `.pre-commit-config.yaml` | 新增主机单测要加钩子 | 并集处理 |
|
||
| `docs/内存管理/RAM使用分析与优化方案.md`(及其 `mem-numbers` 快照) | `mem-numbers` 快照 | **绝不手工合数字**,合并后 `scripts/check_mem_numbers.py --write` 重生成 |
|
||
|
||
### 6.2 合并残留审计(**必做**)
|
||
|
||
双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,可能留下:
|
||
|
||
- **死规则**:指向已删目录的忽略项
|
||
- **重复条目**:同一规则出现两次
|
||
|
||
判据 = 拿 base / 两个 tip **三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
|
||
**不要用 `tail -N` 目测**(会漏看被合并在中间的行),也**不要把合并前就存在的历史重复算进来**。
|
||
|
||
### 6.3 冲突时的解决位置
|
||
|
||
`done` 先做**非破坏性预演**:`git merge-tree --write-tree v2-on-ui task/<ID>-<slug>`。
|
||
|
||
- `rc=0`(无冲突)→ 继续合并
|
||
- `rc≠0` → **在任务分支上** `git merge v2-on-ui` 解决冲突并提交,再重跑 `done`
|
||
|
||
在一次性分支上解冲突,**永不污染主干**。
|
||
|
||
---
|
||
|
||
## 7. 资源串行化
|
||
|
||
### 7.1 台架锁
|
||
|
||
以下物理资源**各只有一份**,故 `@test` / 板级套件 / 烧录必须串行:
|
||
|
||
- 串口 `COM6` @115200
|
||
- ST-LINK SN `0B250D12A218303030303032`
|
||
- 设备 IP `192.168.1.100`
|
||
|
||
机制:`_wt/.state/locks/{build,bench}.lock` 记录持有者(任务 ID + pid + 时间戳 + 备注 + 自报 TTL)。
|
||
|
||
**锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在另一个进程里跑。
|
||
若按 pid 判活,acquire 一退出锁就被判为陈旧 → 锁形同虚设(实测踩到)。
|
||
故判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
|
||
同任务重复 acquire 幂等;他人持锁时 acquire 拒绝(`--wait` 可轮询等待)。
|
||
|
||
**TTL 按锁分设**(2026-10-12):`LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}`。
|
||
口径 = 兜底窗口应与**操作真实时长**同量级:编译是秒~分钟级(`keil_build.py` 自持自释,
|
||
窗口 ≈ UV4 运行时长),1h 已足够宽松;bench 是**会话级**(flash→`@test`→探针),才配得上 6h。
|
||
持有者还可用 `lock acquire ... --ttl <小时>` **自报**更精确的窗口——
|
||
`keil_build.py` 即按其 `--timeout` 自报(`timeout/3600 + 15min`,下限 0.5h)。
|
||
|
||
`done` 前置检查:**陈旧锁自动接管释放**(不再硬失败),仅当锁仍被有效占用时才拒绝回收。
|
||
|
||
> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标)
|
||
> 不因引入工作树而改变,仍按 `docs/测试说明/自动化测试流程.md` 执行。
|
||
|
||
### 7.2 构建
|
||
|
||
`@build`(UV4 增量)在新空间里是**独占目录**,但 **UV4 不能两个实例同时 `-b`**(2026-10-12 用户确认,
|
||
无需实测)——因此构建**同样纳入串行锁**(`build`),与台架锁并列。
|
||
|
||
> 结论:任务期真正可并行的只有**编码与静态门禁**(`ci_local.py` 的 L0 / L0.5,纯软件、零硬件);
|
||
> `@build` 与烧录/台架都必须排队。
|
||
|
||
### 7.3 并行道与写范围规则(规划层 ↔ 执行层映射)
|
||
|
||
规划层(当前任务卡在 `docs/任务规划/UI补齐.md`,后续事项在 `docs/任务规划/登记簿.md`;
|
||
五字段 = **DoD / 硬依赖 / 资源 / 写范围 / 状态**)
|
||
按**资源属性**把任务切进三条道,本方案(工作树 + 锁)是它的执行层:
|
||
|
||
| 道 | 资源 | 并行性 | 锁 / 机制 |
|
||
|---|---|---|---|
|
||
| **台架道** | COM6 / ST-LINK / 设备(§7.1) | **串行**——约束资源,**永不空转**(约束优先,TOC):代码道在跑时必须为下一个台架任务备料 | `bench.lock` |
|
||
| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(31 钩子 `ci_local`,同 Linux 内核子系统树并行而能合回的逻辑,见 `docs.kernel.org/process/2.Process.html` 的 subsystem tree 模型) | 无锁;分支 ≤2 天、绿即合(trunk-based 短分支) |
|
||
| **外部窗口** | 标准源 / 第三方主站等外部设备 | **Fixed Date 类**(Kanban 服务分类):窗口一到全道让路;窗口前 2 日特性冻结(只修稳定,Zephyr RC 语义 → `docs.zephyrproject.org/latest/project/release_process.html`) | `bench.lock` + 外部预约 |
|
||
|
||
**四条规则**:
|
||
|
||
1. **WIP ≤ 2**:每道最多 1 个进行中(典型 = 台架道 1 + 代码道 1),先完再开——登记簿需可见
|
||
`⬜ 未开始 / 🟦 进行中` 两态,销项仍删行。
|
||
2. **写范围相交 = 声明合并次序**:任务卡必须写写范围(目录级);相交不排队等待,而是**声明谁先合**
|
||
(后合者在主工作树 rebase 后继续),claim 时定。
|
||
3. **分支存活 ≤2 天、绿即合**:合并走 `done` 预演(§6.3);单写者文档(§6.1)仍由主工作树统一处理。
|
||
4. **里程碑不拖延、功能顺延下一轮**(时间驱动):里程碑的退出判据写在登记簿,判据不满足则该**功能**
|
||
移入下一波次,里程碑本身按判据改期而非无限等待。
|
||
|
||
> 登记簿是规划层唯一权威(§2 铁律不变);本节只定义执行层映射,**不在本文排任务顺序**。
|
||
|
||
---
|
||
|
||
## 8. SKILL 设计
|
||
|
||
**名称**:`task-worktree` **短命令**:`@task`(沿用本仓库 `@build` / `@test` 的 `@` 惯例)
|
||
|
||
**执行体**:`.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库,
|
||
照 `keil_build.py` 的写法,整目录可拷到别的项目复用)。**已落地并端到端实测**(见 §11)。
|
||
|
||
| 子命令 | 作用 |
|
||
| ----------------------------------------------------- | ----------------------------------------------------------- |
|
||
| `new <ID> <slug> [--base B] [--lean] [--needs-bench]` | 建分支 `task/<ID>-<slug>` + `_wt/<ID>-<slug>/` 工作树 + 共享联接 + 台账 |
|
||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||
| `status <ID>` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 |
|
||
| `sync <ID>` | `rebase` 追平主干,把冲突控制在任务分支上 |
|
||
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) |
|
||
| `done <ID> [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 |
|
||
| `abort <ID> [--force]` | 放弃任务(有未提交/未合并内容时不加 `--force` 会拒绝) |
|
||
|
||
**环境变量**:`DTU_WT_ROOT`(任务容器位置)· `DTU_TASK_BASE`(默认基线)·
|
||
`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树;用于隔离演练/测试)。
|
||
|
||
**关键约束**:
|
||
|
||
1. **`done` 只能在主工作树执行**——主干 `v2-on-ui` 检出在主工作树,git 不允许同一分支在两处检出。
|
||
2. **`done` 前置检查主工作树干净**,脏则中止并提示(避免把未提交工作卷进合并)。
|
||
3. **`done` 不含 `push`**:结束于本地合入 + 门禁全绿,打印提示等用户点头。
|
||
4. `--needs-bench` 任务若没有台架 PASS 戳,`done` 拒绝合并。
|
||
5. 门禁未全绿时 **`done` 保留工作树与分支**(只留下合并提交),便于修正或回退——不静默清场。
|
||
6. 清场顺序:**先摘联接,再 `git worktree remove`**(见 3.3)。
|
||
|
||
---
|
||
|
||
## 9. 门禁与验收
|
||
|
||
| 层级 | 内容 | 时机 |
|
||
| ------- | ------------------------------------ | ------------------------------ |
|
||
| 任务期 | `scripts/ci_local.py`(L0 + L0.5,可并行) | 每个任务各自跑 |
|
||
| 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 |
|
||
| **新空间首次** | `@build` 一次,生成 `.map` | 见下表下注释:不先构建,体积类门禁必然红 |
|
||
| 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 |
|
||
| 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) |
|
||
| 人工 | push 许可 | 回收后 |
|
||
|
||
> ⚠ **新开工作树后先 `@build` 一次再谈门禁**(实测 2026-10-25):`.map` 是每棵树各自的构建产物、
|
||
> 不入版本库,所以依赖它的事实源的门禁——`mem-numbers`、`mem-budget`、以及 `ci_local.py` 的
|
||
> `L0c`——在从未构建过的新空间里**必然红**。这不是代码缺陷,脚本此时也会把这句原因打出来
|
||
> (`check_mem_numbers.py` / `ram_baseline.py` 的提示文案)。顺带一条同源提醒:`done` 的门禁
|
||
> 排在 `@build` 之前(见下文陷阱条目),同一族误报在回收期也会重现一次。
|
||
|
||
**`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `@build` 0 错 0 警
|
||
|
||
- 残留审计无死规则/重复条目 + 工作树已移除。
|
||
|
||
---
|
||
|
||
## 10. 已知风险与待验证项
|
||
|
||
| 项 | 状态 | 处置 |
|
||
| -------------------------------------------- | --------------------- | ------------------------------------------- |
|
||
| UV4 能否并发构建 | **已确认不可**(2026-10-12) | `build` 与 `bench` 一并纳入串行锁 |
|
||
| 跳过 Git LFS smudge 是否影响门禁 | **已实测**(2026-10-12) | 见下表:只省磁盘、不省时间,两种方式工作区都干净 → 默认全量,`--lean` 备选 |
|
||
| 主工作树当前不干净 | 已知 | `done` 的前置检查会拦下 |
|
||
| 遗留 worktree(`_v1pin-build` / `_v2oui-build`) | 已知 | 落地时 `git worktree remove` 清理,避免新旧两套并存 |
|
||
| **「主工作树绿、干净工作树红」的隐藏依赖** | **已实证**(2026-10-12) | 见 10.2。属**原任务**漏提交,非本 skill 缺陷 |
|
||
| `git worktree remove` 中途被中断 → 残留「裸目录」 | **已实证**(2026-10-12) | 见下方 10.3 |
|
||
|
||
### 10.1 LFS 实测数据(2026-10-12,2396 个受控文件 / 707 个 LFS `*.jpg`)
|
||
|
||
| 方式 | 建树耗时 | 检出体积 | `git status` | 参考图 |
|
||
| --------------------------------- | ------ | ----- | ------------ | ---------- |
|
||
| 全量 smudge(默认) | 7.25 s | 83 MB | 干净 | 真实 JPEG,可看 |
|
||
| `--lean`(`GIT_LFS_SKIP_SMUDGE=1`) | 7.05 s | 45 MB | 干净 | 指针文本 |
|
||
|
||
**结论**:LFS 对象已在本地,smudge 几乎不耗时,跳过**只省约 38 MB 磁盘**。
|
||
故默认走全量(保留参考图可读性),磁盘紧张时才用 `--lean`。
|
||
另外两种方式在全新工作树里跑 `pre-commit`(含 `doc-links`)都**正常通过**,
|
||
证明 hooks 经 common dir 共享、门禁不因工作树而失效。
|
||
|
||
### 10.2 工作树是「提交物自洽性」的照妖镜(2026-10-12 实证)
|
||
|
||
演练中 `done` 的门禁在干净工作树里报出 2 处失效引用,而**主工作树里同一门禁是绿的**:
|
||
|
||
```text
|
||
docs\内存管理\RAM使用分析与优化方案.md:277 [路径引用] scripts/ram_baseline.tsv
|
||
docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内存管理/RAM预算台账.md
|
||
```
|
||
|
||
根因:该文档**已提交且无改动**,但它引用的这两个文件**从未 `git add`**(属另一在途任务),
|
||
只在作者的主工作树磁盘上存在 → `doc-links` 查的是**文件系统**,于是在主工作树里通过;
|
||
一旦换到干净工作树(本演练的 `_probe-base2`),文件不存在 → 必然 FAIL。
|
||
|
||
**判据与处置**:这不是本 skill 的缺陷,而是被工作树**逼出来的真问题**——
|
||
「一个提交是否自洽」不能靠「在我的工作目录里能过」来证明。修复归属**引用方任务**
|
||
(补 `git add`,或给引用加 `[计划]` 前缀 / 写明「已删除」状态词,见 `docs/文档规范.md`)。
|
||
|
||
**副作用提醒**:这同时说明**共享主工作树里的绿门禁不可尽信**——只要有未提交文件躺在磁盘上,
|
||
门禁就可能对着「未提交的现状」放行。多任务并行期更该用工作树 + `done` 复核。
|
||
|
||
### 10.3 `git worktree remove` 中断会留下「裸目录」(2026-10-12 实证)
|
||
|
||
清理遗留 worktree 时踩到:`git worktree remove --force` 被中断后,`_v2oui-build` 的 `.git` **已被删除**
|
||
(于是 `git worktree list` 显示 `prunable`),但目录里**还躺着 2774 个条目**没删完。
|
||
|
||
**判据**:看到 `prunable` + 目录仍存在 = git 已删掉 gitfile、删除未完成。此时那棵树**已不再是 worktree**
|
||
(git 不认它了),`git worktree remove` 无法再用,只能按普通目录删除。
|
||
|
||
|
||
**处置**:`git worktree prune` 清台账 → 手工删目录。删之前必须**重新扫一遍 reparse**——
|
||
理由它有 `.git` 时没有联接,不等于现在没有(尤其别让他人趁乱建了联接)。
|
||
清除手段用 Python 自底向上删并顺带 `chmod +w`(MDK 产物有 2 个只读文件),
|
||
**切忌 `rm -rf`**(MSYS 会递归进联接,实测见 3.3)。
|
||
|
||
**已知不做**:不引入第二套并行机制(如 git submodule / 多克隆),
|
||
保持「同一能力只有一个实现」的收敛原则。
|
||
|
||
---
|
||
|
||
## 11. 落地清单与实测记录
|
||
|
||
| 动作 | 目标 | 状态 |
|
||
|---|---|---|
|
||
| 新建 | `.dsh/skills/task-worktree/SKILL.md`(含 `name` / `description` / `whenToUse` frontmatter) | ✅ |
|
||
| 新建 | `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库) | ✅ |
|
||
| 新建 | `.dsh/skills/task-worktree/.gitignore` | ✅ |
|
||
| 修改 | `AGENTS.md`「按主题查哪份文档」加一行指向本文档 | ✅ |
|
||
| 修改 | `docs/README.md` §9.2 登记本文档 | ✅ |
|
||
| 清理 | 移除遗留 worktree `_v1pin-build` / `_v2oui-build` | ✅ 2026-10-12 已清理(脏改动先留档,见 §2) |
|
||
| 验证 | §10 的两个待验证项 | ✅ 见 §10 / §10.1 |
|
||
|
||
### 11.1 端到端实测记录(2026-10-12,隔离集成点演练)
|
||
|
||
用 `DTU_INTEGRATION_WORKTREE` 把集成点指到一次性工作树、基线用一次性分支,
|
||
**全程未触碰真实主线 `v2-on-ui`**:
|
||
|
||
| 场景 | 结果 |
|
||
|---|---|
|
||
| `new` 建树 + 双联接(`.venv` / `simulator/SDL2`) | ✅ 透过联接跑解释器得 `Python 3.12.4` |
|
||
| 新工作树里跑 `pre-commit`(共享 hooks) | ✅ `doc-links` 等通过;gitlint 也正常拦下非法邮箱 |
|
||
| `list` / `status` | ✅ 落后领先、脏文件数、联接清单、锁归属均正确 |
|
||
| `lock` acquire / 冲突拒绝 / 幂等 / 错误持有者拒绝 release | ✅ 期望的 rc 全部命中 |
|
||
| `done --dry-run` 无冲突 | ✅ 输出预演结果树,零改动 |
|
||
| `done --dry-run` 有冲突 | ✅ 拒绝合并(rc=1),提示到任务分支上解冲突 |
|
||
| `done` 完整回收(合并 + 审计 + 重生成 + 清场) | ✅ 合并提交生成合规(标题 40 字符 ≤100、正文 ≤80) |
|
||
| 清场安全性 | ✅ 联接摘除前后 `.venv` 文件数 2809 → 2809,解释器完好 |
|
||
| `abort` 未加 `--force`(有未合并提交) | ✅ 拒绝(rc=1)并列出将丢弃的内容 |
|
||
| `abort --force` | ✅ 联接摘除、工作树移除、分支删除、`.venv` 2809 → 2809 |
|
||
|
||
**实测暴露并已修复的 4 个缺陷**:
|
||
|
||
1. **锁绑定进程存活 → 形同虚设**:`lock acquire` 是短命进程,一退出锁就被判陈旧、下次必被接管。
|
||
改为**时效判定**(当时全局 6h;2026-10-12 起改为按锁分设 TTL,见 §7.1)+ 显式 `--force`,pid 仅作记录。
|
||
2. **`git branch -d` 按环境 HEAD 判「已合并」**:集成点被改走时误判、拒绝删除。
|
||
改为显式 `merge-base --is-ancestor <branch> <base>` 后再删。
|
||
3. **直调 Win32 建联接被拒**(Win32 4392):改走 `mklink /J`(详见 3.3)。
|
||
4. **`done` 门禁跑错工作树**(最隐蔽的一个):`run_script` 原用 `REPO_ROOT / script` 拼脚本路径,
|
||
而 `REPO_ROOT` 由 `task_worktree.py` 自身位置反推 = **主工作树**。于是 `done` 本意是在集成
|
||
工作树里校验,实际却跑了主工作树里的同名脚本;该脚本再按它自己的 `__file__` 反推,又去校验
|
||
**主工作树**——校验对象整体错位。
|
||
证据(回归前/后对照):直接在工作树里跑 `ci_local.py` 报「扫描 .c: 68 / PASS」,经 `done` 调用
|
||
却报「扫描 .c: 71 / FAIL」并附 `[GAP] App/protocol/iec104/*.c | 缺 uvprojx`(那 3 个文件只以
|
||
**未跟踪**状态存在于主工作树)。修法 = 从传入的 `cwd`(= 集成工作树)解析脚本路径
|
||
(`scripts/ci_local.py` L46 / `scripts/check_registration.py` L52 的 `__file__` 反推是既定约定,
|
||
脚本必须由**目标树**里的那一份执行)。修复后同一条命令的
|
||
`双端注册反向校验 (uvprojx + CMakeLists)` 由 FAIL 转 **Passed**。
|
||
|
||
> 缺陷 4 的教训具有普适性:**任何「按 `__file__` 反推仓库根」的项目脚本,都必须由目标工作树里
|
||
> 的那一份来执行**,否则门禁会静默地校验另一棵树。本 skill 的 `run_script` 已统一按 `cwd` 解析。
|
||
|
||
---
|
||
|
||
## 12. 关联文档
|
||
|
||
| 主题 | 去处 |
|
||
|---|---|
|
||
| 提交门禁 40 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
|
||
| 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` |
|
||
| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` |
|
||
| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` |
|
||
| 待办与排序登记簿 | `docs/任务规划/README.md` |
|