@ucptools/validator 1.0.0 → 1.0.1

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 (236) hide show
  1. package/.claude/settings.local.json +60 -0
  2. package/.vercel/README.txt +11 -0
  3. package/.vercel/project.json +1 -0
  4. package/dist/cli/index.d.ts +6 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +279 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/compliance/compliance-generator.d.ts +34 -0
  9. package/dist/compliance/compliance-generator.d.ts.map +1 -0
  10. package/dist/compliance/compliance-generator.js +320 -0
  11. package/dist/compliance/compliance-generator.js.map +1 -0
  12. package/dist/compliance/index.d.ts +8 -0
  13. package/dist/compliance/index.d.ts.map +1 -0
  14. package/dist/compliance/index.js +17 -0
  15. package/dist/compliance/index.js.map +1 -0
  16. package/dist/compliance/templates.d.ts +34 -0
  17. package/dist/compliance/templates.d.ts.map +1 -0
  18. package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
  19. package/dist/compliance/templates.js.map +1 -0
  20. package/dist/compliance/types.d.ts +64 -0
  21. package/dist/compliance/types.d.ts.map +1 -0
  22. package/dist/compliance/types.js +64 -0
  23. package/dist/compliance/types.js.map +1 -0
  24. package/dist/db/index.d.ts +11 -0
  25. package/dist/db/index.d.ts.map +1 -0
  26. package/dist/db/index.js +63 -0
  27. package/dist/db/index.js.map +1 -0
  28. package/dist/db/schema.d.ts +444 -0
  29. package/dist/db/schema.d.ts.map +1 -0
  30. package/dist/db/schema.js +65 -0
  31. package/dist/db/schema.js.map +1 -0
  32. package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
  33. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
  34. package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +642 -726
  35. package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
  36. package/dist/feed-analyzer/index.d.ts +8 -0
  37. package/dist/feed-analyzer/index.d.ts.map +1 -0
  38. package/dist/feed-analyzer/index.js +19 -0
  39. package/dist/feed-analyzer/index.js.map +1 -0
  40. package/dist/feed-analyzer/types.d.ts +204 -0
  41. package/dist/feed-analyzer/types.d.ts.map +1 -0
  42. package/dist/feed-analyzer/types.js +162 -0
  43. package/dist/feed-analyzer/types.js.map +1 -0
  44. package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
  45. package/dist/generator/index.d.ts.map +1 -0
  46. package/dist/generator/index.js +13 -0
  47. package/dist/generator/index.js.map +1 -0
  48. package/dist/generator/key-generator.d.ts +24 -0
  49. package/dist/generator/key-generator.d.ts.map +1 -0
  50. package/dist/generator/key-generator.js +144 -0
  51. package/dist/generator/key-generator.js.map +1 -0
  52. package/dist/generator/profile-builder.d.ts +15 -0
  53. package/dist/generator/profile-builder.d.ts.map +1 -0
  54. package/dist/generator/profile-builder.js +338 -0
  55. package/dist/generator/profile-builder.js.map +1 -0
  56. package/dist/hosting/artifacts-generator.d.ts +10 -0
  57. package/dist/hosting/artifacts-generator.d.ts.map +1 -0
  58. package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
  59. package/dist/hosting/artifacts-generator.js.map +1 -0
  60. package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
  61. package/dist/hosting/index.d.ts.map +1 -0
  62. package/dist/hosting/index.js +10 -0
  63. package/dist/hosting/index.js.map +1 -0
  64. package/dist/index.d.ts +18 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +78 -0
  67. package/dist/index.js.map +1 -0
  68. package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
  69. package/dist/security/index.d.ts.map +1 -0
  70. package/dist/security/index.js +12 -0
  71. package/dist/security/index.js.map +1 -0
  72. package/dist/security/security-scanner.d.ts +10 -0
  73. package/dist/security/security-scanner.d.ts.map +1 -0
  74. package/dist/security/security-scanner.js +541 -0
  75. package/dist/security/security-scanner.js.map +1 -0
  76. package/dist/security/types.d.ts +48 -0
  77. package/dist/security/types.d.ts.map +1 -0
  78. package/dist/security/types.js +21 -0
  79. package/dist/security/types.js.map +1 -0
  80. package/dist/services/directory.d.ts +104 -0
  81. package/dist/services/directory.d.ts.map +1 -0
  82. package/dist/services/directory.js +333 -0
  83. package/dist/services/directory.js.map +1 -0
  84. package/dist/simulator/agent-simulator.d.ts +69 -0
  85. package/dist/simulator/agent-simulator.d.ts.map +1 -0
  86. package/{src/simulator/agent-simulator.ts → dist/simulator/agent-simulator.js} +650 -941
  87. package/dist/simulator/agent-simulator.js.map +1 -0
  88. package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
  89. package/dist/simulator/index.d.ts.map +1 -0
  90. package/dist/simulator/index.js +23 -0
  91. package/dist/simulator/index.js.map +1 -0
  92. package/{src/simulator/types.ts → dist/simulator/types.d.ts} +145 -170
  93. package/dist/simulator/types.d.ts.map +1 -0
  94. package/dist/simulator/types.js +18 -0
  95. package/dist/simulator/types.js.map +1 -0
  96. package/dist/types/generator.d.ts +106 -0
  97. package/dist/types/generator.d.ts.map +1 -0
  98. package/dist/types/generator.js +6 -0
  99. package/dist/types/generator.js.map +1 -0
  100. package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
  101. package/dist/types/index.d.ts.map +1 -0
  102. package/dist/types/index.js +23 -0
  103. package/dist/types/index.js.map +1 -0
  104. package/dist/types/ucp-profile.d.ts +103 -0
  105. package/dist/types/ucp-profile.d.ts.map +1 -0
  106. package/dist/types/ucp-profile.js +45 -0
  107. package/dist/types/ucp-profile.js.map +1 -0
  108. package/dist/types/validation.d.ts +68 -0
  109. package/dist/types/validation.d.ts.map +1 -0
  110. package/dist/types/validation.js +32 -0
  111. package/dist/types/validation.js.map +1 -0
  112. package/dist/validator/index.d.ts +26 -0
  113. package/dist/validator/index.d.ts.map +1 -0
  114. package/dist/validator/index.js +161 -0
  115. package/dist/validator/index.js.map +1 -0
  116. package/dist/validator/network-validator.d.ts +28 -0
  117. package/dist/validator/network-validator.d.ts.map +1 -0
  118. package/dist/validator/network-validator.js +319 -0
  119. package/dist/validator/network-validator.js.map +1 -0
  120. package/dist/validator/rules-validator.d.ts +11 -0
  121. package/dist/validator/rules-validator.d.ts.map +1 -0
  122. package/dist/validator/rules-validator.js +257 -0
  123. package/dist/validator/rules-validator.js.map +1 -0
  124. package/dist/validator/sdk-validator.d.ts +58 -0
  125. package/dist/validator/sdk-validator.d.ts.map +1 -0
  126. package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
  127. package/dist/validator/sdk-validator.js.map +1 -0
  128. package/dist/validator/structural-validator.d.ts +11 -0
  129. package/dist/validator/structural-validator.d.ts.map +1 -0
  130. package/dist/validator/structural-validator.js +415 -0
  131. package/dist/validator/structural-validator.js.map +1 -0
  132. package/package.json +1 -1
  133. package/publish-output.txt +0 -0
  134. package/CLAUDE.md +0 -109
  135. package/api/analyze-feed.js +0 -140
  136. package/api/badge.js +0 -185
  137. package/api/benchmark.js +0 -177
  138. package/api/directory-stats.ts +0 -29
  139. package/api/directory.ts +0 -73
  140. package/api/generate-compliance.js +0 -143
  141. package/api/generate-schema.js +0 -457
  142. package/api/generate.js +0 -132
  143. package/api/security-scan.js +0 -133
  144. package/api/simulate.js +0 -187
  145. package/api/tsconfig.json +0 -10
  146. package/api/validate.js +0 -1351
  147. package/apify-actor/.actor/actor.json +0 -68
  148. package/apify-actor/.actor/input_schema.json +0 -32
  149. package/apify-actor/APIFY-STORE-LISTING.md +0 -412
  150. package/apify-actor/Dockerfile +0 -8
  151. package/apify-actor/README.md +0 -166
  152. package/apify-actor/main.ts +0 -111
  153. package/apify-actor/package.json +0 -17
  154. package/apify-actor/src/main.js +0 -199
  155. package/docs/BRAND-IDENTITY.md +0 -238
  156. package/docs/BRAND-STYLE-GUIDE.md +0 -356
  157. package/drizzle/0000_black_king_cobra.sql +0 -39
  158. package/drizzle/meta/0000_snapshot.json +0 -309
  159. package/drizzle/meta/_journal.json +0 -13
  160. package/drizzle.config.ts +0 -10
  161. package/public/.well-known/ucp +0 -25
  162. package/public/android-chrome-192x192.png +0 -0
  163. package/public/android-chrome-512x512.png +0 -0
  164. package/public/apple-touch-icon.png +0 -0
  165. package/public/brand.css +0 -321
  166. package/public/directory.html +0 -701
  167. package/public/favicon-16x16.png +0 -0
  168. package/public/favicon-32x32.png +0 -0
  169. package/public/favicon.ico +0 -0
  170. package/public/guides/bigcommerce.html +0 -743
  171. package/public/guides/fastucp.html +0 -838
  172. package/public/guides/magento.html +0 -779
  173. package/public/guides/shopify.html +0 -726
  174. package/public/guides/squarespace.html +0 -749
  175. package/public/guides/wix.html +0 -747
  176. package/public/guides/woocommerce.html +0 -733
  177. package/public/index.html +0 -3835
  178. package/public/learn.html +0 -396
  179. package/public/logo.jpeg +0 -0
  180. package/public/og-image-icon.png +0 -0
  181. package/public/og-image.png +0 -0
  182. package/public/robots.txt +0 -6
  183. package/public/site.webmanifest +0 -31
  184. package/public/sitemap.xml +0 -69
  185. package/public/social/linkedin-banner-1128x191.png +0 -0
  186. package/public/social/temp.PNG +0 -0
  187. package/public/social/x-header-1500x500.png +0 -0
  188. package/public/verify.html +0 -410
  189. package/scripts/generate-favicons.js +0 -44
  190. package/scripts/generate-ico.js +0 -23
  191. package/scripts/generate-og-image.js +0 -45
  192. package/scripts/reset-db.ts +0 -77
  193. package/scripts/seed-db.ts +0 -71
  194. package/scripts/setup-benchmark-db.js +0 -70
  195. package/src/api/server.ts +0 -266
  196. package/src/cli/index.ts +0 -302
  197. package/src/compliance/compliance-generator.ts +0 -452
  198. package/src/compliance/index.ts +0 -28
  199. package/src/compliance/types.ts +0 -170
  200. package/src/db/index.ts +0 -28
  201. package/src/db/schema.ts +0 -84
  202. package/src/feed-analyzer/index.ts +0 -34
  203. package/src/feed-analyzer/types.ts +0 -354
  204. package/src/generator/key-generator.ts +0 -124
  205. package/src/generator/profile-builder.ts +0 -402
  206. package/src/index.ts +0 -105
  207. package/src/security/security-scanner.ts +0 -604
  208. package/src/security/types.ts +0 -55
  209. package/src/services/directory.ts +0 -434
  210. package/src/types/generator.ts +0 -140
  211. package/src/types/ucp-profile.ts +0 -140
  212. package/src/types/validation.ts +0 -89
  213. package/src/validator/index.ts +0 -194
  214. package/src/validator/network-validator.ts +0 -417
  215. package/src/validator/rules-validator.ts +0 -297
  216. package/src/validator/structural-validator.ts +0 -476
  217. package/tests/fixtures/non-compliant-profile.json +0 -25
  218. package/tests/fixtures/official-sample-profile.json +0 -75
  219. package/tests/integration/benchmark.test.ts +0 -207
  220. package/tests/integration/database.test.ts +0 -163
  221. package/tests/integration/directory-api.test.ts +0 -268
  222. package/tests/integration/simulate-api.test.ts +0 -230
  223. package/tests/integration/validate-api.test.ts +0 -269
  224. package/tests/setup.ts +0 -15
  225. package/tests/unit/agent-simulator.test.ts +0 -575
  226. package/tests/unit/compliance-generator.test.ts +0 -374
  227. package/tests/unit/directory-service.test.ts +0 -272
  228. package/tests/unit/feed-analyzer.test.ts +0 -517
  229. package/tests/unit/lint-suggestions.test.ts +0 -423
  230. package/tests/unit/official-samples.test.ts +0 -211
  231. package/tests/unit/pdf-report.test.ts +0 -390
  232. package/tests/unit/sdk-validator.test.ts +0 -531
  233. package/tests/unit/security-scanner.test.ts +0 -410
  234. package/tests/unit/validation.test.ts +0 -390
  235. package/vercel.json +0 -34
  236. package/vitest.config.ts +0 -22
@@ -1,726 +1,642 @@
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
+ // Calculate product score
252
+ const score = calculateProductScore(attributes);
253
+ return {
254
+ name: product.name || 'Unknown Product',
255
+ url: product.url,
256
+ sku: product.sku,
257
+ score,
258
+ issues,
259
+ attributes,
260
+ };
261
+ }
262
+ /**
263
+ * Create a quality check issue
264
+ */
265
+ function createIssue(checkId, productName, details) {
266
+ const checkDef = types_js_1.QUALITY_CHECKS[checkId];
267
+ return {
268
+ id: checkId,
269
+ name: checkDef.name,
270
+ category: checkDef.category,
271
+ passed: false,
272
+ severity: checkDef.severity,
273
+ message: checkDef.description,
274
+ details,
275
+ affectedProducts: productName ? [productName] : undefined,
276
+ };
277
+ }
278
+ /**
279
+ * Calculate product score based on attributes
280
+ */
281
+ function calculateProductScore(attributes) {
282
+ let score = 0;
283
+ const weights = {
284
+ hasName: 15,
285
+ hasDescription: 10,
286
+ hasSku: 5,
287
+ hasGtin: 10,
288
+ hasBrand: 10,
289
+ hasImage: 15,
290
+ hasPrice: 20,
291
+ hasAvailability: 5,
292
+ hasCategory: 5,
293
+ descriptionQuality: 5,
294
+ };
295
+ if (attributes.hasName)
296
+ score += weights.hasName;
297
+ if (attributes.hasDescription)
298
+ score += weights.hasDescription;
299
+ if (attributes.hasSku)
300
+ score += weights.hasSku;
301
+ if (attributes.hasGtin)
302
+ score += weights.hasGtin;
303
+ if (attributes.hasBrand)
304
+ score += weights.hasBrand;
305
+ if (attributes.hasImage)
306
+ score += weights.hasImage;
307
+ if (attributes.hasPrice)
308
+ score += weights.hasPrice;
309
+ if (attributes.hasAvailability)
310
+ score += weights.hasAvailability;
311
+ if (attributes.hasCategory)
312
+ score += weights.hasCategory;
313
+ // Bonus for good description length
314
+ if (attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH) {
315
+ score += weights.descriptionQuality;
316
+ }
317
+ return Math.min(100, score);
318
+ }
319
+ /**
320
+ * Calculate category scores from product analyses
321
+ */
322
+ function calculateCategoryScores(products) {
323
+ if (products.length === 0) {
324
+ return {
325
+ completeness: 0,
326
+ identifiers: 0,
327
+ images: 0,
328
+ pricing: 0,
329
+ descriptions: 0,
330
+ categories: 0,
331
+ availability: 0,
332
+ };
333
+ }
334
+ const totals = products.reduce((acc, p) => {
335
+ // Completeness: name, brand
336
+ acc.completeness += (p.attributes.hasName ? 50 : 0) + (p.attributes.hasBrand ? 50 : 0);
337
+ // Identifiers: SKU, GTIN
338
+ acc.identifiers += (p.attributes.hasSku ? 50 : 0) + (p.attributes.hasGtin ? 50 : 0);
339
+ // Images
340
+ acc.images += p.attributes.hasImage ? (p.attributes.imageCount > 1 ? 100 : 70) : 0;
341
+ // Pricing
342
+ acc.pricing += p.attributes.hasPrice ? 100 : 0;
343
+ // Descriptions
344
+ if (p.attributes.hasDescription) {
345
+ acc.descriptions += p.attributes.descriptionLength >= MIN_DESCRIPTION_LENGTH ? 100 : 60;
346
+ }
347
+ // Categories
348
+ acc.categories += p.attributes.hasCategory ? 100 : 0;
349
+ // Availability
350
+ acc.availability += p.attributes.hasAvailability ? 100 : 0;
351
+ return acc;
352
+ }, { completeness: 0, identifiers: 0, images: 0, pricing: 0, descriptions: 0, categories: 0, availability: 0 });
353
+ const count = products.length;
354
+ return {
355
+ completeness: Math.round(totals.completeness / count),
356
+ identifiers: Math.round(totals.identifiers / count),
357
+ images: Math.round(totals.images / count),
358
+ pricing: Math.round(totals.pricing / count),
359
+ descriptions: Math.round(totals.descriptions / count),
360
+ categories: Math.round(totals.categories / count),
361
+ availability: Math.round(totals.availability / count),
362
+ };
363
+ }
364
+ /**
365
+ * Calculate overall score from category scores
366
+ */
367
+ function calculateOverallScore(categoryScores) {
368
+ let weightedSum = 0;
369
+ let totalWeight = 0;
370
+ for (const [category, score] of Object.entries(categoryScores)) {
371
+ const weight = types_js_1.CATEGORY_WEIGHTS[category];
372
+ weightedSum += score * weight;
373
+ totalWeight += weight;
374
+ }
375
+ return Math.round(weightedSum / totalWeight);
376
+ }
377
+ /**
378
+ * Calculate agent visibility score
379
+ * This is a specialized score focusing on what AI agents need most
380
+ */
381
+ function calculateAgentVisibilityScore(summary) {
382
+ if (summary.totalProducts === 0)
383
+ return 0;
384
+ const total = summary.totalProducts;
385
+ // Critical factors for AI agents (weighted heavily)
386
+ const criticalScore = ((summary.withName / total) * 30) +
387
+ ((summary.withPrice / total) * 30) +
388
+ ((summary.withImages / total) * 20);
389
+ // Important factors
390
+ const importantScore = ((summary.withDescription / total) * 10) +
391
+ ((summary.withGtin / total) * 5) +
392
+ ((summary.withAvailability / total) * 5);
393
+ return Math.round(criticalScore + importantScore);
394
+ }
395
+ /**
396
+ * Get grade from score
397
+ */
398
+ function getGrade(score) {
399
+ if (score >= types_js_1.GRADE_THRESHOLDS.A)
400
+ return 'A';
401
+ if (score >= types_js_1.GRADE_THRESHOLDS.B)
402
+ return 'B';
403
+ if (score >= types_js_1.GRADE_THRESHOLDS.C)
404
+ return 'C';
405
+ if (score >= types_js_1.GRADE_THRESHOLDS.D)
406
+ return 'D';
407
+ return 'F';
408
+ }
409
+ /**
410
+ * Generate recommendations based on analysis
411
+ */
412
+ function generateRecommendations(summary, issues) {
413
+ const recommendations = [];
414
+ const total = summary.totalProducts;
415
+ if (total === 0)
416
+ return recommendations;
417
+ // Missing prices - critical
418
+ const missingPrices = total - summary.withPrice;
419
+ if (missingPrices > 0) {
420
+ recommendations.push({
421
+ priority: 'high',
422
+ category: 'pricing',
423
+ title: 'Add Missing Prices',
424
+ description: `${missingPrices} products are missing price information. AI agents cannot complete purchases without prices.`,
425
+ impact: 'Critical - Products without prices cannot be purchased through AI agents',
426
+ affectedCount: missingPrices,
427
+ });
428
+ }
429
+ // Missing images - critical
430
+ const missingImages = total - summary.withImages;
431
+ if (missingImages > 0) {
432
+ recommendations.push({
433
+ priority: 'high',
434
+ category: 'images',
435
+ title: 'Add Product Images',
436
+ description: `${missingImages} products are missing images. Visual product information is essential for AI shopping.`,
437
+ impact: 'High - Products without images are less likely to be recommended',
438
+ affectedCount: missingImages,
439
+ });
440
+ }
441
+ // Missing names - critical
442
+ const missingNames = total - summary.withName;
443
+ if (missingNames > 0) {
444
+ recommendations.push({
445
+ priority: 'high',
446
+ category: 'completeness',
447
+ title: 'Add Product Names',
448
+ description: `${missingNames} products are missing names. This is required for product identification.`,
449
+ impact: 'Critical - Products cannot be identified without names',
450
+ affectedCount: missingNames,
451
+ });
452
+ }
453
+ // Missing GTIN - medium priority
454
+ const missingGtin = total - summary.withGtin;
455
+ if (missingGtin > 0 && missingGtin / total > 0.5) {
456
+ recommendations.push({
457
+ priority: 'medium',
458
+ category: 'identifiers',
459
+ title: 'Add Global Identifiers (GTIN/UPC/EAN)',
460
+ description: `${missingGtin} products are missing global identifiers. These enable cross-platform product matching.`,
461
+ impact: 'Medium - Improves product matching across AI platforms',
462
+ affectedCount: missingGtin,
463
+ });
464
+ }
465
+ // Missing descriptions - medium priority
466
+ const missingDescriptions = total - summary.withDescription;
467
+ if (missingDescriptions > 0 && missingDescriptions / total > 0.3) {
468
+ recommendations.push({
469
+ priority: 'medium',
470
+ category: 'descriptions',
471
+ title: 'Add Product Descriptions',
472
+ description: `${missingDescriptions} products are missing descriptions. Good descriptions help AI agents understand and recommend products.`,
473
+ impact: 'Medium - Better descriptions improve AI recommendations',
474
+ affectedCount: missingDescriptions,
475
+ });
476
+ }
477
+ // Short descriptions
478
+ if (summary.averageDescriptionLength < MIN_DESCRIPTION_LENGTH && summary.withDescription > 0) {
479
+ recommendations.push({
480
+ priority: 'low',
481
+ category: 'descriptions',
482
+ title: 'Improve Description Length',
483
+ description: `Average description length is ${Math.round(summary.averageDescriptionLength)} characters. Aim for at least ${MIN_DESCRIPTION_LENGTH} characters.`,
484
+ impact: 'Low - Longer descriptions provide more context for AI agents',
485
+ affectedCount: summary.withDescription,
486
+ });
487
+ }
488
+ // Missing availability
489
+ const missingAvailability = total - summary.withAvailability;
490
+ if (missingAvailability > 0 && missingAvailability / total > 0.3) {
491
+ recommendations.push({
492
+ priority: 'medium',
493
+ category: 'availability',
494
+ title: 'Add Availability Status',
495
+ description: `${missingAvailability} products are missing availability information.`,
496
+ impact: 'Medium - Availability helps AI agents make informed purchase decisions',
497
+ affectedCount: missingAvailability,
498
+ });
499
+ }
500
+ // Sort by priority
501
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
502
+ recommendations.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
503
+ return recommendations;
504
+ }
505
+ /**
506
+ * Aggregate issues across all products
507
+ */
508
+ function aggregateIssues(products) {
509
+ const issueMap = new Map();
510
+ for (const product of products) {
511
+ for (const issue of product.issues) {
512
+ const existing = issueMap.get(issue.id);
513
+ if (existing) {
514
+ // Aggregate affected products
515
+ if (issue.affectedProducts && existing.affectedProducts) {
516
+ existing.affectedProducts.push(...issue.affectedProducts);
517
+ }
518
+ }
519
+ else {
520
+ issueMap.set(issue.id, { ...issue, affectedProducts: [...(issue.affectedProducts || [])] });
521
+ }
522
+ }
523
+ }
524
+ return Array.from(issueMap.values());
525
+ }
526
+ /**
527
+ * Calculate feed summary statistics
528
+ */
529
+ function calculateSummary(products) {
530
+ const total = products.length;
531
+ if (total === 0) {
532
+ return {
533
+ totalProducts: 0,
534
+ withName: 0,
535
+ withDescription: 0,
536
+ withSku: 0,
537
+ withGtin: 0,
538
+ withBrand: 0,
539
+ withImages: 0,
540
+ withPrice: 0,
541
+ withAvailability: 0,
542
+ withCategory: 0,
543
+ averageDescriptionLength: 0,
544
+ averageImageCount: 0,
545
+ };
546
+ }
547
+ const summary = products.reduce((acc, p) => {
548
+ if (p.attributes.hasName)
549
+ acc.withName++;
550
+ if (p.attributes.hasDescription)
551
+ acc.withDescription++;
552
+ if (p.attributes.hasSku)
553
+ acc.withSku++;
554
+ if (p.attributes.hasGtin)
555
+ acc.withGtin++;
556
+ if (p.attributes.hasBrand)
557
+ acc.withBrand++;
558
+ if (p.attributes.hasImage)
559
+ acc.withImages++;
560
+ if (p.attributes.hasPrice)
561
+ acc.withPrice++;
562
+ if (p.attributes.hasAvailability)
563
+ acc.withAvailability++;
564
+ if (p.attributes.hasCategory)
565
+ acc.withCategory++;
566
+ acc.totalDescriptionLength += p.attributes.descriptionLength;
567
+ acc.totalImageCount += p.attributes.imageCount;
568
+ return acc;
569
+ }, {
570
+ withName: 0,
571
+ withDescription: 0,
572
+ withSku: 0,
573
+ withGtin: 0,
574
+ withBrand: 0,
575
+ withImages: 0,
576
+ withPrice: 0,
577
+ withAvailability: 0,
578
+ withCategory: 0,
579
+ totalDescriptionLength: 0,
580
+ totalImageCount: 0,
581
+ });
582
+ return {
583
+ totalProducts: total,
584
+ withName: summary.withName,
585
+ withDescription: summary.withDescription,
586
+ withSku: summary.withSku,
587
+ withGtin: summary.withGtin,
588
+ withBrand: summary.withBrand,
589
+ withImages: summary.withImages,
590
+ withPrice: summary.withPrice,
591
+ withAvailability: summary.withAvailability,
592
+ withCategory: summary.withCategory,
593
+ averageDescriptionLength: summary.totalDescriptionLength / total,
594
+ averageImageCount: summary.totalImageCount / total,
595
+ };
596
+ }
597
+ /**
598
+ * Analyze product feed from raw product data
599
+ */
600
+ function analyzeProductFeed(products, url, maxProducts = DEFAULT_MAX_PRODUCTS, includeProductDetails = true) {
601
+ const productsToAnalyze = products.slice(0, maxProducts);
602
+ const productAnalyses = productsToAnalyze.map(analyzeProduct);
603
+ const summary = calculateSummary(productAnalyses);
604
+ const categoryScores = calculateCategoryScores(productAnalyses);
605
+ const overallScore = calculateOverallScore(categoryScores);
606
+ const agentVisibilityScore = calculateAgentVisibilityScore(summary);
607
+ const issues = aggregateIssues(productAnalyses);
608
+ const recommendations = generateRecommendations(summary, issues);
609
+ // Get top issues (most impactful)
610
+ const topIssues = issues
611
+ .filter(i => i.severity === 'critical' || i.severity === 'warning')
612
+ .sort((a, b) => {
613
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
614
+ return severityOrder[a.severity] - severityOrder[b.severity];
615
+ })
616
+ .slice(0, 5);
617
+ return {
618
+ url,
619
+ analyzedAt: new Date().toISOString(),
620
+ productsFound: products.length,
621
+ productsAnalyzed: productsToAnalyze.length,
622
+ overallScore,
623
+ agentVisibilityScore,
624
+ grade: getGrade(overallScore),
625
+ categoryScores,
626
+ issues,
627
+ topIssues,
628
+ products: includeProductDetails ? productAnalyses : [],
629
+ recommendations,
630
+ summary,
631
+ };
632
+ }
633
+ /**
634
+ * Analyze a product feed from HTML content
635
+ */
636
+ function analyzeProductFeedFromHtml(html, url, options = {}) {
637
+ const products = extractProductsFromHtml(html);
638
+ const maxProducts = options.maxProducts ?? DEFAULT_MAX_PRODUCTS;
639
+ const includeProductDetails = options.includeProductDetails ?? true;
640
+ return analyzeProductFeed(products, url, maxProducts, includeProductDetails);
641
+ }
642
+ //# sourceMappingURL=feed-analyzer.js.map