DSH / Atlas
2026-08-08implementedprocess

Automatically compose translation pairing records

自动组合翻译配对记录

A bilingual consistency record contains the two owner files' exact blob hashes. Two branches that independently update different parts of the same confirmed pair therefore conflict on both hash lines even when Git cleanly composes both Markdown owners. Selecting either side leaves stale hashes, while regenerating the record by hand repeats a deterministic operation and prevents an otherwise automatic merge.

English

Problem

A bilingual consistency record contains the two owner files' exact blob hashes. Two branches that independently update different parts of the same confirmed pair therefore conflict on both hash lines even when Git cleanly composes both Markdown owners. Selecting either side leaves stale hashes, while regenerating the record by hand repeats a deterministic operation and prevents an otherwise automatic merge.

Decision

*.i18n.yaml uses the repository-owned dsh-translation-pairing merge driver. The worktree-local Git installer registers its command alongside Lefthook setup; Git configuration remains local because a tracked attribute can name a driver but cannot carry its executable command.

The installer loads the exact Node/tsx entrypoint before publishing worktree integration. Git invokes a checked-in shell launcher that does not require Node and repeats this probe before every driver execution. When the runtime or entrypoint is unavailable, the launcher materializes Git's ordinary three-way text result in the sidecar but returns a conflict even when that text merge is clean, so Git retains the unmerged index stages and never accepts unverified metadata.

The driver parses the ancestor, current, and other records and loads the six owner blobs named by their hashes. It independently runs Git's default three-way text merge for the English and Chinese triplets, requires both merges to be clean, verifies language switchers and the pairing structural signature, stores the two merged blobs, and writes their hashes as the canonical record. This composes confirmations already present in both parents; it never records an ordinary one-sided documentation edit.

The driver fails with an ordinary unresolved sidecar when a record is malformed, an object is missing, an owner uses another merge strategy (including a non-text merge.default inherited by an otherwise unspecified path), either owner has content conflicts, or the merged pair violates structural checks. Add/delete and rename shapes remain manual because their path ownership is not the same three-record operation.

pnpm run resolve-translation-pairing-conflicts applies the same algorithm after a merge has already stopped. Before writing any sidecar, it proves that the sidecar still contains Git's untouched conflict result and that the staged owner blob IDs and working-tree bytes equal its independent merges. It writes and stages every safe record as one batch even when another pair still needs manual work, then reports the remaining pairing conflicts and exits unsuccessfully so callers cannot mistake a partial resolution for a completed merge.

pre-merge-commit and pre-commit verify staged .i18n.yaml files against the exact index bytes of their owners. They validate driver output but do not regenerate records, so bypassing a hook cannot silently bless translation drift; the corpus-wide doc-sync check remains authoritative in CI.

Failure contract

Failure during a normal git mergeObservable stateRecovery
A fresh install cannot probe the driver or install LefthookNo new driver or hook-path configuration is published; any newly added integration is rolled back to the previous hook lookup.Restore the dependencies and rerun node scripts/install-lefthook.mjs.
Node, tsx, or the driver entrypoint becomes unavailable after installationThe merge stops with the sidecar at UU, index stages 1/2/3 remain, the worktree sidecar contains Git's text result, MERGE_HEAD exists, and no commit is created.Restore the dependencies and run pnpm run resolve-translation-pairing-conflicts, or run git merge --abort.
The repository-aware driver rejects the recordsThe merge stops with the sidecar unresolved and no commit; the driver prints the owner-repair and explicit-resolver path.Repair the owner conflict or record, then run the printed resolver workflow or abort.
The driver process crashes with a status above 128Git aborts the merge strategy without publishing MERGE_HEAD or unmerged index stages.Repair the runtime and rerun the merge.
pre-merge-commit rejects an otherwise clean file mergeNo unmerged entries remain, the complete result is staged with MERGE_HEAD, and no merge commit is created.Repair the hook failure and run git commit, or run git merge --abort.

An installer rollback failure reports both the original installation error and every rollback error. Because worktree configuration may then be partial, the contributor repairs or inspects it before merging instead of relying on a silent fallback.

Verification

Script tests exercise clean composition through the installed launcher, missing-runtime and broken-entrypoint text fallback, installer probe rollback, a rejecting pre-merge-commit hook, explicit recovery from an unresolved index, mixed safe and owner-conflicted pairs, edited sidecars, non-text default merge configuration, record parsing, and worktree-local installation. The existing corpus verifier continues to prove that a committed record matches its two owners.

Alternatives considered

Take ours or use Git's union driver. Either parent record names pre-merge content, while union produces duplicate or unordered hash keys. Neither represents the merged owners.

Regenerate in post-merge or only in a commit hook. post-merge does not run after a conflicted merge and cannot affect its outcome. Commit hooks are reached only after the index has no unresolved entries, so a hook alone cannot clear the generated conflict.

Wrap every merge in a repository command. A wrapper can resolve the conflict from the populated index, and the explicit resolver retains that recovery path, but raw Git, stack tooling, rebases, and cherry-picks would still stop before it. The merge driver is the file-level extension point shared by those operations.

Resolve on GitHub through Actions or an app. Hosted automation could update PR branches, but it adds credentials, concurrency control, and branch mutation. Local and agent-driven merge-forward workflows already have a checkout and push authority; the repository keeps remote automation out of this mechanism.

Consequences

Installed worktrees automatically remove pairing-record-only conflicts while preserving human judgment for owner conflicts and translation quality. GitHub's hosted mergeability calculation does not run the worktree-local executable, so a contributor or agent must still merge the base and push the resulting commit before the remote conflict badge clears.

The installer reserves merge.dsh-translation-pairing.* in worktree configuration and refuses a conflicting custom value. Automatic composition depends on the installed Node dependencies, like the repository's contributor hooks; runtime loss produces a visible unresolved text result rather than selecting stale metadata.

中文

问题

一份双语一致性记录包含两侧 Markdown 文件的精确 blob hash。因此,当两个分支分别更新同一已确认配对的不同部分时,即使 Git 能干净合并两侧 Markdown 文件,记录中的两行 hash 仍会发生冲突。选择任一侧都会留下陈旧 hash;手工重新生成记录则会重复执行一项确定性操作,并阻止本可自动完成的合并。

决策

*.i18n.yaml 使用由仓库维护的 dsh-translation-pairing 合并驱动。worktree 本地的 Git 安装脚本在设置 Lefthook 的同时注册该驱动命令;Git 配置仍限定于当前 worktree,因为受跟踪的属性可以指定驱动,却无法携带其可执行命令。

安装脚本会在发布 worktree 集成前加载确切的 Node/tsx 入口点。Git 会调用仓库内已提交且不依赖 Node 的 shell 启动器;该启动器会在每次执行驱动前重复这项探测。运行时或入口点不可用时,即使文本合并干净完成,启动器也会将 Git 的普通三方文本合并结果写入伴随文件并返回冲突状态,使 Git 保留未合并的索引阶段,绝不接受未经验证的元数据。

驱动会解析祖先、当前和另一侧的记录,并加载其中 hash 指向的六个 Markdown blob。它分别对英文文档的三个 blob 与中文文档的三个 blob 运行 Git 默认的三方文本合并,要求两次合并都干净完成,校验语言切换行与配对结构签名,存储两个合并后的 blob,再将其 hash 写入规范记录。这一过程组合两个父提交中已经存在的确认;它绝不会为普通的单侧文档编辑写入记录。

记录格式错误、对象缺失、配对文档采用其他合并策略(包括原本未单独指定策略的路径继承了非文本 merge.default)、任一配对文档存在内容冲突,或合并后的配对违反结构检查时,驱动会让伴随文件保持为普通的未解决冲突。新增、删除及重命名情形仍需手工处理,因为它们的路径归属关系无法由上述基于三份记录的操作处理。

pnpm run resolve-translation-pairing-conflicts 会在合并已经停止后应用同一算法。写入任何伴随文件之前,它会验证伴随文件仍保留 Git 生成且未经改动的冲突结果,并验证暂存区中配对文档的 blob ID 以及工作树中的文件字节都与工具独立完成的合并结果一致。即使其他配对仍需手工处理,它也会将所有可安全生成的记录作为一个批次写入并暂存;随后报告剩余的配对冲突并以非零状态退出,确保调用方不会把仅部分解决误认为合并已经完成。

pre-merge-commitpre-commit 会对照两侧文档在索引中的精确字节,校验暂存的 .i18n.yaml 文件。这些钩子会验证驱动输出,但不会重新生成记录,因此绕过钩子也无法默默确认发生漂移的译文;CI 中全语料的 doc-sync(文档同步门禁)仍是权威检查。

<a id="failure-contract"></a>

失败约定

普通 git merge 期间的失败可观察状态恢复
全新安装无法探测驱动或安装 Lefthook不会发布新的驱动或钩子路径配置;任何新增集成都会回滚到原有的钩子查找方式。恢复依赖并重新运行 node scripts/install-lefthook.mjs
安装后 Node、tsx 或驱动入口点变得不可用合并停止,伴随文件状态为 UU;索引阶段 1/2/3 保留,worktree 中的伴随文件包含 Git 的文本结果,MERGE_HEAD 存在,且不创建提交。恢复依赖并运行 pnpm run resolve-translation-pairing-conflicts,或运行 git merge --abort
了解仓库状态的驱动拒绝记录合并停止,伴随文件保持未解决状态且不创建提交;驱动会打印修复配对文档和运行显式冲突解决命令的路径。修复配对文档冲突或记录,然后运行打印出的冲突解决工作流,或中止合并。
驱动进程以大于 128 的状态码崩溃Git 中止合并策略,既不写入 MERGE_HEAD,也不产生未合并的索引阶段。修复运行时并重新执行合并。
pre-merge-commit 拒绝原本能干净完成的文件合并不再有未合并条目,完整结果已暂存且 MERGE_HEAD 存在,但不会创建合并提交。修复钩子失败并运行 git commit,或运行 git merge --abort

安装脚本回滚失败时,会同时报告最初的安装错误和每一项回滚错误。此时 worktree 配置可能只完成了一部分,因此贡献者会先修复或检查配置再执行合并,而不是依赖静默回退。

验证

脚本测试通过已安装的启动器覆盖无冲突组合,并覆盖运行时缺失与入口点损坏时的文本回退、安装脚本探测回滚、拒绝合并的 pre-merge-commit 钩子、从含未解决条目的索引中显式恢复、可安全解决的配对与配对文档冲突的配对并存、被编辑过的伴随文件、非文本默认合并配置、记录解析和 worktree 本地安装。既有的全语料校验器继续证明已提交的记录与两侧配对文档相符。

考虑过的替代方案

选择 ours 或使用 Git 的 union 驱动。 任一父提交的记录都指向合并前的内容,而 union 会产生重复或乱序的 hash 键。两者都无法表示合并后的配对文档。

post-merge 中或仅通过提交钩子重新生成。 发生冲突的合并不会运行 post-merge,而且该钩子无法改变合并结果。只有暂存区不含未解决条目后才会触发提交钩子,因此仅用钩子无法清除生成文件冲突。

用仓库命令包装每次合并。 包装命令可以根据已由合并填充的索引解决冲突,显式冲突解决命令也保留这条恢复路径,但直接运行 Git、stack 工具、rebase 和 cherry-pick 时仍会在此前停止。合并驱动才是这些操作共用的文件级扩展点。

通过 GitHub Actions 或应用解决。 托管自动化可以更新 PR(Pull Request)分支,却会引入凭据、并发控制与分支修改。本地执行或由 agent(智能体)驱动的 merge-forward 工作流已经具有检出目录与推送权限,因此仓库不把远程自动化纳入该机制。

后果

完成安装的 worktree 会自动消除仅由配对记录引起的冲突,同时仍由人工处理配对文档冲突并评判翻译质量。GitHub 托管的可合并性计算不会运行 worktree 本地的可执行命令,因此仍须由贡献者或 agent 将 base 分支合入当前分支并推送结果提交,远端冲突标记才会消失。

安装脚本会在 worktree 配置中预留 merge.dsh-translation-pairing.*,并拒绝与之冲突的自定义值。与仓库贡献者钩子一样,自动组合依赖已安装的 Node 依赖;运行时不可用时会产生可见的未解决文本结果,而不是选择陈旧元数据。