docs(adr): ADR-0022 for the upstream reconnection, corrected by the test

Decision sorb: option B, a one-time real merge rather than a shared replace ref. What
decided it was visibility, not effort - a replace ref works only while everyone
remembers to fetch it, and for a repo whose core problem is "git says nothing", a
mechanism that silently differs per clone is the wrong shape.

The test corrected the option's own description. --allow-unrelated-histories on its own
gives a two-way comparison and 1757 conflicts; with the graft set locally it is 32. So
the graft is not the alternative to B, it is how B is performed: set it, let the merge
compute against it, commit, and the merge commit then carries the real parents so the
graft can go.

Also recorded because it cost time and looked like a fundamental problem: tags fetched
with --depth=1 leave a shallow boundary, so v1.12.26 was walled off at a single commit
even though develop carried the same commit in full. Every merge attempt failed with
"refusing to merge unrelated histories" until fetch --unshallow.

The merge itself is measured but deliberately not executed. apps/web/package.json
carries two product decisions rather than conflicts - our Element Call fork against
upstream's, and a matrix-js-sdk git pin against a released version - and resolving the
first one wrongly would silently delete the noise suppression work from #0054. Neither
is safe without a build and the ClamAV functional test.
This commit is contained in:
Thore Cimbal
2026-08-19 12:00:00 +00:00
parent 1874ddfa92
commit bae97e6ca6
3 changed files with 143 additions and 1 deletions
@@ -0,0 +1,75 @@
---
type: adr
id: "0022"
status: accepted
date: 2026-08-19
supersedes: null
superseded_by: null
related:
- "docs/issues/0099-threadnet-web-12-upstream-sicherheitsfixes-lassen-sich-nicht-m.md"
---
# ADR-0022: Anschluss an Upstream durch einen einmaligen Merge, nicht durch einen geteilten Graft
## Kontext
`ThreadNet-Web` enthält keine Upstream-Historie: Am 2026-05-10 kam ein kompletter
Element-Web-Baum in einem Commit herein. Ohne gemeinsamen Vorfahren ist
`git merge upstream/develop` unmöglich, und jedes Update bedeutet, zwölf eigene Patches
von Hand auf einen neuen Baum aufzutragen. Das ist nicht nur mühsam, sondern gefährlich:
Verschiebt Element eine Datei, verschwinden unsere Zeilen **ohne Konflikt** (#0099).
Am 2026-08-19 wurde der tatsächliche Ursprung gemessen statt geraten: **`deadd548`
vom 2026-05-08** (nicht der Tag `v1.12.17`, wie zuvor angenommen). Der Beleg ist die
Baumdistanz — 43 abweichende Dateien, davon 31 reine Modus-Änderungen und der Rest
unser eigenes Feature.
Im Wegwerf-Klon getestet: Mit gesetztem Vorfahren läuft ein Merge von drei Monaten
`develop` durch und erzeugt **32 konfliktbehaftete Dateien, davon nur vier Quellcode**
genau unsere Patches.
## Optionen
**A: Geteilter Replace-Ref.** `git replace --graft` und `refs/replace/*` mitliefern.
Die Historie bleibt formal unverändert, Git *interpretiert* sie nur anders. Nachteil:
Jeder Klon braucht einen zusätzlichen Fetch, und wer ihn vergisst, sieht eine andere
Historie als alle anderen — ein stiller Unterschied, der sich erst im Konfliktfall
zeigt.
**B: Einmaliger echter Merge**, danach normale Merges.
⚠️ **Praezisierung nach dem Test:** `--allow-unrelated-histories` allein liefert
einen ZWEI-Wege-Vergleich und damit 1757 Konflikte — gemessen. Der Graft ist kein
Gegenentwurf zu B, sondern sein Werkzeug: lokal setzen, den Merge damit rechnen
lassen (32 Konflikte), committen. Der Merge-Commit traegt danach die echten Eltern,
der Graft kann weg, und die Abstammung laeuft ueber den Merge-Commit selbst.
Die Nahtstelle wird ein sichtbarer Merge-Commit. Kein Sonderwissen, kein Zusatzschritt,
kein Klon kann sie versehentlich übersehen.
**C: So weiterarbeiten wie bisher** — Patches von Hand auftragen. Verworfen: Das ist der
Zustand, der die stille Klasse überhaupt erst erzeugt.
## Entscheidung
**Option B** (sorb, 2026-08-19).
Ausschlaggebend ist nicht der Aufwand — beide Wege sind ähnlich billig — sondern die
Sichtbarkeit. A funktioniert nur, solange alle daran denken; B trägt sich selbst. Für
ein Repo, dessen Kernproblem „Git meldet nichts" ist, wäre ein Mechanismus, der
stillschweigend unterschiedlich wirkt, die falsche Wahl.
`deadd548` bleibt trotzdem wichtig: Es ist der Stand, gegen den der Merge gefahren wird,
und ohne diese Messung wäre der Merge auf eine erfundene Grundlage gelaufen.
## Konsequenzen
- Der erste Merge ist ein einmaliger Kraftakt mit vier Quellcode-Konflikten; danach ist
ein Upstream-Update ein gewöhnlicher Merge.
- **Die stille Klasse verschwindet**: Verschiebt Upstream eine Datei, die wir angefasst
haben, meldet Git künftig einen Konflikt, statt unsere Zeilen wortlos fallen zu lassen.
- Die Nahtstelle bleibt als Merge-Commit dauerhaft sichtbar — gewollt, nicht geduldet.
- **Abnahme ist kein Build, sondern ein Funktionstest**: Die ClamAV-Patches liegen in der
Medien-Pipeline. Ohne den Test aus #0099 (verschlüsselte Datei senden, abgelehnte
empfangen) ist der Merge nicht abgenommen.
- Schritt 4 aus #0099 — ein Verfahren zum Auftragen der Patches — wird damit
gegenstandslos.
@@ -229,3 +229,69 @@ Merge-Commit mit `--allow-unrelated-histories` bauen und ab dann normal weiterme
Beides ändert die Historie-Wahrnehmung des Repos dauerhaft und ist damit
**ADR-pflichtig**. Entscheidung sorb steht aus; Schritt 4 (Auftragsverfahren für die
Patches) wird bei Weg 1 oder 2 weitgehend gegenstandslos.
## Weg B belegt 2026-08-19 — mit einer wichtigen Präzisierung
Entscheidung sorb: **Weg B** ([ADR-0022](../adr/0022-upstream-anschluss-durch-einmaligen-merge.md)).
### `--allow-unrelated-histories` allein genügt NICHT
Gemessen gegen `v1.12.26`, im selben Klon, nacheinander:
| Vorgehen | Konflikte |
|---|---|
| nur `--allow-unrelated-histories`, ohne Vorfahren | **1757** |
| mit lokal gesetztem Graft auf `deadd548` | **32** |
Ohne gemeinsamen Vorfahren macht Git einen Zwei-Wege-Vergleich — das ist die „praktisch
jede Datei konfliktet"-Lage aus der Fork-Doku. **Der Graft ist also kein Gegenentwurf zu
Weg B, sondern sein Werkzeug:** lokal setzen, den Merge damit rechnen lassen, committen —
der Merge-Commit trägt danach die echten Eltern, und der Graft kann weg. Ab dann läuft
die Abstammung über den Merge-Commit selbst. Das steht so nicht in ADR-0022s
Optionsbeschreibung und ist hier nachgetragen.
⚠️ **Stolperstein auf dem Weg dahin:** Die mit `--depth=1` geholten Tags tragen eine
`.git/shallow`-Grenze. `v1.12.26` war dadurch auf **einen** Commit abgemauert, obwohl
derselbe Commit über `develop` vollständig vorlag — jeder Merge-Versuch scheiterte mit
„refusing to merge unrelated histories", was wie ein Grundproblem aussah und keines war.
`git fetch --unshallow upstream` löst es.
### Die 32 Konflikte, vollständig aufgeschlüsselt
| Menge | Was | Auflösung |
|---|---|---|
| 24 | `.github/workflows/*` | unsere Fassung behalten — eigene CI-Strecke |
| 1 | `pnpm-lock.yaml` | regenerieren |
| 1 | `apps/web/package.json` | **Produktentscheidung, siehe unten** |
| 6 | Quellcode, je 12 Konfliktblöcke | von Hand |
**Der Fund, der die ganze Übung rechtfertigt:** Upstream hat
`RoomListItemAccessibilityWrapper` in `RoomListItemWrapper` **umbenannt**. Weil es jetzt
einen echten Vorfahren gibt, **meldet Git den Konflikt** — genau der Fall, den dieses
Issue als stille Gefahr beschreibt („unsere Zeilen sind schlicht weg, und Git meldet
nichts").
### Warum ich hier gestoppt habe
`apps/web/package.json` enthält zwei Zeilen, die keine Konfliktauflösung sind, sondern
Entscheidungen:
```
ours: "@sorb/threadnet-call-embedded": "0.19.2-threadnet.12"
upstream: "@element-hq/element-call-embedded": "0.22.0"
ours: "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d19cb751..."
upstream: "matrix-js-sdk": "42.2.0"
```
Die erste falsch aufgelöst, und die **gesamte KI-Geräuschunterdrückung (#0054) ist
stillschweigend weg**. Die zweite betrifft den Git-Ref-Pin, der seinerzeit ein Build-Fix
war (Abschnitt 3 der Fork-Doku) — ob Upstreams 42.2.0 ihn erübrigt, zeigt nur ein Build.
Beides ist ohne Build und ohne den ClamAV-Funktionstest nicht absicherbar. Der Merge ist
damit **vorbereitet und vermessen, aber nicht ausgeführt**; das Experiment wurde
abgeräumt (kein Zweig, kein Replace-Ref, Arbeitsverzeichnis sauber). Die Upstream-Historie
bleibt im Klon liegen, damit der nächste Anlauf nicht wieder 600 MB holen muss.
**Nächster Schritt:** den Merge als eigenes Vorhaben fahren, mit Build und
ClamAV-Abnahme als Voraussetzung — nicht als Nebenschritt.