Add SYNC_GUARDS thresholds so a partial/degenerate upstream response never triggers a destructive operation: - skip stale category deletion when the fetched tree is suspiciously small vs the existing SDS category count - skip delist detection unless both leaf-category and seen-product counts are healthy Verified: 77 tests pass; live SDS returns 226 categories (guard off), incident-case ratios (2/226, 2/2) are correctly blocked.
87 lines
4.5 KiB
Markdown
87 lines
4.5 KiB
Markdown
# SDS Sync Guard Fix Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Prevent the SDS (mapi.sdspod.com) sync from deleting the category/product library when the upstream API returns a degenerate/partial response, and recover the data lost in the 2026-08-20 incident.
|
|
|
|
**Background / Root cause:**
|
|
- On 2026-08-20 06:31 a manual sync ran while `category/tree/3` returned only 2 nodes (normal: 226).
|
|
- `syncCategories` treats SDS as the single source of truth: `staleDeleted=225` deleted all previously synced SDS categories.
|
|
- With SDS categories gone, `syncProducts` had `leafCategories=0/2` → `delisted=523` marked 523/536 origin goods as delisted.
|
|
- Result: all 536 origin goods `delisted=true`; all 240 website `Good` rows reference delisted origin goods → `/public/goods` returns `total: 0` → product-center page appears empty.
|
|
|
|
**Architecture:** Add safety guards to `SyncService` and `SdsClientService` so a partial/failed upstream response never triggers destructive operations (`staleDeleted` / delist marking). Recovery is a data fix (re-enable delisted origin goods) followed by normal re-sync.
|
|
|
|
**Tech Stack:** NestJS, TypeScript, Prisma, Jest.
|
|
|
|
---
|
|
|
|
### Task 1: Data recovery — re-enable all origin goods
|
|
|
|
**Files:**
|
|
- Run ad-hoc prisma script (read+write to DB via apps/api @prisma/client)
|
|
|
|
- [ ] **Step 1: Reset `delisted=false` for all `originGood` rows**
|
|
All 536 rows were mis-flagged by the 08-20 partial sync. Run
|
|
`prisma.originGood.updateMany({ where: {}, data: { delisted: false } })`.
|
|
- [ ] **Step 2: Trigger category sync then product sync**
|
|
Verify `/sync/status` logs show SUCCESS with healthy numbers
|
|
(categories ≈ 225+, products ≈ 519+).
|
|
- [ ] **Step 3: Verify `/public/goods` non-empty and product-center page renders products.**
|
|
|
|
### Task 2: Hardening — category sync stale-deletion guard
|
|
|
|
**Files:**
|
|
- Modify: `apps/api/src/sync/sync.service.ts`
|
|
- Modify: `apps/api/src/sync/sync.service.spec.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
Given existing SDS-linked categories in DB, when `fetchCategoryTree` returns a
|
|
degenerate small tree (< MIN_CATEGORY_COUNT), `syncCategories` must NOT delete
|
|
existing categories (no stale deletion) and must log a warning.
|
|
- [ ] **Step 2: Run test, verify RED.**
|
|
- [ ] **Step 3: Implement guard**
|
|
Before the stale-deletion block, compute `existingSdsCount = categories with sdsCategoryId`.
|
|
Skip stale deletion when `flat.length < MIN_CATEGORY_COUNT (10)` OR
|
|
`flat.length < MIN_CATEGORY_RATIO (0.5) * existingSdsCount`. Log warning with counts.
|
|
- [ ] **Step 4: Verify GREEN + no regressions.**
|
|
|
|
### Task 3: Hardening — product sync delist guard
|
|
|
|
**Files:**
|
|
- Modify: `apps/api/src/sync/sync.service.ts`
|
|
- Modify: `apps/api/src/sync/sync.service.spec.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
When there are very few leaf categories (< MIN_LEAF_CATEGORIES) or very few seen
|
|
products (< MIN_SEEN_GOODS), `syncProducts` must skip the delist/reactivate marking
|
|
and log a warning instead of mass-delisting.
|
|
- [ ] **Step 2: Run test, verify RED.**
|
|
- [ ] **Step 3: Implement guard**
|
|
Only run delist detection when `leafRows.length >= MIN_LEAF_CATEGORIES (10)` AND
|
|
`seenSdsGoodIds.size >= MIN_SEEN_GOODS (50)`. Otherwise skip and log warning.
|
|
- [ ] **Step 4: Verify GREEN + no regressions.**
|
|
|
|
### Task 4: Hardening — SDS client response sanity validation
|
|
|
|
**Files:**
|
|
- Modify: `apps/api/src/sync/sds-client.service.ts`
|
|
- Modify: `apps/api/src/sync/sds-client.service.spec.ts` (create if absent)
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
`fetchCategoryTree` throws when the returned array is degenerate (e.g. fewer than
|
|
`MIN_SDS_CATEGORY_NODES`), so callers never operate on a bad tree.
|
|
- [ ] **Step 2: Run test, verify RED.**
|
|
- [ ] **Step 3: Implement**
|
|
If the fetched category array length < MIN_SDS_CATEGORY_NODES (10), throw an Error
|
|
with the received count so sync logs FAILED (non-destructive) instead of deleting data.
|
|
- [ ] **Step 4: Verify GREEN + no regressions.**
|
|
|
|
### Task 5: Full verification
|
|
|
|
- [ ] **Step 1:** Run `apps/api` full jest suite — all green.
|
|
- [ ] **Step 2:** Typecheck `apps/api` (`tsc -p tsconfig.json --noEmit` or nest build).
|
|
- [ ] **Step 3:** Trigger a real sync via the admin UI / HTTP; confirm sync logs show
|
|
healthy category/product counts and `/public/goods` returns data.
|
|
- [ ] **Step 4:** Confirm product-center page at `http://192.168.124.137:3000/product-center` renders products.
|