set-listing-catalog-cohort.ts 4.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. import 'dotenv/config';
  2. import { z } from 'zod';
  3. import { ParseRestClient } from '../src/db/parse-rest.client.js';
  4. import { VOC_PARSE_CLASSES } from '../src/db/parse-rest.schema.js';
  5. import type { ListingScoreResult, ListingSourceSnapshot } from '../src/modules/listing-ai/domain.js';
  6. import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js';
  7. const COHORT = 'listing-jd-v3-formal-625';
  8. interface Stored<T> { objectId: string; productId?: string; isCurrent?: boolean; payload: T }
  9. async function main() {
  10. const env = z.object({ PARSE_SERVER_URL: z.url(), PARSE_APP_ID: z.string().min(1), PARSE_MASTER_KEY: z.string().min(1), SAAS_DEFAULT_WORKSPACE_ID: z.string().default('demashi') }).parse(process.env);
  11. const apply = process.argv.includes('--apply=true');
  12. const expected = Number(process.argv.find((value) => value.startsWith('--expected-count='))?.split('=')[1] ?? 625);
  13. const client = new ParseRestClient({ serverUrl: env.PARSE_SERVER_URL, appId: env.PARSE_APP_ID, masterKey: env.PARSE_MASTER_KEY });
  14. const workspaceId = env.SAAS_DEFAULT_WORKSPACE_ID;
  15. const [scores, sources] = await Promise.all([
  16. client.findAll<Stored<ListingScoreResult>>(VOC_PARSE_CLASSES.listingCurrentScore, { workspaceId }),
  17. client.findAll<Stored<ListingSourceSnapshot>>(VOC_PARSE_CLASSES.listingSourceSnapshot, { workspaceId, platform: 'jd', isCurrent: true }),
  18. ]);
  19. const included = new Set(scores.map((row) => row.payload).filter((score) => typeof score.overallScore === 'number' && Number.isFinite(score.overallScore)).map((score) => score.productId));
  20. if (included.size !== expected) throw new Error(`cohort_count_mismatch expected=${expected} actual=${included.size}`);
  21. const report = { mode: apply ? 'apply' : 'dry-run', workspaceId, cohort: COHORT, currentSources: sources.length, included: included.size, excluded: sources.filter((row) => !included.has(row.payload.productId)).length };
  22. console.log(JSON.stringify(report, null, 2));
  23. if (!apply) return;
  24. const requests = sources.map((row) => ({ method: 'PUT' as const, path: `/classes/${VOC_PARSE_CLASSES.listingSourceSnapshot}/${row.objectId}`, body: { catalogIncluded: included.has(row.payload.productId), catalogCohort: COHORT } }));
  25. for (let index = 0; index < requests.length; index += 50) await client.batch(requests.slice(index, index + 50));
  26. const active = await client.count(VOC_PARSE_CLASSES.listingSourceSnapshot, { workspaceId, platform: 'jd', isCurrent: true, catalogIncluded: true, catalogCohort: COHORT });
  27. if (active !== expected) throw new Error(`cohort_apply_verification_failed expected=${expected} actual=${active}`);
  28. const repository = new ParseRestListingAiRepository(client);
  29. const excludedProductId = sources.find((row) => !included.has(row.payload.productId))?.payload.productId ?? null;
  30. const includedProductId = visibleProductId(included);
  31. const [visibleSources, summary, firstPage] = await Promise.all([
  32. repository.listAllSources(workspaceId, 'jd'),
  33. repository.catalogSummary(workspaceId, 'jd'),
  34. repository.listProducts({ workspaceId, platform: 'jd', cursor: null, limit: 25, sort: 'productId' }),
  35. ]);
  36. const [includedSource, excludedSource] = await Promise.all([
  37. includedProductId ? repository.getSource(workspaceId, 'jd', includedProductId) : null,
  38. excludedProductId ? repository.getSource(workspaceId, 'jd', excludedProductId) : null,
  39. ]);
  40. const firstPageOutsideCohort = firstPage.items.filter((item) => !included.has(item.productId)).length;
  41. if (visibleSources.length !== expected || summary.sourceTotal !== expected || firstPage.items.length !== 25 || firstPageOutsideCohort > 0 || !includedSource || excludedSource) {
  42. throw new Error(`cohort_repository_verification_failed sources=${visibleSources.length} summary=${summary.sourceTotal} pageItems=${firstPage.items.length} outside=${firstPageOutsideCohort} includedVisible=${Boolean(includedSource)} excludedVisible=${Boolean(excludedSource)}`);
  43. }
  44. console.log(JSON.stringify({ status: 'completed', active, repositoryVisible: visibleSources.length, catalogTotal: summary.sourceTotal, firstPageItems: firstPage.items.length, firstPageOutsideCohort, includedProductVisible: Boolean(includedSource), excludedProductVisible: Boolean(excludedSource) }, null, 2));
  45. }
  46. function visibleProductId(productIds: Set<string>): string | null {
  47. return productIds.values().next().value ?? null;
  48. }
  49. main().catch((error) => { console.error(`[set-listing-catalog-cohort] ${error instanceof Error ? error.message : error}`); process.exitCode = 1; });