From 81534236e47afb36ae1ba29ddc66e64533dad839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Tue, 13 Oct 2026 12:20:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20sync=20=E8=A7=A3=E9=99=A4=20brief=20?= =?UTF-8?q?=E7=A1=AC=E4=BE=9D=E8=B5=96=EF=BC=8C=E5=9F=BA=E7=BA=BF=E5=8F=96?= =?UTF-8?q?=20--base-branch=20>=20brief=20>=20=E5=BD=93=E5=89=8D=E5=88=86?= =?UTF-8?q?=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- lib/commands.mjs | 4 +- lib/domains/sync.mjs | 29 +++- .../20261013-fix-sync-briefless-base/brief.md | 138 ++++++++++++++++++ 4 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md diff --git a/README.md b/README.md index 7d527e6..97f65de 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ shadow-dev issue execute --name <名称> --confirm |------|------| | `change create \| approve \| amend \| list` | 创建 / 批准变更 brief;`amend` 登记实现期扩面;`list` 列变更(`--all` 含归档、`--archived` 只列归档) | | `branch plan \| execute` | 从基线分支建功能分支 | -| `sync plan \| execute` | fast-forward 同步上游(分叉时拒绝自动合并) | +| `sync plan \| execute` | fast-forward 同步上游:基线取 `--base-branch` > brief > 当前分支;squash 收口可无 brief,此时 `--plan-hash` 为唯一凭证(分叉仍拒绝自动合并) | | `conflict inspect` | 检查活动变更间的文件重叠 | | `worktree inspect` | 列出 git worktree(分支/脏净/归属),按 brief 复杂度评级给并行建议 | | `worktree plan \| execute` | 为变更创建独立 worktree(无分支则随 add 派生;不切换当前工作区) | diff --git a/lib/commands.mjs b/lib/commands.mjs index cb90c66..d235672 100644 --- a/lib/commands.mjs +++ b/lib/commands.mjs @@ -24,8 +24,8 @@ export const COMMANDS = { 'issue.execute': c('issue execute', '创建 GitHub issue(正文读 brief 快照与 plan 校验)', 'create the GitHub issue (body re-derived from the brief and hash-checked)', [N, PH, CF], 'shadow-dev issue execute --name --confirm', null), 'branch.plan': c('branch plan', '预览建功能分支', 'plan creating the feature branch', [N], 'shadow-dev branch plan --name ', 'branch execute --name {name} --confirm'), 'branch.execute': c('branch execute', '从基线分支创建并切换', 'create and switch to the feature branch', [N, PH, CF], 'shadow-dev branch execute --name --confirm', null), - 'sync.plan': c('sync plan', '预览 fast-forward 同步上游', 'plan a fast-forward sync with upstream', [N], 'shadow-dev sync plan --name ', 'sync execute --name {name} --confirm'), - 'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [N, PH, CF], 'shadow-dev sync execute --name --confirm', null), + 'sync.plan': c('sync plan', '预览 fast-forward 同步上游(brief 非前置:squash 收口时 brief 尚未落地)', 'plan a fast-forward sync with upstream (the brief is not a prerequisite: post-squash it has not landed yet)', [f('--name', false, '变更名;brief 在盘上时基线取自它,否则退回当前分支', 'change name; the brief supplies the base branch when present, otherwise the current branch is used'), f('--base-branch', false, '基线分支,优先级高于 brief 与当前分支', 'base branch, overriding both the brief and the current branch')], 'shadow-dev sync plan --base-branch main', 'sync execute --plan-hash {planHash} --confirm'), + 'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [f('--name', false, '变更名;与 plan 一致时凭证取自 brief 持久化的 planHash', 'change name; matching the plan uses the planHash persisted in the brief'), f('--base-branch', false, '基线分支,须与 plan 一致(参与 planHash 重算)', 'base branch, must match the plan (it is re-hashed for the planHash check)'), PH, CF], 'shadow-dev sync execute --plan-hash --confirm', null), 'conflict.inspect': c('conflict inspect', '检查与其他 active brief 的文件重叠', 'check file overlaps with other active briefs', [N], 'shadow-dev conflict inspect --name ', null), 'worktree.inspect': c('worktree inspect', '列出 git worktree 与并行建议(按 brief 复杂度评级)', 'list git worktrees with a parallelism recommendation based on the brief rating', [N], 'shadow-dev worktree inspect --name ', null), 'worktree.plan': c('worktree plan', '预览创建独立 worktree(brief 无分支则随 add 派生)', 'plan a dedicated worktree (branch derived on add when the brief has none)', [N, f('--path', true, '新 worktree 目录(绝对或仓库相对路径)', 'directory of the new worktree (absolute or repo-relative)')], 'shadow-dev worktree plan --name --path ', 'worktree execute --name {name} --confirm'), diff --git a/lib/domains/sync.mjs b/lib/domains/sync.mjs index 5bff6ab..5e74f89 100644 --- a/lib/domains/sync.mjs +++ b/lib/domains/sync.mjs @@ -1,16 +1,37 @@ import { git, repo } from '../git.mjs' import { brief } from '../brief.mjs' -import { name } from '../input.mjs' import { ext, err } from '../errors.mjs' +// 基线分支解析优先级恒为:--base-branch > brief.baseBranch > 当前分支。 +// brief 不是 sync 的前置条件:squash 合并后收口时调用者已站在基线分支上,而 brief 只在 ff +// 之后才落到工作树——把 brief 当硬凭证会让 sync 恰好在它唯一被需要的场景里必然 +// BRIEF_NOT_FOUND(issue #59 / #65)。显式代价:非当前分支的基线必须传 --base-branch。 +function baseBranch(r, o, current) { + if (o['base-branch']) return String(o['base-branch']) + if (!o.name) return current + try { + return brief(r, o.name).data.baseBranch || current + } catch (e) { + if (e.code !== 'BRIEF_NOT_FOUND') throw e + return current + } +} + export async function planData(r, o) { - const n = name(o), b = brief(r, n), q = repo(r) + const q = repo(r) const dirty = q.changedFiles.filter(x => !x.startsWith('shadow-docs/')) if (dirty.length) throw err('DIRTY_WORKTREE') try { git(r, ['fetch', 'origin', '--prune'], { timeout: 120000 }) } catch { ext('GIT_FETCH_FAILED') } - const up = `origin/${b.data.baseBranch || q.branch}`, to = git(r, ['rev-parse', up]) + const base = baseBranch(r, o, q.branch) + const upstream = `origin/${base}` + let to + try { + to = git(r, ['rev-parse', upstream]) + } catch { + throw err('SYNC_UPSTREAM_NOT_FOUND', `SYNC_UPSTREAM_NOT_FOUND: ${upstream} does not resolve — pass --base-branch explicitly`, 1) + } try { git(r, ['merge-base', '--is-ancestor', q.head, to]) } catch { throw err('SYNC_NOT_FAST_FORWARD') } - return { name: n, from: q.head, to, upstream: up } + return { name: o.name ?? null, base, from: q.head, to, upstream } } export function execute(r, o, x) { diff --git a/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md b/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md new file mode 100644 index 0000000..11ea39e --- /dev/null +++ b/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md @@ -0,0 +1,138 @@ +--- +{ + "schema": "shadow-dev/v1", + "name": "20261013-fix-sync-briefless-base", + "type": "fix", + "scope": null, + "status": "reviewed", + "baseBranch": "main", + "branch": "fix/20261013-fix-sync-briefless-base", + "files": [ + "README.md", + "lib/commands.mjs", + "lib/domains/sync.mjs", + "shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md" + ], + "github": { + "repository": "stack-wuh/shadow-dev-cli", + "issue": null, + "issueUrl": null, + "pullRequest": null, + "pullRequestUrl": null + }, + "review": { + "conclusion": "passed", + "verifiedCommit": "e5d83a9a04a68003f2b82e21c08f02703a4955f9", + "verifiedAt": "2026-10-13T04:19:09.758Z" + }, + "workflow": { + "operation": null, + "checkpoint": null, + "planHash": "2291cd9e4f75d0e08deb989a055249cf3b69ed3e1509d9d3fb166582f5d1565b", + "updatedAt": null, + "lastError": null, + "release": { + "files": [ + "README.md", + "lib/commands.mjs", + "lib/domains/sync.mjs", + "shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md" + ], + "message": "fix: sync 解除 brief 硬依赖,基线取 --base-branch > brief > 当前分支", + "title": "fix: sync 解除 brief 硬依赖,基线取 --base-branch > brief > 当前分支", + "body": "## 变更\n- `lib/domains/sync.mjs`:基线解析改为 `--base-branch` > `brief.baseBranch` > 当前分支\n- brief 缺失不再阻断 sync;`origin/` 不可解析时以 `SYNC_UPSTREAM_NOT_FOUND` 响亮拒绝\n- `lib/commands.mjs`:sync 两键的 `--name` 由必填降为可选,新增 `--base-branch`\n- `sync plan` 的 nextStep 改为无 brief 形态 `sync execute --plan-hash {planHash} --confirm`\n- `README.md`:sync 行同步新调用形态与基线解析序\n\n## 影响\n- 行为/触发面变化:squash 合并后的收口不再需要人工 `git merge --ff-only`\n- 在基线分支上 `sync plan` + `sync execute --plan-hash --confirm` 即可闭合\n- 代价显式化:非当前分支的基线必须传 `--base-branch`,原先由 brief 代答\n- 明确不受影响的范围:`cli.mjs` harness 与凭证模型零改动\n- 带 `--name` 且 brief 在盘的旧调用形态行为不变;`--name` 降为可选是放宽,旧写法不会失败\n\n## 验证\n- 观察点脚本(真实临时仓 + bare remote,不入库)逐条覆盖 6 条不变量,含三条拒绝路径\n- INV-1 无 brief 可 plan:`planHash 96c5f0a6…`;execute 后 `head` 与远端 tip 逐字相等\n- INV-2 `--base-branch release/9` 压过 brief 的 `baseBranch: main`\n- INV-4 无 brief 时脏树仍 `DIRTY_WORKTREE`;INV-6 未知 base → `SYNC_UPSTREAM_NOT_FOUND`\n- INV-3 有 brief 时基线仍取 brief;本地分叉 → `SYNC_NOT_FAST_FORWARD`\n- INV-5 带 brief 通路仍把 planHash 写入 brief:`equal: true`\n- 参考回归:`node --test --test-name-pattern \"sync (plan|fetches|blocks)\"` 6/6 绿\n- 未覆盖项:全量 `npm test` 本机约 270s,超 60s 阈值未在本机跑,由 `test.yml` 的 PR 触发补\n- 未覆盖项:#59 的 `verifiedCommit` 漂移那一半不放宽,属设计内的门,归档前在 main 重跑 review\n\n## 关联\n- Closes #65\n- Refs #59\n- brief: shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md" + } + }, + "knowledge": { + "action": "无需变更", + "target": null, + "reason": null + } +} +--- + +# 20261013-fix-sync-briefless-base + +## 动机 + +squash 合并后收口时 `sync` 两头卡死(issue #59),#65 已证明浅修复被 harness 的 `readBrief` + `write` 两处堵死、出路只能是改契约。现场是:调用者已经站在基线分支上,而 brief 只在 ff 之后才落到工作树——于是 `sync` 恰好在它唯一被需要的场景里必然 `BRIEF_NOT_FOUND`,人只能退回裸 `git merge --ff-only`(本会话已这样绕过 6 次)。 + +## 复杂度评级 + +- **评级:** L +- **理由:** 改的是 `sync` 命令域的对外参数契约(`--name` 由必填降为可选、新增 `--base-branch`、新增拒绝码 `SYNC_UPSTREAM_NOT_FOUND`)。命中 `norms/verification.md` 三要素的「契约变更」与「命令域行为」;改坏要到合并后收口路径才炸,可发现性差。 +- **期望验证深度:** runtime——真实临时仓 + 真实 remote + 真实 plan/execute 端到端跑,不停在 code-read。 +- **取证策略:** observation(默认形态)。**不用 assert-first**:本轮先按 L 选了 assert-first 并新建 4 条套件断言,被用户裁定驳回——`norms/verification.md` 的默认取证形态就是观察点,assert-first 是可选项且须 propose 时由人批准,不得默认新建测试。断言改动已全部回退(`git checkout -- test/cli.test.mjs`),取证改由下方一次性观察点脚本产出,脚本落在临时目录、不入库。 + +## 领域模型 + +- **限界上下文:** 变更生命周期收口(merged → 本地基线同步 → 归档) +- **统一语言增量:** 「基线分支 base」与「上游 upstream」分离——upstream 恒为 `origin/`;`sync` 的输入是 base,不是 brief。base 解析优先级恒为 `--base-branch` > `brief.baseBranch` > 当前分支。 +- **不变量:** + - INV-1 brief 不是 sync 的前置条件:`shadow-docs/changes//brief.md` 不存在时 `sync plan` 不得以 `BRIEF_NOT_FOUND` 结束,且 `sync execute --plan-hash` 仍能完成 ff。 + - INV-2 base 解析优先级恒为 `--base-branch` > `brief.baseBranch` > 当前分支;显式入参永远压过 brief。 + - INV-3 仅快进语义不变:`sync execute` 仍只做 `git merge --ff-only`,分叉一律 `SYNC_NOT_FAST_FORWARD`。 + - INV-4 零脏树门不放宽:非 `shadow-docs/` 的脏文件仍 `DIRTY_WORKTREE`,无 brief 时同样生效。 + - INV-5 凭证语义不扩张:brief 在盘上时 planHash 仍写入 brief(带 `--name` 的既有通路零改动);无 brief 时 `--plan-hash` 为唯一凭证,复用无 brief 域既有机制,不改 harness。 + - INV-6 base 不可解析时响亮失败:`origin/` 不存在 → `SYNC_UPSTREAM_NOT_FOUND` 并带出 base,绝不退化成空操作。 +- **领域事件:** `sync.planned`(base / from / to / upstream)、`sync.fast-forwarded`(head 前移) + +## 引用规范 + +- `norms/verification.md` + - 当前结论: 三层职责分离;默认取证形态是观察点,assert-first 为 L 级可选且须 propose 批准;regression 探针仅在 CI 实测 ≤ 60s 的仓里作硬门槛。 + - 适用 scope: `lib/domains/sync.mjs`, `README.md` + - 与本变更的关系: 评级、取证形态与门槛适用性的依据(本变更正是被这条 norm 驳回了一次)。 + +## 覆盖范围 + +- `lib/domains/sync.mjs`:base 三级优先解析、brief 缺失不再抛错、`origin/` 缺失响亮拒绝;plan 数据补 `base` 字段,无 `--name` 时 `name` 恒为 null。 +- `lib/commands.mjs`:`sync.plan` / `sync.execute` 的 `--name` 降为可选、新增 `--base-branch`、`sync plan` 的 nextStep 改为无 brief 形态。 +- `README.md`:sync 行改为新调用形态与基线解析序。 +- 载体 brief 自身。 + +## 决策 + +- 采纳 #65 的方案 **B**,不采纳 A(新增 `transientPlan` seam 并改 `sync execute` 对外形态)与 C(把收口定义为纯人工,等于承认 CLI 不覆盖该路径)。 +- **harness 零改动**:`planDomain` / `executeDomain` 的 `!o.name` 分支已是无 brief 域凭证通路,缺陷只在 `sync.mjs` 自己硬调 `brief()`。这条是 B 比 A 窄的实证依据,也是本 change 只碰 3 个实现文件的原因。 +- `--name` 保留可用(旧 SKILL 写法与既有流程不炸),仅从必填降为可选。 +- **#59 的另一半不在本 change 放宽**:squash 后 `verifiedCommit` 与 HEAD 漂移导致归档前须在 main 重跑 review,这是设计内的门,不是缺陷。本 change 只解除 sync 卡死,并在 issue 中显式拆分记录,不偷偷改 archive 门。 +- 本仓无 `scripts/check-brief.mjs`(guard 探针在 workflow 宿主仓);`npm test` 全量约 270s > 60s 阈值,全量回归由本仓 `test.yml` 的 `pull_request` 触发补跑,缺口在此登记不静默免除。 + +## 任务 + +### Phase 1 · sync 域解 brief + +- [x] task-1 — `lib/domains/sync.mjs` — base 三级优先解析 + brief 缺失不抛 + `SYNC_UPSTREAM_NOT_FOUND` +- [x] task-2 — `lib/commands.mjs` — sync 两键的参数契约与 nextStep 模板 + +### Phase 2 · 观察点取证 + +- [x] task-3 — 临时脚本 `obs.mjs`(不入库)— 每条 `INV-n` 一个真实观察点,含 INV-3b/INV-4/INV-6 三条拒绝路径 + +### Phase 3 · 文档对齐 + +- [x] task-4 — `README.md` — sync 行与调用形态 + +## 结果 + +评级 L,取证策略 observation。观察点脚本:`node /c/Users/596540/AppData/Local/Temp/obs.mjs`(自建临时仓 + bare remote + clone 推进分支,逐条真实调用 `node cli.mjs sync plan/execute --json`)。以下 JSON 均为该脚本的真实输出。 + +- **INV-1**(无 brief 可 plan 并 ff) + - `sync plan` → `{"rc":0,"data":{"name":null,"base":"main","from":"6cbccb5d…","to":"202f40af…","upstream":"origin/main"},"planHash":"96c5f0a64310"}`,`extra: remoteTip=202f40afeadeb3e38d93397f1e380e68430c2c98` + - `sync execute --plan-hash 96c5f0a6… --confirm` → `{"rc":0,"data":{"head":"202f40afeadeb3e38d93397f1e380e68430c2c98"}}`,`extra: noteMainOnDisk=true headEqTip=true`——`--plan-hash` 为唯一凭证即可 ff,head 与远端 tip 逐字相等。 +- **INV-2**(显式入参压过 brief):brief 的 `baseBranch` 是 `main`,`sync plan --name sample --base-branch release/9` → `{"rc":0,"data":{"name":"sample","base":"release/9","upstream":"origin/release/9","to":"22f47435…"}}`。 +- **INV-5**(凭证语义不扩张 / harness 未改):同上这次 plan 后读 brief frontmatter → `{"briefPlanHash":"b53f9225f221","planHash":"b53f9225f221","equal":true}`;带 `--name` 通路照旧写盘。`cli.mjs` 与 `test/cli.test.mjs` 均无改动(`git diff --stat` 只有 `README.md`、`lib/commands.mjs`、`lib/domains/sync.mjs` 三文件 + 载体 brief)。 +- **INV-4**(脏树门不因无 brief 放宽):删 brief 后改脏 `README.md` → `{"rc":1,"code":"DIRTY_WORKTREE"}`。 +- **INV-6**(base 不可解析响亮失败):`sync plan --base-branch nope/1` → `{"rc":1,"code":"SYNC_UPSTREAM_NOT_FOUND","message":"SYNC_UPSTREAM_NOT_FOUND: origin/nope/1 does not resolve — pass --base-branch explicitly"}`,不退化成空操作。 +- **INV-3**(仅快进语义与向后兼容) + - 有 brief 无 flag:`sync plan --name sample` → `{"rc":0,"data":{"base":"main","upstream":"origin/main"}}`——基线仍取自 brief,旧调用零变化。 + - 分叉拒绝:本地多一枚 commit 后 `sync plan --name sample` → `{"rc":1,"code":"SYNC_NOT_FAST_FORWARD"}`。 +- **既有套件**:`test/cli.test.mjs` 本轮未改动(改动已 `git checkout --` 回退)。上一次定向跑留下的参考结论:`node --test --test-name-pattern "sync (plan|fetches|blocks)"` 6/6 绿(含既有两条 sync 断言),用于佐证实现不破坏既有语义。 + +**未覆盖项(按门槛层显式登记)** + +- 全量 `npm test` 本机约 270s > 60s,未在本机跑全量;由本仓 `test.yml`(`pull_request`)在全量套件上补,PR checks 未绿前不视为交付完成。 +- 真实 squash 现场的端到端复演就在本 change 自己的收口阶段执行:合并后在 main 上以 `sync plan` + `sync execute --plan-hash` 新形态收口,届时把真实输出追加到本节。 +- #59 的 `verifiedCommit` 漂移那一半本 change 不覆盖(设计内的门),在 issue 中拆分记录。 + From ba3e71f3c2f12c9b6f20113cac5043ca5e5eb8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Tue, 13 Oct 2026 12:20:43 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(shadow):=20brief=20=E6=9C=80=E7=BB=88?= =?UTF-8?q?=E6=80=81=E2=80=94=E2=80=94published,=20PR=20#70?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../changes/20261013-fix-sync-briefless-base/brief.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md b/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md index 11ea39e..e89f999 100644 --- a/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md +++ b/shadow-docs/changes/20261013-fix-sync-briefless-base/brief.md @@ -4,7 +4,7 @@ "name": "20261013-fix-sync-briefless-base", "type": "fix", "scope": null, - "status": "reviewed", + "status": "published", "baseBranch": "main", "branch": "fix/20261013-fix-sync-briefless-base", "files": [ @@ -17,8 +17,8 @@ "repository": "stack-wuh/shadow-dev-cli", "issue": null, "issueUrl": null, - "pullRequest": null, - "pullRequestUrl": null + "pullRequest": 70, + "pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/70" }, "review": { "conclusion": "passed", @@ -27,7 +27,7 @@ }, "workflow": { "operation": null, - "checkpoint": null, + "checkpoint": "pr:70", "planHash": "2291cd9e4f75d0e08deb989a055249cf3b69ed3e1509d9d3fb166582f5d1565b", "updatedAt": null, "lastError": null,