@ucptools/validator 1.0.0 → 1.1.0

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.
Files changed (330) hide show
  1. package/dist/auth/config.d.ts +20 -0
  2. package/dist/auth/config.d.ts.map +1 -0
  3. package/dist/auth/config.js +114 -0
  4. package/dist/auth/config.js.map +1 -0
  5. package/dist/auth/index.d.ts +5 -0
  6. package/dist/auth/index.d.ts.map +1 -0
  7. package/dist/auth/index.js +17 -0
  8. package/dist/auth/index.js.map +1 -0
  9. package/dist/auth/middleware.d.ts +45 -0
  10. package/dist/auth/middleware.d.ts.map +1 -0
  11. package/dist/auth/middleware.js +170 -0
  12. package/dist/auth/middleware.js.map +1 -0
  13. package/dist/auth/service.d.ts +80 -0
  14. package/dist/auth/service.d.ts.map +1 -0
  15. package/dist/auth/service.js +298 -0
  16. package/dist/auth/service.js.map +1 -0
  17. package/dist/cli/index.d.ts +6 -0
  18. package/dist/cli/index.d.ts.map +1 -0
  19. package/dist/cli/index.js +375 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/cli/mock-server.d.ts +20 -0
  22. package/dist/cli/mock-server.d.ts.map +1 -0
  23. package/dist/cli/mock-server.js +261 -0
  24. package/dist/cli/mock-server.js.map +1 -0
  25. package/dist/compliance/compliance-generator.d.ts +34 -0
  26. package/dist/compliance/compliance-generator.d.ts.map +1 -0
  27. package/dist/compliance/compliance-generator.js +320 -0
  28. package/dist/compliance/compliance-generator.js.map +1 -0
  29. package/dist/compliance/index.d.ts +8 -0
  30. package/dist/compliance/index.d.ts.map +1 -0
  31. package/dist/compliance/index.js +17 -0
  32. package/dist/compliance/index.js.map +1 -0
  33. package/dist/compliance/templates.d.ts +34 -0
  34. package/dist/compliance/templates.d.ts.map +1 -0
  35. package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
  36. package/dist/compliance/templates.js.map +1 -0
  37. package/dist/compliance/types.d.ts +64 -0
  38. package/dist/compliance/types.d.ts.map +1 -0
  39. package/dist/compliance/types.js +64 -0
  40. package/dist/compliance/types.js.map +1 -0
  41. package/dist/db/index.d.ts +17 -0
  42. package/dist/db/index.d.ts.map +1 -0
  43. package/dist/db/index.js +80 -0
  44. package/dist/db/index.js.map +1 -0
  45. package/dist/db/schema.d.ts +3886 -0
  46. package/dist/db/schema.d.ts.map +1 -0
  47. package/dist/db/schema.js +425 -0
  48. package/dist/db/schema.js.map +1 -0
  49. package/dist/db/utils.d.ts +252 -0
  50. package/dist/db/utils.d.ts.map +1 -0
  51. package/dist/db/utils.js +295 -0
  52. package/dist/db/utils.js.map +1 -0
  53. package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
  54. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
  55. package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +856 -726
  56. package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
  57. package/dist/feed-analyzer/index.d.ts +8 -0
  58. package/dist/feed-analyzer/index.d.ts.map +1 -0
  59. package/dist/feed-analyzer/index.js +19 -0
  60. package/dist/feed-analyzer/index.js.map +1 -0
  61. package/dist/feed-analyzer/types.d.ts +285 -0
  62. package/dist/feed-analyzer/types.d.ts.map +1 -0
  63. package/dist/feed-analyzer/types.js +175 -0
  64. package/dist/feed-analyzer/types.js.map +1 -0
  65. package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
  66. package/dist/generator/index.d.ts.map +1 -0
  67. package/dist/generator/index.js +13 -0
  68. package/dist/generator/index.js.map +1 -0
  69. package/dist/generator/key-generator.d.ts +24 -0
  70. package/dist/generator/key-generator.d.ts.map +1 -0
  71. package/dist/generator/key-generator.js +144 -0
  72. package/dist/generator/key-generator.js.map +1 -0
  73. package/dist/generator/profile-builder.d.ts +15 -0
  74. package/dist/generator/profile-builder.d.ts.map +1 -0
  75. package/dist/generator/profile-builder.js +338 -0
  76. package/dist/generator/profile-builder.js.map +1 -0
  77. package/dist/hosting/artifacts-generator.d.ts +10 -0
  78. package/dist/hosting/artifacts-generator.d.ts.map +1 -0
  79. package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
  80. package/dist/hosting/artifacts-generator.js.map +1 -0
  81. package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
  82. package/dist/hosting/index.d.ts.map +1 -0
  83. package/dist/hosting/index.js +10 -0
  84. package/dist/hosting/index.js.map +1 -0
  85. package/dist/index.d.ts +18 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +78 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/lib/analytics.d.ts +337 -0
  90. package/dist/lib/analytics.d.ts.map +1 -0
  91. package/dist/lib/analytics.js +188 -0
  92. package/dist/lib/analytics.js.map +1 -0
  93. package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
  94. package/dist/security/index.d.ts.map +1 -0
  95. package/dist/security/index.js +12 -0
  96. package/dist/security/index.js.map +1 -0
  97. package/dist/security/security-scanner.d.ts +10 -0
  98. package/dist/security/security-scanner.d.ts.map +1 -0
  99. package/dist/security/security-scanner.js +669 -0
  100. package/dist/security/security-scanner.js.map +1 -0
  101. package/dist/security/types.d.ts +80 -0
  102. package/dist/security/types.d.ts.map +1 -0
  103. package/dist/security/types.js +21 -0
  104. package/dist/security/types.js.map +1 -0
  105. package/dist/services/analytics.d.ts +114 -0
  106. package/dist/services/analytics.d.ts.map +1 -0
  107. package/dist/services/analytics.js +862 -0
  108. package/dist/services/analytics.js.map +1 -0
  109. package/dist/services/badge.d.ts +31 -0
  110. package/dist/services/badge.d.ts.map +1 -0
  111. package/dist/services/badge.js +152 -0
  112. package/dist/services/badge.js.map +1 -0
  113. package/dist/services/cron.d.ts +125 -0
  114. package/dist/services/cron.d.ts.map +1 -0
  115. package/dist/services/cron.js +613 -0
  116. package/dist/services/cron.js.map +1 -0
  117. package/dist/services/directory.d.ts +106 -0
  118. package/dist/services/directory.d.ts.map +1 -0
  119. package/dist/services/directory.js +351 -0
  120. package/dist/services/directory.js.map +1 -0
  121. package/dist/services/email.d.ts +112 -0
  122. package/dist/services/email.d.ts.map +1 -0
  123. package/dist/services/email.js +772 -0
  124. package/dist/services/email.js.map +1 -0
  125. package/dist/services/hosted-profiles.d.ts +77 -0
  126. package/dist/services/hosted-profiles.d.ts.map +1 -0
  127. package/dist/services/hosted-profiles.js +433 -0
  128. package/dist/services/hosted-profiles.js.map +1 -0
  129. package/dist/services/latency.d.ts +67 -0
  130. package/dist/services/latency.d.ts.map +1 -0
  131. package/dist/services/latency.js +274 -0
  132. package/dist/services/latency.js.map +1 -0
  133. package/dist/services/manifest-compliance.d.ts +64 -0
  134. package/dist/services/manifest-compliance.d.ts.map +1 -0
  135. package/dist/services/manifest-compliance.js +271 -0
  136. package/dist/services/manifest-compliance.js.map +1 -0
  137. package/dist/services/monitoring-diff.d.ts +31 -0
  138. package/dist/services/monitoring-diff.d.ts.map +1 -0
  139. package/dist/services/monitoring-diff.js +189 -0
  140. package/dist/services/monitoring-diff.js.map +1 -0
  141. package/dist/services/notifications.d.ts +46 -0
  142. package/dist/services/notifications.d.ts.map +1 -0
  143. package/dist/services/notifications.js +88 -0
  144. package/dist/services/notifications.js.map +1 -0
  145. package/dist/services/stripe.d.ts +93 -0
  146. package/dist/services/stripe.d.ts.map +1 -0
  147. package/dist/services/stripe.js +490 -0
  148. package/dist/services/stripe.js.map +1 -0
  149. package/dist/services/validation-history.d.ts +99 -0
  150. package/dist/services/validation-history.d.ts.map +1 -0
  151. package/dist/services/validation-history.js +344 -0
  152. package/dist/services/validation-history.js.map +1 -0
  153. package/dist/services/validation-logging.d.ts +103 -0
  154. package/dist/services/validation-logging.d.ts.map +1 -0
  155. package/dist/services/validation-logging.js +210 -0
  156. package/dist/services/validation-logging.js.map +1 -0
  157. package/dist/services/validation.d.ts +119 -0
  158. package/dist/services/validation.d.ts.map +1 -0
  159. package/dist/services/validation.js +1185 -0
  160. package/dist/services/validation.js.map +1 -0
  161. package/dist/simulator/agent-simulator.d.ts +69 -0
  162. package/dist/simulator/agent-simulator.d.ts.map +1 -0
  163. package/dist/simulator/agent-simulator.js +870 -0
  164. package/dist/simulator/agent-simulator.js.map +1 -0
  165. package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
  166. package/dist/simulator/index.d.ts.map +1 -0
  167. package/dist/simulator/index.js +23 -0
  168. package/dist/simulator/index.js.map +1 -0
  169. package/{src/simulator/types.ts → dist/simulator/types.d.ts} +171 -170
  170. package/dist/simulator/types.d.ts.map +1 -0
  171. package/dist/simulator/types.js +18 -0
  172. package/dist/simulator/types.js.map +1 -0
  173. package/dist/types/acp-validation.d.ts +87 -0
  174. package/dist/types/acp-validation.d.ts.map +1 -0
  175. package/dist/types/acp-validation.js +40 -0
  176. package/dist/types/acp-validation.js.map +1 -0
  177. package/dist/types/analytics.d.ts +182 -0
  178. package/dist/types/analytics.d.ts.map +1 -0
  179. package/dist/types/analytics.js +7 -0
  180. package/dist/types/analytics.js.map +1 -0
  181. package/dist/types/generator.d.ts +106 -0
  182. package/dist/types/generator.d.ts.map +1 -0
  183. package/dist/types/generator.js +6 -0
  184. package/dist/types/generator.js.map +1 -0
  185. package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +23 -0
  188. package/dist/types/index.js.map +1 -0
  189. package/dist/types/ucp-profile.d.ts +111 -0
  190. package/dist/types/ucp-profile.d.ts.map +1 -0
  191. package/dist/types/ucp-profile.js +45 -0
  192. package/dist/types/ucp-profile.js.map +1 -0
  193. package/dist/types/validation.d.ts +76 -0
  194. package/dist/types/validation.d.ts.map +1 -0
  195. package/dist/types/validation.js +42 -0
  196. package/dist/types/validation.js.map +1 -0
  197. package/dist/validator/acp/index.d.ts +31 -0
  198. package/dist/validator/acp/index.d.ts.map +1 -0
  199. package/dist/validator/acp/index.js +574 -0
  200. package/dist/validator/acp/index.js.map +1 -0
  201. package/dist/validator/index.d.ts +26 -0
  202. package/dist/validator/index.d.ts.map +1 -0
  203. package/dist/validator/index.js +161 -0
  204. package/dist/validator/index.js.map +1 -0
  205. package/dist/validator/network-validator.d.ts +28 -0
  206. package/dist/validator/network-validator.d.ts.map +1 -0
  207. package/dist/validator/network-validator.js +319 -0
  208. package/dist/validator/network-validator.js.map +1 -0
  209. package/dist/validator/rules-validator.d.ts +19 -0
  210. package/dist/validator/rules-validator.d.ts.map +1 -0
  211. package/dist/validator/rules-validator.js +306 -0
  212. package/dist/validator/rules-validator.js.map +1 -0
  213. package/dist/validator/sdk-validator.d.ts +58 -0
  214. package/dist/validator/sdk-validator.d.ts.map +1 -0
  215. package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
  216. package/dist/validator/sdk-validator.js.map +1 -0
  217. package/dist/validator/structural-validator.d.ts +11 -0
  218. package/dist/validator/structural-validator.d.ts.map +1 -0
  219. package/dist/validator/structural-validator.js +549 -0
  220. package/dist/validator/structural-validator.js.map +1 -0
  221. package/dist/validator/utils.d.ts +51 -0
  222. package/dist/validator/utils.d.ts.map +1 -0
  223. package/dist/validator/utils.js +132 -0
  224. package/dist/validator/utils.js.map +1 -0
  225. package/package.json +44 -12
  226. package/CLAUDE.md +0 -109
  227. package/api/analyze-feed.js +0 -140
  228. package/api/badge.js +0 -185
  229. package/api/benchmark.js +0 -177
  230. package/api/directory-stats.ts +0 -29
  231. package/api/directory.ts +0 -73
  232. package/api/generate-compliance.js +0 -143
  233. package/api/generate-schema.js +0 -457
  234. package/api/generate.js +0 -132
  235. package/api/security-scan.js +0 -133
  236. package/api/simulate.js +0 -187
  237. package/api/tsconfig.json +0 -10
  238. package/api/validate.js +0 -1351
  239. package/apify-actor/.actor/actor.json +0 -68
  240. package/apify-actor/.actor/input_schema.json +0 -32
  241. package/apify-actor/APIFY-STORE-LISTING.md +0 -412
  242. package/apify-actor/Dockerfile +0 -8
  243. package/apify-actor/README.md +0 -166
  244. package/apify-actor/main.ts +0 -111
  245. package/apify-actor/package.json +0 -17
  246. package/apify-actor/src/main.js +0 -199
  247. package/docs/BRAND-IDENTITY.md +0 -238
  248. package/docs/BRAND-STYLE-GUIDE.md +0 -356
  249. package/drizzle/0000_black_king_cobra.sql +0 -39
  250. package/drizzle/meta/0000_snapshot.json +0 -309
  251. package/drizzle/meta/_journal.json +0 -13
  252. package/drizzle.config.ts +0 -10
  253. package/public/.well-known/ucp +0 -25
  254. package/public/android-chrome-192x192.png +0 -0
  255. package/public/android-chrome-512x512.png +0 -0
  256. package/public/apple-touch-icon.png +0 -0
  257. package/public/brand.css +0 -321
  258. package/public/directory.html +0 -701
  259. package/public/favicon-16x16.png +0 -0
  260. package/public/favicon-32x32.png +0 -0
  261. package/public/favicon.ico +0 -0
  262. package/public/guides/bigcommerce.html +0 -743
  263. package/public/guides/fastucp.html +0 -838
  264. package/public/guides/magento.html +0 -779
  265. package/public/guides/shopify.html +0 -726
  266. package/public/guides/squarespace.html +0 -749
  267. package/public/guides/wix.html +0 -747
  268. package/public/guides/woocommerce.html +0 -733
  269. package/public/index.html +0 -3835
  270. package/public/learn.html +0 -396
  271. package/public/logo.jpeg +0 -0
  272. package/public/og-image-icon.png +0 -0
  273. package/public/og-image.png +0 -0
  274. package/public/robots.txt +0 -6
  275. package/public/site.webmanifest +0 -31
  276. package/public/sitemap.xml +0 -69
  277. package/public/social/linkedin-banner-1128x191.png +0 -0
  278. package/public/social/temp.PNG +0 -0
  279. package/public/social/x-header-1500x500.png +0 -0
  280. package/public/verify.html +0 -410
  281. package/scripts/generate-favicons.js +0 -44
  282. package/scripts/generate-ico.js +0 -23
  283. package/scripts/generate-og-image.js +0 -45
  284. package/scripts/reset-db.ts +0 -77
  285. package/scripts/seed-db.ts +0 -71
  286. package/scripts/setup-benchmark-db.js +0 -70
  287. package/src/api/server.ts +0 -266
  288. package/src/cli/index.ts +0 -302
  289. package/src/compliance/compliance-generator.ts +0 -452
  290. package/src/compliance/index.ts +0 -28
  291. package/src/compliance/types.ts +0 -170
  292. package/src/db/index.ts +0 -28
  293. package/src/db/schema.ts +0 -84
  294. package/src/feed-analyzer/index.ts +0 -34
  295. package/src/feed-analyzer/types.ts +0 -354
  296. package/src/generator/key-generator.ts +0 -124
  297. package/src/generator/profile-builder.ts +0 -402
  298. package/src/index.ts +0 -105
  299. package/src/security/security-scanner.ts +0 -604
  300. package/src/security/types.ts +0 -55
  301. package/src/services/directory.ts +0 -434
  302. package/src/simulator/agent-simulator.ts +0 -941
  303. package/src/types/generator.ts +0 -140
  304. package/src/types/ucp-profile.ts +0 -140
  305. package/src/types/validation.ts +0 -89
  306. package/src/validator/index.ts +0 -194
  307. package/src/validator/network-validator.ts +0 -417
  308. package/src/validator/rules-validator.ts +0 -297
  309. package/src/validator/structural-validator.ts +0 -476
  310. package/tests/fixtures/non-compliant-profile.json +0 -25
  311. package/tests/fixtures/official-sample-profile.json +0 -75
  312. package/tests/integration/benchmark.test.ts +0 -207
  313. package/tests/integration/database.test.ts +0 -163
  314. package/tests/integration/directory-api.test.ts +0 -268
  315. package/tests/integration/simulate-api.test.ts +0 -230
  316. package/tests/integration/validate-api.test.ts +0 -269
  317. package/tests/setup.ts +0 -15
  318. package/tests/unit/agent-simulator.test.ts +0 -575
  319. package/tests/unit/compliance-generator.test.ts +0 -374
  320. package/tests/unit/directory-service.test.ts +0 -272
  321. package/tests/unit/feed-analyzer.test.ts +0 -517
  322. package/tests/unit/lint-suggestions.test.ts +0 -423
  323. package/tests/unit/official-samples.test.ts +0 -211
  324. package/tests/unit/pdf-report.test.ts +0 -390
  325. package/tests/unit/sdk-validator.test.ts +0 -531
  326. package/tests/unit/security-scanner.test.ts +0 -410
  327. package/tests/unit/validation.test.ts +0 -390
  328. package/tsconfig.json +0 -20
  329. package/vercel.json +0 -34
  330. package/vitest.config.ts +0 -22
@@ -1,726 +1,856 @@
1
- /**
2
- * Product Feed Quality Analyzer
3
- * Deep analysis of product data quality for AI agent visibility
4
- */
5
-
6
- import type {
7
- ProductData,
8
- ProductOffer,
9
- ProductAnalysis,
10
- QualityCheck,
11
- FeedAnalysisResult,
12
- FeedAnalysisInput,
13
- CategoryScores,
14
- Recommendation,
15
- FeedSummary,
16
- GtinValidation,
17
- CheckCategory,
18
- } from './types.js';
19
-
20
- import {
21
- QUALITY_CHECKS,
22
- VALID_AVAILABILITY_VALUES,
23
- CATEGORY_WEIGHTS,
24
- GRADE_THRESHOLDS,
25
- } from './types.js';
26
-
27
- /**
28
- * Default maximum products to analyze
29
- */
30
- const DEFAULT_MAX_PRODUCTS = 50;
31
-
32
- /**
33
- * Minimum description length considered "good"
34
- */
35
- const MIN_DESCRIPTION_LENGTH = 50;
36
-
37
- /**
38
- * Validate a GTIN/UPC/EAN identifier
39
- */
40
- export function validateGtin(gtin: string): GtinValidation {
41
- if (!gtin || typeof gtin !== 'string') {
42
- return { isValid: false, error: 'GTIN is empty or not a string' };
43
- }
44
-
45
- // Remove any spaces or dashes
46
- const cleaned = gtin.replace(/[\s-]/g, '');
47
-
48
- // Check if it's all digits
49
- if (!/^\d+$/.test(cleaned)) {
50
- return { isValid: false, error: 'GTIN must contain only digits' };
51
- }
52
-
53
- const length = cleaned.length;
54
-
55
- // Determine type based on length
56
- let type: GtinValidation['type'];
57
- switch (length) {
58
- case 8:
59
- type = 'GTIN-8';
60
- break;
61
- case 12:
62
- type = 'UPC';
63
- break;
64
- case 13:
65
- type = 'EAN';
66
- break;
67
- case 14:
68
- type = 'GTIN-14';
69
- break;
70
- default:
71
- return { isValid: false, error: `Invalid GTIN length: ${length}. Expected 8, 12, 13, or 14 digits` };
72
- }
73
-
74
- // Validate check digit
75
- if (!validateGtinCheckDigit(cleaned)) {
76
- return { isValid: false, type, error: 'Invalid check digit' };
77
- }
78
-
79
- return { isValid: true, type };
80
- }
81
-
82
- /**
83
- * Validate GTIN check digit using modulo 10 algorithm
84
- */
85
- function validateGtinCheckDigit(gtin: string): boolean {
86
- const digits = gtin.split('').map(Number);
87
- const checkDigit = digits.pop()!;
88
-
89
- let sum = 0;
90
- const multipliers = gtin.length === 13 || gtin.length === 8
91
- ? [1, 3] // EAN-13, GTIN-8
92
- : [3, 1]; // UPC-12, GTIN-14
93
-
94
- for (let i = 0; i < digits.length; i++) {
95
- sum += digits[i] * multipliers[i % 2];
96
- }
97
-
98
- const calculatedCheck = (10 - (sum % 10)) % 10;
99
- return calculatedCheck === checkDigit;
100
- }
101
-
102
- /**
103
- * Extract products from HTML content
104
- */
105
- export function extractProductsFromHtml(html: string): ProductData[] {
106
- const products: ProductData[] = [];
107
-
108
- // Find all JSON-LD script tags
109
- const jsonLdRegex = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
110
- let match;
111
-
112
- while ((match = jsonLdRegex.exec(html)) !== null) {
113
- try {
114
- const jsonContent = match[1].trim();
115
- const data = JSON.parse(jsonContent);
116
-
117
- // Handle single object or array
118
- const items = Array.isArray(data) ? data : [data];
119
-
120
- for (const item of items) {
121
- // Check if it's a Product
122
- if (item['@type'] === 'Product') {
123
- products.push(item as ProductData);
124
- }
125
- // Check for @graph containing products
126
- else if (item['@graph'] && Array.isArray(item['@graph'])) {
127
- for (const graphItem of item['@graph']) {
128
- if (graphItem['@type'] === 'Product') {
129
- products.push(graphItem as ProductData);
130
- }
131
- }
132
- }
133
- // Check for ItemList containing products
134
- else if (item['@type'] === 'ItemList' && item.itemListElement) {
135
- const listItems = Array.isArray(item.itemListElement)
136
- ? item.itemListElement
137
- : [item.itemListElement];
138
- for (const listItem of listItems) {
139
- if (listItem.item && listItem.item['@type'] === 'Product') {
140
- products.push(listItem.item as ProductData);
141
- } else if (listItem['@type'] === 'Product') {
142
- products.push(listItem as ProductData);
143
- }
144
- }
145
- }
146
- }
147
- } catch {
148
- // Skip invalid JSON
149
- continue;
150
- }
151
- }
152
-
153
- return products;
154
- }
155
-
156
- /**
157
- * Get the primary offer from a product
158
- */
159
- function getPrimaryOffer(product: ProductData): ProductOffer | undefined {
160
- if (!product.offers) return undefined;
161
-
162
- if (Array.isArray(product.offers)) {
163
- return product.offers[0];
164
- }
165
-
166
- return product.offers;
167
- }
168
-
169
- /**
170
- * Get image count from product data
171
- */
172
- function getImageCount(product: ProductData): number {
173
- if (!product.image) return 0;
174
-
175
- if (typeof product.image === 'string') return 1;
176
-
177
- if (Array.isArray(product.image)) {
178
- return product.image.length;
179
- }
180
-
181
- return 1;
182
- }
183
-
184
- /**
185
- * Get brand name from product
186
- */
187
- function getBrandName(product: ProductData): string | undefined {
188
- if (!product.brand) return undefined;
189
-
190
- if (typeof product.brand === 'string') return product.brand;
191
-
192
- return product.brand.name;
193
- }
194
-
195
- /**
196
- * Get any GTIN value from product
197
- */
198
- function getGtin(product: ProductData): string | undefined {
199
- return product.gtin || product.gtin13 || product.gtin12 || product.gtin14 || product.gtin8;
200
- }
201
-
202
- /**
203
- * Analyze a single product
204
- */
205
- export function analyzeProduct(product: ProductData): ProductAnalysis {
206
- const issues: QualityCheck[] = [];
207
- const offer = getPrimaryOffer(product);
208
- const brandName = getBrandName(product);
209
- const gtin = getGtin(product);
210
- const imageCount = getImageCount(product);
211
- const descriptionLength = product.description?.length || 0;
212
-
213
- // Build attributes object
214
- const attributes = {
215
- hasName: Boolean(product.name && product.name.trim()),
216
- hasDescription: Boolean(product.description && product.description.trim()),
217
- hasSku: Boolean(product.sku),
218
- hasGtin: Boolean(gtin),
219
- hasBrand: Boolean(brandName),
220
- hasImage: imageCount > 0,
221
- hasPrice: offer?.price !== undefined && offer?.price !== null,
222
- hasAvailability: Boolean(offer?.availability),
223
- hasCategory: Boolean(product.category),
224
- descriptionLength,
225
- imageCount,
226
- };
227
-
228
- // Completeness checks
229
- if (!attributes.hasName) {
230
- issues.push(createIssue('missing-name', product.name));
231
- }
232
-
233
- if (!attributes.hasDescription) {
234
- issues.push(createIssue('missing-description', product.name));
235
- } else if (descriptionLength < MIN_DESCRIPTION_LENGTH) {
236
- issues.push(createIssue('short-description', product.name, `Description is only ${descriptionLength} characters`));
237
- }
238
-
239
- if (!attributes.hasBrand) {
240
- issues.push(createIssue('missing-brand', product.name));
241
- }
242
-
243
- // Identifier checks
244
- if (!attributes.hasSku) {
245
- issues.push(createIssue('missing-sku', product.name));
246
- }
247
-
248
- if (!attributes.hasGtin) {
249
- issues.push(createIssue('missing-gtin', product.name));
250
- } else {
251
- const gtinValidation = validateGtin(gtin!);
252
- if (!gtinValidation.isValid) {
253
- issues.push(createIssue('invalid-gtin', product.name, gtinValidation.error));
254
- }
255
- }
256
-
257
- // Image checks
258
- if (!attributes.hasImage) {
259
- issues.push(createIssue('missing-image', product.name));
260
- } else if (imageCount === 1) {
261
- issues.push(createIssue('single-image', product.name));
262
- }
263
-
264
- // Pricing checks
265
- if (!offer) {
266
- issues.push(createIssue('missing-price', product.name));
267
- } else {
268
- if (!offer.price && offer.price !== 0) {
269
- issues.push(createIssue('missing-price', product.name));
270
- } else {
271
- const price = typeof offer.price === 'string' ? parseFloat(offer.price) : offer.price;
272
- if (isNaN(price)) {
273
- issues.push(createIssue('invalid-price', product.name, `Price value: "${offer.price}"`));
274
- }
275
- }
276
-
277
- if (!offer.priceCurrency) {
278
- issues.push(createIssue('missing-currency', product.name));
279
- }
280
- }
281
-
282
- // Availability checks
283
- if (!attributes.hasAvailability) {
284
- issues.push(createIssue('missing-availability', product.name));
285
- } else if (offer?.availability && !VALID_AVAILABILITY_VALUES.includes(offer.availability)) {
286
- issues.push(createIssue('invalid-availability', product.name, `Value: "${offer.availability}"`));
287
- }
288
-
289
- // Category checks
290
- if (!attributes.hasCategory) {
291
- issues.push(createIssue('missing-category', product.name));
292
- }
293
-
294
- // Calculate product score
295
- const score = calculateProductScore(attributes);
296
-
297
- return {
298
- name: product.name || 'Unknown Product',
299
- url: product.url,
300
- sku: product.sku,
301
- score,
302
- issues,
303
- attributes,
304
- };
305
- }
306
-
307
- /**
308
- * Create a quality check issue
309
- */
310
- function createIssue(checkId: string, productName?: string, details?: string): QualityCheck {
311
- const checkDef = QUALITY_CHECKS[checkId];
312
-
313
- return {
314
- id: checkId,
315
- name: checkDef.name,
316
- category: checkDef.category,
317
- passed: false,
318
- severity: checkDef.severity,
319
- message: checkDef.description,
320
- details,
321
- affectedProducts: productName ? [productName] : undefined,
322
- };
323
- }
324
-
325
- /**
326
- * Calculate product score based on attributes
327
- */
328
- function calculateProductScore(attributes: ProductAnalysis['attributes']): number {
329
- let score = 0;
330
- const weights = {
331
- hasName: 15,
332
- hasDescription: 10,
333
- hasSku: 5,
334
- hasGtin: 10,
335
- hasBrand: 10,
336
- hasImage: 15,
337
- hasPrice: 20,
338
- hasAvailability: 5,
339
- hasCategory: 5,
340
- descriptionQuality: 5,
341
- };
342
-
343
- if (attributes.hasName) score += weights.hasName;
344
- if (attributes.hasDescription) score += weights.hasDescription;
345
- if (attributes.hasSku) score += weights.hasSku;
346
- if (attributes.hasGtin) score += weights.hasGtin;
347
- if (attributes.hasBrand) score += weights.hasBrand;
348
- if (attributes.hasImage) score += weights.hasImage;
349
- if (attributes.hasPrice) score += weights.hasPrice;
350
- if (attributes.hasAvailability) score += weights.hasAvailability;
351
- if (attributes.hasCategory) score += weights.hasCategory;
352
-
353
- // Bonus for good description length
354
- if (attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH) {
355
- score += weights.descriptionQuality;
356
- }
357
-
358
- return Math.min(100, score);
359
- }
360
-
361
- /**
362
- * Calculate category scores from product analyses
363
- */
364
- function calculateCategoryScores(products: ProductAnalysis[]): CategoryScores {
365
- if (products.length === 0) {
366
- return {
367
- completeness: 0,
368
- identifiers: 0,
369
- images: 0,
370
- pricing: 0,
371
- descriptions: 0,
372
- categories: 0,
373
- availability: 0,
374
- };
375
- }
376
-
377
- const totals = products.reduce(
378
- (acc, p) => {
379
- // Completeness: name, brand
380
- acc.completeness += (p.attributes.hasName ? 50 : 0) + (p.attributes.hasBrand ? 50 : 0);
381
-
382
- // Identifiers: SKU, GTIN
383
- acc.identifiers += (p.attributes.hasSku ? 50 : 0) + (p.attributes.hasGtin ? 50 : 0);
384
-
385
- // Images
386
- acc.images += p.attributes.hasImage ? (p.attributes.imageCount > 1 ? 100 : 70) : 0;
387
-
388
- // Pricing
389
- acc.pricing += p.attributes.hasPrice ? 100 : 0;
390
-
391
- // Descriptions
392
- if (p.attributes.hasDescription) {
393
- acc.descriptions += p.attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH ? 100 : 60;
394
- }
395
-
396
- // Categories
397
- acc.categories += p.attributes.hasCategory ? 100 : 0;
398
-
399
- // Availability
400
- acc.availability += p.attributes.hasAvailability ? 100 : 0;
401
-
402
- return acc;
403
- },
404
- { completeness: 0, identifiers: 0, images: 0, pricing: 0, descriptions: 0, categories: 0, availability: 0 }
405
- );
406
-
407
- const count = products.length;
408
- return {
409
- completeness: Math.round(totals.completeness / count),
410
- identifiers: Math.round(totals.identifiers / count),
411
- images: Math.round(totals.images / count),
412
- pricing: Math.round(totals.pricing / count),
413
- descriptions: Math.round(totals.descriptions / count),
414
- categories: Math.round(totals.categories / count),
415
- availability: Math.round(totals.availability / count),
416
- };
417
- }
418
-
419
- /**
420
- * Calculate overall score from category scores
421
- */
422
- function calculateOverallScore(categoryScores: CategoryScores): number {
423
- let weightedSum = 0;
424
- let totalWeight = 0;
425
-
426
- for (const [category, score] of Object.entries(categoryScores)) {
427
- const weight = CATEGORY_WEIGHTS[category as CheckCategory];
428
- weightedSum += score * weight;
429
- totalWeight += weight;
430
- }
431
-
432
- return Math.round(weightedSum / totalWeight);
433
- }
434
-
435
- /**
436
- * Calculate agent visibility score
437
- * This is a specialized score focusing on what AI agents need most
438
- */
439
- function calculateAgentVisibilityScore(summary: FeedSummary): number {
440
- if (summary.totalProducts === 0) return 0;
441
-
442
- const total = summary.totalProducts;
443
-
444
- // Critical factors for AI agents (weighted heavily)
445
- const criticalScore =
446
- ((summary.withName / total) * 30) +
447
- ((summary.withPrice / total) * 30) +
448
- ((summary.withImages / total) * 20);
449
-
450
- // Important factors
451
- const importantScore =
452
- ((summary.withDescription / total) * 10) +
453
- ((summary.withGtin / total) * 5) +
454
- ((summary.withAvailability / total) * 5);
455
-
456
- return Math.round(criticalScore + importantScore);
457
- }
458
-
459
- /**
460
- * Get grade from score
461
- */
462
- function getGrade(score: number): FeedAnalysisResult['grade'] {
463
- if (score >= GRADE_THRESHOLDS.A) return 'A';
464
- if (score >= GRADE_THRESHOLDS.B) return 'B';
465
- if (score >= GRADE_THRESHOLDS.C) return 'C';
466
- if (score >= GRADE_THRESHOLDS.D) return 'D';
467
- return 'F';
468
- }
469
-
470
- /**
471
- * Generate recommendations based on analysis
472
- */
473
- function generateRecommendations(summary: FeedSummary, issues: QualityCheck[]): Recommendation[] {
474
- const recommendations: Recommendation[] = [];
475
- const total = summary.totalProducts;
476
-
477
- if (total === 0) return recommendations;
478
-
479
- // Missing prices - critical
480
- const missingPrices = total - summary.withPrice;
481
- if (missingPrices > 0) {
482
- recommendations.push({
483
- priority: 'high',
484
- category: 'pricing',
485
- title: 'Add Missing Prices',
486
- description: `${missingPrices} products are missing price information. AI agents cannot complete purchases without prices.`,
487
- impact: 'Critical - Products without prices cannot be purchased through AI agents',
488
- affectedCount: missingPrices,
489
- });
490
- }
491
-
492
- // Missing images - critical
493
- const missingImages = total - summary.withImages;
494
- if (missingImages > 0) {
495
- recommendations.push({
496
- priority: 'high',
497
- category: 'images',
498
- title: 'Add Product Images',
499
- description: `${missingImages} products are missing images. Visual product information is essential for AI shopping.`,
500
- impact: 'High - Products without images are less likely to be recommended',
501
- affectedCount: missingImages,
502
- });
503
- }
504
-
505
- // Missing names - critical
506
- const missingNames = total - summary.withName;
507
- if (missingNames > 0) {
508
- recommendations.push({
509
- priority: 'high',
510
- category: 'completeness',
511
- title: 'Add Product Names',
512
- description: `${missingNames} products are missing names. This is required for product identification.`,
513
- impact: 'Critical - Products cannot be identified without names',
514
- affectedCount: missingNames,
515
- });
516
- }
517
-
518
- // Missing GTIN - medium priority
519
- const missingGtin = total - summary.withGtin;
520
- if (missingGtin > 0 && missingGtin / total > 0.5) {
521
- recommendations.push({
522
- priority: 'medium',
523
- category: 'identifiers',
524
- title: 'Add Global Identifiers (GTIN/UPC/EAN)',
525
- description: `${missingGtin} products are missing global identifiers. These enable cross-platform product matching.`,
526
- impact: 'Medium - Improves product matching across AI platforms',
527
- affectedCount: missingGtin,
528
- });
529
- }
530
-
531
- // Missing descriptions - medium priority
532
- const missingDescriptions = total - summary.withDescription;
533
- if (missingDescriptions > 0 && missingDescriptions / total > 0.3) {
534
- recommendations.push({
535
- priority: 'medium',
536
- category: 'descriptions',
537
- title: 'Add Product Descriptions',
538
- description: `${missingDescriptions} products are missing descriptions. Good descriptions help AI agents understand and recommend products.`,
539
- impact: 'Medium - Better descriptions improve AI recommendations',
540
- affectedCount: missingDescriptions,
541
- });
542
- }
543
-
544
- // Short descriptions
545
- if (summary.averageDescriptionLength < MIN_DESCRIPTION_LENGTH && summary.withDescription > 0) {
546
- recommendations.push({
547
- priority: 'low',
548
- category: 'descriptions',
549
- title: 'Improve Description Length',
550
- description: `Average description length is ${Math.round(summary.averageDescriptionLength)} characters. Aim for at least ${MIN_DESCRIPTION_LENGTH} characters.`,
551
- impact: 'Low - Longer descriptions provide more context for AI agents',
552
- affectedCount: summary.withDescription,
553
- });
554
- }
555
-
556
- // Missing availability
557
- const missingAvailability = total - summary.withAvailability;
558
- if (missingAvailability > 0 && missingAvailability / total > 0.3) {
559
- recommendations.push({
560
- priority: 'medium',
561
- category: 'availability',
562
- title: 'Add Availability Status',
563
- description: `${missingAvailability} products are missing availability information.`,
564
- impact: 'Medium - Availability helps AI agents make informed purchase decisions',
565
- affectedCount: missingAvailability,
566
- });
567
- }
568
-
569
- // Sort by priority
570
- const priorityOrder = { high: 0, medium: 1, low: 2 };
571
- recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
572
-
573
- return recommendations;
574
- }
575
-
576
- /**
577
- * Aggregate issues across all products
578
- */
579
- function aggregateIssues(products: ProductAnalysis[]): QualityCheck[] {
580
- const issueMap = new Map<string, QualityCheck>();
581
-
582
- for (const product of products) {
583
- for (const issue of product.issues) {
584
- const existing = issueMap.get(issue.id);
585
- if (existing) {
586
- // Aggregate affected products
587
- if (issue.affectedProducts && existing.affectedProducts) {
588
- existing.affectedProducts.push(...issue.affectedProducts);
589
- }
590
- } else {
591
- issueMap.set(issue.id, { ...issue, affectedProducts: [...(issue.affectedProducts || [])] });
592
- }
593
- }
594
- }
595
-
596
- return Array.from(issueMap.values());
597
- }
598
-
599
- /**
600
- * Calculate feed summary statistics
601
- */
602
- function calculateSummary(products: ProductAnalysis[]): FeedSummary {
603
- const total = products.length;
604
-
605
- if (total === 0) {
606
- return {
607
- totalProducts: 0,
608
- withName: 0,
609
- withDescription: 0,
610
- withSku: 0,
611
- withGtin: 0,
612
- withBrand: 0,
613
- withImages: 0,
614
- withPrice: 0,
615
- withAvailability: 0,
616
- withCategory: 0,
617
- averageDescriptionLength: 0,
618
- averageImageCount: 0,
619
- };
620
- }
621
-
622
- const summary = products.reduce(
623
- (acc, p) => {
624
- if (p.attributes.hasName) acc.withName++;
625
- if (p.attributes.hasDescription) acc.withDescription++;
626
- if (p.attributes.hasSku) acc.withSku++;
627
- if (p.attributes.hasGtin) acc.withGtin++;
628
- if (p.attributes.hasBrand) acc.withBrand++;
629
- if (p.attributes.hasImage) acc.withImages++;
630
- if (p.attributes.hasPrice) acc.withPrice++;
631
- if (p.attributes.hasAvailability) acc.withAvailability++;
632
- if (p.attributes.hasCategory) acc.withCategory++;
633
- acc.totalDescriptionLength += p.attributes.descriptionLength;
634
- acc.totalImageCount += p.attributes.imageCount;
635
- return acc;
636
- },
637
- {
638
- withName: 0,
639
- withDescription: 0,
640
- withSku: 0,
641
- withGtin: 0,
642
- withBrand: 0,
643
- withImages: 0,
644
- withPrice: 0,
645
- withAvailability: 0,
646
- withCategory: 0,
647
- totalDescriptionLength: 0,
648
- totalImageCount: 0,
649
- }
650
- );
651
-
652
- return {
653
- totalProducts: total,
654
- withName: summary.withName,
655
- withDescription: summary.withDescription,
656
- withSku: summary.withSku,
657
- withGtin: summary.withGtin,
658
- withBrand: summary.withBrand,
659
- withImages: summary.withImages,
660
- withPrice: summary.withPrice,
661
- withAvailability: summary.withAvailability,
662
- withCategory: summary.withCategory,
663
- averageDescriptionLength: summary.totalDescriptionLength / total,
664
- averageImageCount: summary.totalImageCount / total,
665
- };
666
- }
667
-
668
- /**
669
- * Analyze product feed from raw product data
670
- */
671
- export function analyzeProductFeed(
672
- products: ProductData[],
673
- url: string,
674
- maxProducts: number = DEFAULT_MAX_PRODUCTS,
675
- includeProductDetails: boolean = true
676
- ): FeedAnalysisResult {
677
- const productsToAnalyze = products.slice(0, maxProducts);
678
- const productAnalyses = productsToAnalyze.map(analyzeProduct);
679
-
680
- const summary = calculateSummary(productAnalyses);
681
- const categoryScores = calculateCategoryScores(productAnalyses);
682
- const overallScore = calculateOverallScore(categoryScores);
683
- const agentVisibilityScore = calculateAgentVisibilityScore(summary);
684
- const issues = aggregateIssues(productAnalyses);
685
- const recommendations = generateRecommendations(summary, issues);
686
-
687
- // Get top issues (most impactful)
688
- const topIssues = issues
689
- .filter(i => i.severity === 'critical' || i.severity === 'warning')
690
- .sort((a, b) => {
691
- const severityOrder = { critical: 0, warning: 1, info: 2 };
692
- return severityOrder[a.severity] - severityOrder[b.severity];
693
- })
694
- .slice(0, 5);
695
-
696
- return {
697
- url,
698
- analyzedAt: new Date().toISOString(),
699
- productsFound: products.length,
700
- productsAnalyzed: productsToAnalyze.length,
701
- overallScore,
702
- agentVisibilityScore,
703
- grade: getGrade(overallScore),
704
- categoryScores,
705
- issues,
706
- topIssues,
707
- products: includeProductDetails ? productAnalyses : [],
708
- recommendations,
709
- summary,
710
- };
711
- }
712
-
713
- /**
714
- * Analyze a product feed from HTML content
715
- */
716
- export function analyzeProductFeedFromHtml(
717
- html: string,
718
- url: string,
719
- options: Partial<FeedAnalysisInput> = {}
720
- ): FeedAnalysisResult {
721
- const products = extractProductsFromHtml(html);
722
- const maxProducts = options.maxProducts ?? DEFAULT_MAX_PRODUCTS;
723
- const includeProductDetails = options.includeProductDetails ?? true;
724
-
725
- return analyzeProductFeed(products, url, maxProducts, includeProductDetails);
726
- }
1
+ "use strict";
2
+ /**
3
+ * Product Feed Quality Analyzer
4
+ * Deep analysis of product data quality for AI agent visibility
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateGtin = validateGtin;
8
+ exports.extractProductsFromHtml = extractProductsFromHtml;
9
+ exports.analyzeProduct = analyzeProduct;
10
+ exports.analyzeProductFeed = analyzeProductFeed;
11
+ exports.analyzeProductFeedFromHtml = analyzeProductFeedFromHtml;
12
+ const types_js_1 = require("./types.js");
13
+ /**
14
+ * Default maximum products to analyze
15
+ */
16
+ const DEFAULT_MAX_PRODUCTS = 50;
17
+ /**
18
+ * Minimum description length considered "good"
19
+ */
20
+ const MIN_DESCRIPTION_LENGTH = 50;
21
+ /**
22
+ * Validate a GTIN/UPC/EAN identifier
23
+ */
24
+ function validateGtin(gtin) {
25
+ if (!gtin || typeof gtin !== 'string') {
26
+ return { isValid: false, error: 'GTIN is empty or not a string' };
27
+ }
28
+ // Remove any spaces or dashes
29
+ const cleaned = gtin.replace(/[\s-]/g, '');
30
+ // Check if it's all digits
31
+ if (!/^\d+$/.test(cleaned)) {
32
+ return { isValid: false, error: 'GTIN must contain only digits' };
33
+ }
34
+ const length = cleaned.length;
35
+ // Determine type based on length
36
+ let type;
37
+ switch (length) {
38
+ case 8:
39
+ type = 'GTIN-8';
40
+ break;
41
+ case 12:
42
+ type = 'UPC';
43
+ break;
44
+ case 13:
45
+ type = 'EAN';
46
+ break;
47
+ case 14:
48
+ type = 'GTIN-14';
49
+ break;
50
+ default:
51
+ return { isValid: false, error: `Invalid GTIN length: ${length}. Expected 8, 12, 13, or 14 digits` };
52
+ }
53
+ // Validate check digit
54
+ if (!validateGtinCheckDigit(cleaned)) {
55
+ return { isValid: false, type, error: 'Invalid check digit' };
56
+ }
57
+ return { isValid: true, type };
58
+ }
59
+ /**
60
+ * Validate GTIN check digit using modulo 10 algorithm
61
+ */
62
+ function validateGtinCheckDigit(gtin) {
63
+ const digits = gtin.split('').map(Number);
64
+ const checkDigit = digits.pop();
65
+ let sum = 0;
66
+ const multipliers = gtin.length === 13 || gtin.length === 8
67
+ ? [1, 3] // EAN-13, GTIN-8
68
+ : [3, 1]; // UPC-12, GTIN-14
69
+ for (let i = 0; i < digits.length; i++) {
70
+ sum += digits[i] * multipliers[i % 2];
71
+ }
72
+ const calculatedCheck = (10 - (sum % 10)) % 10;
73
+ return calculatedCheck === checkDigit;
74
+ }
75
+ /**
76
+ * Extract products from HTML content
77
+ */
78
+ function extractProductsFromHtml(html) {
79
+ const products = [];
80
+ // Find all JSON-LD script tags
81
+ const jsonLdRegex = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
82
+ let match;
83
+ while ((match = jsonLdRegex.exec(html)) !== null) {
84
+ try {
85
+ const jsonContent = match[1].trim();
86
+ const data = JSON.parse(jsonContent);
87
+ // Handle single object or array
88
+ const items = Array.isArray(data) ? data : [data];
89
+ for (const item of items) {
90
+ // Check if it's a Product
91
+ if (item['@type'] === 'Product') {
92
+ products.push(item);
93
+ }
94
+ // Check for @graph containing products
95
+ else if (item['@graph'] && Array.isArray(item['@graph'])) {
96
+ for (const graphItem of item['@graph']) {
97
+ if (graphItem['@type'] === 'Product') {
98
+ products.push(graphItem);
99
+ }
100
+ }
101
+ }
102
+ // Check for ItemList containing products
103
+ else if (item['@type'] === 'ItemList' && item.itemListElement) {
104
+ const listItems = Array.isArray(item.itemListElement)
105
+ ? item.itemListElement
106
+ : [item.itemListElement];
107
+ for (const listItem of listItems) {
108
+ if (listItem.item && listItem.item['@type'] === 'Product') {
109
+ products.push(listItem.item);
110
+ }
111
+ else if (listItem['@type'] === 'Product') {
112
+ products.push(listItem);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ catch {
119
+ // Skip invalid JSON
120
+ continue;
121
+ }
122
+ }
123
+ return products;
124
+ }
125
+ /**
126
+ * Get the primary offer from a product
127
+ */
128
+ function getPrimaryOffer(product) {
129
+ if (!product.offers)
130
+ return undefined;
131
+ if (Array.isArray(product.offers)) {
132
+ return product.offers[0];
133
+ }
134
+ return product.offers;
135
+ }
136
+ /**
137
+ * Get image count from product data
138
+ */
139
+ function getImageCount(product) {
140
+ if (!product.image)
141
+ return 0;
142
+ if (typeof product.image === 'string')
143
+ return 1;
144
+ if (Array.isArray(product.image)) {
145
+ return product.image.length;
146
+ }
147
+ return 1;
148
+ }
149
+ /**
150
+ * Get brand name from product
151
+ */
152
+ function getBrandName(product) {
153
+ if (!product.brand)
154
+ return undefined;
155
+ if (typeof product.brand === 'string')
156
+ return product.brand;
157
+ return product.brand.name;
158
+ }
159
+ /**
160
+ * Get any GTIN value from product
161
+ */
162
+ function getGtin(product) {
163
+ return product.gtin || product.gtin13 || product.gtin12 || product.gtin14 || product.gtin8;
164
+ }
165
+ /**
166
+ * Analyze a single product
167
+ */
168
+ function analyzeProduct(product) {
169
+ const issues = [];
170
+ const offer = getPrimaryOffer(product);
171
+ const brandName = getBrandName(product);
172
+ const gtin = getGtin(product);
173
+ const imageCount = getImageCount(product);
174
+ const descriptionLength = product.description?.length || 0;
175
+ // Build attributes object
176
+ const attributes = {
177
+ hasName: Boolean(product.name && product.name.trim()),
178
+ hasDescription: Boolean(product.description && product.description.trim()),
179
+ hasSku: Boolean(product.sku),
180
+ hasGtin: Boolean(gtin),
181
+ hasBrand: Boolean(brandName),
182
+ hasImage: imageCount > 0,
183
+ hasPrice: offer?.price !== undefined && offer?.price !== null,
184
+ hasAvailability: Boolean(offer?.availability),
185
+ hasCategory: Boolean(product.category),
186
+ descriptionLength,
187
+ imageCount,
188
+ };
189
+ // Completeness checks
190
+ if (!attributes.hasName) {
191
+ issues.push(createIssue('missing-name', product.name));
192
+ }
193
+ if (!attributes.hasDescription) {
194
+ issues.push(createIssue('missing-description', product.name));
195
+ }
196
+ else if (descriptionLength < MIN_DESCRIPTION_LENGTH) {
197
+ issues.push(createIssue('short-description', product.name, `Description is only ${descriptionLength} characters`));
198
+ }
199
+ if (!attributes.hasBrand) {
200
+ issues.push(createIssue('missing-brand', product.name));
201
+ }
202
+ // Identifier checks
203
+ if (!attributes.hasSku) {
204
+ issues.push(createIssue('missing-sku', product.name));
205
+ }
206
+ if (!attributes.hasGtin) {
207
+ issues.push(createIssue('missing-gtin', product.name));
208
+ }
209
+ else {
210
+ const gtinValidation = validateGtin(gtin);
211
+ if (!gtinValidation.isValid) {
212
+ issues.push(createIssue('invalid-gtin', product.name, gtinValidation.error));
213
+ }
214
+ }
215
+ // Image checks
216
+ if (!attributes.hasImage) {
217
+ issues.push(createIssue('missing-image', product.name));
218
+ }
219
+ else if (imageCount === 1) {
220
+ issues.push(createIssue('single-image', product.name));
221
+ }
222
+ // Pricing checks
223
+ if (!offer) {
224
+ issues.push(createIssue('missing-price', product.name));
225
+ }
226
+ else {
227
+ if (!offer.price && offer.price !== 0) {
228
+ issues.push(createIssue('missing-price', product.name));
229
+ }
230
+ else {
231
+ const price = typeof offer.price === 'string' ? parseFloat(offer.price) : offer.price;
232
+ if (isNaN(price)) {
233
+ issues.push(createIssue('invalid-price', product.name, `Price value: "${offer.price}"`));
234
+ }
235
+ }
236
+ if (!offer.priceCurrency) {
237
+ issues.push(createIssue('missing-currency', product.name));
238
+ }
239
+ }
240
+ // Availability checks
241
+ if (!attributes.hasAvailability) {
242
+ issues.push(createIssue('missing-availability', product.name));
243
+ }
244
+ else if (offer?.availability && !types_js_1.VALID_AVAILABILITY_VALUES.includes(offer.availability)) {
245
+ issues.push(createIssue('invalid-availability', product.name, `Value: "${offer.availability}"`));
246
+ }
247
+ // Category checks
248
+ if (!attributes.hasCategory) {
249
+ issues.push(createIssue('missing-category', product.name));
250
+ }
251
+ // ACP-specific checks
252
+ if (product.name && product.name.length > 150) {
253
+ issues.push(createIssue('acp-title-too-long', product.name, `Title is ${product.name.length} characters (ACP limit: 150)`));
254
+ }
255
+ if (product.description && product.description.length > 5000) {
256
+ issues.push(createIssue('acp-description-too-long', product.name, `Description is ${product.description.length} characters (ACP limit: 5000)`));
257
+ }
258
+ // Calculate product score
259
+ const score = calculateProductScore(attributes);
260
+ return {
261
+ name: product.name || 'Unknown Product',
262
+ url: product.url,
263
+ sku: product.sku,
264
+ score,
265
+ issues,
266
+ attributes,
267
+ };
268
+ }
269
+ /**
270
+ * Create a quality check issue
271
+ */
272
+ function createIssue(checkId, productName, details) {
273
+ const checkDef = types_js_1.QUALITY_CHECKS[checkId];
274
+ return {
275
+ id: checkId,
276
+ name: checkDef.name,
277
+ category: checkDef.category,
278
+ passed: false,
279
+ severity: checkDef.severity,
280
+ message: checkDef.description,
281
+ details,
282
+ affectedProducts: productName ? [productName] : undefined,
283
+ };
284
+ }
285
+ /**
286
+ * Calculate product score based on attributes
287
+ */
288
+ function calculateProductScore(attributes) {
289
+ let score = 0;
290
+ const weights = {
291
+ hasName: 15,
292
+ hasDescription: 10,
293
+ hasSku: 5,
294
+ hasGtin: 10,
295
+ hasBrand: 10,
296
+ hasImage: 15,
297
+ hasPrice: 20,
298
+ hasAvailability: 5,
299
+ hasCategory: 5,
300
+ descriptionQuality: 5,
301
+ };
302
+ if (attributes.hasName)
303
+ score += weights.hasName;
304
+ if (attributes.hasDescription)
305
+ score += weights.hasDescription;
306
+ if (attributes.hasSku)
307
+ score += weights.hasSku;
308
+ if (attributes.hasGtin)
309
+ score += weights.hasGtin;
310
+ if (attributes.hasBrand)
311
+ score += weights.hasBrand;
312
+ if (attributes.hasImage)
313
+ score += weights.hasImage;
314
+ if (attributes.hasPrice)
315
+ score += weights.hasPrice;
316
+ if (attributes.hasAvailability)
317
+ score += weights.hasAvailability;
318
+ if (attributes.hasCategory)
319
+ score += weights.hasCategory;
320
+ // Bonus for good description length
321
+ if (attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH) {
322
+ score += weights.descriptionQuality;
323
+ }
324
+ return Math.min(100, score);
325
+ }
326
+ /**
327
+ * Calculate category scores from product analyses
328
+ */
329
+ function calculateCategoryScores(products) {
330
+ if (products.length === 0) {
331
+ return {
332
+ completeness: 0,
333
+ identifiers: 0,
334
+ images: 0,
335
+ pricing: 0,
336
+ descriptions: 0,
337
+ categories: 0,
338
+ availability: 0,
339
+ };
340
+ }
341
+ const totals = products.reduce((acc, p) => {
342
+ // Completeness: name, brand
343
+ acc.completeness += (p.attributes.hasName ? 50 : 0) + (p.attributes.hasBrand ? 50 : 0);
344
+ // Identifiers: SKU, GTIN
345
+ acc.identifiers += (p.attributes.hasSku ? 50 : 0) + (p.attributes.hasGtin ? 50 : 0);
346
+ // Images
347
+ acc.images += p.attributes.hasImage ? (p.attributes.imageCount > 1 ? 100 : 70) : 0;
348
+ // Pricing
349
+ acc.pricing += p.attributes.hasPrice ? 100 : 0;
350
+ // Descriptions
351
+ if (p.attributes.hasDescription) {
352
+ acc.descriptions += p.attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH ? 100 : 60;
353
+ }
354
+ // Categories
355
+ acc.categories += p.attributes.hasCategory ? 100 : 0;
356
+ // Availability
357
+ acc.availability += p.attributes.hasAvailability ? 100 : 0;
358
+ return acc;
359
+ }, { completeness: 0, identifiers: 0, images: 0, pricing: 0, descriptions: 0, categories: 0, availability: 0 });
360
+ const count = products.length;
361
+ return {
362
+ completeness: Math.round(totals.completeness / count),
363
+ identifiers: Math.round(totals.identifiers / count),
364
+ images: Math.round(totals.images / count),
365
+ pricing: Math.round(totals.pricing / count),
366
+ descriptions: Math.round(totals.descriptions / count),
367
+ categories: Math.round(totals.categories / count),
368
+ availability: Math.round(totals.availability / count),
369
+ };
370
+ }
371
+ /**
372
+ * Calculate overall score from category scores
373
+ */
374
+ function calculateOverallScore(categoryScores) {
375
+ let weightedSum = 0;
376
+ let totalWeight = 0;
377
+ for (const [category, score] of Object.entries(categoryScores)) {
378
+ const weight = types_js_1.CATEGORY_WEIGHTS[category];
379
+ weightedSum += score * weight;
380
+ totalWeight += weight;
381
+ }
382
+ return Math.round(weightedSum / totalWeight);
383
+ }
384
+ /**
385
+ * Calculate agent visibility score (legacy - kept for compatibility)
386
+ * This is a specialized score focusing on what AI agents need most
387
+ */
388
+ function calculateAgentVisibilityScore(summary) {
389
+ if (summary.totalProducts === 0)
390
+ return 0;
391
+ const total = summary.totalProducts;
392
+ // Critical factors for AI agents (weighted heavily)
393
+ const criticalScore = ((summary.withName / total) * 30) +
394
+ ((summary.withPrice / total) * 30) +
395
+ ((summary.withImages / total) * 20);
396
+ // Important factors
397
+ const importantScore = ((summary.withDescription / total) * 10) +
398
+ ((summary.withGtin / total) * 5) +
399
+ ((summary.withAvailability / total) * 5);
400
+ return Math.round(criticalScore + importantScore);
401
+ }
402
+ /**
403
+ * Calculate new additive score breakdown
404
+ *
405
+ * Scoring Model (0-100, additive):
406
+ * - Core Data: 40 pts max (name 15, price 15, availability 10)
407
+ * - Product Media: 25 pts max (has images 15, multiple images 10)
408
+ * - Identifiers: 20 pts max (SKU 10, GTIN 10)
409
+ * - Descriptions: 15 pts max (has description 10, good length 5)
410
+ */
411
+ function calculateScoreBreakdown(summary, products) {
412
+ const total = summary.totalProducts;
413
+ // Zero state: no products = 0 score
414
+ if (total === 0) {
415
+ return {
416
+ coreData: {
417
+ score: 0,
418
+ maxScore: 40,
419
+ status: 'No products found - nothing to sell',
420
+ details: {
421
+ hasName: { score: 0, percent: 0 },
422
+ hasPrice: { score: 0, percent: 0 },
423
+ hasAvailability: { score: 0, percent: 0 },
424
+ },
425
+ },
426
+ productMedia: {
427
+ score: 0,
428
+ maxScore: 25,
429
+ status: 'No products to analyze',
430
+ details: {
431
+ hasImages: { score: 0, percent: 0 },
432
+ multipleImages: { score: 0, percent: 0 },
433
+ },
434
+ },
435
+ identifiers: {
436
+ score: 0,
437
+ maxScore: 20,
438
+ status: 'No products to analyze',
439
+ details: {
440
+ hasSku: { score: 0, percent: 0 },
441
+ hasGtin: { score: 0, percent: 0 },
442
+ },
443
+ },
444
+ descriptions: {
445
+ score: 0,
446
+ maxScore: 15,
447
+ status: 'No products to analyze',
448
+ details: {
449
+ hasDescription: { score: 0, percent: 0 },
450
+ goodLength: { score: 0, percent: 0 },
451
+ },
452
+ },
453
+ total: 0,
454
+ maxTotal: 100,
455
+ };
456
+ }
457
+ // Calculate percentages
458
+ const namePercent = Math.round((summary.withName / total) * 100);
459
+ const pricePercent = Math.round((summary.withPrice / total) * 100);
460
+ const availabilityPercent = Math.round((summary.withAvailability / total) * 100);
461
+ const imagesPercent = Math.round((summary.withImages / total) * 100);
462
+ const skuPercent = Math.round((summary.withSku / total) * 100);
463
+ const gtinPercent = Math.round((summary.withGtin / total) * 100);
464
+ const descriptionPercent = Math.round((summary.withDescription / total) * 100);
465
+ // Count products with multiple images
466
+ const withMultipleImages = products.filter(p => p.attributes.imageCount > 1).length;
467
+ const multipleImagesPercent = Math.round((withMultipleImages / total) * 100);
468
+ // Count products with good description length
469
+ const withGoodDescription = products.filter(p => p.attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH).length;
470
+ const goodDescriptionPercent = Math.round((withGoodDescription / total) * 100);
471
+ // Core Data scoring (40 max)
472
+ const nameScore = Math.round((summary.withName / total) * 15);
473
+ const priceScore = Math.round((summary.withPrice / total) * 15);
474
+ const availabilityScore = Math.round((summary.withAvailability / total) * 10);
475
+ const coreDataScore = nameScore + priceScore + availabilityScore;
476
+ // Product Media scoring (25 max)
477
+ const hasImagesScore = Math.round((summary.withImages / total) * 15);
478
+ const multipleImagesScore = Math.round((withMultipleImages / total) * 10);
479
+ const productMediaScore = hasImagesScore + multipleImagesScore;
480
+ // Identifiers scoring (20 max)
481
+ const skuScore = Math.round((summary.withSku / total) * 10);
482
+ const gtinScore = Math.round((summary.withGtin / total) * 10);
483
+ const identifiersScore = skuScore + gtinScore;
484
+ // Descriptions scoring (15 max)
485
+ const hasDescriptionScore = Math.round((summary.withDescription / total) * 10);
486
+ const goodLengthScore = Math.round((withGoodDescription / total) * 5);
487
+ const descriptionsScore = hasDescriptionScore + goodLengthScore;
488
+ // Generate status messages
489
+ const coreDataStatus = generateCoreDataStatus(namePercent, pricePercent, availabilityPercent);
490
+ const productMediaStatus = generateMediaStatus(imagesPercent, multipleImagesPercent);
491
+ const identifiersStatus = generateIdentifiersStatus(skuPercent, gtinPercent);
492
+ const descriptionsStatus = generateDescriptionsStatus(descriptionPercent, goodDescriptionPercent, summary.averageDescriptionLength);
493
+ const totalScore = coreDataScore + productMediaScore + identifiersScore + descriptionsScore;
494
+ return {
495
+ coreData: {
496
+ score: coreDataScore,
497
+ maxScore: 40,
498
+ status: coreDataStatus,
499
+ details: {
500
+ hasName: { score: nameScore, percent: namePercent },
501
+ hasPrice: { score: priceScore, percent: pricePercent },
502
+ hasAvailability: { score: availabilityScore, percent: availabilityPercent },
503
+ },
504
+ },
505
+ productMedia: {
506
+ score: productMediaScore,
507
+ maxScore: 25,
508
+ status: productMediaStatus,
509
+ details: {
510
+ hasImages: { score: hasImagesScore, percent: imagesPercent },
511
+ multipleImages: { score: multipleImagesScore, percent: multipleImagesPercent },
512
+ },
513
+ },
514
+ identifiers: {
515
+ score: identifiersScore,
516
+ maxScore: 20,
517
+ status: identifiersStatus,
518
+ details: {
519
+ hasSku: { score: skuScore, percent: skuPercent },
520
+ hasGtin: { score: gtinScore, percent: gtinPercent },
521
+ },
522
+ },
523
+ descriptions: {
524
+ score: descriptionsScore,
525
+ maxScore: 15,
526
+ status: descriptionsStatus,
527
+ details: {
528
+ hasDescription: { score: hasDescriptionScore, percent: descriptionPercent },
529
+ goodLength: { score: goodLengthScore, percent: goodDescriptionPercent },
530
+ },
531
+ },
532
+ total: totalScore,
533
+ maxTotal: 100,
534
+ };
535
+ }
536
+ /**
537
+ * Generate status message for core data
538
+ */
539
+ function generateCoreDataStatus(namePercent, pricePercent, availabilityPercent) {
540
+ if (namePercent >= 95 && pricePercent >= 95 && availabilityPercent >= 90) {
541
+ return 'Excellent - All products have complete core data';
542
+ }
543
+ const issues = [];
544
+ if (namePercent < 95)
545
+ issues.push(`${100 - namePercent}% missing names`);
546
+ if (pricePercent < 95)
547
+ issues.push(`${100 - pricePercent}% missing prices`);
548
+ if (availabilityPercent < 90)
549
+ issues.push(`${100 - availabilityPercent}% missing availability`);
550
+ if (issues.length === 0) {
551
+ return `${namePercent}% with name, ${pricePercent}% with price, ${availabilityPercent}% with availability`;
552
+ }
553
+ return issues.join(', ');
554
+ }
555
+ /**
556
+ * Generate status message for product media
557
+ */
558
+ function generateMediaStatus(imagesPercent, multipleImagesPercent) {
559
+ if (imagesPercent >= 95 && multipleImagesPercent >= 80) {
560
+ return `All products have images (${multipleImagesPercent}% have multiple)`;
561
+ }
562
+ if (imagesPercent < 80) {
563
+ return `${100 - imagesPercent}% of products missing images`;
564
+ }
565
+ return `${imagesPercent}% have images, ${multipleImagesPercent}% have multiple images`;
566
+ }
567
+ /**
568
+ * Generate status message for identifiers
569
+ */
570
+ function generateIdentifiersStatus(skuPercent, gtinPercent) {
571
+ if (skuPercent >= 90 && gtinPercent >= 80) {
572
+ return `Strong identifiers - ${skuPercent}% SKUs, ${gtinPercent}% GTINs`;
573
+ }
574
+ if (gtinPercent < 50) {
575
+ return `${100 - gtinPercent}% missing GTINs for cross-platform matching`;
576
+ }
577
+ return `${skuPercent}% have SKUs, ${gtinPercent}% have GTINs`;
578
+ }
579
+ /**
580
+ * Generate status message for descriptions
581
+ */
582
+ function generateDescriptionsStatus(descPercent, goodLengthPercent, avgLength) {
583
+ if (descPercent >= 95 && goodLengthPercent >= 80) {
584
+ return `Strong descriptions (avg ${Math.round(avgLength)} chars)`;
585
+ }
586
+ if (descPercent < 70) {
587
+ return `${100 - descPercent}% of products missing descriptions`;
588
+ }
589
+ if (goodLengthPercent < 50) {
590
+ return `Short descriptions (avg ${Math.round(avgLength)} chars) - aim for 50+`;
591
+ }
592
+ return `${descPercent}% have descriptions (avg ${Math.round(avgLength)} chars)`;
593
+ }
594
+ /**
595
+ * Get grade from score
596
+ */
597
+ function getGrade(score) {
598
+ if (score >= types_js_1.GRADE_THRESHOLDS.A)
599
+ return 'A';
600
+ if (score >= types_js_1.GRADE_THRESHOLDS.B)
601
+ return 'B';
602
+ if (score >= types_js_1.GRADE_THRESHOLDS.C)
603
+ return 'C';
604
+ if (score >= types_js_1.GRADE_THRESHOLDS.D)
605
+ return 'D';
606
+ return 'F';
607
+ }
608
+ /**
609
+ * Generate recommendations based on analysis
610
+ */
611
+ function generateRecommendations(summary, issues) {
612
+ const recommendations = [];
613
+ const total = summary.totalProducts;
614
+ if (total === 0)
615
+ return recommendations;
616
+ // Missing prices - critical
617
+ const missingPrices = total - summary.withPrice;
618
+ if (missingPrices > 0) {
619
+ recommendations.push({
620
+ priority: 'high',
621
+ category: 'pricing',
622
+ title: 'Add Missing Prices',
623
+ description: `${missingPrices} products are missing price information. AI agents cannot complete purchases without prices.`,
624
+ impact: 'Critical - Products without prices cannot be purchased through AI agents',
625
+ affectedCount: missingPrices,
626
+ });
627
+ }
628
+ // Missing images - critical
629
+ const missingImages = total - summary.withImages;
630
+ if (missingImages > 0) {
631
+ recommendations.push({
632
+ priority: 'high',
633
+ category: 'images',
634
+ title: 'Add Product Images',
635
+ description: `${missingImages} products are missing images. Visual product information is essential for AI shopping.`,
636
+ impact: 'High - Products without images are less likely to be recommended',
637
+ affectedCount: missingImages,
638
+ });
639
+ }
640
+ // Missing names - critical
641
+ const missingNames = total - summary.withName;
642
+ if (missingNames > 0) {
643
+ recommendations.push({
644
+ priority: 'high',
645
+ category: 'completeness',
646
+ title: 'Add Product Names',
647
+ description: `${missingNames} products are missing names. This is required for product identification.`,
648
+ impact: 'Critical - Products cannot be identified without names',
649
+ affectedCount: missingNames,
650
+ });
651
+ }
652
+ // Missing GTIN - medium priority
653
+ const missingGtin = total - summary.withGtin;
654
+ if (missingGtin > 0 && missingGtin / total > 0.5) {
655
+ recommendations.push({
656
+ priority: 'medium',
657
+ category: 'identifiers',
658
+ title: 'Add Global Identifiers (GTIN/UPC/EAN)',
659
+ description: `${missingGtin} products are missing global identifiers. These enable cross-platform product matching.`,
660
+ impact: 'Medium - Improves product matching across AI platforms',
661
+ affectedCount: missingGtin,
662
+ });
663
+ }
664
+ // Missing descriptions - medium priority
665
+ const missingDescriptions = total - summary.withDescription;
666
+ if (missingDescriptions > 0 && missingDescriptions / total > 0.3) {
667
+ recommendations.push({
668
+ priority: 'medium',
669
+ category: 'descriptions',
670
+ title: 'Add Product Descriptions',
671
+ description: `${missingDescriptions} products are missing descriptions. Good descriptions help AI agents understand and recommend products.`,
672
+ impact: 'Medium - Better descriptions improve AI recommendations',
673
+ affectedCount: missingDescriptions,
674
+ });
675
+ }
676
+ // Short descriptions
677
+ if (summary.averageDescriptionLength < MIN_DESCRIPTION_LENGTH && summary.withDescription > 0) {
678
+ recommendations.push({
679
+ priority: 'low',
680
+ category: 'descriptions',
681
+ title: 'Improve Description Length',
682
+ description: `Average description length is ${Math.round(summary.averageDescriptionLength)} characters. Aim for at least ${MIN_DESCRIPTION_LENGTH} characters.`,
683
+ impact: 'Low - Longer descriptions provide more context for AI agents',
684
+ affectedCount: summary.withDescription,
685
+ });
686
+ }
687
+ // Missing availability
688
+ const missingAvailability = total - summary.withAvailability;
689
+ if (missingAvailability > 0 && missingAvailability / total > 0.3) {
690
+ recommendations.push({
691
+ priority: 'medium',
692
+ category: 'availability',
693
+ title: 'Add Availability Status',
694
+ description: `${missingAvailability} products are missing availability information.`,
695
+ impact: 'Medium - Availability helps AI agents make informed purchase decisions',
696
+ affectedCount: missingAvailability,
697
+ });
698
+ }
699
+ // Sort by priority
700
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
701
+ recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
702
+ return recommendations;
703
+ }
704
+ /**
705
+ * Aggregate issues across all products
706
+ */
707
+ function aggregateIssues(products) {
708
+ const issueMap = new Map();
709
+ for (const product of products) {
710
+ for (const issue of product.issues) {
711
+ const existing = issueMap.get(issue.id);
712
+ if (existing) {
713
+ // Aggregate affected products
714
+ if (issue.affectedProducts && existing.affectedProducts) {
715
+ existing.affectedProducts.push(...issue.affectedProducts);
716
+ }
717
+ }
718
+ else {
719
+ issueMap.set(issue.id, { ...issue, affectedProducts: [...(issue.affectedProducts || [])] });
720
+ }
721
+ }
722
+ }
723
+ return Array.from(issueMap.values());
724
+ }
725
+ /**
726
+ * Calculate feed summary statistics
727
+ */
728
+ function calculateSummary(products) {
729
+ const total = products.length;
730
+ if (total === 0) {
731
+ return {
732
+ totalProducts: 0,
733
+ withName: 0,
734
+ withDescription: 0,
735
+ withSku: 0,
736
+ withGtin: 0,
737
+ withBrand: 0,
738
+ withImages: 0,
739
+ withPrice: 0,
740
+ withAvailability: 0,
741
+ withCategory: 0,
742
+ averageDescriptionLength: 0,
743
+ averageImageCount: 0,
744
+ };
745
+ }
746
+ const summary = products.reduce((acc, p) => {
747
+ if (p.attributes.hasName)
748
+ acc.withName++;
749
+ if (p.attributes.hasDescription)
750
+ acc.withDescription++;
751
+ if (p.attributes.hasSku)
752
+ acc.withSku++;
753
+ if (p.attributes.hasGtin)
754
+ acc.withGtin++;
755
+ if (p.attributes.hasBrand)
756
+ acc.withBrand++;
757
+ if (p.attributes.hasImage)
758
+ acc.withImages++;
759
+ if (p.attributes.hasPrice)
760
+ acc.withPrice++;
761
+ if (p.attributes.hasAvailability)
762
+ acc.withAvailability++;
763
+ if (p.attributes.hasCategory)
764
+ acc.withCategory++;
765
+ acc.totalDescriptionLength += p.attributes.descriptionLength;
766
+ acc.totalImageCount += p.attributes.imageCount;
767
+ return acc;
768
+ }, {
769
+ withName: 0,
770
+ withDescription: 0,
771
+ withSku: 0,
772
+ withGtin: 0,
773
+ withBrand: 0,
774
+ withImages: 0,
775
+ withPrice: 0,
776
+ withAvailability: 0,
777
+ withCategory: 0,
778
+ totalDescriptionLength: 0,
779
+ totalImageCount: 0,
780
+ });
781
+ return {
782
+ totalProducts: total,
783
+ withName: summary.withName,
784
+ withDescription: summary.withDescription,
785
+ withSku: summary.withSku,
786
+ withGtin: summary.withGtin,
787
+ withBrand: summary.withBrand,
788
+ withImages: summary.withImages,
789
+ withPrice: summary.withPrice,
790
+ withAvailability: summary.withAvailability,
791
+ withCategory: summary.withCategory,
792
+ averageDescriptionLength: summary.totalDescriptionLength / total,
793
+ averageImageCount: summary.totalImageCount / total,
794
+ };
795
+ }
796
+ /**
797
+ * Analyze product feed from raw product data
798
+ */
799
+ function analyzeProductFeed(products, url, maxProducts = DEFAULT_MAX_PRODUCTS, includeProductDetails = true) {
800
+ const productsToAnalyze = products.slice(0, maxProducts);
801
+ const productAnalyses = productsToAnalyze.map(analyzeProduct);
802
+ const summary = calculateSummary(productAnalyses);
803
+ const categoryScores = calculateCategoryScores(productAnalyses);
804
+ const score_breakdown = calculateScoreBreakdown(summary, productAnalyses);
805
+ const overallScore = score_breakdown.total; // Use additive score as primary
806
+ const agentVisibilityScore = overallScore; // Kept for backwards compatibility
807
+ const issues = aggregateIssues(productAnalyses);
808
+ const recommendations = generateRecommendations(summary, issues);
809
+ // Get top issues (most impactful)
810
+ const topIssues = issues
811
+ .filter(i => i.severity === 'critical' || i.severity === 'warning')
812
+ .sort((a, b) => {
813
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
814
+ return severityOrder[a.severity] - severityOrder[b.severity];
815
+ })
816
+ .slice(0, 5);
817
+ // ACP feed compliance summary
818
+ const acpTitleOk = productAnalyses.filter(p => !p.issues.some(i => i.id === 'acp-title-too-long')).length;
819
+ const acpDescOk = productAnalyses.filter(p => !p.issues.some(i => i.id === 'acp-description-too-long')).length;
820
+ const acpIssueCount = productAnalyses.reduce((count, p) => count + p.issues.filter(i => i.id === 'acp-title-too-long' || i.id === 'acp-description-too-long').length, 0);
821
+ const acp_compliance = {
822
+ title_length_ok: acpTitleOk,
823
+ description_length_ok: acpDescOk,
824
+ has_gtin: summary.withGtin,
825
+ has_currency: productAnalyses.filter(p => p.attributes.hasPrice).length, // currency required with price
826
+ total: productAnalyses.length,
827
+ acp_issues: acpIssueCount,
828
+ };
829
+ return {
830
+ url,
831
+ analyzedAt: new Date().toISOString(),
832
+ productsFound: products.length,
833
+ productsAnalyzed: productsToAnalyze.length,
834
+ overallScore,
835
+ agentVisibilityScore,
836
+ grade: getGrade(overallScore),
837
+ score_breakdown,
838
+ categoryScores, // Legacy - kept for compatibility
839
+ issues,
840
+ topIssues,
841
+ products: includeProductDetails ? productAnalyses : [],
842
+ recommendations,
843
+ summary,
844
+ acp_compliance,
845
+ };
846
+ }
847
+ /**
848
+ * Analyze a product feed from HTML content
849
+ */
850
+ function analyzeProductFeedFromHtml(html, url, options = {}) {
851
+ const products = extractProductsFromHtml(html);
852
+ const maxProducts = options.maxProducts ?? DEFAULT_MAX_PRODUCTS;
853
+ const includeProductDetails = options.includeProductDetails ?? true;
854
+ return analyzeProductFeed(products, url, maxProducts, includeProductDetails);
855
+ }
856
+ //# sourceMappingURL=feed-analyzer.js.map