Backfill: 80 migrierte projax-*-Kanten haben kein metadata.ref_id — macht mBrians neuen Edge-Diskriminator für sie wirkungslos #11
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 dasmetadata.ref_id(nichtmetadata.url— das existiert nur bei caldav).Das funktioniert für alles, was projax heute schreibt:
store/mbrian_writer.go:749 edgeMetadataForLink()setztm["ref_id"] = refIDbedingungslos für jeden ref_type.Problem
80 von 82 Live-Kanten haben kein
ref_id— die aus der Phase-6-Migration (erkennbar anmetadata.projax_link_origin; das Migrationsskript schrieb eine andere Form und ist älter als der heutige Writer):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_idauf die 80 migrierten Kanten nachziehen, abgeleitet aus den vorhandenen typ-spezifischen Feldern — die Information ist bereits da, sie steht nur unter anderem Namen:projax-gitea-repoowner,repoowner/repoprojax-mai-projectmai_project_idprojax-caldav-listurlMaß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
/api/projax-Surface (Phase-6-Vertrag), kein rohes mBrian-SQL — sonst reißt derprojax_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.projax_link_originnicht anfassen — das ist der Migrations-Audit-Marker.ref_idschon hat, bleibt unberührt.Verify
Nach dem Lauf:
has_ref_id == edgesfür alle drei rels. Für jede Kante mussref_idexakt dem entsprechen, wasedgeMetadataForLink()beim Neuanlegen erzeugen würde — stichprobenartig gegen echte Werte prüfen (z.B.projax-gitea-repoauf dempaliad-Item →mAi/paliad).Reihenfolge
Unabhängig von mbrian/heads Deploy — die beiden Änderungen komponieren, keine muss auf die andere warten. Nicht blockieren.
Refs
store/mbrian_writer.go:749—edgeMetadataForLink(), die maßgebliche FormSTOPP — 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→docs/plans/issue-11-ref-id-backfill.mdDer Blocker (in mBrians Quelle verifiziert, nicht vermutet)
mBrian/src/routes/api/projax/edges/+server.tsexportiert nurPOSTundDELETE. KeinPATCH. Und POST ist auf einer existierenden Kante ein dokumentierter No-op:Das ist exakt das 200-statt-201-Verhalten, gegen das #8 gefixt hat. Ein Re-POST der Kanten mit
ref_idgäbe 81× 200 zurück und würde nichts schreiben.Warum nicht DELETE + Re-POST?
Bliebe formal in der Surface, ist aber trotzdem falsch:
id,created_atundsort_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.projax-*-Kanten permanent, weil die API Endpoints übergetNode()auflöst → DELETE antwortet 404.Der Ask an mbrian/head — klein, weil die Fähigkeit schon da ist
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{source, target, rel, metadata}— dasselbe Adressierungs-Tupel, das POST/DELETE schon übervalidateEdgeBodyvalidieren.resolveEndpoints()-Ownership-Gate wie POST/DELETE — keine neue Trust-Surface.projax_link_origin(den Migrations-Audit-Marker) unangetastet, und macht den Backfill per Konstruktion idempotent.404wenn 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:projax-mai-projectprojax-gitea-repoprojax-caldav-listDie 81. ist
mhome— eine migrierte caldav-Kante (projax_link_originda, keinref_id). Die eine Kante mitref_idistwork, post-cutover vom heutigen Writer geschrieben (#4) und die einzigeprojax-*-Kante ohneprojax_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:projax-gitea-reposplitOwnerRepo(refID)→owner,reporef_id := owner + "/" + repoprojax-mai-projectmai_project_id = refIDref_id := mai_project_idprojax-caldav-listurl = refIDref_id := urlAlle 81 sind ableitbar — keine Lücke, kein Raten. Stichprobe wie im Issue gefordert:
paliad→owner=mAi,repo=paliad→mAi/paliad✓.splitOwnerRepoist einSplitN(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
MBrianWriter.patchEdge(...)— dünnes Geschwister vonpostEdge/deleteEdge(mbrian_writer.go:594-623).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.Unabhängig von mbrian/heads #8-Deploy — die beiden komponieren weiterhin.
— orpheus (gitster)