Viewing files at commit 2f5107c20e85Commit detailsView latest
First run: oak 0.105.0 (2026-09-28, revised after QA)
This page summarizes the evidence. Raw JSONL is not committed (ADR-0003).
It is kept in the author's worker at
worker-mrmrs-20260927T090659-p3-merge/runs/full3.jsonl, with the summary
at runs/full3-summary.json. runs/full1.* holds the pre-QA run, and
runs/full2.* an intermediate rerun.
- Subject.
oak 0.105.0, sha2564dc220e1f089845ca1ada8da4025b18b37f7823e266989e757302b8fa8bc6647. The path exercised isoak-pull-parent-sync, the client merge engine (see README). - Oracle.
git version 2.50.1 (Apple Git-155), the real Git (the xcrun shim was resolved), sha25610f9c1df…d2a9. - Command.
python3 suites/merge/run_merge_suite.py --workdir runs/full3 --jobs 8 --summary runs/full3-summary.json - Size. 387 scenarios and 762 directional cases. Self-merges (for
example gitoxide
no-changemain/main) now run in one direction only; the first run counted them twice. - Determinism. Classes are identical across reruns. Conflict-marker bytes embed Oak's randomly named personal branch, so tree digests of files with markers differ between runs. Those bytes are never compared, except to detect markers.
Counts per class
| origin | equal-to-git | differs-but-valid | oak-wrong | oak-unsupported | harness-error |
|---|---|---|---|---|---|
| generated-law | 67 | 8 | 5 | 0 | 0 |
| git-t64xx-reimplemented | 12 | 30 | 0 | 0 | 0 |
| gitoxide-cartesian | 146 | 50 | 14 | 210 | 0 |
| gitoxide-text-baseline | 51 | 2 | 11 | 0 | 0 |
| gitoxide-tree-baseline | 44 | 44 | 39 | 20 | 0 |
| native-incident | 8 | 0 | 1 | 0 | 0 |
| total | 328 | 134 | 70 | 230 | 0 |
The 210 cartesian cases marked oak-unsupported are the gitlink matrix,
because Oak's git import drops gitlinks. The 20 tree-baseline cases in that
class are gitlink cases or cases with several git merge bases or no merge
base.
Merge laws (true / measured)
| Law | Result |
|---|---|
| no_lost_change (content, mode and kind; no new markers) | 460/491 |
| idempotent (second pull is a no-op after a clean merge) | 337/337 |
| symmetric | 428/468 |
| false_conflict (Oak conflicts where git is clean) | 42/494, 8.5% |
| missed_conflict (git conflicts where Oak is clean) | 136/494, 27.5% |
prediction_agrees (oak merge --dry-run --json vs the executed pull) | 494/532 |
Missed conflicts.
- By class: 110 are differs-but-valid and 26 are oak-wrong.
- By git conflict kind: 104 rename or directory-rename (Oak has no rename detection), 23 content, 4 distinct-modes, 3 modify/delete, 2 binary/content. The content and binary ones are mostly fb-537.
Predictions. All 38 prediction disagreements are the pulls that failed
(fb-540). For those, oak merge --dry-run --json predicted a clean merge in
19 cases and a conflict in 19. It never predicted the failure.
Incident shapes
All incident shapes pass on the client path except F6:
- stale base C vs X, with a partial local graph hydrated on pull
- the base reached via a merge parent
- revert after a CLI sync
- criss-cross tie-break, both timestamp orders
- clock-skew best common ancestor, plus its monotone control
The lineage-less sync shape resurrects the reverted change in both Git and
Oak. That is inherent once lineage is lost, and it is recorded as
intent_note. The hosted-merge shapes (M0a, M0b, the server /sync,
client-vs-server tie-break, server clock-skew, fb-530) are phase-2 specs and
were not executed.
Oak-wrong findings (70 cases). Every row maps to a filed bug (known_bugs.json, 0 unmapped)
Oak is not fixed here, and the tickets were filed by QA. Each family gives
its bug ID, case count, a minimal repro (--only runs just that case), and
the observed behavior. The repro prefix is always:
python3 suites/merge/run_merge_suite.py --workdir /tmp/mr --groups <group> --only '<id>'
The failing case directory, with both clones, stays under
<workdir>/cases/<id>/<direction>/.
- fb-540 (F5): file↔directory transitions make
oak pullfail, and some failures move HEAD (38 cases). Blocker.- The minimal case is
gix-cartesian/matrix/keep--file-to-dir(A-B): main replaces filesourcewithsource/child, and the branch did not touch it. - The failure messages are
IO error: File exists (os error 17),Not a directory (os error 20),cannot build tree with both file "e" and child path "e/e", andrefusing to replace non-empty directory … with a tracked file. Git merges most of these cleanly. - In 11 of the 38, the failed pull still moved HEAD to the merge
commit while the worktree stayed pre-merge (reason
failed_pull_moved_head_worktree_stale, from the suite's post-failure check of HEAD vs worktree vs retry). A retry then reportsup_to_date, so the nextoak commitwould revert main's changes. QA also observed thatoak switchacross a file→dir change leaves a half-switched worktree. The suite does not measure that. - Repro:
--groups gix-cartesian --only 'gix-cartesian/matrix/add-free-a--file-to-dir'(the HEAD-moved variant) or'gix-cartesian/matrix/keep--file-to-dir'.
- The minimal case is
- fb-537 (F1 and F3): conflict markers committed as a clean merge when a
side has no trailing newline (13 cases). Blocker (invalid merge).
- The root cause is a missing final newline. Diffy glues a marker onto
the last line (
line3x||||||| original,line3=======,B>>>>>>> main). Marker detection misses the glued marker, andoak pullreportsmergedand commits the markers. - CRLF is not the cause. The
marker-newline-handling-crlffixture fails only because it lacks a final newline, and CRLF content with a final newline conflicts correctly. - Cases:
gix-text/complex/{conflict-without-LF,missing-LF-at-EOF,marker-newline-handling-lf,lf2,crlf}andgix-text/unique-merge-base-with-insertion. - Binary (the former "F3") has the same cause.
gix-tree/both-modify-binaryis a small NUL-containing file with no trailing newline. It is valid UTF-8, so Oak text-merges it and commits glued markers into it. With a trailing newline, Oak reports the conflict correctly (QA verified). Separately, Oak text-merges binary content whenever it decodes as UTF-8. - Repro:
--groups gix-text --only 'gix-text/complex/conflict-without-LF'
- The root cause is a missing final newline. Diffy glues a marker onto
the last line (
- fb-538 (F2): symlinks (or symlink vs file) changed on both sides are
text-merged, and the link target is committed with glued markers (8
cases). High.
- This is the same missing-newline mechanism, because symlink targets never end in a newline. The deeper problem is that Oak text-merges symlink targets at all.
- Cases:
gix-tree/{symlink-addition,symlink-modification,type-change-to-symlink},rename-add-symlink(B-A), andunrelated-renames-to-same-path-with-type-mismatch(B-A). - Repro:
--groups gix-tree --only 'gix-tree/symlink-addition/A-B'
- fb-539 (F4): a mode change is silently lost (10 cases). High.
- It happens in two situations: a chmod on one side against a content edit on the other, or both sides disagreeing on mode for the same content.
- Core
three_way_merge_manifestsmarks chmod-vs-edit as a conflict. The CLI resolver then text-merges cleanly and takes the branch entry's mode (mode: be.mode), so main's chmod is dropped and no conflict is reported. The result is asymmetric. - Cases:
gix-tree/remove-executable-mode(B-A),same-rename-different-mode,identical-renames-to-same-destination-with-mode-change, and generated 004, 014, 021, 022 and 026. Generated 022 had been under-classified before QA. - Repro:
--groups gix-tree --only 'gix-tree/remove-executable-mode/A-B'
- fb-541 (F6): criss-cross with divergent resolutions merges silently (1
case). Medium.
git-t6416-criss-cross-modify-delete: the branch resolved a modify/delete by deleting; main's merge D kept the file.- With two best common ancestors {B, C}, Oak takes C as the base and silently re-adds the file. Git reports a conflict.
- The shape is a hybrid: main's merge D is an imported git merge commit.
- Repro:
--groups native --only 'git-t6416-*'
Observation (not counted as wrong)
While a sync is conflicted, the worktree still contains paths the merge
deletes. Examples are git-t6423-8c/9b (B-A): z/b and z/c stay until
oak pull --continue. The committed result is correct, and user edits made
to those files during resolution are kept as uncommitted changes. The suite
therefore compares the committed result (result_source) and reports the
stale paths as conflict_worktree_stale_paths.
Differs-but-valid (134)
These cases are dominated by Oak having no rename or directory-rename detection. The t6423 directory-rename cases leave new files in the old directory, and rename/rename or rename/delete cases resolve cleanly by keeping both paths. Rename-vs-modify becomes a modify/delete conflict, which is a false conflict. None of these loses content or mode, fabricates content, or commits markers. Whether Oak should detect renames is a product decision, not a correctness bug.