架构演进:ADR-3942 将确认从工作区文件迁移到 Commit Trailer 的完整设计与实现)
【免费下载链接】gsd-coreGit. Ship. Done - Core项目地址https://gitcode.com/gh_mirrors/ge/gsd-core点击查看免费下载导读本文以 gsd-core 仓库的 ADR-3942 为骨架系统讲解发射产物漂移确认emitted-drift acknowledgment如何从提交到工作区的 JSON 文件演进为PR 自身提交上的 git commit trailer。你将理解为什么这个数据天然只有 PR 生命周期、为什么.changeset/类比在语义层失效、commit trailer 语法与解析规则Emitted-Drift-Ack-Hash/Emitted-Drift-Ack-Growth、merge-base 范围语义三圆点 vs 两圆点为何是成败关键以及staleAcks、双键空间、失败关闭fail-closed等机制如何与 tests/helpers/emitted-diff.cjs 中的守恒定律conservation law和 tests/helpers/emitted-runtime.cjs 中的 IO 外壳协同工作。读完本文你可以直接在 gsd-core 的 PR 流程中编写、阅读并验证自己的漂移确认 trailer。背景发射产物守恒定律与逃生舱要理解 ADR-3942必须先回到它取代的设计——ADR-2719。该 ADR 提出用计算守恒定律取代提交黄金 fixtures在nextHEAD 与 PR HEAD 分别构建发射清单emitted manifest凡是哈希发生移动的发射路径都必须能够通过声明式溯源表provenance table归属到该 PR 实际修改的路径上。无法归属的 delta 是硬失败并且要指名道姓地列出39 emitted paths changed that nothing in this diff explains: gsd-core/workflows/execute-phase.md agents/gsd-planner.md ...这套机制被刻意设计为相对不变量relative invariant而非绝对不变量——只要改了一个转换器converter那些源文件没人碰过的发射字节也会移动这就是 ADR-2264 所说的git 无法审查的约 5%它必须有一条出路。ADR-2719 §3 给出了出路逃生舱是一个承诺式文档committed acknowledgment而不是一个 flag。它刻意不是环境变量或 CLI 开关——因为面对红色闸门贡献者本能地会去设一个 flag这正是UPDATE_GOLDEN1今天的样子。该设计的关键属性是确认文件只在某处意外波纹扩散时出现在 changed-files 列表里——触碰确认本身即是警报。它把一个无声的重新生成silent regeneration转化为一个显眼的声明。然而ADR-2719 §3 对逃生舱的存储位置做出了错误的选择它选对了形状一段散文式声明而非 flag却选错了载体提交到工作区的文件。ADR-3942 只改存储不改守恒定律本身。ADR-2719 的 §1、§2、§4–§7 继续保留并被依赖。问题数据的生命周期与存储位置不匹配ADR-3942 的核心诊断只有一句话一个确认acknowledgment解释的是某一个 PR 的不可归属 delta一旦该 PR 合并它解释的 delta 已经进入 base这个确认再也无法清除任何东西。也就是说这份数据的生命周期是 PR 级的却被存储在永久、共享、走 merge 路径的状态里。被删除的tests/emitted-drift-acks/目录的 README 第 6 行早已言明健康稳态该目录现已按 §6 删除This directory being empty is the healthy steady state. A fragment appearing in a diffisthe alarm; a fragment sitting here onnextis spent cruft.这个目录为空才是健康的稳态。fragment 出现在 diff 里就是警报躺在next上的 fragment 是已消耗的垃圾。该家族中的每一个缺陷都源于这一错配而每次修复又制造了下一个缺陷。ADR 用一张表记录了完整的失败谱系#修复它制造的缺陷—单一tests/emitted-drift-ack.json每个 PR 一个共享可变文件。一个开放队列里 6 个冲突 PR 中有 5 个撞在这个文件上别的都没撞CONTRIBUTING.md 原 1086 行#2914拆分为每 PR 一个 fragment仿照.changeset/fragment 不共享文件但共享一个path-key 命名空间#3078守卫让next因已消耗 fragment 变红45 个 fragment 拥有 403 个路径每个已消耗 fragment 都堵死了下一个要在它的某个 key 上生长的 PR#3842清扫已消耗 fragment给三个进行中的外部 PR#3330、#3774、#3648各制造一个modify/delete冲突需要--defer-to-open-prs#3823手写清扫分支时计算、合并时评估守卫输给了 #3809 的竞态让next连续24 次推送保持红色#3875定时清扫器ack-fragment-sweep.yml它自己的 PR 无法合并自己见下文为什么.changeset/是错误类比#2914 的理由是独立命名的 fragment 不会冲突就像.changeset/一样。但 changeset fragment 是真正独立的——两个 fragment 永远不会命名同一个实体。而 ack fragment 的 key 进入的是一个共享命名空间两个源声明同一个 key 是硬性的重复键错误原 scripts/lint-emitted-drift-ack.cjs 872–885 行该脚本已按 §6 删除。这个类比在文件系统层成立在语义层失效——#3078 测量了代价。自动化无法闭环清扫器首次生产运行的三重缺陷PR #3927——清扫器的第一次生产运行——在开启后 2 小时 38 分被人工合并此时validate-title和Required tests仍是红色。三个独立、确定性的缺陷没有一个属于 flaky.github/workflows/ack-fragment-sweep.yml第 266 行硬编码chore: sweep spent ack fragments from next (${SHORT_SHA})而 scripts/release-notes/conventional-title.cjs 第 28、92 行要求 type 之后紧跟(#issue)。括号在末尾永远不会被解析为 scope而 git sha 不含#——每次运行都失败。清扫的 diff 按构造就是tests/emitted-drift-acks/下的删除。而 scripts/ci-test-scope.cjs 没有任何规则匹配该路径于是classify()落入 #408 的 fallback把unit套件哨兵丢进不分片、15 分钟封顶的车道。运行日志suiteall files827在第 13/14 块被杀死跑了 15 分 18 秒对撞timeout-minutes: 15。不存在自动合并路径。工作流止步于gh pr create加标签。这些缺陷单独都可修。ADR 把它们列出来不是作为问题本身而是作为问题形状的证据一个需要自己的 CI 车道、自己的标题规范、自己的合并故事的垃圾回收器是一大堆机制来支撑一个正确稳态是不存在的工件。当前设计中的一个潜在缺陷两个键空间只是约定在旧设计中两个键空间只是约定层面的哈希波纹hash ripple按发射路径取键ackEntries.has(rel)tests/helpers/emitted-diff.cjs 第 597 行增长growth按裸文件名取键ackEntries.has(name)第 632 行。两者读的是同一个pathsmap没有任何 schema 差异——旧tests/emitted-drift-acks/README.md第 20–23 行记录了这种拆分但没有任何机制强制执行它。一个打算放进某个空间的 key会静默满足另一个空间的查找。决策六条设计决定1. 确认移入 PR 自身提交的 commit trailerADR-3942 的第一条决定是把确认从工作区文件迁移到 commit trailerEmitted-Drift-Ack-Hash: emitted/path — reason Emitted-Drift-Ack-Growth: filename — reason读取范围是git log $(git merge-base base HEAD)..HEAD——即 PR 自己的提交且只有这些提交。Amendment#3942 实现2026-08-27本节最初写的是git log base..head未说明范围语义。两圆点会是缺陷。changedPaths来自git diff base...HEAD——三圆点即 merge-base——所以两圆点的 ack 范围会让确认集合与变更集合在哪些提交属于这个 PR上不一致trailer 就可能为不在 diff 里的 delta 开脱。§2 中已消耗变为结构性的主张也完全依赖 merge-base正是它让已合并的 trailer 按构造落到范围之外。这一主张被一个测试钉死测试 fork 一个 topic 分支、在两侧各放一个 trailer、断言只有 topic 侧的 trailer 被读取。这个设计保留了 ADR-2719 §3 真正在意的东西。其声明的设计属性是确认文件只在某处意外波纹扩散时出现在 changed-files 列表里……触碰确认就是警报。 Trailer 仍然是一个显眼、可审查、携带散文的声明出现在 PR 的 diff 中——它不是 §3 拒绝的UPDATE_GOLDEN1flag。变化在于声明不再比它声明的东西活得久。在源码中这个语法的实现位于 tests/helpers/emitted-diff.cjs 第 98–110 行/** Trailer key naming an emitted PATH whose HASH moved (grammar: 40-design.md). */ const ACK_TRAILER_HASH Emitted-Drift-Ack-Hash; /** Trailer key naming a bare workflow/agent FILENAME that grew. */ const ACK_TRAILER_GROWTH Emitted-Drift-Ack-Growth; /** Key/reason delimiter: space, EM DASH (U2014), space — split on the FIRST occurrence only. */ const ACK_TRAILER_DELIM — ;语法是key — reason只在第一个—空格 全角破折号 U2014 空格处切分因此 reason 本身可以包含更多破折号。renderAckTrailer与parseAckTrailers互为逆操作round-trip 纪律文档里教出的示例永远不可能与解析器实际接受的内容漂移。解析器parseAckTrailers第 687–768 行逐值检查以下错误并大声命名违规 trailername them and say why来自 ADR-2719 §3缺少—分隔符空 key空 reasonkey 属于保留集RESERVED_ACK_KEYS new Set([__proto__, constructor, prototype])——这些既是 JS 对象 footgun也永远不可能是合法路径key 包含、或空白——这样文档里像emitted/path — reason这样的占位符示例永远不会把自己武装成一个活的然后过期的ack。同一 key 在同一空间内声明两次若规范化去掉不可见字符、折叠空白见normalizeAckReason与INVISIBLE正则后 reason 相同则静默去重、保留首次声明若 reason 真正不同则是硬错误declared twice with ambiguous, conflicting reasons该 key 从该空间整体移除——模棱两可的声明绝不能静默地选一个赢家。两个空间hash/growth相互独立同一个 key 可以合法地同时出现在两个空间。去重后还有一个上限MAX_ACK_TRAILERS 128第 649 行按去重后的 distinct (key, reason) 数计数而非原始输入数——因为 trailer 与旧 fragment 文件不同它合法地经受 rebase相同的 trailer 文本会随每个被 rebase 的提交向前携带git log会对它所在的每个提交各报一次。按原始计数会把只是被 rebase 了很多次、但每个值都塌缩成同一 map 条目的完全合法的分支误杀。计数去重后真正存活的东西才让上限意味着distinct 声明太多而不是git 对象碰巧携带这段文本太多。超限仍然 throw、绝不截断——截断的读取会静默丢弃确认。2. 已消耗spent这个概念消失#2789 Amendment 构建了 spent 检测是因为文档持久存在于 base 上所以staleAcks无法区分从未解释任何东西与它的波纹已被吸收进 base。把 trailer 限定在base..head范围让这个区分从计算变成结构性位于 PR 提交范围内的 trailer 按定义就是这个 PR 的没有 base 侧的副本可以比较。这是比 #2789 想要的严格更强的形式。readAckFileAtRef、listAckFragmentFilesAtRef、readAckSourcesAtRef、spent/re-arm 散文规范化以及assertNoAllSpentFragments全部变成不可达代码。但staleAcks本身被保留——一个声明了某个 key、却没有 delta 消费它的 trailer 仍然是错误tests/helpers/emitted-diff.cjs 第 648 行。这个检查是每 PR 的不依赖持久化。实现位于diffEmitted末尾第 433–436 行且按空间报告{key, space}space 取hash或growth这样错误信息能说出是哪个 trailer 声明了未消费的 key。字符串前缀约定hash:key被刻意拒绝——它把命名空间编码进一个消费者必须记得剥离的字符串正是 #3942 设计在别处明确拒绝的约定而非代码弱点。旧设计的baseAck/spentAcks机制及其调用者readAckFileAtRef、readAckSourcesAtRef等已按 §6 连同删除。3. 两个键空间在结构上变得互不相同两个 trailer key 取代了一个 map。增长确认不再可能因为命名巧合而满足哈希查找——上文潜在缺陷被关闭而非携带向前。在diffEmitted中tests/helpers/emitted-diff.cjs 第 244 行起ackHash与ackGrowth是两个独立参数都是Mapstring, {reason}且都经过类型守卫第 273–278 行传入{}、null或普通字符串会得到指名道姓的错误 verdict而不是未处理的 TypeError——这与baseline/current/changedPaths已受的守卫属于同一崩溃类#2778 形状守卫的 #3942 扩展。哈希通道只查ackHash第 374 行增长通道只查ackGrowth第 412 行。跨空间豁免——把一个只该出现在增长空间的裸文件名写进Emitted-Drift-Ack-Hash或反之——绝不能开脱任何东西这正是旧单 map 设计中的潜在缺陷旧设计第 3/6 行所记。4. 纯定律pure law不变diffEmitted接收的本来就是普通的Mapkey, {reason}。存储介质完全活在 tests/helpers/emitted-runtime.cjs 的 IO 外壳之后。把readAckSources换成 trailer 读取器只是一个 adapter 替换tests/helpers/emitted-diff.cjs——定律本身——除了 §3 的键空间拆分外原封不动。这个模块被刻意做成纯函数这正是它可测试性的来源无 fs、无 git、无安装器、无时钟。每个 #2723 的失败优先准则都是毫秒级表测试而且 Stryker 变异测试闸门能咬住它——一个 20 分支的纯函数是可变异测试的一个 40 分钟的集成测试不是。它从不重新派生任何字节ADR-2719 §1 明确说断言emitted transform(source)是 ADR-2264 Amendment 拒绝的同义反复它只约束哪些 key 被允许移动。它也从不修改仓库状态不重新生成、不自动 ack——UPDATE_GOLDEN1正是该设计移除的逃生舱。仓库内有该机制的现成先例gsd-core/workflows/ship.md 第 312–320 行已经在用git log --format...%(trailers:keygate-status,valueonly,separator%x2c)...解析gate-status:trailer以在 squash-merge 丢弃之前重建每提交的 TDD 门控轨迹。它同样锚定在 merge-base 上RANGE_BASE$(git merge-base ${BASE_BRANCH} HEAD)并同样坚持用 git 原生 trailer 机制而不是对%B做原始 grep——后者会连散文里写到的字符串一起匹配。ACK trailer 读取器沿用了完全相同的控制字符分隔符纪律\x1e/\x1f/\x1d确保它们永远不会与真实 trailer 内容冲突。5. PR 测试车道必须能取到提交范围闸门必须能看到 PR 的提交范围并且在其不可用时失败关闭fail closed。Amendment 1——前提是错的#3942 实现2026-08-27本节最初断言.github/workflows/test.yml第 107–110 行——testjob——没有fetch-depthkey因此以 depth 1 检出并把fetch-depth: 0列为必需变更。这是假的也不需要任何工作流变更。第 107 行位于lint-testsjob 内部矩阵testjob——真正运行tests/emitted-attribution.test.cjs的那个——从test.yml:130开始并且已经在它的 Windowsv5.0.1和 Linux/macOSv6.0.2两个 checkout 步骤上都设置了fetch-depth: 0。这个说法是从一个没有对照 job 边界核实的行引用进入本 ADR 的。该要求作为需要保持的属性而非要做的变更成立如果哪天那个fetch-depth: 0被移除读者读取器仍必须失败关闭。Amendment 2——失败模式被误述#3942 实现2026-08-27本节最初把 depth-1 失败模式称为空洞通过vacuous pass。这对fragment守卫成立对trailer 读取器则不成立而这个说法是被不加批判地照搬过来的。对 fragmentdepth-1 让每个 fragment 读起来都是全新的——因此是活的——守卫通过假绿。对 trailer一个无法计算的范围产生零个确认所以需要确认的 PR 会失败假红。只要读取器 throw 而不是返回空集depth-1 失败在两个方向都是响亮的这是本 ADR 低估的一个真正改进。fetch-depth: 0仍然必需忘记它如今只是碍事而非危险。这个 throw 被一个测试钉死该测试构建一个真实的 shallow clone而不是模拟一个。我在当前仓库验证了这一点.github/workflows/test.yml的changesjob第 124–126 行与矩阵testjob第 360、367、539、632、639、822 行等多处都带fetch-depth: 0。6. 要删除什么ADR-3942 §6 列出了随本设计一起删除的整套机制.github/workflows/ack-fragment-sweep.yml237 行guard-no-ack-on-next.github/workflows/test.yml原 865–920 行scripts/lint-emitted-drift-ack.cjs937 行及其package.json:124的lint:ci调用tests/emitted-drift-acks/及其 READMEtests/helpers/emitted-runtime.cjs的 at-ref/spent 半边我已在当前仓库确认这些删除已经落地scripts/lint-emitted-drift-ack.cjs、tests/emitted-drift-acks/目录、.github/workflows/ack-fragment-sweep.yml均不存在。而 tests/emitted-attribution.test.cjs 第 2775–2778 行有一整段注释记录了这一点// ADR-3942 §6 deletes the legacy pre-merge lint / guard-no-ack-on-next / the #3842 and // #3875 open-PR-deferred fragment-sweep machinery, plus tests/emitted-drift-acks/ itself // — the commit-trailer acknowledgment leaves no tree artifact to sweep, defer, or guard. // Every describe block that lived here exercised that now-deleted script and is gone with it.读取器的真实实现readAckTrailersemitted-runtime.cjs 第 920–998 行实现了核心读取器readAckTrailers({ baseRef, headRef HEAD, ... })值得单独拆解为什么在这里解析 merge-base 而不是照单全收baseRefchangedPathsresolveChangedPaths按构造就是三圆点的merge-base而 40-design.md 的 Correction 2 要求 trailer 范围与之对齐——否则 trailer 可以为结构上位于 diff 之外的 delta 开脱。读mergeBase..headRef绝不读baseRef..headRef才让 fork 另一侧的 trailer 正确地落到范围之外row 7/8——结构性的 spentness。tests/emitted-ack-trailer.test.cjs 第 478–493 行用真实 git fixture 钉死了这一点在 fork 点两侧各放一个 trailer断言main-key.mdbase 侧读不到、topic-key.mdtopic 侧读得到。为什么不可计算的范围 THROW 而不是返回空shallow clone或任何与headRef无共享历史的 ref让 merge-base 无法计算。这里返回空结果会被读成不需要 ack——一个静默解除闸门的假绿。因此每个失败路径都 throw并指名range/merge-base/shallow。测试用git clone --depth 1构建了一个真实的 shallow clone第 773–783 行断言readAckTrailersthrow还用一个notARepo目录断言 git 失败被暴露而非吞掉第 786–795 行以及一个故意短到不可能完成的时间上限20ms 5 万提交的fast-import历史证明读不完的 git 调用必须 throw 超时绝不挂起第 806–814 行。取值用 git 自己的 trailer 解析器%(trailers:key...,valueonly,separator%x1d)绝不解析消息体——这样消息中间只是提到trailer 语法row 32本 PR 自己的文档就在教这个语法时正确地保持惰性。每个值在解析前剥离\rCRLF 提交消息必须与 LF 解析一致。一个值得一提的%x陷阱%(trailers:...)内部的separator本身是一个 pretty-format 字符串git 会在其中替换%xhex转义。若只写裸的十六进制码不带%x前缀git 会把它作为两个字面字符输出随后的.split()永远找不到真正的分隔符——同一 key 的两个 trailer 在一个提交上塌缩成一个拼接值静默数据丢失正是MAX_ACK_TRAILERS存在的目的。修复是显式发出%x转义第 948 行。squash-merge 掩埋#4454 后续%(trailers:...)只识别真正是提交消息终端块的 trailer 块——连续的Token: value行一直延伸到消息结尾后面什么都不许有。GitHub 的 squash-merge 提交体是每个组成提交的 subjectbody 按顺序拼接后面再跟上它自己附加的---------分隔符和Co-authored-by:trailer。两个落在携带 ack 的提交之后的独立提交——包括 GitHub 无条件附加在每个squash 提交后的后缀——会静默掩埋它git 的解析器从后向前扫描到第一个不合规行就停。已针对真实 squash 提交gsd-core 78013b3b74确认%(trailers)只返回 GitHub 自己的两行Co-authored-by:。修复extractSquashBuriedTrailers第 855–885 行对原始消息的每个 squash 子弹子块独立跑git interpret-trailers --parse与%(trailers:...)相同的底层算法但能通过 stdin 对任意文本运行每个原始提交自己的终端 trailer 块都能按自己的条件被找到。作用域收得很紧以免重新引入%(trailers:...)被选中要防的假阳性类只有当原始消息体包含 GitHub 特有的 squash 后缀标记空行 9 个连字符独占一行 空行时才激活子块通道。普通提交的消息体里若恰好含 markdown 子弹则完全不匹配、完全不受影响。且后缀标记的查找使用findLastGithubSquashSuffixIndex找最后一个匹配防止一个早期子弹自己合法地使用---------形状的 markdown 水平线时提前截断beforeSuffix。闸门接线三个before输入被钉在同一个 merge-base 上emitted-runtime.cjs 的resolveAttributionInputs第 1027–1044 行把差分闸门的三个before输入全部钉到resolveAttributionBase返回的同一个merge-base sha#5008baselineresolveBaselineFn、changed-path 范围resolveChangedPaths、ack-trailer 范围readAckTrailers。三者共享一个起点否则HEAD fork 之后只落在 base 侧的变更会出现在 baseline-vs-current 里而merge-base..HEAD中没有路径能解释它——这正是release.ymlfinalize 车道撞上的形状next在create与finalize之间吸收了 #4937在nexttip构建的 baseline 把那笔合并归到了 release 分支头上。传解析出的sha而非 ref 名字是同一 bug 的另一半ref 名字会在每次调用内部重新解析所以运行中途的一次 fetch 可能把 changed-path 范围和 ack 范围移到比 baseline 构建所在提交更新的提交尽管三者都从同一个attributionBase出发。sha 是惰性的——没有任何东西能让它解析到别处。每个依赖都被注入resolveBaselineFn、buildBaselineFn、readJson让这段此前内联且无法测试的接线本身可测。而 base 解析的候选链baseRefCandidates第 312–320 行解释了为什么范围不可用不是罕见边缘GSD_EMITTED_BASE环境变量可指名 refGITHUB_BASE_REF由 Actions 在pull_request事件上设置gsd-test runner 的 shallow clone 容器里根本没有origin/*GitHub Actions 的 checkout 默认不为其他分支创建 remote-tracking 分支这正是changeset-required.yml带显式git fetch步骤的原因。后果冲突面归零。不是更小——确认不再是 tree 对象所以它不可能在文件、key 命名空间或 modify/delete 上冲突。--defer-to-open-prs变得不必要而非仅仅正确。next不再可能被文书工作染红。把它染红的守卫随它守卫的状态一起被删除。确认不会活到next上而这正是重点。仓库允许 squash、merge 和 rebaseallow_squash_merge、allow_merge_commit、allow_rebase_merge均为 truesquash_merge_commit_message: COMMIT_MESSAGES且 gsd-core/workflows/ship.md 第 306 行附近把每提交 trailer 视为在 squash-merge 下不可靠存续。在COMMIT_MESSAGES下文本确实会拼进 squash 体但那是可变仓库设置、且合并者可以编辑体所以本 ADR 不在next上主张任何持久审计记录。确认在 PR 期间被读取——那是它唯一有意义的窗口。本设计早期的一个表述曾声称 trailer 永久且可审计与今天相同那个主张是错的已在这里撤回而不是随船出海。修改一个确认意味着修改一个提交。编辑文件比重写历史便宜。这是真实的体验代价但它同时是一个正确性属性确认不可能与它解释的 diff 漂移失同步因为改变任一方都会改变 sha 并重新运行闸门。审查体验变化。审查者在提交消息里而不是文件 diff 里读确认。GitHub 在 Commits 标签页渲染提交消息而不是内联在 Files 标签页——比变更文件更不显眼。缓解闸门的失败输出本身就指名了自己的补救办法CONTRIBUTING.md而 trailer 文本出现在 PR 自己的提交列表中。本地运行零网络。git log base..head不需要任何 API 调用——这是 label 方案和 body 方案都被拒绝的原因两者在 CI 运行后都可变、都需要测试侧做一次带认证的 API 调用而且 label 比逐 key 的散文更粗——一个豁免这个 PR的 blanket label 会让真正的回归搭便车。闸门失败消息中的自服务补救文本REMEDIATIONtests/helpers/emitted-diff.cjs 第 127–176 行也随设计更新它教的是 Add a trailer to a commit in this PR (never a new file)分Emitted-Drift-Ack-Hash与Emitted-Drift-Ack-Growth两个按空间选取的字段一个只触发增长分支的报告绝不能去教哈希 trailer反之亦然并附 Do NOT regenerate anything to silence this — there is nothing left to regenerate.。staleAcks的补救是 Remove the trailer, or correct it to name the ripple you actually made.。何时需要重新审视ADR-3942 明确列出的三个revisit if条件squash-merge 停止保留提交体且需要某种具体用途的持久 on-next审计轨迹。今天没有任何东西消费它。trailer key 空间需要两个以上成员——那将暗示归属表ADR-2719 §2存在一个逃生舱正在吸收的缺口。testjob 上的fetch-depth: 0可测量地拖慢 PR 车道。实现证据一览对希望深入源码的读者以下是本文引用的全部关键路径关注点位置ADR 本体docs/adr/3942-emitted-drift-ack-commit-trailer.md被取代的守恒定律设计§1/§2/§4–§7 保留docs/adr/2719-emitted-artifact-attribution.md纯定律diffEmitted、parseAckTrailers、renderAckTrailer、REMEDIATION、MAX_ACK_TRAILERStests/helpers/emitted-diff.cjsIO 外壳readAckTrailers、resolveChangedPaths、resolveAttributionBase、resolveAttributionInputs、extractSquashBuriedTrailerstests/helpers/emitted-runtime.cjstrailer 语法/边界/失败模式的 37 行测试矩阵tests/emitted-ack-trailer.test.cjs守恒定律端到端测试merge-base 钉扎、§6 删除记录tests/emitted-attribution.test.cjsin-repo trailer 解析先例gate-status:gsd-core/workflows/ship.mdCIfetch-depth: 0配置.github/workflows/test.ymlclassify()的 #408 unit fallback 语义scripts/ci-test-scope.cjs失败谱系中的CONTRIBUTING.md引用与门控补救文本CONTRIBUTING.md实践要点回顾当你的 PR 触发emitted-attribution闸门且失败信息列出不可归属的发射路径时正确的回应是往 PR 的某个提交消息里追加一个 trailergit commit --amend或新增提交均可形如Emitted-Drift-Ack-Hash: 发射路径 — 为什么这次波纹是有意的当它报告某个 workflow/agent 文件增长未确认时用Emitted-Drift-Ack-Growth: 裸文件名 — 为什么这次增长是有意的。key 必须精确匹配报告中打印的路径/文件名reason 里写真实的解释占位符式...会被解析器拒绝。千万不要为了让它变绿而去重新生成任何东西——本设计下已经没有可重新生成的对象了。赞分享【免费下载链接】gsd-coreGit. Ship. Done - Core项目地址https://gitcode.com/gh_mirrors/ge/gsd-core点击查看免费下载相关推荐gsd-core ADR-457 TypeScript 源码迁移实录10 个运行时模块从手写 CommonJS 到 tsc 构建产物gsd core ADR 457 TypeScript 源码迁移实录10 个运行时模块从手写 CommonJS 到 tsc 构建产物 本篇以归档 changelocal_auth_ios 演进全解从联邦架构迁移到 iOS 本地认证的工程细节local_auth_ios 演进全解从联邦架构迁移到 iOS 本地认证的工程细节 local_auth_ios 是 Flutter 官方维护的 local_移动开发跨平台Atlantis 架构决策记录ADR实践指南从决策流程到 API 与漂移检测演进Atlantis 架构决策记录ADR实践指南从决策流程到 API 与漂移检测演进 导读 本文以 Atlantis 仓库中 docs/adr/0001 reDevOpsCI/CD基础设施上一篇FastLED I/O Trampoline 架构重构解析从 ifdef 爆炸到平台无关串口抽象层下一篇如何用WindowsCleaner快速解决C盘爆红问题终极免费清理指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考