# Rescan and refresh both sources

Start with `stats()` and `source_scan_status()`. Read [browser.md](browser.md).
Keep archived copies. Sync alone does not authorize unsaving. For requested source
cleanup or previously authorized resume, follow [cleanup.md](cleanup.md).

## Account and scope

LinkedIn: https://www.linkedin.com/my-items/saved-posts/
X: https://x.com/i/bookmarks

Verify signed-in account through visible UI, not a saved post's author. Use its
stable profile URL or handle as account. Include every saved-content section and
bookmark folder visible to that account. Unsupported types or inaccessible
folders remain explicit exceptions.

Call `start_source_scan(platform, account)`. Keep its scan id across batches.
For resume, inspect `source_scan_status(scan_id)` and verify the same account.

## Capture, import, checkpoint

1. `start_sync(platform, route)`: browser supports both sources. File accepts
   capture JSON or LinkedIn Saved Items CSV/ZIP; X account archives are unsupported.
2. `get_capture_script(platform, sync_id)` returns the optional console adapter
   and route terms. Read terms when choosing the route. Preserve existing session
   authorization; do not ask repeatedly. LinkedIn also offers its own data export,
   typically links/dates only.
3. Follow browser.md. Prefer batches of at most 50 rendered cards. Upload directly
   to the signed URL (10 MiB/part), or `push_items(sync_id, items)` (50 maximum).
   Explain that push arguments carry post text through this conversation; direct
   browser uploads avoid that.
4. `finish_sync(sync_id)`, then `sync_status(sync_id)`. Check received,
   imported, updated, unchanged and rejected counts. Resolve rejected rows.
5. `record_source_scan_batch(scan_id, sync_id, account)` records exact identities
   from that finished import. Commit each batch before continuing. Repeating a
   batch deduplicates it. Interrupted unfinished uploads require recapture;
   completed imports and scan receipts survive restart.

Import identity is owner/platform/source id. Refresh adds new posts and upgrades
longer captures; confirmed categories survive. Shorter/equal-length edited text
does not replace stored text. Before cleanup, compare the archive with the visible
original; incomplete or mismatched content must remain saved. URL-only exports
cannot authorize removal.

## Coverage proof

Quiet scrolling, timeout, round limits and a Send button do not prove coverage.
Console adapters label output PARTIAL. Capture virtualized cards before scrolling,
expand truncated content, distinguish outer posts from quoted posts.

Call `complete_source_scan(scan_id, account, expected_count, unresolved, end_evidence, evidence)`
only with explicit source end or independently displayed total, matching unique
count and zero unresolved cards. end_evidence: explicit_end, displayed_total,
empty_state. Do not derive a supposed source total from your own captured count.
Without terminal/count evidence, retain partial status.

For cleanup, drain verified batches until explicit empty state; then empty_state
can close the scan. expected_count is cumulative unique inventory, and every
observed item must have a verified removal outcome.

## Report and expiry

Use `source_scan_status(scan_id)`, `stats()` and `dashboard_url()`.
Per platform/account report unique observed, imported/updated, rejected/unresolved,
coverage complete/partial and pending cleanup. Archive count is not source count.
Check `sync_status(sync_id)` for category progress and `list_categories()` before
offering category setup.

Signed URLs expire in 30 minutes. Upload staging expires after 30 idle minutes,
two hours maximum, or restart. Start a new sync and recapture; do not unsave
anything whose finished import cannot be read back.
