@@ -113,8 +113,8 @@ ingest me / thread / engagement (existing)
113113 │ normalized Post (sparse for reply edges)
114114 ▼
115115threads-store::upsert_post_tx
116- load_post(existing) ─► Post::merge(existing, incoming) ─► write merged row
117- └► reconcile edges/children
116+ @-sentinel author ─► load_post ─► Post::merge ─► write merged row + reconcile children
117+ real (numeric) author ─► overwrite (trust the full fetch; allows clearing fields)
118118 + author resolution: rewrite @handle → real id when (username→id) is known
119119```
120120
@@ -143,9 +143,14 @@ Merge rules:
143143| ` id ` , ` raw ` | ` incoming ` |
144144
145145** ` crates/threads-store/src/query.rs ` ** — ` upsert_post_tx ` :
146- - Before writing, ` load_post(&tx, id) ` ; if ` Some ` , compute ` Post::merge(existing, incoming) `
146+ - Before writing, ** and only when the incoming author is the ` @username ` sentinel** (i.e. a
147+ sparse reply-edge fetch), ` load_post(&tx, id) ` ; if ` Some ` , compute ` Post::merge(existing, incoming) `
147148 and use the merged value for the row write, child-table rebuild, and edge reconciliation.
148- - Keep the existing delete-then-reinsert of children/edges, but drive it from the merged post.
149+ - For a real (non-` @ ` ) author — a full fetch — skip the merge and keep the prior overwrite, so
150+ intentional corrections (e.g. clearing ` parent_id ` ) still propagate. This reconciles the
151+ merge's field-preservation rules with the existing ` reupsert_without_parent_drops_stale_edges `
152+ behavior; the ` @ ` sentinel is the signal that the fetch is structurally incomplete.
153+ - Keep the existing delete-then-reinsert of children/edges, but drive it from the resulting post.
149154
150155** Author resolution** (` threads-provider-official ` + ` threads-store ` + ` threads-ingest ` ):
151156- * Current state:* the live author synthesis is in ` provider::dto_to_post ` , which uses a
@@ -162,8 +167,9 @@ Merge rules:
162167 ` ingest_engagement ` (currently ` me ` is fetched then discarded except ` .id ` ).
163168- ` threads-store ` : add ` resolve_author(username, real_id) ` that, in one txn, upserts the
164169 real user, runs ` UPDATE posts SET author_id = real_id WHERE author_id = '@' || username ` ,
165- rewrites owned edges whose ` from_id ` was the sentinel, and deletes the ` @username `
166- placeholder user row.
170+ then deletes the ` @username ` placeholder user row (posts are re-keyed before the delete so
171+ the ` ON DELETE CASCADE ` is a no-op). ` edges ` are intentionally NOT touched — ` edges.from_id `
172+ holds post ids, never an author handle, so an author rewrite has nothing to reconcile there.
167173- Trigger resolution whenever a ` (username, real_id) ` pair is observed (from ` /me ` , or any
168174 post DTO carrying both ` owner.id ` and ` username ` ).
169175- The store is pre-1.0 and re-ingestable, so any rows written under the old bare-username
0 commit comments