From 0e51e5b3b89027268fda766410b431e02a831f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Tejero=20Cantero?= Date: Thu, 1 Oct 2026 11:45:31 +0200 Subject: [PATCH] :bug: Keep the conflict refusal's error page and fix the guide The backend answers a :conflict refusal (:file-modified, :file-locked) with HTTP 409 since the previous commit. On the frontend that type had no ptk/handle-error method, so the version-restore flow would have fallen to the generic toast instead of the error page the old 500 reached. errors.cljs now maps :conflict to the same exceptional-state handler. The technical guide's audit paragraph said rename, archive and delete report nothing; they report a duration and an outcome. The merge step list now records the re-validation after the repair and its refusal, and the update section names the same refusal. AI-assisted-by: mimo-v2.6-pro, claude-opus-5-5 --- docs/technical-guide/developer/branching.md | 35 ++++++++++++--------- frontend/src/app/main/errors.cljs | 4 +++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/technical-guide/developer/branching.md b/docs/technical-guide/developer/branching.md index cf699e9035..2846bac4fe 100644 --- a/docs/technical-guide/developer/branching.md +++ b/docs/technical-guide/developer/branching.md @@ -437,7 +437,11 @@ The flow: version history; * applies the change vector through `cpc/process-changes`, bumping `revn`; * **validates** the merged file (`cfv/validate-file`) and, if it finds - problems, runs `cfr/repair-file` and applies the repair changes; + problems, runs `cfr/repair-file` and applies the repair changes, then + re-validates: a result that is still invalid refuses with + `:merge-result-invalid`, so a bad repair never reaches main (the update + refuses `:update-result-invalid` the same way, and materialise logs the + result instead of refusing); * copies the `file_media_object` rows of media added on the branch into main, under the pre-allocated ids the changes reference; * writes a `file_change` (xlog) row, persists the file @@ -536,8 +540,9 @@ permissions on the *branch* file, locks the *branch* file id, and applies main's changes (plus any conflicts resolved to `:main`) into the branch. It works like a merge with `compute-merge`/`compute-changes` run with the sides -swapped, the same `:conflicts` / `:unsupported` gates, and the same safety -snapshot. Two extra steps at the end keep the op log sound: +swapped, the same `:conflicts` / `:unsupported` gates, the same re-validation +refusal (`:update-result-invalid`), and the same safety snapshot. Two extra +steps at the end keep the op log sound: * it **squashes the op log**: the repositioned base already carries everything main contributed, so the log is replaced with the net @@ -616,19 +621,19 @@ branch's op log, attaches `:branch-operation`, `:branch-outcome` and `:branch-duration-ms` to its own audit event, because the generic RPC event records who called what and not how long it took, and duration is the number the first enterprise trial will be asked about. Creation, the listing, -compare, merge, update-from-main, and materialise attach them through -`audited`, and the listing adds `:branches-compared` and `:branches-cached`, -the two numbers that explain its duration. The save attaches them on the -`update-file` event, whose props it replaces. Every outcome these commands -return carries them, the no-op merge, the idempotent materialise, and the -`:conflicts` and `:unsupported` refusals included. A refusal raised as an -error, such as a size gate's `:restriction` or `:file-modified`, carries -none of them: the audit middleware records a call only when it returns, so -the error writes no audit event at all. +compare, merge, update-from-main, materialise, and the three small +mutations (`update-file-branch`, `archive-file-branch`, +`delete-file-branch`) attach them through `audited`, and the listing adds +`:branches-compared` and `:branches-cached`, the two numbers that explain +its duration. The save attaches them on the `update-file` event, whose +props it replaces. Every outcome these commands return carries them, the +no-op merge, the idempotent materialise, and the `:conflicts` and +`:unsupported` refusals included. A refusal raised as an error, such as a +size gate's `:restriction` or `:file-modified`, carries none of them: the +audit middleware records a call only when it returns, so the error writes +no audit event at all. -Three mutations are the exception: `update-file-branch`, -`archive-file-branch`, and `delete-file-branch` do not run through `audited` -and carry only the generic event. Two reads are the same: the branch +Two reads are the exception and carry only the generic event: the branch context, which the frontend asks for on every file open, and the limits. ## RPC API summary diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 928722f889..274af8cf65 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -795,6 +795,10 @@ (defmethod ptk/handle-error :not-found [error] (handle-exceptional-state error)) (defmethod ptk/handle-error :bad-gateway [error] (handle-exceptional-state error)) +;; Refused optimistic-concurrency checks (:file-modified, :file-locked) +;; got this surface as 500s. Keep it now the backend answers 409. Callers +;; matching :code still find it in the error. +(defmethod ptk/handle-error :conflict [error] (handle-exceptional-state error)) (defmethod ptk/handle-error :service-unavailable [error] (handle-exceptional-state error)) (defmethod ptk/handle-error :nitrate-unavailable [error] (handle-exceptional-state error)) (defmethod ptk/handle-error :nitrate-not-configured [error] (handle-exceptional-state error))