# 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.
