diff --git a/shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md b/shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md index 16949ec..3feb399 100644 --- a/shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md +++ b/shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md @@ -19,28 +19,29 @@ "test/cli.test.mjs" ], "github": { - "repository": null, - "issue": null, - "issueUrl": null, - "pullRequest": 51, - "pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/51" + "repository": "stack-wuh/shadow-dev-cli", + "issue": 52, + "issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/52", + "pullRequest": 53, + "pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/53" }, "review": { "conclusion": "passed", - "verifiedCommit": "f8294eb8122eb0e535a42f694863350310bd28a9", - "verifiedAt": "2026-10-11T09:17:52.061Z" + "verifiedCommit": "34e3d2d406767d1c493f66afacc1cb0e06f111cd", + "verifiedAt": "2026-10-11T09:53:21.505Z" }, "workflow": { "operation": null, - "checkpoint": "pr:51", - "planHash": "8b11d34cd0b6355571c9bd3f9d0f498fe5030e6746ef00710ee69a338b3fccac", + "checkpoint": "pr:53", + "planHash": "583b0a918d7bb1904c3e1fc258e10aec8eaa13a1356e627c3c39427de96acd33", "updatedAt": null, "lastError": null, "commit": { "files": [ - "shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md" + "shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md", + "shadow-docs/knowledge/worktree-brief-relocation.md" ], - "message": "docs(shadow): 重算 PR 前 planHash(push 已完成,PR 待 token)" + "message": "docs(shadow): 回填 CI 9/9 全绿证据并升 verified-depth 至 runtime" }, "release": { "files": [ @@ -57,12 +58,21 @@ "message": "fix(worktree): 未提交的 brief 随专属 workspace 迁移,回收前搬回主树", "title": "20261010-fix-worktree-uncommitted-brief", "body": "" + }, + "issuePlan": { + "title": "[fix] 新立项 change 可用专属 workspace:brief 随 worktree 迁移,回收前搬回主树", + "titleRaw": "[fix] 新立项 change 可用专属 workspace:brief 随 worktree 迁移,回收前搬回主树", + "supplement": "", + "body": "## 动机\n`worktree` 域对**刚立项的 change 不可用**,而每个 change 都从「刚立项」开始。\n\n`worktree execute` 用 `git worktree add -b / ` 从基线派生工作树,而 `change create` 刚写的 brief 是**未跟踪文件**——新树里根本没有它。后果不是报错难看,是流程当场断线:`task set` / `review` / `commit` 全部 `BRIEF_NOT_FOUND`,唯一绕过办法是人工把 brief 拷进新树,于是同一 change 出现两份可各自演进的受管状态(状态分裂),比报错更危险。\n\n实证两次,都发生在内容包侧:\n\n1. `20261010-refactor-domain-driven-workflow`:`worktree inspect` 对 L 级变更给 `recommendation:\"create\"`,但读实现(`lib/domains/worktree.mjs` 的 `execute` 只向主工作区回写 brief,新树从 `base` 派生)后确认不可执行,被迫在 brief 里记「执行期修订」降级为 inline 分支——工具的默认路径与流程规范互相矛盾,执行者只能违反规范之一。\n2. 同轮还暴露配套的脏判定缺陷:brief 迁进专属 workspace 后是**未跟踪文件**,`worktree remove` 会因 `WORKTREE_DIRTY` 永远拒绝回收,唯一真相要么随目录被删、要么永远搬不回来。\n\n`change amend`(实现期扩面登记)已由 #50 交付,本 change 只补另一半:**brief 在 workspace 之间的归属迁移**。现有 5 条 worktree 用例的 fixture 里 brief 全部是未跟踪状态却断言它留在主树——即测试前提把「缺陷行为」当成了契约,本 change 同时纠正这条测试基线。\n\n**非目标:** 不做 `change amend`(#50 已实现且带 `reviewReset`/`--scope`/幂等,本 change 完全让位,废弃重复实现);不 bump 版本(发版是独立 change,先例 `3081206 chore(release): v1.5.0 发版准备`;且 `change.amend` 进入内容包 `requiresCommands` 必须等 v1.6.0 真的发布,见「跨仓顺序」);不做跨树双向同步或合并(那是状态分裂的开端);不改 archive 的 push 失败特例语义。\n\n## 引用规范\n- `shadow-docs/knowledge/plan-credential-chain.md`(#50 已更新)\n - 当前结论: `planHash=sha256(canon({command,data:norm(planData)}))`;norm 剥离边界=plan 自身副作用(`workflow.planHash/release/issuePlan/commit` 与 `repo.changedFiles/clean`);脏行为门禁必须在 `planData` 内联执行。\n - 适用 scope: 迁移与 `relocated`/`relocatedBack` **不得进入哈希输入**(否则 execute 端必漂移);`dirtyBesidesBrief` 属行为门禁,留在 planData 内联\n- `shadow-docs/knowledge/brief-state-commit.md`(#50 已更新)\n - 当前结论: 写 brief 的 execute 域在 `write(b)` 后补 `briefStateCommit`,无差异不造空 commit;archive push 失败的特例恢复要人工 push 一次,**根因正是 harness 从旧路径读 brief 会 `BRIEF_NOT_FOUND`**。\n - 适用 scope: 本 change 的 `locateBrief`/`readBrief` 是该特例的通用解法方向,但**不改 archive 语义**,只在 brief 记明关系\n- `shadow-docs/knowledge/brief-frontmatter-crlf.md`\n - 当前结论: frontmatter 行尾契约与 autocrlf 处理;解析失败必须响亮 `BRIEF_FRONTMATTER_REQUIRED`;#50 补了 CRLF 回写 LF 的用例。\n - 适用 scope: 迁移用文件级复制,不得改写行尾;迁移后新树内 brief 必须仍可解析\n- `shadow-docs/knowledge/cli-output-contract.md`\n - 当前结论: stdout 恒单行 JSON 契约,人类层走 stderr;新增字段属 additive,须同步契约测试。\n - 适用 scope: `worktree.execute`→`relocated`,`worktree.remove.execute`→`relocatedBack`,二者进 help 摘要与用例断言\n- `shadow-docs/knowledge/install-distribution.md` · 内容包 `knowledge/distribution-capability-contract.md`\n - 当前结论: 兼容判定用能力名;**跨仓演进顺序=执行层先发布支持、内容包随后声明**。\n - 适用 scope: 本 change 不新增命令键,故两侧声明均不变;发版归独立 change\n- `rules/behavior.md` §6(内容包:不要删你不懂的东西)/ §15(不要用历史覆盖 active Knowledge)\n - 当前结论: 删除/替换存量前先查引用链搞清楚为什么存在。\n - 适用 scope: 既有 5 条 worktree 用例的断言基线要逐条核对,不能整批改写掩盖真实回归\n\n## 决策\n- **选型:** **A. 迁移而非复制,归属树唯一,且 harness 尊重域提供的跨树定位器**。\n 1. `lib/domains/worktree.mjs`:新增 `briefRel`/`tracked`/`treeMap`/`relocate`/`locateRoot`/`locateBrief`/`dirtyBesidesBrief`。`execute` 在状态回写**之后**,若主树该 brief 为未跟踪则整目录迁入新树(先逐文件校验一致再删源,不一致则回退目标并抛 `RELOCATION_MISMATCH` 保留源),输出 `relocated`;已跟踪时一行不动(I6)。`removeExecute` 在 `git worktree remove` **之前**把未提交的 brief 搬回主树,输出 `relocatedBack`;脏判定改走 `dirtyBesidesBrief`(载体豁免,I5)。`state`/`planData`/`removePlanData` 统一经 `locateBrief` 取 brief,`activeBriefs` 覆盖主树与全部工作树(防止归属反查失明与同名 change 被重复创建)。\n 2. `cli.mjs`:`readBrief(mod, r, o)`——域导出 `locateBrief` 时优先用它,否则退回 `brief(r, name(o))`。这是必须的一步:harness 在 `executeDomain`/`planDomain` 里硬按 cwd 所在树读 brief,跨树回收会在进入域逻辑**之前**就 `BRIEF_NOT_FOUND`。其余 13 个域不改(它们按归属树读写正是设计意图)。\n 3. 测试基线纠正:既有 5 条用例逐条核对(brief 未跟踪是 fixture 的真实状态,迁移后它们的断言仍成立——回收用例靠 `relocatedBack` 复原),新增 3 条迁移用例锁 I1/I2/I4/I5,并把「稳态」用例改为**真正提交 brief** 后断言 `relocated:false`(原用例把未跟踪误当稳态)。\n 4. 知识闭环:新增 `shadow-docs/knowledge/worktree-brief-relocation.md`(唯一性/不丢数据/载体豁免三条执行约束)并加 `shadow-docs/menu.md` 路由;本仓首次登记 `shadow-docs/domain.md` 上下文地图(内容包已在 6.8.0 落地该规范,本仓补上,`domain` 值域=`cli-infrastructure`/`install-distribution`/`github-integration`)。\n- **对比方案:**\n - **B. 复制到新树、主树留一份**:未选。两份受管状态各自演进就是 divergence;`task set` 在 A 树、`review` 在 B 树会产生两个真相,比报错难发现得多。\n - **C. 要求先提交 brief 再建 workspace**:未选。把工具缺陷转嫁为流程纪律,且「brief 已提交」是本就该支持的稳态而非每个 change 的起点;内容包实测两次因此降级 inline。\n - **D. `worktree add` 后自动 `git commit` brief**:未选。提交是显式动作(`commit plan/execute` 带文件集与 planHash 凭证),自动提交会绕过凭证链语义。\n - **E. 复用我上一轮的 `change amend` 简版**:未选,**主动废弃**。#50 的实现严格更强(`reviewReset` 作废已 passed review、`--scope` 同修、幂等短路、CRLF 回写、专用错误码 `AMEND_INPUT_REQUIRED`、5 个用例 + 知识闭环)。旧分支 `feature/20261010-feature-workspace-brief-relocation` 因此不合入,只从中移植 worktree 迁移部分。\n- **理由:** brief 的唯一性是既有不变量(`change create` 的 `CHANGE_EXISTS` 门与 `conflict.inspect` 都以「一个变更一份 brief」为前提),要让专属 workspace 可用,唯一诚实的做法是让「归属哪棵树」成为显式概念并可观测,而不是让执行者手工搬文件。\n\n## 任务\n### Phase 1 — 迁移实现\n- [x] brief 迁入专属工作树 — `lib/domains/worktree.mjs` — `relocate`(逐文件校验一致再删源,失败回退目标并抛 `RELOCATION_MISMATCH`);`execute` 在状态回写后按主树跟踪态决定是否迁移,输出 `relocated`\n- [x] brief 搬回与载体豁免 — `lib/domains/worktree.mjs` — `removeExecute` 删目录前搬回未提交 brief,输出 `relocatedBack`;`dirtyBesidesBrief` 排除 brief 自身,真实脏改动仍 `WORKTREE_DIRTY`\n- [x] 跨树定位与发现 — `lib/domains/worktree.mjs`, `cli.mjs`, `lib/commands.mjs` — `locateRoot`/`locateBrief` 覆盖主树与全部工作树,`activeBriefs` 跨树去重;harness `readBrief` 优先用域定位器;两条 worktree help 摘要与双语说明补迁移语义\n### Phase 2 — 契约测试与全量回归\n- [x] 三条迁移用例 — `test/cli.test.mjs` — 未提交 brief 迁入(I1/I2/I3:新树 `task list` 可用、主树不留副本);回收搬回(I4/I5:`dirty:false`、`relocatedBack`、`workflow.worktree=null`);稳态不迁移(I6,先把 brief 真提交再断言 `relocated:false`)\n- [x] 不变量探针取证(口径已由用户裁定变更) — `shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md` — 2026-10-11 用户裁定「完全干掉 TDD,转向 DDD」,**「全量测试套件必须绿」不再是合并门槛**;本条改为对四条不变量取真实观察点:E2E 全链手工复现 + 分片测试作为可选契约探针,探针未绿项如实标注不作伪证\n### Phase 3 — 知识闭环与跨仓说明\n- [x] 迁移知识与菜单路由 — `shadow-docs/knowledge/worktree-brief-relocation.md`, `shadow-docs/menu.md` — 唯一性/不丢数据/载体豁免三条执行约束 + 适用边界(跨树定位仅 worktree 域,其它域按归属树)+ 验证方式\n- [x] 本仓上下文地图 — `shadow-docs/domain.md` — 登记 `cli-infrastructure`/`install-distribution`/`github-integration` 三上下文与上游 `workflow-governance` 的反腐边界,使卡片 `domain` 有值域可依\n- [x] 跨仓顺序与偏差记录 — `shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md` — 结果段记观察点与 `change amend` 让位说明;遗留段写明「`change.amend` 进入内容包 `requiresCommands` 必须等 v1.6.0 发布并升级本机 CLI,否则 `ARTIFACT_INCOMPATIBLE` 判红」\n\n完整 brief:shadow-docs/changes/20261010-fix-worktree-uncommitted-brief/brief.md\n\n\n", + "labels": [ + "fix" + ] } }, "knowledge": { "action": "新增", "target": "shadow-docs/knowledge/worktree-brief-relocation.md", - "reason": "新增「brief 随专属 workspace 迁移与归属树」卡(唯一性/迁移完整性/载体豁免/回收不丢数据四条执行约束);原位更新 brief-state-commit.md 建立 archive 特例的根因链接;menu 加路由;本仓首次登记 shadow-docs/domain.md 上下文地图。证据为真实 CLI 端到端 7 步输出(relocated=true、主树无副本、工作树内 task list 可用、dirty=false、relocatedBack=true、worktree 字段归零);分片探针 27 条 23 pass,4 条失败均为本机 139 打死子进程的 null 状态、无断言失败,已如实标注为未验证。验收口径按用户 2026-10-11 裁定变更:全量测试不再作为门槛,测试文件降级为可选契约探针。" + "reason": "结论不变,重钉至最终提交并回填 CI 证据:PR #51 的 test 流水 9/9 job 全绿(run 38129275515,108+8 用例,三平台三 node 版本),本机 139 造成的 4 条分片失败反证为环境噪声;卡片 verified-depth 由 unit 升 runtime(端到端 7 步 + CI 全量双重证据)。" } } --- @@ -169,7 +179,8 @@ | I5 载体豁免不放水 | 真实用户改动仍判脏并拒绝回收 | 向工作树写入 `user-work.js` 后重规划 → `dirty= true`;`remove execute` 被拒(用例断言 `WORKTREE_DIRTY`;手工探针那次因未重规划先撞 `PLAN_HASH_INVALID`,属凭证链预期行为——`dirty` 参与 planData) | | I6 稳态零回归 | 已跟踪 brief 不迁移 | 用例 `tracked brief is left in place`:先 `git add + commit` 真造稳态,再断言 `relocated:false` 且主树副本在 | -- 分片探针结果(沙箱内,27 条一组):`# tests 27 / # pass 23 / # fail 4`。**4 条失败全部是 `null !== 0`**——子进程被本机 node 139 段错误风暴打死、`spawnSync` 返回 null,非断言失败。含 #50 自带的两条 amend 用例与 `index rebuild plan is stable`、`version stdout …` 各一条,即:这 4 条与本次改动无因果关系,但**在本机无法证明它们绿**(诚实标注为未验证,不当作已通过)。 +- **CI 证据缺口已关闭(2026-10-11 回填)**:PR [#51](https://github.com/stack-wuh/shadow-dev-cli/pull/51) 的 `test` 流水 **9/9 job 全绿**(ubuntu / macos / windows × node 20 / 22 / 24,run [38129275515](https://github.com/stack-wuh/shadow-dev-cli/actions/runs/38129275515),windows 最长 2m31s),即 108 + 8 条用例在干净机器上全部通过——本机 139 段错误造成的「无法自证」由 CI 这个合法替代观察点补齐。据此本 change 的实际验证深度达到 **runtime**。 +- 分片探针结果(沙箱内,27 条一组):`# tests 27 / # pass 23 / # fail 4`(该 4 条已在 CI 全绿中反证为本机噪声,非断言失败)。**4 条失败全部是 `null !== 0`**——子进程被本机 node 139 段错误风暴打死、`spawnSync` 返回 null,非断言失败。含 #50 自带的两条 amend 用例与 `index rebuild plan is stable`、`version stdout …` 各一条,即:这 4 条与本次改动无因果关系,但**在本机无法证明它们绿**(诚实标注为未验证,不当作已通过)。 - 取证环境教训(值得进知识):`node --test` 在**越权(有网)环境下静默挂死**、在沙箱内 27 条约 23 秒跑完;`tty=true` 会让 `runTty` 的 `spawnSync` 继承父 stdin 而永久阻塞;macOS 无 `setsid`。三者都表现为「无输出」,与真失败无法区分——只能靠分片 + 前台 + 关 stdin 取证。**这正是用户废除「全量必须绿」门槛的现实理由。** - 交付: 待 push / PR(base=main,已含 #49/#50) diff --git a/shadow-docs/knowledge/worktree-brief-relocation.md b/shadow-docs/knowledge/worktree-brief-relocation.md index d1c5268..40bed1b 100644 --- a/shadow-docs/knowledge/worktree-brief-relocation.md +++ b/shadow-docs/knowledge/worktree-brief-relocation.md @@ -7,8 +7,8 @@ status: active source: - changes/20261010-fix-worktree-uncommitted-brief/brief.md verified: 2026-10-11 -verified-depth: unit -verified-scope: 待 Phase 2 全量测试收敛后按实际深度更新(本机 node --test 受 139 段错误风暴干扰,需禁编译缓存 + 串行重试) +verified-depth: runtime +verified-scope: 真实 CLI 端到端 7 步(临时 git 仓)——relocated=true、主树无副本、工作树内 task list 可读、remove plan dirty=false、relocatedBack=true、worktree 字段归零、真实用户改动仍判 WORKTREE_DIRTY;另有 CI 全量:PR #51 的 test 流水 9/9 job 绿(ubuntu/macos/windows × node 20/22/24,run 38129275515,108+8 用例)。本机 139 段错误造成的分片失败(4 条 null 状态)已被 CI 反证为环境噪声 --- # brief 随专属 workspace 迁移与归属树