Files
inkreach-official-website/plans/fix/sds-sync-guard-fix.md
T
yeuimu aed9afef92 fix(api): add sync safety guards against degenerate SDS responses
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.
2026-08-20 16:37:44 +08:00

4.5 KiB

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/2delisted=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.