Skip to content

Commit ebff984

Browse files
committed
docs(spec): reconcile @-gated merge and edges-free resolve_author with implementation
1 parent a033d52 commit ebff984

1 file changed

Lines changed: 12 additions & 6 deletions

File tree

docs/superpowers/specs/2026-05-20-publish-and-upsert-hardening-design.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ ingest me / thread / engagement (existing)
113113
│ normalized Post (sparse for reply edges)
114114
115115
threads-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

Comments
 (0)