@factorypure/client-helpers 1.1.13 → 1.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -506,6 +506,30 @@ export declare const immersiveScrapeResultsSchema: z.ZodObject<{
506
506
  normalized: z.ZodString;
507
507
  }, z.core.$strip>>;
508
508
  }, z.core.$strip>>;
509
+ ai_extracted_sku: z.ZodOptional<z.ZodNullable<z.ZodString>>;
510
+ ai_extracted_brand: z.ZodOptional<z.ZodNullable<z.ZodString>>;
511
+ ai_best_match_sku: z.ZodOptional<z.ZodNullable<z.ZodString>>;
512
+ ai_alternate_match_skus: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
513
+ ai_extraction_confidence: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
514
+ ai_extraction_reasoning: z.ZodOptional<z.ZodNullable<z.ZodString>>;
515
+ ai_extraction_quality: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
516
+ high: "high";
517
+ medium: "medium";
518
+ low: "low";
519
+ }>>>;
520
+ ai_extraction_method: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
521
+ openai: "openai";
522
+ regex_fallback: "regex_fallback";
523
+ cached: "cached";
524
+ none: "none";
525
+ }>>>;
526
+ ai_processed_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
527
+ listing_hash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
528
+ ai_unique_skus_found: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodNumber>>>;
529
+ ai_most_common_sku: z.ZodOptional<z.ZodNullable<z.ZodString>>;
530
+ ai_sku_confidence_avg: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
531
+ ai_high_quality_listing_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
532
+ ai_total_listing_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
509
533
  inline_link: z.ZodOptional<z.ZodNull>;
510
534
  delivery: z.ZodOptional<z.ZodNull>;
511
535
  }, z.core.$strip>;
@@ -576,6 +600,15 @@ export declare const globalScrapeOptionsSchema: z.ZodObject<{
576
600
  scam_sources: z.ZodArray<z.ZodString>;
577
601
  }, z.core.$strip>;
578
602
  export type GlobalScrapeOptionsType = z.infer<typeof globalScrapeOptionsSchema>;
603
+ /**
604
+ * AI Comparison result data structure
605
+ */
606
+ export type ComparisonMapType = Record<string, Record<string, {
607
+ is_match: number;
608
+ sufficient_info: number;
609
+ result_description: string;
610
+ compared_at: string;
611
+ }>>;
579
612
  /**
580
613
  * Context provided to filter rules during evaluation
581
614
  */
@@ -593,6 +626,7 @@ export type FilterContext<T = ScrapeResultsType | ImmersiveScrapeResultsType> =
593
626
  variantScrapeOptions: VariantScrapeOptionsType;
594
627
  vendorScrapeOptions: VendorScrapeOptionsType;
595
628
  globalScrapeOptions: GlobalScrapeOptionsType;
629
+ comparisonMap?: ComparisonMapType;
596
630
  };
597
631
  /**
598
632
  * Base interface for filter rules
@@ -654,6 +688,7 @@ export declare const HIDE_REASONS: {
654
688
  VENDOR_EXCLUSION: string;
655
689
  CALCULATED_BRAND_MISMATCH: string;
656
690
  SCAM_SOURCE_EXCLUSION: string;
691
+ AI_COMPARISON_MISMATCH: string;
657
692
  };
658
693
  export declare const HIDE_OVERRIDE_REASONS: {
659
694
  SKU_MATCH: string;
@@ -708,7 +743,7 @@ export declare class FilterEngine {
708
743
  /**
709
744
  * Evaluate rules for a batch of results
710
745
  */
711
- evaluateBatch<T extends ScrapeResultsType | ImmersiveScrapeResultsType>(results: T[], variant: FilterContext<T>['variant'], variantScrapeOptions: VariantScrapeOptionsType, vendorScrapeOptions: VendorScrapeOptionsType, globalScrapeOptions: GlobalScrapeOptionsType): T[];
746
+ evaluateBatch<T extends ScrapeResultsType | ImmersiveScrapeResultsType>(results: T[], variant: FilterContext<T>['variant'], variantScrapeOptions: VariantScrapeOptionsType, vendorScrapeOptions: VendorScrapeOptionsType, globalScrapeOptions: GlobalScrapeOptionsType, comparisonMap?: ComparisonMapType): T[];
712
747
  /**
713
748
  * Calculate visibility state based on filter results
714
749
  */
@@ -755,7 +790,7 @@ export declare function createCustomFilterRule(config: {
755
790
  * Filter scrape results using the new rule-based engine
756
791
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
757
792
  */
758
- export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, }: {
793
+ export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, }: {
759
794
  scrapeResults: T[];
760
795
  variant: {
761
796
  id: number;
@@ -771,6 +806,7 @@ export declare const filterScrapeResultsV2: <T extends ScrapeResultsType | Immer
771
806
  globalScrapeOptions: GlobalScrapeOptionsType;
772
807
  filterConfig?: FilterConfiguration;
773
808
  customRules?: FilterRule[];
809
+ comparisonMap?: ComparisonMapType;
774
810
  }) => T[];
775
811
  export declare const filterScrapeResults: <T extends ScrapeResultsType | ImmersiveScrapeResultsType>({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, }: {
776
812
  scrapeResults: T[];
package/dist/index.js CHANGED
@@ -331,6 +331,23 @@ export const immersiveScrapeResultsSchema = z.object({
331
331
  brand: z.string().nullable(),
332
332
  company_id: z.number().nullable(),
333
333
  regexUnitResults: z.nullable(regexUnitResultsSchema),
334
+ // AI extraction fields (per store listing)
335
+ ai_extracted_sku: z.string().nullable().optional(),
336
+ ai_extracted_brand: z.string().nullable().optional(),
337
+ ai_best_match_sku: z.string().nullable().optional(),
338
+ ai_alternate_match_skus: z.array(z.string()).nullable().optional(),
339
+ ai_extraction_confidence: z.number().nullable().optional(),
340
+ ai_extraction_reasoning: z.string().nullable().optional(),
341
+ ai_extraction_quality: z.enum(['high', 'medium', 'low']).nullable().optional(),
342
+ ai_extraction_method: z.enum(['openai', 'regex_fallback', 'cached', 'none']).nullable().optional(),
343
+ ai_processed_at: z.string().nullable().optional(),
344
+ listing_hash: z.string().nullable().optional(),
345
+ // AI aggregate fields (from variant_scrape_found_product_ids)
346
+ ai_unique_skus_found: z.record(z.string(), z.number()).nullable().optional(),
347
+ ai_most_common_sku: z.string().nullable().optional(),
348
+ ai_sku_confidence_avg: z.number().nullable().optional(),
349
+ ai_high_quality_listing_count: z.number().nullable().optional(),
350
+ ai_total_listing_count: z.number().nullable().optional(),
334
351
  inline_link: z.null().optional(),
335
352
  delivery: z.null().optional(),
336
353
  });
@@ -403,6 +420,7 @@ export const HIDE_REASONS = {
403
420
  VENDOR_EXCLUSION: 'Vendor Exclusion',
404
421
  CALCULATED_BRAND_MISMATCH: 'Calculated Brand Mismatch',
405
422
  SCAM_SOURCE_EXCLUSION: 'Scam Source Exclusion',
423
+ AI_COMPARISON_MISMATCH: 'AI Comparison Mismatch',
406
424
  };
407
425
  export const HIDE_OVERRIDE_REASONS = {
408
426
  SKU_MATCH: 'SKU Match',
@@ -427,6 +445,7 @@ const HIDE_ALWAYS_MAP = {
427
445
  [HIDE_REASONS.SKIP_SKU]: false,
428
446
  [HIDE_REASONS.VENDOR_EXCLUSION]: false,
429
447
  [HIDE_REASONS.SCAM_SOURCE_EXCLUSION]: true,
448
+ [HIDE_REASONS.AI_COMPARISON_MISMATCH]: false,
430
449
  };
431
450
  export const TOO_CHEAP_MULTIPLIER = 0.75;
432
451
  export const TOO_EXPENSIVE_MULTIPLIER = 1.15;
@@ -517,7 +536,7 @@ export class FilterEngine {
517
536
  /**
518
537
  * Evaluate rules for a batch of results
519
538
  */
520
- evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions) {
539
+ evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap) {
521
540
  return results.map((result) => {
522
541
  const context = {
523
542
  result,
@@ -525,6 +544,7 @@ export class FilterEngine {
525
544
  variantScrapeOptions,
526
545
  vendorScrapeOptions,
527
546
  globalScrapeOptions,
547
+ comparisonMap,
528
548
  };
529
549
  const filterResults = this.evaluateResult(context);
530
550
  // Merge with existing filter_results from batch processing (duplicates, search exclusions, etc.)
@@ -538,7 +558,9 @@ export class FilterEngine {
538
558
  result.hide_reasons = allFilterResults
539
559
  .filter((fr) => fr.severity === FilterSeverity.BLOCK || fr.severity === FilterSeverity.WARNING)
540
560
  .map((fr) => fr.message);
541
- result.hide_override_reasons = allFilterResults.filter((fr) => fr.metadata?.isOverride).map((fr) => fr.message);
561
+ result.hide_override_reasons = allFilterResults
562
+ .filter((fr) => fr.metadata?.isOverride)
563
+ .map((fr) => fr.message);
542
564
  result.ignore_result = visibilityState === VisibilityState.HIDDEN;
543
565
  return result;
544
566
  });
@@ -583,8 +605,20 @@ export class FilterEngine {
583
605
  if (overridableBlocks.length > 0) {
584
606
  return VisibilityState.HIDDEN;
585
607
  }
586
- // If we have warnings, show with warnings
587
- if (warningResults.length > 0) {
608
+ // Check for warnings that haven't been overridden
609
+ const nonOverriddenWarnings = warningResults.filter((wr) => {
610
+ const rule = this.registry.getRule(wr.ruleId);
611
+ if (!rule || !rule.canBeOverridden)
612
+ return true;
613
+ // Check if any override can override this warning
614
+ const canBeOverridden = overrideResults.some((or) => {
615
+ const overrideRule = this.registry.getRule(or.ruleId);
616
+ return overrideRule && rule.overridableBy.includes(overrideRule.id);
617
+ });
618
+ return !canBeOverridden; // Still a warning if no override present
619
+ });
620
+ // If we have non-overridden warnings, show with warnings
621
+ if (nonOverriddenWarnings.length > 0) {
588
622
  return VisibilityState.VISIBLE_WITH_WARNINGS;
589
623
  }
590
624
  return VisibilityState.VISIBLE;
@@ -714,6 +748,55 @@ class CompetitorExclusionRule {
714
748
  return null;
715
749
  }
716
750
  }
751
+ /**
752
+ * Rule for filtering AI comparison mismatches
753
+ */
754
+ class AIComparisonMismatchRule {
755
+ id = 'ai_comparison_mismatch';
756
+ name = 'AI Comparison Mismatch';
757
+ description = 'Filters results where AI comparison determined the listing does not match the variant';
758
+ severity = FilterSeverity.BLOCK;
759
+ priority = 85;
760
+ enabled = true;
761
+ canBeOverridden = true;
762
+ overridableBy = ['sku_match', 'calculated_sku_match', 'alt_sku_match'];
763
+ evaluate(context) {
764
+ // Only applies to results with listing_hash (primarily immersive results)
765
+ const result = context.result;
766
+ if (!result.listing_hash || !context.comparisonMap) {
767
+ return null;
768
+ }
769
+ const variantId = String(context.variant.id);
770
+ const listingHash = result.listing_hash;
771
+ // Check if we have comparison data for this variant and listing
772
+ const variantComparisons = context.comparisonMap[variantId];
773
+ if (!variantComparisons) {
774
+ return null;
775
+ }
776
+ const comparison = variantComparisons[listingHash];
777
+ if (!comparison) {
778
+ return null;
779
+ }
780
+ // Only filter confirmed mismatches (is_match = 0 AND sufficient_info = 1)
781
+ // Allow results where we don't have sufficient info to compare
782
+ if (comparison.is_match === 0 && comparison.sufficient_info === 1) {
783
+ return {
784
+ ruleId: this.id,
785
+ severity: this.severity,
786
+ message: HIDE_REASONS.AI_COMPARISON_MISMATCH,
787
+ metadata: {
788
+ listingHash,
789
+ isMatch: comparison.is_match,
790
+ sufficientInfo: comparison.sufficient_info,
791
+ resultDescription: comparison.result_description,
792
+ comparedAt: comparison.compared_at,
793
+ },
794
+ timestamp: new Date().toISOString(),
795
+ };
796
+ }
797
+ return null;
798
+ }
799
+ }
717
800
  /**
718
801
  * Rule for filtering duplicates
719
802
  */
@@ -1218,6 +1301,7 @@ export function createDefaultFilterRegistry() {
1218
1301
  registry.registerRule(new LowPriceOutlierRule());
1219
1302
  registry.registerRule(new DateOutlierRule());
1220
1303
  registry.registerRule(new CompetitorExclusionRule());
1304
+ registry.registerRule(new AIComparisonMismatchRule());
1221
1305
  registry.registerRule(new DuplicateRule());
1222
1306
  registry.registerRule(new SearchExclusionRule());
1223
1307
  registry.registerRule(new SkipSkuRule());
@@ -1325,7 +1409,7 @@ export function createCustomFilterRule(config) {
1325
1409
  * Filter scrape results using the new rule-based engine
1326
1410
  * This provides more flexibility and better extensibility than the legacy filterScrapeResults
1327
1411
  */
1328
- export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, }) => {
1412
+ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, filterConfig, customRules, comparisonMap, }) => {
1329
1413
  const registry = createDefaultFilterRegistry();
1330
1414
  // Register custom rules if provided
1331
1415
  if (customRules) {
@@ -1340,7 +1424,7 @@ export const filterScrapeResultsV2 = ({ scrapeResults, variant, variantScrapeOpt
1340
1424
  let results = handleDuplicatesV2(scrapeResults);
1341
1425
  results = handleSearchExclusionsV2(results, variantScrapeOptions, variant, registry);
1342
1426
  // Evaluate all other rules
1343
- results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions);
1427
+ results = engine.evaluateBatch(results, variant, variantScrapeOptions, vendorScrapeOptions, globalScrapeOptions, comparisonMap);
1344
1428
  return results;
1345
1429
  };
1346
1430
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorypure/client-helpers",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",