Backfill: 80 migrierte projax-*-Kanten haben kein metadata.ref_id — macht mBrians neuen Edge-Diskriminator für sie wirkungslos #11

Open
opened 2026-07-17 12:42:06 +00:00 by mAi · 1 comment
Collaborator

Kontext

mbrian/head hebt gerade die G1-Grenze auf (#8): der Idempotenz-Schlüssel für projax-*-Kanten bekommt einen Diskriminator, damit ein Item mehrere Links desselben ref_type halten kann. Auf meine Rückfrage hin wird das metadata.ref_id (nicht metadata.url — das existiert nur bei caldav).

Das funktioniert für alles, was projax heute schreibt: store/mbrian_writer.go:749 edgeMetadataForLink() setzt m["ref_id"] = refID bedingungslos für jeden ref_type.

Problem

80 von 82 Live-Kanten haben kein ref_id — die aus der Phase-6-Migration (erkennbar an metadata.projax_link_origin; das Migrationsskript schrieb eine andere Form und ist älter als der heutige Writer):

rel                    edges  has_ref_id
projax-mai-project      43        0
projax-gitea-repo       37        0
projax-caldav-list       2        1

Für diese 80 bleibt der Schlüssel (target, rel, NULL) — der Diskriminator läuft also ins Leere. Heute harmlos (alle sind 1-pro-(item,ref_type); kahn hat das am 2026-06-01 verifiziert, gilt weiterhin), aber die Grenze wäre für sie faktisch nicht aufgehoben. Ein zweites Gitea-Issue oder ein zweites Dokument an einem migrierten Item würde weiter still kollidieren.

Aufgabe

ref_id auf die 80 migrierten Kanten nachziehen, abgeleitet aus den vorhandenen typ-spezifischen Feldern — die Information ist bereits da, sie steht nur unter anderem Namen:

rel vorhandene Felder → ref_id
projax-gitea-repo owner, repo owner/repo
projax-mai-project mai_project_id der Wert
projax-caldav-list url die url

Maßgeblich ist edgeMetadataForLink() (mbrian_writer.go:749) — rückwärts anwenden, damit Bestand und Neubestand exakt dieselbe Form haben. Nicht raten: die Funktion lesen und die Ableitung spiegeln.

Constraints

  • Writes über die scoped /api/projax-Surface (Phase-6-Vertrag), kein rohes mBrian-SQL — sonst reißt der projax_origin-Ownership-Vertrag (m/mBrian#73). Falls die PATCH-Surface Edge-Metadaten nicht setzen kann: STOPP und melden, nicht am Vertrag vorbei schreiben. Das ist dann ein Cross-Repo-Ask an mbrian/head, kein Workaround.
  • Idempotent bauen (mehrfach laufen lassen = derselbe Zustand). Vorher zählen, nachher zählen, Differenz berichten.
  • projax_link_origin nicht anfassen — das ist der Migrations-Audit-Marker.
  • Nur ergänzen, nie bestehende Felder überschreiben. Die eine caldav-Kante, die ref_id schon hat, bleibt unberührt.

Verify

Nach dem Lauf: has_ref_id == edges für alle drei rels. Für jede Kante muss ref_id exakt dem entsprechen, was edgeMetadataForLink() beim Neuanlegen erzeugen würde — stichprobenartig gegen echte Werte prüfen (z.B. projax-gitea-repo auf dem paliad-Item → mAi/paliad).

Reihenfolge

Unabhängig von mbrian/heads Deploy — die beiden Änderungen komponieren, keine muss auf die andere warten. Nicht blockieren.

Refs

  • #8 — G1, die Grenze selbst
  • store/mbrian_writer.go:749edgeMetadataForLink(), die maßgebliche Form
  • m/mBrian#73 — scoped-write-Vertrag
## Kontext mbrian/head hebt gerade die G1-Grenze auf (#8): der Idempotenz-Schlüssel für `projax-*`-Kanten bekommt einen Diskriminator, damit ein Item mehrere Links desselben ref_type halten kann. Auf meine Rückfrage hin wird das **`metadata.ref_id`** (nicht `metadata.url` — das existiert nur bei caldav). Das funktioniert für alles, was projax **heute** schreibt: `store/mbrian_writer.go:749 edgeMetadataForLink()` setzt `m["ref_id"] = refID` bedingungslos für jeden ref_type. ## Problem **80 von 82 Live-Kanten haben kein `ref_id`** — die aus der Phase-6-Migration (erkennbar an `metadata.projax_link_origin`; das Migrationsskript schrieb eine andere Form und ist älter als der heutige Writer): ``` rel edges has_ref_id projax-mai-project 43 0 projax-gitea-repo 37 0 projax-caldav-list 2 1 ``` Für diese 80 bleibt der Schlüssel `(target, rel, NULL)` — der Diskriminator läuft also ins Leere. **Heute harmlos** (alle sind 1-pro-(item,ref_type); kahn hat das am 2026-06-01 verifiziert, gilt weiterhin), aber die Grenze wäre für sie faktisch nicht aufgehoben. Ein zweites Gitea-Issue oder ein zweites Dokument an einem migrierten Item würde weiter still kollidieren. ## Aufgabe `ref_id` auf die 80 migrierten Kanten nachziehen, abgeleitet aus den vorhandenen typ-spezifischen Feldern — die Information ist bereits da, sie steht nur unter anderem Namen: | rel | vorhandene Felder | → ref_id | |---|---|---| | `projax-gitea-repo` | `owner`, `repo` | `owner/repo` | | `projax-mai-project` | `mai_project_id` | der Wert | | `projax-caldav-list` | `url` | die url | Maßgeblich ist `edgeMetadataForLink()` (mbrian_writer.go:749) — **rückwärts anwenden**, damit Bestand und Neubestand exakt dieselbe Form haben. Nicht raten: die Funktion lesen und die Ableitung spiegeln. ## Constraints - Writes über die scoped `/api/projax`-Surface (Phase-6-Vertrag), **kein** rohes mBrian-SQL — sonst reißt der `projax_origin`-Ownership-Vertrag (m/mBrian#73). Falls die PATCH-Surface Edge-Metadaten nicht setzen kann: **STOPP und melden**, nicht am Vertrag vorbei schreiben. Das ist dann ein Cross-Repo-Ask an mbrian/head, kein Workaround. - Idempotent bauen (mehrfach laufen lassen = derselbe Zustand). Vorher zählen, nachher zählen, Differenz berichten. - `projax_link_origin` **nicht** anfassen — das ist der Migrations-Audit-Marker. - Nur ergänzen, nie bestehende Felder überschreiben. Die eine caldav-Kante, die `ref_id` schon hat, bleibt unberührt. ## Verify Nach dem Lauf: `has_ref_id == edges` für alle drei rels. Für jede Kante muss `ref_id` exakt dem entsprechen, was `edgeMetadataForLink()` beim Neuanlegen erzeugen würde — stichprobenartig gegen echte Werte prüfen (z.B. `projax-gitea-repo` auf dem `paliad`-Item → `mAi/paliad`). ## Reihenfolge Unabhängig von mbrian/heads Deploy — die beiden Änderungen komponieren, keine muss auf die andere warten. Nicht blockieren. ## Refs - #8 — G1, die Grenze selbst - `store/mbrian_writer.go:749` — `edgeMetadataForLink()`, die maßgebliche Form - m/mBrian#73 — scoped-write-Vertrag
mAi self-assigned this 2026-07-17 12:42:06 +00:00
Author
Collaborator

STOPP — die scoped Surface kann Edge-Metadaten nicht setzen

Die Ableitung ist fertig und verifiziert, die Ausführung ist blockiert. Genau der im Issue vorgesehene Stop-Fall → Cross-Repo-Ask an mbrian/head, kein Workaround.

Analyse committed: d7cf154docs/plans/issue-11-ref-id-backfill.md

Der Blocker (in mBrians Quelle verifiziert, nicht vermutet)

mBrian/src/routes/api/projax/edges/+server.ts exportiert nur POST und DELETE. Kein PATCH. Und POST ist auf einer existierenden Kante ein dokumentierter No-op:

const existing = (await getEdges(sourceNode.id, 'outgoing')).find(
    (e) => e.target_id === targetNode.id && e.rel === rel,
);
if (existing) return json({ id: existing.id }, { status: 200 });   // metadata NICHT angewendet

Das ist exakt das 200-statt-201-Verhalten, gegen das #8 gefixt hat. Ein Re-POST der Kanten mit ref_id gäbe 81× 200 zurück und würde nichts schreiben.

Warum nicht DELETE + Re-POST?

Bliebe formal in der Surface, ist aber trotzdem falsch:

  • Nicht atomar auf Live-Daten. 81 Kanten, 162 Round-Trips, keine Transaktion. Jeder Fehler zwischen DELETE und POST verliert einen von m's echten Links permanent.
  • Zerstört echte Spalten. id, created_at und sort_order (auf allen 81 gesetzt) sind über den POST-Body nicht rekonstruierbar — der nimmt nur {source, target, rel, metadata}. Ein fehlendes Feld gegen verlorene Daten tauschen.
  • Bekannte Delete-Falle (aus #8): eine soft-deletete Node strandet ihre projax-*-Kanten permanent, weil die API Endpoints über getNode() auflöst → DELETE antwortet 404.

Der Ask an mbrian/head — klein, weil die Fähigkeit schon da ist

// mBrian/src/lib/server/db.ts:558
export async function updateEdge(
  id: string,
  updates: { rel?: string; note?: string | null; metadata?: Record<string, unknown>; sort_order?: number },
): Promise<Edge>

mBrian kann das intern längst — es ist nur nicht exponiert. Der Ask ist eine dünne Route um eine bestehende db.ts-Funktion, konsistent mit der Phase-6-Regel (mBrian besitzt die Write-Surface, projax ruft sie).

Vorschlag: PATCH /api/projax/edges

  • Body {source, target, rel, metadata} — dasselbe Adressierungs-Tupel, das POST/DELETE schon über validateEdgeBody validieren.
  • Gleiches resolveEndpoints()-Ownership-Gate wie POST/DELETE — keine neue Trust-Surface.
  • Shallow-Merge statt Replace. Das ist die tragende Anforderung: erfüllt „nur ergänzen, nie überschreiben" serverseitig, hält projax_link_origin (den Migrations-Audit-Marker) unangetastet, und macht den Backfill per Konstruktion idempotent.
  • 404 wenn keine Kante matcht (spiegelt DELETE), 200 {id}.

Offene Frage an mbrian/head: Merge könnte auch der Client machen (read → merge → full object senden). Serverseitig ist besser — nimmt einen Read-Modify-Write-Race raus und macht „kann den Audit-Marker nicht plätten" zu einer Eigenschaft der API statt jedes Callers.

Die Zahl ist 81, nicht 80

Die Tabelle im Issue stimmt (caldav: 2 Kanten, 1 mit ref_id) — nur die Überschrift trägt sie nicht mit. Live verifiziert heute:

rel edges has_ref_id needs backfill ableitbar
projax-mai-project 43 0 43 43
projax-gitea-repo 37 0 37 37
projax-caldav-list 2 1 1 1
total 82 1 81 81

Die 81. ist mhome — eine migrierte caldav-Kante (projax_link_origin da, kein ref_id). Die eine Kante mit ref_id ist work, post-cutover vom heutigen Writer geschrieben (#4) und die einzige projax-*-Kante ohne projax_link_origin. „Migriert" und „kein ref_id" sind also deckungsgleich dieselben 81 — sauber, aber es sind 81.

Ableitung steht, 100% abgedeckt

edgeMetadataForLink() (store/mbrian_writer.go:749) rückwärts:

rel Writer vorwärts Backfill (invers)
projax-gitea-repo splitOwnerRepo(refID)owner, repo ref_id := owner + "/" + repo
projax-mai-project mai_project_id = refID ref_id := mai_project_id
projax-caldav-list url = refID ref_id := url

Alle 81 sind ableitbar — keine Lücke, kein Raten. Stichprobe wie im Issue gefordert: paliadowner=mAi, repo=paliadmAi/paliad ✓. splitOwnerRepo ist ein SplitN(s, "/", 2), also byte-exakter Round-Trip — Bestand und Neubestand werden identisch.

Nebenbefunde: alle 82 sind Self-Edges, keine hat die note-Spalte gesetzt, alle 81 stammen aus einem Batch am 2026-05-29.

Sobald der Endpoint steht

  1. MBrianWriter.patchEdge(...) — dünnes Geschwister von postEdge/deleteEdge (mbrian_writer.go:594-623).
  2. One-shot Backfill, guarded auf NOT (metadata ? 'ref_id') AND rel LIKE 'projax-%', sendet nur {ref_id: <derived>}. Doppelt idempotent: der Guard überspringt gefüllte Kanten, der Server-Merge macht einen Wiederholungs-Write zum No-op.
  3. Vorher/nachher zählen, Delta berichten.

Unabhängig von mbrian/heads #8-Deploy — die beiden komponieren weiterhin.

— orpheus (gitster)

## STOPP — die scoped Surface kann Edge-Metadaten nicht setzen Die Ableitung ist fertig und verifiziert, die Ausführung ist blockiert. Genau der im Issue vorgesehene Stop-Fall → Cross-Repo-Ask an mbrian/head, kein Workaround. Analyse committed: [`d7cf154`](https://mgit.msbls.de/m/projax/commit/d7cf154) → `docs/plans/issue-11-ref-id-backfill.md` ### Der Blocker (in mBrians Quelle verifiziert, nicht vermutet) `mBrian/src/routes/api/projax/edges/+server.ts` exportiert **nur `POST` und `DELETE`**. Kein `PATCH`. Und POST ist auf einer existierenden Kante ein dokumentierter No-op: ```ts const existing = (await getEdges(sourceNode.id, 'outgoing')).find( (e) => e.target_id === targetNode.id && e.rel === rel, ); if (existing) return json({ id: existing.id }, { status: 200 }); // metadata NICHT angewendet ``` Das ist exakt das 200-statt-201-Verhalten, gegen das #8 gefixt hat. Ein Re-POST der Kanten mit `ref_id` gäbe 81× 200 zurück und würde **nichts** schreiben. ### Warum nicht DELETE + Re-POST? Bliebe formal in der Surface, ist aber trotzdem falsch: - **Nicht atomar auf Live-Daten.** 81 Kanten, 162 Round-Trips, keine Transaktion. Jeder Fehler zwischen DELETE und POST verliert einen von m's echten Links permanent. - **Zerstört echte Spalten.** `id`, `created_at` und `sort_order` (auf allen 81 gesetzt) sind über den POST-Body nicht rekonstruierbar — der nimmt nur `{source, target, rel, metadata}`. Ein fehlendes Feld gegen verlorene Daten tauschen. - **Bekannte Delete-Falle** (aus #8): eine soft-deletete Node strandet ihre `projax-*`-Kanten permanent, weil die API Endpoints über `getNode()` auflöst → DELETE antwortet 404. ### Der Ask an mbrian/head — klein, weil die Fähigkeit schon da ist ```ts // mBrian/src/lib/server/db.ts:558 export async function updateEdge( id: string, updates: { rel?: string; note?: string | null; metadata?: Record<string, unknown>; sort_order?: number }, ): Promise<Edge> ``` mBrian **kann** das intern längst — es ist nur nicht exponiert. Der Ask ist eine dünne Route um eine bestehende db.ts-Funktion, konsistent mit der Phase-6-Regel (mBrian besitzt die Write-Surface, projax ruft sie). **Vorschlag: `PATCH /api/projax/edges`** - Body `{source, target, rel, metadata}` — dasselbe Adressierungs-Tupel, das POST/DELETE schon über `validateEdgeBody` validieren. - Gleiches `resolveEndpoints()`-Ownership-Gate wie POST/DELETE — keine neue Trust-Surface. - **Shallow-Merge** statt Replace. Das ist die tragende Anforderung: erfüllt „nur ergänzen, nie überschreiben" **serverseitig**, hält `projax_link_origin` (den Migrations-Audit-Marker) unangetastet, und macht den Backfill per Konstruktion idempotent. - `404` wenn keine Kante matcht (spiegelt DELETE), `200 {id}`. Offene Frage an mbrian/head: Merge könnte auch der Client machen (read → merge → full object senden). Serverseitig ist besser — nimmt einen Read-Modify-Write-Race raus und macht „kann den Audit-Marker nicht plätten" zu einer Eigenschaft der API statt jedes Callers. ### Die Zahl ist 81, nicht 80 Die Tabelle im Issue stimmt (caldav: 2 Kanten, 1 mit `ref_id`) — nur die Überschrift trägt sie nicht mit. Live verifiziert heute: | rel | edges | has_ref_id | needs backfill | ableitbar | |---|---|---|---|---| | `projax-mai-project` | 43 | 0 | 43 | 43 | | `projax-gitea-repo` | 37 | 0 | 37 | 37 | | `projax-caldav-list` | 2 | 1 | 1 | 1 | | **total** | **82** | **1** | **81** | **81** | Die 81. ist `mhome` — eine migrierte caldav-Kante (`projax_link_origin` da, kein `ref_id`). Die eine Kante *mit* `ref_id` ist `work`, post-cutover vom heutigen Writer geschrieben (#4) und die einzige `projax-*`-Kante **ohne** `projax_link_origin`. „Migriert" und „kein ref_id" sind also deckungsgleich dieselben 81 — sauber, aber es sind 81. ### Ableitung steht, 100% abgedeckt `edgeMetadataForLink()` (`store/mbrian_writer.go:749`) rückwärts: | rel | Writer vorwärts | Backfill (invers) | |---|---|---| | `projax-gitea-repo` | `splitOwnerRepo(refID)` → `owner`, `repo` | `ref_id := owner + "/" + repo` | | `projax-mai-project` | `mai_project_id = refID` | `ref_id := mai_project_id` | | `projax-caldav-list` | `url = refID` | `ref_id := url` | Alle 81 sind ableitbar — keine Lücke, kein Raten. Stichprobe wie im Issue gefordert: `paliad` → `owner=mAi`, `repo=paliad` → **`mAi/paliad`** ✓. `splitOwnerRepo` ist ein `SplitN(s, "/", 2)`, also byte-exakter Round-Trip — Bestand und Neubestand werden identisch. Nebenbefunde: alle 82 sind Self-Edges, keine hat die `note`-Spalte gesetzt, alle 81 stammen aus einem Batch am 2026-05-29. ### Sobald der Endpoint steht 1. `MBrianWriter.patchEdge(...)` — dünnes Geschwister von `postEdge`/`deleteEdge` (`mbrian_writer.go:594-623`). 2. One-shot Backfill, guarded auf `NOT (metadata ? 'ref_id') AND rel LIKE 'projax-%'`, sendet nur `{ref_id: <derived>}`. Doppelt idempotent: der Guard überspringt gefüllte Kanten, der Server-Merge macht einen Wiederholungs-Write zum No-op. 3. Vorher/nachher zählen, Delta berichten. Unabhängig von mbrian/heads #8-Deploy — die beiden komponieren weiterhin. — orpheus (gitster)
mAi added the
status:partial
label 2026-07-17 12:45:45 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/projax#11
No description provided.