Skip to content

feat(sync): rfc-003 phase a.2.1 — dual-shape ingest on /sync/ops - #52

Merged
InstaZDLL merged 4 commits into
mainfrom
feat/sync-v2-phase-a-2-dual-shape-ingest
Jun 13, 2026
Merged

feat(sync): rfc-003 phase a.2.1 — dual-shape ingest on /sync/ops#52
InstaZDLL merged 4 commits into
mainfrom
feat/sync-v2-phase-a-2-dual-shape-ingest

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

Phase A.2.1 of the RFC-003 sync v2 rollout. The POST /api/v1/sync/ops endpoint accepts an optional v2 hlc: { wall, logical } field on each op while keeping v1 clients working unchanged. The server prefers v2 when present; otherwise it derives the HLC pair from lamport_ts exactly the way the A.1.1 backfill did.

Follows #50 (A.1.1 sync_op HLC columns) + #51 (A.1.2 entity tables HLC columns + metadata_digest_version).

Wire shape

Purely additive. v1 client payload still works verbatim:

{ "operation_id": "", "lamport_ts": 1, "entity": "playlist", "entity_id": "pl-1", "op": "set", "field": "name", "payload": { "value": "Soirée" } }

v2 client adds hlc:

{ "operation_id": "", "lamport_ts": 1, "entity": "playlist", "entity_id": "pl-1", "op": "set", "field": "name", "payload": { "value": "Live" },
  "hlc": { "wall": 1700000000000, "logical": 7 } }

The §2 total-order tiebreaker origin_device_id rides through the existing sync_op.device_id TEXT column per A.1.1's design — UUID-shaped TEXT round-trips losslessly, no new column needed at the sync_op layer. v2 desktops format their device_id as a UUID string; v1 desktops keep their free-form one. The apply pipeline (A.2.2 follow-up) will parse device_id as UUID when writing entity tables' UUID origin_device_id column.

Code change

  • src/sync.rs — new Hlc { wall: i64, logical: i32 } type; SyncOpIn gains optional hlc; SyncOp echo gains non-optional hlc (server stamps the derived pair on v1-shape rows so every read carries a usable total-order tuple).
  • src/db.rs::insert_op_returning — takes optional hlc: Option<(i64, i32)>; binds the pair verbatim when Some, derives (0, lamport_ts) when None. v1 path keeps the existing lamport_ts range check + narrowing.
  • src/db.rspull_ops_since + fetch_op_by_operation_id SELECTs now project hlc_wall + hlc_logical.
  • src/api/sync.rs::push_ops — validates hlc.wall >= 0 at the API boundary (400 on negative wall); row_to_op reads + echoes the pair.

What this does NOT do

  • Apply pipeline propagation onto entity tables — A.2.2 follow-up. The new HLC columns on profile / library / track / playlist / etc. stay default-zero for now.
  • payload_hash computation + metadata_digest_version bump — A.2.2.
  • User-scoped digest routing for liked_track / track_rating — A.2.3.

Test plan

  • cargo check --all-targets --all-features — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo test --all-features — full suite passes (175 tests)
  • 3 new round-trip cases in tests/sync.rs:
    • push_v1_only_pulls_derived_hlc — push without hlc, pull echoes { wall: 0, logical: lamport_ts }
    • push_v2_hlc_round_trips_verbatim — push with hlc, pull echoes verbatim
    • push_v2_negative_wall_rejected — push with hlc.wall = -1 ⇒ 400

Refs

Summary by CodeRabbit

  • New Features

    • Support HLC (horloge hybride) sur l’API de synchronisation v2 ; les réponses incluent toujours une paire HLC. Rétrocompatibilité pour clients v1 (paire dérivée).
  • Bug Fixes / Comportement

    • Rejet des HLC avec composante wall ou logical négative (400 Bad Request).
    • Détection des collisions HLC renvoyant 409 avec payload HlcRegression précisant l’HLC en conflit et le device_id; régressions Lamport maintenues.
  • Tests

    • Nouveaux tests E2E couvrant round-trip HLC, rejets pour valeurs négatives et collisions HLC.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 850c596a-307d-4f56-9401-ada716f448ed

📥 Commits

Reviewing files that changed from the base of the PR and between 69762c4 and 93c4b76.

📒 Files selected for processing (1)
  • src/api/sync.rs

📝 Walkthrough

Walkthrough

Cette PR ajoute HLC complet au protocole de synchronisation : nouveau type Hlc, validation côté push, persistance et RETURNING de hlc_wall/hlc_logical, discrimination des collisions HLC (409 + HlcRegression), et tests E2E push→pull.

Changes

RFC-003 Phase A.2: Hybrid Logical Clock on Sync Operations

Couche / Fichiers Résumé
Type et contrats HLC
src/sync.rs
Nouveau type public Hlc { wall: i64, logical: i32 }. SyncOpIn reçoit pub hlc: Option<Hlc> (#[serde(default)]), SyncOp expose pub hlc: Hlc.
Persistance et récupération HLC
src/db.rs
sync::insert_op_returning prend hlc: Option<(i64,i32)>, dérive (0, lamport_ts) si absent, valide bornes, écrit/retourne hlc_wall/hlc_logical. fetch_op_by_operation_id et pull_ops_since projettent ces colonnes.
Validation API et gestion des conflits
src/api/sync.rs
Import Hlc; validation hlc.wall >= 0/hlc.logical >= 0 (400 si négatif); passe hlc_pair à l’insert. Gestion des 23505 : si contrainte sync_op_user_device_hlc_uniq -> 409 + HlcRegression { device_id, offending_hlc }, sinon comportement Lamport existant. row_to_op lit hlc_wall/hlc_logical et reconstruit SyncOp.hlc.
Tests E2E du cycle push→pull
tests/sync.rs
Ajout des tests : push_v1_only_pulls_derived_hlc, push_v2_hlc_round_trips_verbatim, push_v2_negative_wall_rejected, push_v2_duplicate_hlc_returns_hlc_regression, push_v2_negative_logical_rejected.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as api::push_ops
  participant DB as db::sync::insert_op_returning
  participant Postgres
  Client->>API: push SyncOpIn (hlc optional)
  API->>API: validate hlc (wall/logical >= 0) or rollback -> 400
  API->>DB: insert_op_returning(..., hlc_pair)
  DB->>DB: derive/validate hlc or use provided pair
  DB->>Postgres: INSERT ... RETURNING hlc_wall, hlc_logical
  Postgres-->>DB: row with hlc_wall/hlc_logical
  DB-->>API: inserted row
  API-->>Client: 200 or 409(HlcRegression) / 400
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#19: Le main PR étend directement la logique sync du PR #19 (handlers POST /ops/row_to_op/insert_op_returning qui géraient déjà lamport_ts et les 409 de régression) en ajoutant le support HLC (hlc_wall/hlc_logical) et une nouvelle branche de régression dédiée HlcRegression basée sur la contrainte SQL spécifique.
  • InstaZDLL/waveflow-server#50: Le PR #50 introduit les colonnes HLC hlc_wall/hlc_logical, la backfill et l’usage (fixe) de hlc_wall = 0 / hlc_logical = lamport_ts dans sync::insert_op_returning, que le main PR étend ensuite pour accepter une paire HLC fournie par le client et la renvoyer/valider via push_ops/row_to_op.
  • InstaZDLL/waveflow-server#26: Les deux PR étendent le même flux “push/read sync ops” en modifiant src/api/sync.rs et surtout db::sync::insert_op_returning/ses RETURNING pour ajouter de nouveaux champs de sync_op (HLC vs profile_canonical_id), ce qui les rend directement couplées au niveau des signatures et du mapping DB.

Poem

⏱️ Murmure des murs et compteurs alignés,
v1 prête le temps, v2 porte sa paire,
l’API vérifie, la DB enregistre,
conflits nommés renvoient leur éclair,
cinq tests veillent et gardent la mise.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning La description couvre les changements, la forme wire, le code modifié et le plan de test, mais manque certaines sections du template obligatoires. Ajouter une liste à puces structurée sous ## Changes, inclure les cases à cocher de test (cargo check, clippy, test), et ajouter la signature DCO requise à la fin.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit précisément la phase RFC-003 implémentée (A.2.1) et le changement principal : support dual-shape sur /sync/ops.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sync-v2-phase-a-2-dual-shape-ingest

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added type: feat New feature size: m 50-200 lines scope: server Server core (Rust) scope: db SQLite schema, migrations, queries scope: api Native /api/v2 surface scope: sync User-data sync and removed type: feat New feature labels Jun 13, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/sync.rs (1)

272-299: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ne mappez pas tous les 23505 à une régression Lamport.

Depuis l’ajout de l’unicité sur (user_id, device_id, hlc_wall, hlc_logical), cette branche couvre aussi les collisions HLC v2. Dans ce cas, répondre lamport_regression avec stored_max envoie un signal de reprise faux au client. Il faut distinguer au moins la contrainte HLC via db_err.constraint() et ne construire stored_max que pour la vraie contrainte Lamport.

💡 Direction de correction
-            Err(sqlx::Error::Database(db_err)) if db_err.code().as_deref() == Some("23505") => {
+            Err(sqlx::Error::Database(db_err)) if db_err.code().as_deref() == Some("23505") => {
+                if db_err.constraint() == Some("sync_op_user_device_hlc_uniq") {
+                    tx.rollback().await.ok();
+                    return (StatusCode::CONFLICT, "hlc_conflict").into_response();
+                }
                 // chemin lamport_regression existant

As per coding guidelines, src/api/sync.rs: "Return 409 + stored_max when a sync operation arrives with a lamport timestamp less than the last stored value (lamport regression)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/sync.rs` around lines 272 - 299, The current match arm treats every
SQL 23505 as a lamport regression; update it to inspect db_err.constraint() (via
db_err.constraint().as_deref()) and only run the lamport-specific path (call
db::sync::lamport_max(pool, user_id, device_id) and return the LamportRegression
JSON with stored_max and offending_lamport_ts) when the constraint name matches
the lamport unique index; for other constraint names (e.g. the HLC v2
constraint) return an appropriate 409 or other error response without computing
stored_max (or delegate to the existing ON CONFLICT behavior), ensuring you
still rollback the transaction as currently done and keep using the same
symbols: db_err.constraint(), lamport_max, LamportRegression, and the 23505
match.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/db.rs`:
- Around line 133-143: Dans le match qui construit (hlc_wall, hlc_logical) à
partir de hlc (le Some((wall, logical)) branch), ajoute une validation
symétrique à celle du chemin None : vérifier que logical >= 0 et que lamport_ts
est dans 0..=i64::from(i32::MAX) (ou que logical tient dans i32 si c'est
l'intention), et rejeter avec Err(sqlx::Error::Protocol(...)) le même style de
message si ces contraintes ne sont pas respectées; concrètement, dans le
Some((wall, logical)) arm (et toujours avant de retourner (wall, logical)), si
logical < 0 ou lamport_ts hors de l'intervalle, retourner l'erreur (de la même
forme que le cas None) afin d'empêcher la persistance de valeurs d'ordre
invalides et d'aligner le comportement v2 sur le chemin legacy.

---

Outside diff comments:
In `@src/api/sync.rs`:
- Around line 272-299: The current match arm treats every SQL 23505 as a lamport
regression; update it to inspect db_err.constraint() (via
db_err.constraint().as_deref()) and only run the lamport-specific path (call
db::sync::lamport_max(pool, user_id, device_id) and return the LamportRegression
JSON with stored_max and offending_lamport_ts) when the constraint name matches
the lamport unique index; for other constraint names (e.g. the HLC v2
constraint) return an appropriate 409 or other error response without computing
stored_max (or delegate to the existing ON CONFLICT behavior), ensuring you
still rollback the transaction as currently done and keep using the same
symbols: db_err.constraint(), lamport_max, LamportRegression, and the 23505
match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e3673396-7db4-4946-bcfb-8ad9814f7c07

📥 Commits

Reviewing files that changed from the base of the PR and between f2e9ff9 and 1306d43.

📒 Files selected for processing (4)
  • src/api/sync.rs
  • src/db.rs
  • src/sync.rs
  • tests/sync.rs

Comment thread src/db.rs
Phase A.2.1 of the RFC-003 sync v2 rollout. The /api/v1/sync/ops push
endpoint now accepts an optional v2 wire-shape `hlc: { wall, logical }`
field on each op; v1 clients keep working unchanged. Server prefers v2
when present; otherwise it derives the HLC pair from `lamport_ts` the
same way the A.1.1 backfill did (wall = 0, logical = lamport_ts).

The §2 total-order tiebreaker `origin_device_id` rides through the
existing `sync_op.device_id` TEXT column per A.1.1's design — no new
column, no separate wire field. V2 desktops format their device_id
as a UUID string; v1 desktops keep their free-form string. The apply
pipeline (A.2.2, follow-up PR) will parse `device_id` as UUID when
writing entity tables' UUID `origin_device_id` column.

Wire shape change is purely additive:
- SyncOpIn gains optional `hlc: Hlc { wall: i64, logical: i32 }`.
- SyncOp echo gains non-optional `hlc: Hlc` (server stamps the derived
  pair on v1-shape rows so every read carries a usable total-order
  tuple).
- insert_op_returning takes optional `hlc: Option<(i64, i32)>`; binds
  the pair verbatim when Some, derives from lamport_ts when None.
- pull_ops_since + fetch_op_by_operation_id SELECTs now project
  hlc_wall + hlc_logical so row_to_op can echo them.
- Push validates `hlc.wall >= 0` at the API boundary; negative wall
  ⇒ 400. Logical is i32 by the type, so no further narrowing check
  needed on the v2 path (the v1 path's lamport_ts narrowing guard
  stays in place).

Tests: 3 new round-trip cases in tests/sync.rs covering v1-only push
(derived hlc), v2 push (verbatim echo), and the negative-wall reject
boundary. Full suite green (175 tests).

What this does NOT do:
- Apply pipeline propagation onto entity tables — A.2.2 follow-up.
  The new hlc columns stay default-zero on entity rows for now.
- payload_hash computation + metadata_digest_version bump — A.2.2.
- User-scoped digest routing for liked_track / track_rating — A.2.3.

Refs RFC-003 §2 (total order), §Migration plan / Phase A.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Review fixes on #52:

1. db.rs: the v2 path's Some((wall, logical)) arm now mirrors the v1
   path's range guard — rejects negative wall OR negative logical with
   the same Protocol error shape. The API boundary already rejects
   wall < 0, but the helper is shared so a future caller bypassing
   the handler still gets a typed error instead of a row that breaks
   the §2 total-order invariant. Logical is i32 by the type so a
   negative is structurally legal but semantically wrong per RFC-003
   §2 (unsigned-shaped counter).

2. api/sync.rs: 23505 path now inspects db_err.constraint() to
   discriminate the legacy lamport UNIQUE from the A.1.1
   sync_op_user_device_hlc_uniq. A v2 HLC collision was getting
   reported as a misleading lamport_regression — the stored
   lamport_max is meaningless to a v2 client whose clock is the HLC
   pair, not lamport_ts. New HlcRegression body echoes the offending
   pair so the v2 client can resync its HLC instead of guessing.

Test: push_v2_duplicate_hlc_returns_hlc_regression exercises the
discrimination — different operation_id + advanced lamport_ts + same
HLC pair lands on the HLC UNIQUE, not the lamport one.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL
InstaZDLL force-pushed the feat/sync-v2-phase-a-2-dual-shape-ingest branch from 1306d43 to efccc3b Compare June 13, 2026 21:29
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/sync.rs`:
- Around line 217-222: La validation actuelle n'inspecte que hlc.wall; étendre
la vérification dans le bloc if let Some(hlc) = op_in.hlc pour aussi tester
hlc.logical < 0, appeler tx.rollback().await.ok(); et retourner
(StatusCode::BAD_REQUEST, "hlc.logical must be >= 0").into_response() (même
pattern que pour hlc.wall) afin de renvoyer 400 au lieu d'une erreur DB/500.

In `@tests/sync.rs`:
- Around line 841-866: Add a new test mirroring push_v2_negative_wall_rejected
that verifies negative hlc.logical is rejected: create an async sqlx::test named
push_v2_negative_logical_rejected using spawn_authenticated (e.g.
"hlc-neg-logical"), send the same POST to /api/v1/sync/ops via reqwest with an
ops entry that sets hlc: { "wall": 1_700_000_000_000_i64, "logical": -1_i32 },
and assert the response status is StatusCode::BAD_REQUEST; keep the same
structure/helpers (Uuid::new_v4(), lamport_ts, entity, field, payload) as in
push_v2_negative_wall_rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 244f9bdd-b8d3-4afe-9620-ea708cc51ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 1306d43 and efccc3b.

📒 Files selected for processing (4)
  • src/api/sync.rs
  • src/db.rs
  • src/sync.rs
  • tests/sync.rs

Comment thread src/api/sync.rs
Comment thread tests/sync.rs
Review fix on #52: the api/sync.rs push handler validated
`hlc.wall < 0` but not `hlc.logical < 0` — a v2 client sending a
negative logical would fall through to the db helper's defence-in-
depth `Protocol` error and surface as 500. Symmetric `logical >= 0`
check returns 400 the way `wall >= 0` already does.

New test `push_v2_negative_logical_rejected` mirrors the existing
`push_v2_negative_wall_rejected` shape.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/sync.rs (1)

154-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Documentation OpenAPI incomplète : HlcRegression manque sur le 409.

La route peut désormais renvoyer HlcRegression (collision HLC) ou LamportRegression (régression lamport) sur un 409. L'annotation utoipa ne documente que LamportRegression, ce qui rend le contrat API imprécis pour les clients v2.

🔧 Correctif proposé
         (status = 400, description = "Empty `device_id`, oversized batch, or malformed op"),
         (status = 401, description = "Missing or invalid bearer token"),
-        (status = 409, description = "Lamport regression — stored max returned in body", body = LamportRegression),
+        (status = 409, description = "Lamport or HLC regression — body is LamportRegression or HlcRegression depending on the violated constraint"),
         (status = 500, description = "Database or internal failure"),

Note : utoipa ne supporte pas nativement oneOf pour les réponses, mais le texte de description clarifie au moins le contrat pour les consommateurs de l'API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/sync.rs` around lines 154 - 159, La documentation Utoipa pour la
route de push (l'attribut responses dans src/api/sync.rs) n'indique que
LamportRegression pour le 409 alors que l'endpoint peut aussi renvoyer
HlcRegression; modifiez le bloc responses (référence: PushBatchResponse /
LamportRegression / HlcRegression) pour clarifier le 409 en mentionnant les deux
cas (par ex. description = "Lamport regression or HLC collision — response body
is LamportRegression or HlcRegression") ou, si possible, ajouter HlcRegression
comme body alternatif; conservez la signature actuelle mais assurez-vous que la
description explicite les deux types de réponse pour que le contrat API soit
correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/api/sync.rs`:
- Around line 154-159: La documentation Utoipa pour la route de push (l'attribut
responses dans src/api/sync.rs) n'indique que LamportRegression pour le 409
alors que l'endpoint peut aussi renvoyer HlcRegression; modifiez le bloc
responses (référence: PushBatchResponse / LamportRegression / HlcRegression)
pour clarifier le 409 en mentionnant les deux cas (par ex. description =
"Lamport regression or HLC collision — response body is LamportRegression or
HlcRegression") ou, si possible, ajouter HlcRegression comme body alternatif;
conservez la signature actuelle mais assurez-vous que la description explicite
les deux types de réponse pour que le contrat API soit correct.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49602af4-eae5-4190-bfd1-2b3f1c876a95

📥 Commits

Reviewing files that changed from the base of the PR and between efccc3b and 69762c4.

📒 Files selected for processing (2)
  • src/api/sync.rs
  • tests/sync.rs

Review fix on #52: the utoipa response block on POST /api/v1/sync/ops
listed `LamportRegression` as the only 409 body, but after the
HlcRegression addition the endpoint can return either shape
discriminated on the `error` field. utoipa 5 has no concise `oneOf`
for response bodies, so we list both shapes as sibling 409 entries
and document the discriminating `error` value in each description.
Also clarifies the 400 description to mention the new hlc.wall /
hlc.logical < 0 rejection paths.

No code-level behaviour change.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 13, 2026
@github-actions github-actions Bot added the type: feat New feature label Jun 13, 2026
@InstaZDLL
InstaZDLL merged commit a9a3d8b into main Jun 13, 2026
13 of 14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/sync-v2-phase-a-2-dual-shape-ingest branch June 13, 2026 22:05
InstaZDLL added a commit that referenced this pull request Jun 14, 2026
Two review fixes on #55:

1. apply::liked::apply — switched ON CONFLICT DO NOTHING to DO UPDATE
   so a repeat like on the same (user, file) refreshes hlc_wall /
   hlc_logical / origin_device_id / liked_at to the latest winning
   op. Without this, two devices liking the same file would converge
   on different row hashes once the A.2.3 digest endpoint goes
   live — the materialised row would keep the first-landing op's
   tuple while the canonical "winning" op (by sync_op.id under
   Phase A implicit LWW) would be the second. Mirrors the rating
   handler's existing UPSERT behaviour.

2. cargo fmt --all — fixes a formatting drift in api/sync.rs that
   #52 introduced and rustfmt would now reject on CI.

Both fixes pass clippy + the apply suite's liked / rating / library
stamp tests.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: api Native /api/v2 surface scope: db SQLite schema, migrations, queries scope: server Server core (Rust) scope: sync User-data sync size: m 50-200 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant