Add the mergiraf comparison harness
A shell generator runs mergiraf and d3j over a corpus of base/left/right scenarios and renders a static site of their merges side by side, deployed to GitHub Pages by a new workflow. d3j has no merge CLI yet, so its column renders as pending until one lands.

Scenarios are seeded from the paper (the sequence example as JSON, and insert-insert and switch-to-if in Java) plus independent-edit and relabel cases in Rust and JSON.

Assisted-by: Claude Opus 4.8 via Claude Code
change knsqyuoxqzlqxkrnzrqzwtztnqytwurv
commit bd5ea9abae121cd47025c57941750008735ba51d
author Alpha Chen <alpha@kejadlen.dev>
date
parent mtqxtulo
diff --git a/.github/workflows/compare.yml b/.github/workflows/compare.yml
new file mode 100644
index 0000000..2a17a65
--- /dev/null
+++ b/.github/workflows/compare.yml
@@ -0,0 +1,58 @@
+name: Comparison site
+
+on:
+  push:
+    branches: [main]
+    paths:
+      - compare/**
+      - src/**
+      - Cargo.toml
+      - Cargo.lock
+      - .github/workflows/compare.yml
+  workflow_dispatch:
+
+permissions: {}
+
+# Serialize deploys; don't cancel a deployment already in flight.
+concurrency:
+  group: pages
+  cancel-in-progress: false
+
+env:
+  MERGIRAF_VERSION: 0.18.0
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read # checkout.
+    steps:
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - name: Build d3j
+        run: cargo build --release
+      - name: Install mergiraf
+        # Compiles from source (a couple of minutes); pinned for
+        # reproducible output.
+        run: cargo install mergiraf --locked --version "$MERGIRAF_VERSION"
+      - name: Generate site
+        env:
+          D3J: ${{ github.workspace }}/target/release/d3j
+        run: ./compare/generate.sh dist
+      - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
+        with:
+          path: dist
+
+  deploy:
+    needs: build
+    runs-on: ubuntu-latest
+    permissions:
+      pages: write # deploy to Pages.
+      id-token: write # OIDC token for the Pages deployment.
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    steps:
+      - id: deployment
+        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
diff --git a/.gitignore b/.gitignore
index 2425919..932f7d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /2607.07987v1.pdf
+/dist
 /target
diff --git a/compare/README.md b/compare/README.md
new file mode 100644
index 0000000..64620c5
--- /dev/null
+++ b/compare/README.md
@@ -0,0 +1,38 @@
+# Comparison site
+
+A generated static site for reviewing how d3j's merges differ from
+[mergiraf](https://mergiraf.org)'s. `generate.sh` runs both tools over
+every scenario in `scenarios/` and renders the results side by side. The
+design and rationale live in
+[`docs/plans/2026-07-23-comparison-site-design.md`](../docs/plans/2026-07-23-comparison-site-design.md).
+
+## Running locally
+
+Prerequisites: `mergiraf` on your `PATH` (or pointed at via `MERGIRAF`),
+and a d3j build.
+
+```sh
+cargo build
+D3J=target/debug/d3j ./compare/generate.sh dist
+```
+
+Open `dist/index.html`. Both tools default to `PATH`; override the
+`MERGIRAF` and `D3J` environment variables to point at specific binaries.
+Set `TRACE=1` to trace the generator.
+
+d3j has no working merge yet, so its column reads "pending" until the CLI
+lands — the harness tolerates that and fills in as d3j grows.
+
+## Adding a scenario
+
+Create `scenarios/<name>/` with `base.<ext>`, `left.<ext>`, and
+`right.<ext>` in one of the languages d3j supports (`.rs`, `.java`,
+`.json`); the extension drives language detection. Add a `notes.md`
+describing the case and the expected outcome. The next run picks it up.
+
+## Publishing
+
+`.github/workflows/compare.yml` regenerates the site and deploys it to
+GitHub Pages on pushes to `main` that touch the corpus, the generator, or
+d3j's sources. Publishing requires enabling Pages for the repository with
+the source set to "GitHub Actions" — a one-time setting.
diff --git a/compare/assets/style.css b/compare/assets/style.css
new file mode 100644
index 0000000..4bc8b05
--- /dev/null
+++ b/compare/assets/style.css
@@ -0,0 +1,215 @@
+:root {
+    --bg: #ffffff;
+    --fg: #1a1a1a;
+    --muted: #6b7280;
+    --border: #e2e2e2;
+    --panel: #f7f7f8;
+    --link: #2563eb;
+    --add-bg: #e6f4ea;
+    --add-fg: #14532d;
+    --del-bg: #fbe9e9;
+    --del-fg: #7f1d1d;
+    --hunk: #6b7280;
+}
+
+@media (prefers-color-scheme: dark) {
+    :root {
+        --bg: #16181d;
+        --fg: #e6e6e6;
+        --muted: #9aa0aa;
+        --border: #2c2f36;
+        --panel: #1e2127;
+        --link: #6ea8fe;
+        --add-bg: #133220;
+        --add-fg: #7ee2a8;
+        --del-bg: #3a1a1c;
+        --del-fg: #f4a6a6;
+        --hunk: #9aa0aa;
+    }
+}
+
+* {
+    box-sizing: border-box;
+}
+
+body {
+    margin: 0;
+    background: var(--bg);
+    color: var(--fg);
+    font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
+}
+
+main {
+    max-width: 1100px;
+    margin: 0 auto;
+    padding: 2rem 1.25rem 4rem;
+}
+
+h1 {
+    font-size: 1.6rem;
+    margin: 0 0 1rem;
+}
+
+h2 {
+    font-size: 1.1rem;
+    margin: 2rem 0 0.75rem;
+    padding-bottom: 0.3rem;
+    border-bottom: 1px solid var(--border);
+}
+
+a {
+    color: var(--link);
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+.crumb {
+    margin: 0 0 1rem;
+    font-size: 0.9rem;
+}
+
+.lang {
+    font-size: 0.8rem;
+    color: var(--muted);
+    border: 1px solid var(--border);
+    border-radius: 4px;
+    padding: 0.1rem 0.4rem;
+    vertical-align: middle;
+}
+
+.muted {
+    color: var(--muted);
+    font-style: italic;
+}
+
+.notes pre {
+    white-space: pre-wrap;
+    background: var(--panel);
+    border: 1px solid var(--border);
+    border-radius: 6px;
+    padding: 0.75rem 1rem;
+    color: var(--muted);
+    font: inherit;
+}
+
+.grid {
+    display: grid;
+    gap: 1rem;
+}
+
+.grid-2 {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.grid-3 {
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+@media (max-width: 720px) {
+    .grid-2,
+    .grid-3 {
+        grid-template-columns: 1fr;
+    }
+}
+
+figure.code {
+    margin: 0;
+    border: 1px solid var(--border);
+    border-radius: 6px;
+    overflow: hidden;
+}
+
+figcaption {
+    background: var(--panel);
+    border-bottom: 1px solid var(--border);
+    padding: 0.4rem 0.75rem;
+    font-size: 0.8rem;
+    font-weight: 600;
+}
+
+pre {
+    margin: 0;
+    padding: 0.75rem;
+    overflow-x: auto;
+    font: 13px/1.5 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+}
+
+.badge {
+    font-size: 0.72rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.03em;
+    border-radius: 4px;
+    padding: 0.1rem 0.4rem;
+    color: #fff;
+}
+
+.badge-clean {
+    background: #16a34a;
+}
+
+.badge-conflict {
+    background: #d97706;
+}
+
+.badge-error {
+    background: #dc2626;
+}
+
+.badge-pending {
+    background: #6b7280;
+}
+
+.badge-unavailable {
+    background: #9ca3af;
+}
+
+table {
+    border-collapse: collapse;
+    width: 100%;
+    margin: 0.5rem 0;
+}
+
+th,
+td {
+    text-align: left;
+    padding: 0.5rem 0.75rem;
+    border-bottom: 1px solid var(--border);
+    vertical-align: top;
+}
+
+th {
+    font-size: 0.8rem;
+    color: var(--muted);
+}
+
+.agree-differ {
+    color: var(--del-fg);
+    font-weight: 600;
+}
+
+.agree-identical {
+    color: var(--add-fg);
+    font-weight: 600;
+}
+
+pre.diff .diff-add {
+    display: block;
+    background: var(--add-bg);
+    color: var(--add-fg);
+}
+
+pre.diff .diff-del {
+    display: block;
+    background: var(--del-bg);
+    color: var(--del-fg);
+}
+
+pre.diff .diff-meta,
+pre.diff .diff-hunk {
+    display: block;
+    color: var(--hunk);
+}
diff --git a/compare/generate.sh b/compare/generate.sh
new file mode 100755
index 0000000..abe0a35
--- /dev/null
+++ b/compare/generate.sh
@@ -0,0 +1,228 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+    set -o xtrace
+fi
+
+# Renders a static comparison site: for every scenario under scenarios/,
+# run mergiraf and d3j on the base/left/right inputs and show their
+# merged outputs side by side. Both tools are located via PATH by
+# default; override with the MERGIRAF and D3J environment variables (for
+# example, D3J=target/release/d3j to use a local build).
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+SCENARIOS_DIR="$SCRIPT_DIR/scenarios"
+ASSETS_DIR="$SCRIPT_DIR/assets"
+
+MERGIRAF="${MERGIRAF:-mergiraf}"
+D3J="${D3J:-d3j}"
+
+html_escape() {
+    sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
+}
+
+# Runs a merge tool, writing merged output to $4 and echoing a status
+# word (clean, conflict, error, pending, or unavailable) to stdout.
+run_tool() {
+    local tool="$1" base="$2" left="$3" right="$4" out="$5"
+    if ! command -v "$tool" >/dev/null 2>&1 && [[ ! -x "$tool" ]]; then
+        : >"$out"
+        echo "unavailable"
+        return
+    fi
+
+    local rc=0
+    "$tool" merge "$base" "$left" "$right" >"$out" 2>/dev/null || rc=$?
+
+    if grep -q '<<<<<<<' "$out"; then
+        echo "conflict"
+    elif [[ ! -s "$out" ]]; then
+        # A real merge of these scenarios is never empty; empty output
+        # means the tool has no working merge yet (d3j today).
+        echo "pending"
+    elif [[ "$rc" -eq 0 ]]; then
+        echo "clean"
+    else
+        echo "error"
+    fi
+}
+
+status_badge() {
+    local status="$1"
+    printf '<span class="badge badge-%s">%s</span>' "$status" "$status"
+}
+
+# Emits a labelled <pre> block holding an escaped file's contents.
+code_block() {
+    local title="$1" path="$2"
+    printf '<figure class="code"><figcaption>%s</figcaption><pre>' "$title"
+    html_escape <"$path"
+    printf '</pre></figure>\n'
+}
+
+# Colorizes a unified diff: additions green, deletions red, hunk headers
+# dimmed. Input is raw diff text; output is escaped HTML span lines.
+colorize_diff() {
+    html_escape | awk '
+        /^\+\+\+/ || /^---/ { printf "<span class=\"diff-meta\">%s</span>\n", $0; next }
+        /^@@/                { printf "<span class=\"diff-hunk\">%s</span>\n", $0; next }
+        /^\+/                { printf "<span class=\"diff-add\">%s</span>\n", $0; next }
+        /^-/                 { printf "<span class=\"diff-del\">%s</span>\n", $0; next }
+                             { printf "%s\n", $0 }
+    '
+}
+
+page_head() {
+    local title="$1" stylesheet="$2"
+    cat <<EOF
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>$title</title>
+<link rel="stylesheet" href="$stylesheet">
+</head>
+<body>
+<main>
+EOF
+}
+
+page_foot() {
+    cat <<'EOF'
+</main>
+</body>
+</html>
+EOF
+}
+
+render_scenario() {
+    local name="$1" outdir="$2"
+    local dir="$SCENARIOS_DIR/$name"
+    local base left right ext
+    base=$(echo "$dir"/base.*)
+    left=$(echo "$dir"/left.*)
+    right=$(echo "$dir"/right.*)
+    ext="${base##*.}"
+
+    local mg_out d3_out
+    mg_out=$(mktemp)
+    d3_out=$(mktemp)
+    local mg_status d3_status
+    mg_status=$(run_tool "$MERGIRAF" "$base" "$left" "$right" "$mg_out")
+    d3_status=$(run_tool "$D3J" "$base" "$left" "$right" "$d3_out")
+
+    local agree
+    if [[ "$d3_status" == "pending" || "$d3_status" == "unavailable" ]]; then
+        agree="n/a"
+    elif cmp -s "$mg_out" "$d3_out"; then
+        agree="identical"
+    else
+        agree="differ"
+    fi
+
+    {
+        page_head "$name — d3j vs. mergiraf" "style.css"
+        printf '<p class="crumb"><a href="index.html">&larr; all scenarios</a></p>\n'
+        printf '<h1>%s <span class="lang">%s</span></h1>\n' "$name" "$ext"
+        if [[ -f "$dir/notes.md" ]]; then
+            printf '<div class="notes"><pre>'
+            html_escape <"$dir/notes.md"
+            printf '</pre></div>\n'
+        fi
+
+        printf '<h2>Inputs</h2>\n<div class="grid grid-3">\n'
+        code_block "base" "$base"
+        code_block "left" "$left"
+        code_block "right" "$right"
+        printf '</div>\n'
+
+        printf '<h2>Merged output</h2>\n<div class="grid grid-2">\n'
+        printf '<figure class="code"><figcaption>mergiraf %s</figcaption><pre>' "$(status_badge "$mg_status")"
+        html_escape <"$mg_out"
+        printf '</pre></figure>\n'
+        printf '<figure class="code"><figcaption>d3j %s</figcaption><pre>' "$(status_badge "$d3_status")"
+        if [[ "$d3_status" == "pending" ]]; then
+            printf '<span class="muted">no merge yet — d3j has no working CLI</span>'
+        elif [[ "$d3_status" == "unavailable" ]]; then
+            printf '<span class="muted">d3j binary not found</span>'
+        else
+            html_escape <"$d3_out"
+        fi
+        printf '</pre></figure>\n</div>\n'
+
+        if [[ "$agree" == "differ" ]]; then
+            printf '<h2>Difference (mergiraf &rarr; d3j)</h2>\n<pre class="diff">'
+            diff -u --label mergiraf --label d3j "$mg_out" "$d3_out" | colorize_diff || true
+            printf '</pre>\n'
+        fi
+
+        page_foot
+    } >"$outdir/$name.html"
+
+    rm -f "$mg_out" "$d3_out"
+
+    # Emit one matrix row on stdout for the index to collect.
+    printf '%s\t%s\t%s\t%s\n' "$name" "$mg_status" "$d3_status" "$agree"
+}
+
+render_index() {
+    local outdir="$1" rows="$2"
+    {
+        page_head "d3j vs. mergiraf" "style.css"
+        printf '<h1>d3j vs. mergiraf</h1>\n'
+        cat <<'EOF'
+<p>How <a href="https://github.com/kejadlen/d3j">d3j</a>'s structural
+merges compare with <a href="https://mergiraf.org">mergiraf</a>'s over a
+corpus of scenarios. d3j is early — where its column reads
+<span class="badge badge-pending">pending</span>, its merge engine does
+not exist yet, and the page fills in as it lands.</p>
+
+<h2>Approach</h2>
+<table class="approach">
+<thead><tr><th>Aspect</th><th>d3j</th><th>mergiraf</th></tr></thead>
+<tbody>
+<tr><td>Matching</td><td>Anchored Zhang&ndash;Shasha, base&rarr;each branch</td><td>GumTree classic across all three pairs</td></tr>
+<tr><td>Core object</td><td>Partial inclusion map, merged as a pushout</td><td>PCS triples, 3DM/Spork-style changeset union</td></tr>
+<tr><td>On failure</td><td>Reports a conflict; never falls back to text</td><td>Falls back to line-based diff3</td></tr>
+<tr><td>Correctness</td><td>Universality checker as oracle + self-check</td><td>Pragmatic; no formal guarantee</td></tr>
+</tbody>
+</table>
+
+<h2>Scenarios</h2>
+<table class="matrix">
+<thead><tr><th>Scenario</th><th>mergiraf</th><th>d3j</th><th>agree?</th></tr></thead>
+<tbody>
+EOF
+        local name mg d3 agree
+        while IFS=$'\t' read -r name mg d3 agree; do
+            [[ -z "$name" ]] && continue
+            printf '<tr><td><a href="%s.html">%s</a></td><td>%s</td><td>%s</td><td class="agree-%s">%s</td></tr>\n' \
+                "$name" "$name" "$(status_badge "$mg")" "$(status_badge "$d3")" "$agree" "$agree"
+        done <<<"$rows"
+        cat <<'EOF'
+</tbody>
+</table>
+EOF
+        page_foot
+    } >"$outdir/index.html"
+}
+
+main() {
+    local outdir="${1:-dist}"
+    mkdir -p "$outdir"
+    cp "$ASSETS_DIR/style.css" "$outdir/style.css"
+
+    local rows=""
+    local dir name
+    for dir in "$SCENARIOS_DIR"/*/; do
+        name=$(basename "$dir")
+        rows+="$(render_scenario "$name" "$outdir")"$'\n'
+    done
+
+    render_index "$outdir" "$rows"
+    echo "Wrote site to $outdir"
+}
+
+main "$@"
diff --git a/compare/scenarios/java-insert-insert/base.java b/compare/scenarios/java-insert-insert/base.java
new file mode 100644
index 0000000..12270d3
--- /dev/null
+++ b/compare/scenarios/java-insert-insert/base.java
@@ -0,0 +1,5 @@
+class Greeter {
+    void greet() {
+        System.out.println("hello");
+    }
+}
diff --git a/compare/scenarios/java-insert-insert/left.java b/compare/scenarios/java-insert-insert/left.java
new file mode 100644
index 0000000..4e8878e
--- /dev/null
+++ b/compare/scenarios/java-insert-insert/left.java
@@ -0,0 +1,6 @@
+class Greeter {
+    void greet() {
+        System.out.println("hello");
+        System.out.println("from left");
+    }
+}
diff --git a/compare/scenarios/java-insert-insert/notes.md b/compare/scenarios/java-insert-insert/notes.md
new file mode 100644
index 0000000..569903d
--- /dev/null
+++ b/compare/scenarios/java-insert-insert/notes.md
@@ -0,0 +1,10 @@
+# java-insert-insert
+
+Based on the paper's Figure 12. Both branches insert a new statement at
+the same slot — the end of `greet` — but with different content. The
+insertions collide, so an insert-insert conflict is the correct outcome.
+
+Figure 12's point is subtler: it shows a tool duplicating code when one
+branch's insertion coincidentally matches the other's edit, yielding a
+non-universal conflict-free merge instead of the conflict that should
+have been reported. This scenario keeps the collision explicit.
diff --git a/compare/scenarios/java-insert-insert/right.java b/compare/scenarios/java-insert-insert/right.java
new file mode 100644
index 0000000..2c160e4
--- /dev/null
+++ b/compare/scenarios/java-insert-insert/right.java
@@ -0,0 +1,6 @@
+class Greeter {
+    void greet() {
+        System.out.println("hello");
+        System.out.println("from right");
+    }
+}
diff --git a/compare/scenarios/java-switch-to-if/base.java b/compare/scenarios/java-switch-to-if/base.java
new file mode 100644
index 0000000..624b1eb
--- /dev/null
+++ b/compare/scenarios/java-switch-to-if/base.java
@@ -0,0 +1,12 @@
+class Router {
+    String route(int code) {
+        switch (code) {
+            case 1:
+                return "a";
+            case 2:
+                return "b";
+            default:
+                return "?";
+        }
+    }
+}
diff --git a/compare/scenarios/java-switch-to-if/left.java b/compare/scenarios/java-switch-to-if/left.java
new file mode 100644
index 0000000..2e43128
--- /dev/null
+++ b/compare/scenarios/java-switch-to-if/left.java
@@ -0,0 +1,14 @@
+class Router {
+    String route(int code) {
+        switch (code) {
+            case 1:
+                return "a";
+            case 2:
+                return "b";
+            case 3:
+                return "c";
+            default:
+                return "?";
+        }
+    }
+}
diff --git a/compare/scenarios/java-switch-to-if/notes.md b/compare/scenarios/java-switch-to-if/notes.md
new file mode 100644
index 0000000..da4560c
--- /dev/null
+++ b/compare/scenarios/java-switch-to-if/notes.md
@@ -0,0 +1,10 @@
+# java-switch-to-if
+
+Based on the paper's Figure 13, the one genuine semantic merge its
+authors found in a developer study. Left adds a `case 3` to the switch;
+right refactors the whole switch into a chain of `if` statements.
+
+The edits target the same structure in incompatible ways: right deleted
+the switch that left extended. A conflict is the expected structural
+outcome — reconciling them is the semantic refactoring a human performs,
+beyond what either tool resolves automatically.
diff --git a/compare/scenarios/java-switch-to-if/right.java b/compare/scenarios/java-switch-to-if/right.java
new file mode 100644
index 0000000..05a6fca
--- /dev/null
+++ b/compare/scenarios/java-switch-to-if/right.java
@@ -0,0 +1,11 @@
+class Router {
+    String route(int code) {
+        if (code == 1) {
+            return "a";
+        }
+        if (code == 2) {
+            return "b";
+        }
+        return "?";
+    }
+}
diff --git a/compare/scenarios/json-independent-keys/base.json b/compare/scenarios/json-independent-keys/base.json
new file mode 100644
index 0000000..8d6b85c
--- /dev/null
+++ b/compare/scenarios/json-independent-keys/base.json
@@ -0,0 +1,3 @@
+{
+  "a": 1
+}
diff --git a/compare/scenarios/json-independent-keys/left.json b/compare/scenarios/json-independent-keys/left.json
new file mode 100644
index 0000000..756b033
--- /dev/null
+++ b/compare/scenarios/json-independent-keys/left.json
@@ -0,0 +1,4 @@
+{
+  "a": 1,
+  "b": 2
+}
diff --git a/compare/scenarios/json-independent-keys/notes.md b/compare/scenarios/json-independent-keys/notes.md
new file mode 100644
index 0000000..99816c2
--- /dev/null
+++ b/compare/scenarios/json-independent-keys/notes.md
@@ -0,0 +1,7 @@
+# json-independent-keys
+
+Each branch adds a different key to an object: left adds `"b"`, right
+adds `"c"`. The insertions land in the same unordered collection but do
+not collide, so both tools should merge cleanly to an object with all
+three keys. A baseline case where structural merge and mergiraf are
+expected to agree.
diff --git a/compare/scenarios/json-independent-keys/right.json b/compare/scenarios/json-independent-keys/right.json
new file mode 100644
index 0000000..f25113b
--- /dev/null
+++ b/compare/scenarios/json-independent-keys/right.json
@@ -0,0 +1,4 @@
+{
+  "a": 1,
+  "c": 3
+}
diff --git a/compare/scenarios/json-sequence/base.json b/compare/scenarios/json-sequence/base.json
new file mode 100644
index 0000000..a85b7c9
--- /dev/null
+++ b/compare/scenarios/json-sequence/base.json
@@ -0,0 +1,5 @@
+[
+  1,
+  2,
+  3
+]
diff --git a/compare/scenarios/json-sequence/left.json b/compare/scenarios/json-sequence/left.json
new file mode 100644
index 0000000..721b8bf
--- /dev/null
+++ b/compare/scenarios/json-sequence/left.json
@@ -0,0 +1,7 @@
+[
+  1,
+  2,
+  4,
+  5,
+  3
+]
diff --git a/compare/scenarios/json-sequence/notes.md b/compare/scenarios/json-sequence/notes.md
new file mode 100644
index 0000000..483a7ef
--- /dev/null
+++ b/compare/scenarios/json-sequence/notes.md
@@ -0,0 +1,10 @@
+# json-sequence
+
+The paper's worked example from Figures 2 and 3. Base is `[1, 2, 3]`;
+left inserts `4, 5` after `2`; right replaces `2` with `6`.
+
+The edits are independent — left touches the slot after `2`, right
+relabels `2` — so the paper argues the correct structural merge is
+`[1, 6, 4, 5, 3]`. Line-oriented tools conflict because the changed
+lines overlap, and mergiraf falls back to a line conflict here. This is
+the flagship case d3j aims to merge cleanly.
diff --git a/compare/scenarios/json-sequence/right.json b/compare/scenarios/json-sequence/right.json
new file mode 100644
index 0000000..969d33e
--- /dev/null
+++ b/compare/scenarios/json-sequence/right.json
@@ -0,0 +1,5 @@
+[
+  1,
+  6,
+  3
+]
diff --git a/compare/scenarios/rust-independent-methods/base.rs b/compare/scenarios/rust-independent-methods/base.rs
new file mode 100644
index 0000000..54d7f87
--- /dev/null
+++ b/compare/scenarios/rust-independent-methods/base.rs
@@ -0,0 +1,5 @@
+impl Greeter {
+    fn hello(&self) {
+        println!("hello");
+    }
+}
diff --git a/compare/scenarios/rust-independent-methods/left.rs b/compare/scenarios/rust-independent-methods/left.rs
new file mode 100644
index 0000000..e2bb442
--- /dev/null
+++ b/compare/scenarios/rust-independent-methods/left.rs
@@ -0,0 +1,9 @@
+impl Greeter {
+    fn hello(&self) {
+        println!("hello");
+    }
+
+    fn goodbye(&self) {
+        println!("goodbye");
+    }
+}
diff --git a/compare/scenarios/rust-independent-methods/notes.md b/compare/scenarios/rust-independent-methods/notes.md
new file mode 100644
index 0000000..3ac73c9
--- /dev/null
+++ b/compare/scenarios/rust-independent-methods/notes.md
@@ -0,0 +1,7 @@
+# rust-independent-methods
+
+Each branch appends a different method to the same `impl` block: left
+adds `goodbye`, right adds `ping`. The additions are independent, so a
+clean merge keeping all three methods is expected. A textual tool may
+conflict because both edits touch the closing lines of the block; a
+structural tool should not.
diff --git a/compare/scenarios/rust-independent-methods/right.rs b/compare/scenarios/rust-independent-methods/right.rs
new file mode 100644
index 0000000..15bcfb6
--- /dev/null
+++ b/compare/scenarios/rust-independent-methods/right.rs
@@ -0,0 +1,9 @@
+impl Greeter {
+    fn hello(&self) {
+        println!("hello");
+    }
+
+    fn ping(&self) {
+        println!("ping");
+    }
+}
diff --git a/compare/scenarios/rust-relabel-relabel/base.rs b/compare/scenarios/rust-relabel-relabel/base.rs
new file mode 100644
index 0000000..523c498
--- /dev/null
+++ b/compare/scenarios/rust-relabel-relabel/base.rs
@@ -0,0 +1,3 @@
+fn compute() -> i32 {
+    42
+}
diff --git a/compare/scenarios/rust-relabel-relabel/left.rs b/compare/scenarios/rust-relabel-relabel/left.rs
new file mode 100644
index 0000000..42d84e2
--- /dev/null
+++ b/compare/scenarios/rust-relabel-relabel/left.rs
@@ -0,0 +1,3 @@
+fn calculate() -> i32 {
+    42
+}
diff --git a/compare/scenarios/rust-relabel-relabel/notes.md b/compare/scenarios/rust-relabel-relabel/notes.md
new file mode 100644
index 0000000..d8b77d6
--- /dev/null
+++ b/compare/scenarios/rust-relabel-relabel/notes.md
@@ -0,0 +1,6 @@
+# rust-relabel-relabel
+
+Both branches rename the same function: left to `calculate`, right to
+`evaluate`. This is d3j's relabel-relabel conflict — one base node
+relabeled differently in each branch — and has no clean resolution. A
+conflict is the correct outcome for both tools.
diff --git a/compare/scenarios/rust-relabel-relabel/right.rs b/compare/scenarios/rust-relabel-relabel/right.rs
new file mode 100644
index 0000000..647b591
--- /dev/null
+++ b/compare/scenarios/rust-relabel-relabel/right.rs
@@ -0,0 +1,3 @@
+fn evaluate() -> i32 {
+    42
+}