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
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/&/\&/g' -e 's/</\</g' -e 's/>/\>/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">← 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 → 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–Shasha, base→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
+}