gsd-core 种子 ID 生成重构:并行 Workstream 下基于「日期 + 随机后缀」的 SEED-YYMMDD-xxx 方案(4378)

发布时间:2026/9/28 2:22:28
gsd-core 种子 ID 生成重构:并行 Workstream 下基于「日期 + 随机后缀」的 SEED-YYMMDD-xxx 方案(4378) 【免费下载链接】gsd-coreGit. Ship. Done - Core项目地址https://gitcode.com/gh_mirrors/ge/gsd-core点击查看免费下载本篇技术指南基于 gsd-core 仓库的变更记录 .changeset/tidy-tigers-forage.md 展开。核心议题是当多个 Git worktree并行 workstream各自独立运行/gsd:capture --seed时如何保证铸造出的种子 ID 天然唯一、不依赖共享计数器并保持旧SEED-NNNID 的完整可解析性。读完本文你将掌握 GSD 种子 ID 的旧缺陷根因、新 ID 语法SEED-YYMMDD-xxx的生成算法、残余碰撞上界的数学来源以及list-seeds、audit、new-milestone等读取侧如何统一解析两种 ID。一、问题背景并行 Workstream 与「共享目录 本地可见性」的冲突GSDGit. Ship. Done的种子Seed机制用于捕获前瞻性想法forward-looking idea并附带触发条件待new-milestone扫描时自动浮现。它的工作目录是.planning/seeds/由/gsd:capture --seed写入流程定义在 gsd-core/workflows/plant-seed.md。问题出在「共享目录」与「worktree 本地可见性」的组合上.planning/seeds/目录本身被多个 worktree 共享但每个 worktree 只能看到已经合并merged进当前分支的文件。旧版plant-seed.md用ls | wc -l统计本地 worktree 可见的种子文件数来推导下一个 ID即SEED-NNN递增计数。当两个 workstream 各自从相同基线出发、在任何一方合并之前同时播种时两个 worktree 看到的文件集合完全一致于是计算出相同的下一个序号——两个SEED-NNN文件被 Git 静默合并产生重复 ID。这一缺陷在测试文件 tests/plant-seed-id.test.cjs 的头部注释中被精确记录plant-seed.mdderived the next seed id fromls | wc -l— a count of files the local worktree happens to see. Two workstreams planting before either merges computed the same id and git merged both files silently.计数型 ID 的本质问题是ID 的唯一性依赖一个「全局共享的单调计数器」但每个 worktree 没有可靠的全局视角。解决思路不是去同步计数器在分布式并行场景下代价极高而是让 ID 的生成完全不依赖其他 worktree 的存在——即每个 worktree 仅凭本地信息即可铸造出大概率唯一的 ID。二、新 ID 语法SEED-YYMMDD-xxx修复后generate-seed-id步骤定义于 gsd-core/workflows/plant-seed.md铸造的 ID 语法为SEED-YYMMDD-xxx各段含义段取值来源说明YYMMDD6 位本地日期年-月-日各 2 位date %y%m%d本地时钟任何 worktree 独立可算xxx3 个随机 base36 字符a-z0-9/dev/urandomtrhead -c 3每次播种独立抽样互不共享完整示例SEED-260914-k3x—日期 随机后缀 后续 slug对应在plant-seed.md的generate-seed-id步骤中核心生成逻辑为# Seed id: date 3 random base36 chars (the .planning/quick/ shape). # NO shared counter: .planning/seeds/ is shared, but each worktree only sees # what has merged — counting files collides across parallel workstreams (#4378). SEED_DATE$(date %y%m%d) SEED_ID for _SEED_ATTEMPT in 1 2; do # || true: tr reads an infinite stream, so head -c closing the pipe # takes SIGPIPE — harmless (head already has its 3 bytes) but fatal under # pipefail. SEED_SUFFIX$(LC_ALLC tr -dc a-z0-9 /dev/urandom | head -c 3) || true if [ ${#SEED_SUFFIX} -ne 3 ]; then echo ERROR: could not draw a random id suffix (is /dev/urandom available?) 2 exit 1 fi SEED_IDSEED-${SEED_DATE}-${SEED_SUFFIX} # Same-day regen guard, written as a find existence test (never ls glob: # under a stray nullglob that shape silently degenerates — #3409 drift guard). [ -z $(find .planning/seeds -maxdepth 1 -name ${SEED_ID}-*.md -print 2/dev/null) ] break done if [ -n $(find .planning/seeds -maxdepth 1 -name ${SEED_ID}-*.md -print 2/dev/null) ]; then echo ERROR: could not draw an unused seed id after 2 attempts 2 exit 1 fi这段脚本体现了多个刻意设计逐一说明日期段用本地时钟date %y%m%d在任意 worktree 上独立可执行不需要读取共享目录状态——这正是消除跨 worktree 依赖的关键。随机后缀用[a-z0-9]base36tr -dc a-z0-9 /dev/urandom从熵源过滤出 36 个字符的字母表head -c 3精确截取 3 字节。LC_ALLC保证字符集行为可移植。SIGPIPE 处理tr读取的是无限流head -c 3拿到 3 字节后关闭管道会让tr收到 SIGPIPE——这是无害的head已拿到所需字节但若不加|| true在pipefail下整个管道会被判定为失败。空后缀必须大声失败若抽不到 3 个字符例如/dev/urandom不可用${#SEED_SUFFIX} -ne 3检查会触发exit 1。这是为了防止铸造出SEED-260914-空后缀——所有同日种子的 ID 都会塌缩成裸日期等于把 #4378 的碰撞问题以另一种形式复活。同日再生成保护regen guard抽到后缀后用find .planning/seeds -maxdepth 1 -name ${SEED_ID}-*.md做存在性检查若该 ID 已存在最多重试 2 次。刻意写成find存在性测试而非ls glob——ls通配在nullglob开启时会静默退化成空匹配#3409 漂移防护而find的-print输出不受该 shell 选项影响。双次重试仍碰撞则失败2 次尝试后仍命中已有文件就exit 1绝不静默写出重复 ID。三、残余碰撞上界为什么 1/46656 是「已接受的」风险新方案并非零碰撞而是把跨 worktree 的结构性碰撞计数相同导致必然碰撞替换成了概率性的同日随机碰撞。变更记录中给出的上界是每对约 1/4665636^3 46656即随机后缀是 3 个 base36 字符总共有 46656 种组合。两个 workstream 在同一天、彼此都尚未合并的情况下各自播种抽到相同后缀的概率为 1/46656。这一数量级正是.planning/quick/快速任务方案早已接受的碰撞上界——也就是说种子 ID 现在复用了项目内既有的、经过验证的 ID 随机化模式变更记录原文“the same bound the.planning/quick/scheme already accepts”而不是引入一种全新的、未经验证的唯一性策略。对比旧方案计数型 ID 的碰撞是确定性事件双方看到相同文件数 → 必然同号新方案的碰撞是极低概率的随机事件且同一 worktree 内还有同日重生成保护兜底。从「必然碰撞」到「概率碰撞」是一次质的改善。四、向后兼容旧 SEED-NNN 的完整解析链路变更记录强调现有的SEED-NNN种子继续在 list、enrich、new-milestone 扫描、audit 中可解析。这意味着读取侧必须同时理解两种语法旧语法SEED-NNN 如 SEED-081计数时代产物 新语法SEED-YYMMDD-xxx 如 SEED-260914-k3x日期 随机后缀读取侧的统一解析函数是deriveSeedIdentity实现在 src/commands.ctscmdListSeeds与 audit 共用。其核心是三条正则见 src/commands.ctsconst CANONICAL_SEED_ID_RE /^SEED-(?:\d{6}-[a-z0-9]{3}|\d)$/i; const SEED_ID_PREFIX_RE /^(SEED-(?:\d{6}-[a-z0-9]{3}|\d))/i; const SEED_SLUG_RE /^SEED-(?:\d{6}-[a-z0-9]{3}|\d)-(.)$/i;deriveSeedIdentitysrc/commands.cts的判定顺序为frontmatterid:优先若文件 frontmatter 的id:字段命中CANONICAL_SEED_ID_RESEED-YYYYMMDD-xxx或SEED-NNN则以它为 canonicalseed_id文件名前缀回退frontmatter 缺失或不合语法时从文件名 stem 中提取SEED_ID_PREFIX_RE匹配的前缀作为 IDslug 提取用SEED_SLUG_RE从SEED-…-slug中切出 slug 描述段。这里有一个关键的细节也是 #4378 的回归风险点文件名前缀回退必须保留完整的SEED-YYMMDD-xxx绝不能截断到日期前缀。若截断到SEED-260914那么同一天播种的所有新格式种子在 frontmatter 缺失时都会被解析成同一个 ID——这正是 #4378 想消灭的碰撞以另一种形式回归。测试 tests/list-seeds.test.cjs 与 tests/list-seeds.property.test.cjs 用「5 位日期解析为 legacy、7 位日期解析为 legacy、2 字符后缀不解析为新格式、新格式 ID 端到端 canonical」等边界样例把这一行为钉死。4.1 大小写宽容文档展示与运行时 ID 的差异文档与工作流输出中新格式 ID 常展示为大写占位符SEED-YYMMDD-XXX而运行时铸造的是小写 base36tr -dc a-z0-9只产出小写。因此--enrich的目标提取正则必须大小写宽容。plant-seed.md的parse-idea步骤使用ENRICH_MATCH$(echo $ARGUMENTS | grep -oE \-\-enrich[[:space:]]SEED-[0-9](-[a-zA-Z0-9]{3})? | head -1)该模式的关键设计锚定--enrich标志不能取$ARGUMENTS中最靠左的SEED-[0-9]——那会截断大写/畸形后缀到日期段并可能错误地 enrich 一个无关的同日种子测试用例see SEED-5 first --enrich SEED-7验证了提取器必须跟随标志而非最左 ID。捕获完整 IDSEED-[0-9](-[a-zA-Z0-9]{3})?同时匹配旧格式SEED-081无后缀和新格式SEED-260914-K3X带 3 位后缀。歧义即失败若一个目标匹配到多个种子文件grep -c .大于 1直接报错退出绝不用head -1随机挑一个旧格式重复 ID 无法靠更长 ID 消歧需人工重命名重复文件新格式则要求用完整 ID 重跑。4.2 读取侧与写入侧的语法一致性由测试强制写入侧plant-seed.md的generate-seed-id与读取侧commands.cts的deriveSeedIdentity是「一份语法、两个表面」。为防止两侧漂移tests/plant-seed-id.test.cjs 底部提供了一个property parity 测试从铸造现场的date %y%m%d指令和head -c 3宽度中解析出日期宽度与后缀宽度再用 fast-check 随机生成 200 组SEED-{date}-{suffix}-{slug}组合逐一断言deriveSeedIdentity能原样还原seed_id与slug。任何一侧的宽度漂移都会在此失败。五、audit 扫描与 list-seeds 的 ID 一致性收敛变更记录还包含一个读取侧的收敛细节audit 的种子扫描现在发布与list-seeds相同的 canonical ID其 acknowledge 用任一 ID旧或新都能解析到同一个文件。在 src/audit.cts 中scanSeeds#4378 roll-in通过调用commandsModule.deriveSeedIdentity复用同一解析函数而不是自己另写一套 ID 推导扫描时src/audit.cts先判断文件名是否为良构种子名seedIdMatchRawName再对每个SEED-*.md文件调用deriveSeedIdentity——frontmatterid:命中种子语法旧SEED-NNN或新SEED-YYMMDD-xxx时取 canonical ID否则回退文件名前缀。此前 audit 与 list-seeds 各自推导 ID、彼此不一致会导致 deferral 误归档acknowledge 时src/audit.cts--seed-id参数可能以任一形态到达audit 列表里看到的是新 ID用户可能凭记忆输入旧 ID。代码对.planning/seeds/下每个SEED-*.md推导其 derived ID只要derivedAckId seedId || stem seedId即视为命中最终用requireSafePath约束目标必须在规划目录内PathAcceptance.AbsoluteInsideRoot后写 acknowledge。这保证了「list 里显示什么audit 里就能解析什么」且新旧 ID 都能定位到同一文件。六、端到端使用链路6.1 播种/gsd:capture --seed命令 commands/gsd/capture.md 将--seed标志路由到plant-seed工作流落盘路径为.planning/seeds/SEED-YYMMDD-xxx-slug.md写出的种子文件骨架frontmatter 默认值--- id: {SEED_ID} status: dormant planted: {ISO date} planted_during: {current milestone/phase from STATE.md, or unknown if not in a GSD project} trigger_when: when relevant scope: unknown --- # {SEED_ID}: {$IDEA}plant-seed是一次性one-shot捕获文件立即写入不因任何提问而阻塞。trigger_when默认when relevant种子会在任意 new-milestone 扫描中浮现用户可稍后用--enrich收窄scope默认unknown。随后自动收集面包屑从想法文本抽取关键词、grep 代码库、检查 STATE.md/ROADMAP.md/todos/并提交 git。6.2 增补/gsd:capture --seed --enrich SEED-…--enrich走enrich-seed步骤以AskUserQuestion或--text文本模式收集 Trigger、Why、Scope 三项回填 frontmatter 与正文段落并再次提交。enrich 不会重铸 ID——目标种子文件已存在ID 保持不变。6.3 列出与审计/gsd:capture --list-seeds只读工作流 gsd-core/workflows/list-seeds.md 调用gsd_run list-seeds $STATUS_FILTER支持可选状态过滤如dormant、active、triggered按seed_id排序渲染表格ID Status Scope Trigger Title SEED-001 dormant large when websockets land Real-time collaboration SEED-006 triggered medium MILE-04 planning Remove legacy auth cratescount为 0 时给出引导文案状态过滤无匹配时明确提示。该流程全程只读、绝不改动种子文件。6.4 自动浮现new-milestone的种子扫描gsd-core/workflows/new-milestone.md 的 Step 2.5「Scan Planted Seeds」用ls .planning/seeds/SEED-*.md扫描种子提取标题、trigger_when与planted_during与里程碑目标比对。匹配的种子经用户或--auto模式全选确认后进入 Step 9 的需求定义作为额外输入未选中的种子原样保留绝不删除或修改。种子 ID 无论新旧格式在此扫描中均可正常解析。七、回归防线围绕 #4378 的测试矩阵本修复的回归测试分布在三个文件中构成「写入契约 → 读取契约 → 属性一致性」三层防线测试文件验证内容tests/plant-seed-id.test.cjs写入侧契约date %y%m%d与tr -dc a-z0-9head -c 3必须存在空后缀必须 abort同日 regen guard 必须是find存在性测试而非ls glob旧计数时代的wc -l、NEXT$((EXISTING 1))、printf %03d、SEED-{PADDED}占位符全部不得残留--enrich提取必须锚定标志、捕获完整 ID、大小写宽容、歧义即失败parity 属性测试保证写出的每种 ID 都能被读取侧原样还原tests/list-seeds.test.cjs读取侧行为新格式 ID 是 canonical 的、不被截断到日期前缀legacy 计数 ID 与新格式 ID 可共存文件名回退保留完整新格式 ID大写新格式 ID 端到端 canonicaltests/list-seeds.property.test.cjs属性测试新格式 ID 完整 round-trip缺失 frontmatter 时回退到完整新格式前缀5 位/7 位日期、2 字符后缀等边界解析为 legacy防止正则宽度漂移值得强调的是「写入侧与读取侧各自独立测试、再以 parity 互锁」的做法写入语法与解析语法各有一份行为契约任何一侧修改而另一侧未同步都会在测试矩阵中暴露——这正是变更记录中“writer and reader grammars must not diverge”的实现方式。八、小结#4378 的修复把种子 ID 的唯一性从「共享目录中的单调计数」跨 worktree 结构性碰撞改为「本地日期 随机 base36 后缀」概率性碰撞上界 1/46656与.planning/quick/既有方案一致同时写入侧plant-seed.md的generate-seed-id只用本地信息铸造 ID附带 SIGPIPE 容错、空后缀大声失败、同日find重生成保护读取侧commands.cts的deriveSeedIdentity 三条正则统一解析新旧两种语法文件名前缀回退保留完整新格式 ID一致性audit 扫描与list-seeds共享同一 canonical ID 推导acknowledge 对任一形态 ID 解析到同一文件回归防线三层测试矩阵 写入/读取语法 parity 属性测试杜绝两侧漂移与旧计数时代的占位符残留。对于在多个 worktree 中并行推进 workstream 的团队这套方案意味着播种不再需要「先合并再看数」每个 worktree 可以随时独立安全地铸造种子 ID并把同日碰撞的概率控制在可接受的数量级内。相关实现与测试可直接在仓库中继续研读gsd-core/workflows/plant-seed.md、gsd-core/workflows/list-seeds.md、src/commands.cts、src/audit.cts、tests/plant-seed-id.test.cjs。赞分享【免费下载链接】gsd-coreGit. Ship. Done - Core项目地址https://gitcode.com/gh_mirrors/ge/gsd-core点击查看免费下载相关推荐gsd-core 种子捕获契约修复/gsd-explore 委托 plant-seed 工作流生成真实 SEED 记录gsd core 种子捕获契约修复/gsd explore 委托 plant seed 工作流生成真实 SEED 记录 导读本文围绕 gsd core 仓库Hugo collections.D 函数详解基于种子生成有序不重复随机整数Hugo collections.D 函数详解基于种子生成有序不重复随机整数 collections.D 是 Hugo 0.149.0 引入的集合类模板函数开发工具前端CLIgsd-core 的 Changeset Fragments 机制基于每 PR 变更片段的 CHANGELOG 自动生成方案gsd core 的 Changeset Fragments 机制基于每 PR 变更片段的 CHANGELOG 自动生成方案 核心导读 本篇文章围绕 gsd上一篇终极Git图形客户端Gitnuro让你的版本控制体验焕然一新下一篇E2FGVI 项目安装与使用教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考