@ucptools/validator 1.0.1 → 1.2.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 (162) 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.js +96 -0
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/mock-server.d.ts +20 -0
  20. package/dist/cli/mock-server.d.ts.map +1 -0
  21. package/dist/cli/mock-server.js +261 -0
  22. package/dist/cli/mock-server.js.map +1 -0
  23. package/dist/db/index.d.ts +8 -2
  24. package/dist/db/index.d.ts.map +1 -1
  25. package/dist/db/index.js +22 -5
  26. package/dist/db/index.js.map +1 -1
  27. package/dist/db/schema.d.ts +3570 -128
  28. package/dist/db/schema.d.ts.map +1 -1
  29. package/dist/db/schema.js +377 -17
  30. package/dist/db/schema.js.map +1 -1
  31. package/dist/db/utils.d.ts +252 -0
  32. package/dist/db/utils.d.ts.map +1 -0
  33. package/dist/db/utils.js +295 -0
  34. package/dist/db/utils.js.map +1 -0
  35. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -1
  36. package/dist/feed-analyzer/feed-analyzer.js +218 -4
  37. package/dist/feed-analyzer/feed-analyzer.js.map +1 -1
  38. package/dist/feed-analyzer/types.d.ts +82 -1
  39. package/dist/feed-analyzer/types.d.ts.map +1 -1
  40. package/dist/feed-analyzer/types.js +13 -0
  41. package/dist/feed-analyzer/types.js.map +1 -1
  42. package/dist/generator/profile-builder.d.ts.map +1 -1
  43. package/dist/generator/profile-builder.js +158 -115
  44. package/dist/generator/profile-builder.js.map +1 -1
  45. package/dist/lib/analytics.d.ts +349 -0
  46. package/dist/lib/analytics.d.ts.map +1 -0
  47. package/dist/lib/analytics.js +198 -0
  48. package/dist/lib/analytics.js.map +1 -0
  49. package/dist/security/security-scanner.d.ts.map +1 -1
  50. package/dist/security/security-scanner.js +130 -2
  51. package/dist/security/security-scanner.js.map +1 -1
  52. package/dist/security/types.d.ts +32 -0
  53. package/dist/security/types.d.ts.map +1 -1
  54. package/dist/security/types.js.map +1 -1
  55. package/dist/services/analytics.d.ts +114 -0
  56. package/dist/services/analytics.d.ts.map +1 -0
  57. package/dist/services/analytics.js +862 -0
  58. package/dist/services/analytics.js.map +1 -0
  59. package/dist/services/badge.d.ts +31 -0
  60. package/dist/services/badge.d.ts.map +1 -0
  61. package/dist/services/badge.js +152 -0
  62. package/dist/services/badge.js.map +1 -0
  63. package/dist/services/cron.d.ts +127 -0
  64. package/dist/services/cron.d.ts.map +1 -0
  65. package/dist/services/cron.js +693 -0
  66. package/dist/services/cron.js.map +1 -0
  67. package/dist/services/directory.d.ts +2 -0
  68. package/dist/services/directory.d.ts.map +1 -1
  69. package/dist/services/directory.js +45 -27
  70. package/dist/services/directory.js.map +1 -1
  71. package/dist/services/email.d.ts +127 -0
  72. package/dist/services/email.d.ts.map +1 -0
  73. package/dist/services/email.js +876 -0
  74. package/dist/services/email.js.map +1 -0
  75. package/dist/services/hosted-profiles.d.ts +77 -0
  76. package/dist/services/hosted-profiles.d.ts.map +1 -0
  77. package/dist/services/hosted-profiles.js +433 -0
  78. package/dist/services/hosted-profiles.js.map +1 -0
  79. package/dist/services/latency.d.ts +67 -0
  80. package/dist/services/latency.d.ts.map +1 -0
  81. package/dist/services/latency.js +274 -0
  82. package/dist/services/latency.js.map +1 -0
  83. package/dist/services/manifest-compliance.d.ts +64 -0
  84. package/dist/services/manifest-compliance.d.ts.map +1 -0
  85. package/dist/services/manifest-compliance.js +271 -0
  86. package/dist/services/manifest-compliance.js.map +1 -0
  87. package/dist/services/monitoring-diff.d.ts +31 -0
  88. package/dist/services/monitoring-diff.d.ts.map +1 -0
  89. package/dist/services/monitoring-diff.js +189 -0
  90. package/dist/services/monitoring-diff.js.map +1 -0
  91. package/dist/services/notifications.d.ts +46 -0
  92. package/dist/services/notifications.d.ts.map +1 -0
  93. package/dist/services/notifications.js +88 -0
  94. package/dist/services/notifications.js.map +1 -0
  95. package/dist/services/posthog.d.ts +43 -0
  96. package/dist/services/posthog.d.ts.map +1 -0
  97. package/dist/services/posthog.js +110 -0
  98. package/dist/services/posthog.js.map +1 -0
  99. package/dist/services/stripe.d.ts +93 -0
  100. package/dist/services/stripe.d.ts.map +1 -0
  101. package/dist/services/stripe.js +490 -0
  102. package/dist/services/stripe.js.map +1 -0
  103. package/dist/services/validation-history.d.ts +99 -0
  104. package/dist/services/validation-history.d.ts.map +1 -0
  105. package/dist/services/validation-history.js +344 -0
  106. package/dist/services/validation-history.js.map +1 -0
  107. package/dist/services/validation-logging.d.ts +103 -0
  108. package/dist/services/validation-logging.d.ts.map +1 -0
  109. package/dist/services/validation-logging.js +210 -0
  110. package/dist/services/validation-logging.js.map +1 -0
  111. package/dist/services/validation.d.ts +119 -0
  112. package/dist/services/validation.d.ts.map +1 -0
  113. package/dist/services/validation.js +1185 -0
  114. package/dist/services/validation.js.map +1 -0
  115. package/dist/simulator/agent-simulator.d.ts.map +1 -1
  116. package/dist/simulator/agent-simulator.js +229 -9
  117. package/dist/simulator/agent-simulator.js.map +1 -1
  118. package/dist/simulator/types.d.ts +26 -0
  119. package/dist/simulator/types.d.ts.map +1 -1
  120. package/dist/simulator/types.js.map +1 -1
  121. package/dist/types/acp-validation.d.ts +87 -0
  122. package/dist/types/acp-validation.d.ts.map +1 -0
  123. package/dist/types/acp-validation.js +40 -0
  124. package/dist/types/acp-validation.js.map +1 -0
  125. package/dist/types/analytics.d.ts +182 -0
  126. package/dist/types/analytics.d.ts.map +1 -0
  127. package/dist/types/analytics.js +7 -0
  128. package/dist/types/analytics.js.map +1 -0
  129. package/dist/types/generator.d.ts +4 -0
  130. package/dist/types/generator.d.ts.map +1 -1
  131. package/dist/types/ucp-profile.d.ts +32 -2
  132. package/dist/types/ucp-profile.d.ts.map +1 -1
  133. package/dist/types/ucp-profile.js +31 -1
  134. package/dist/types/ucp-profile.js.map +1 -1
  135. package/dist/types/validation.d.ts +14 -0
  136. package/dist/types/validation.d.ts.map +1 -1
  137. package/dist/types/validation.js +19 -0
  138. package/dist/types/validation.js.map +1 -1
  139. package/dist/validator/acp/index.d.ts +31 -0
  140. package/dist/validator/acp/index.d.ts.map +1 -0
  141. package/dist/validator/acp/index.js +574 -0
  142. package/dist/validator/acp/index.js.map +1 -0
  143. package/dist/validator/network-validator.d.ts.map +1 -1
  144. package/dist/validator/network-validator.js +23 -13
  145. package/dist/validator/network-validator.js.map +1 -1
  146. package/dist/validator/rules-validator.d.ts +8 -0
  147. package/dist/validator/rules-validator.d.ts.map +1 -1
  148. package/dist/validator/rules-validator.js +159 -43
  149. package/dist/validator/rules-validator.js.map +1 -1
  150. package/dist/validator/structural-validator.d.ts.map +1 -1
  151. package/dist/validator/structural-validator.js +283 -53
  152. package/dist/validator/structural-validator.js.map +1 -1
  153. package/dist/validator/utils.d.ts +62 -0
  154. package/dist/validator/utils.d.ts.map +1 -0
  155. package/dist/validator/utils.js +151 -0
  156. package/dist/validator/utils.js.map +1 -0
  157. package/package.json +45 -12
  158. package/.claude/settings.local.json +0 -60
  159. package/.vercel/README.txt +0 -11
  160. package/.vercel/project.json +0 -1
  161. package/publish-output.txt +0 -0
  162. package/tsconfig.json +0 -20
@@ -0,0 +1,1185 @@
1
+ "use strict";
2
+ /**
3
+ * Validation Service
4
+ *
5
+ * Validates UCP Profile + AI Readiness for a domain
6
+ * Refactored from api/validate.js serverless function
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.validateDomainReadiness = validateDomainReadiness;
10
+ const index_js_1 = require("../db/index.js");
11
+ const schema_js_1 = require("../db/schema.js");
12
+ const drizzle_orm_1 = require("drizzle-orm");
13
+ const structural_validator_js_1 = require("../validator/structural-validator.js");
14
+ const rules_validator_js_1 = require("../validator/rules-validator.js");
15
+ const latency_js_1 = require("./latency.js");
16
+ const index_js_2 = require("../validator/acp/index.js");
17
+ // Constants
18
+ const VERSION_REGEX = /^\d{4}-\d{2}-\d{2}$/;
19
+ const PRODUCT_FIELDS = {
20
+ required: ['name', 'offers'],
21
+ recommended: ['description', 'image', 'brand', 'sku', 'aggregateRating'],
22
+ optional: ['gtin', 'mpn', 'review', 'category', 'color', 'material', 'weight']
23
+ };
24
+ const OFFER_FIELDS = {
25
+ required: ['price', 'priceCurrency'],
26
+ recommended: ['availability', 'hasMerchantReturnPolicy', 'shippingDetails'],
27
+ optional: ['priceValidUntil', 'itemCondition', 'seller']
28
+ };
29
+ /**
30
+ * Ensure benchmark tables are initialized with required rows
31
+ */
32
+ async function ensureBenchmarkTablesInitialized(db) {
33
+ // Ensure all score buckets exist (0, 10, 20, ..., 100)
34
+ const existingBuckets = await db.select({ scoreBucket: schema_js_1.benchmarkStats.scoreBucket }).from(schema_js_1.benchmarkStats);
35
+ const existingBucketSet = new Set(existingBuckets.map(r => r.scoreBucket));
36
+ const missingBuckets = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].filter(b => !existingBucketSet.has(b));
37
+ if (missingBuckets.length > 0) {
38
+ // Use raw SQL for INSERT with ON CONFLICT since Drizzle may not support it directly
39
+ for (const bucket of missingBuckets) {
40
+ await db.insert(schema_js_1.benchmarkStats).values({ scoreBucket: bucket, count: 0 }).onConflictDoNothing();
41
+ }
42
+ }
43
+ // Ensure summary row exists
44
+ const [summary] = await db.select().from(schema_js_1.benchmarkSummary).where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkSummary.id, 1));
45
+ if (!summary) {
46
+ await db.insert(schema_js_1.benchmarkSummary).values({
47
+ id: 1,
48
+ totalValidations: 0,
49
+ avgScore: '50',
50
+ updatedAt: new Date(),
51
+ }).onConflictDoNothing();
52
+ }
53
+ }
54
+ /**
55
+ * Record score and calculate percentile using Drizzle
56
+ */
57
+ async function recordAndGetBenchmark(score) {
58
+ try {
59
+ const db = (0, index_js_1.getDb)();
60
+ const bucket = Math.floor(score / 10) * 10;
61
+ // Ensure tables are initialized
62
+ await ensureBenchmarkTablesInitialized(db);
63
+ // Record the score - use upsert pattern
64
+ const [existingBucket] = await db
65
+ .select()
66
+ .from(schema_js_1.benchmarkStats)
67
+ .where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkStats.scoreBucket, bucket));
68
+ if (existingBucket) {
69
+ await db
70
+ .update(schema_js_1.benchmarkStats)
71
+ .set({ count: (0, drizzle_orm_1.sql) `${schema_js_1.benchmarkStats.count} + 1` })
72
+ .where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkStats.scoreBucket, bucket));
73
+ }
74
+ else {
75
+ await db.insert(schema_js_1.benchmarkStats).values({ scoreBucket: bucket, count: 1 });
76
+ }
77
+ // Update summary
78
+ const [summary] = await db.select().from(schema_js_1.benchmarkSummary).where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkSummary.id, 1));
79
+ if (summary) {
80
+ const newTotal = summary.totalValidations + 1;
81
+ const currentAvg = parseFloat(summary.avgScore || '50');
82
+ const newAvg = (currentAvg * summary.totalValidations + score) / newTotal;
83
+ await db
84
+ .update(schema_js_1.benchmarkSummary)
85
+ .set({
86
+ totalValidations: newTotal,
87
+ avgScore: String(newAvg),
88
+ updatedAt: new Date(),
89
+ })
90
+ .where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkSummary.id, 1));
91
+ }
92
+ else {
93
+ // Insert if not exists
94
+ await db.insert(schema_js_1.benchmarkSummary).values({
95
+ id: 1,
96
+ totalValidations: 1,
97
+ avgScore: String(score),
98
+ updatedAt: new Date(),
99
+ });
100
+ }
101
+ // Calculate percentile
102
+ const distribution = await db
103
+ .select({
104
+ scoreBucket: schema_js_1.benchmarkStats.scoreBucket,
105
+ count: schema_js_1.benchmarkStats.count,
106
+ })
107
+ .from(schema_js_1.benchmarkStats)
108
+ .orderBy(schema_js_1.benchmarkStats.scoreBucket);
109
+ const [updatedSummary] = await db.select().from(schema_js_1.benchmarkSummary).where((0, drizzle_orm_1.eq)(schema_js_1.benchmarkSummary.id, 1));
110
+ const total = updatedSummary?.totalValidations || 1;
111
+ const avgScore = Math.round(parseFloat(updatedSummary?.avgScore || '50') * 10) / 10;
112
+ let cumulative = 0;
113
+ let belowCount = 0;
114
+ for (const row of distribution) {
115
+ cumulative += row.count;
116
+ if (row.scoreBucket < bucket) {
117
+ belowCount = cumulative;
118
+ }
119
+ else if (row.scoreBucket === bucket) {
120
+ belowCount = cumulative - Math.floor(row.count / 2);
121
+ break;
122
+ }
123
+ }
124
+ const percentile = Math.round((belowCount / total) * 100);
125
+ return {
126
+ percentile,
127
+ total_validations: total,
128
+ avg_score: avgScore,
129
+ };
130
+ }
131
+ catch (error) {
132
+ console.error('Benchmark error:', error);
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Fetch UCP profile from domain
138
+ */
139
+ async function fetchProfile(domain) {
140
+ const urls = [
141
+ `https://${domain}/.well-known/ucp`,
142
+ `https://${domain}/.well-known/ucp.json`,
143
+ ];
144
+ for (const url of urls) {
145
+ try {
146
+ const res = await fetch(url, {
147
+ headers: {
148
+ 'Accept': 'application/json',
149
+ 'User-Agent': 'UCP-Validator/1.0 (https://ucptools.dev)'
150
+ },
151
+ signal: AbortSignal.timeout(10000),
152
+ });
153
+ if (!res.ok)
154
+ continue;
155
+ const text = await res.text();
156
+ if (text.trim().startsWith('<'))
157
+ continue;
158
+ const profile = JSON.parse(text);
159
+ return { profile, profileUrl: url };
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ }
165
+ return { profile: null, error: 'No UCP profile found at /.well-known/ucp or /.well-known/ucp.json' };
166
+ }
167
+ /**
168
+ * Fetch homepage HTML
169
+ */
170
+ async function fetchHomepage(domain) {
171
+ const url = `https://${domain}`;
172
+ try {
173
+ const res = await fetch(url, {
174
+ headers: {
175
+ 'Accept': 'text/html',
176
+ 'User-Agent': 'UCP-Validator/1.0 (https://ucptools.dev)'
177
+ },
178
+ signal: AbortSignal.timeout(15000),
179
+ });
180
+ if (!res.ok) {
181
+ return { html: null, error: `HTTP ${res.status}` };
182
+ }
183
+ const html = await res.text();
184
+ return { html };
185
+ }
186
+ catch (e) {
187
+ return { html: null, error: e.message };
188
+ }
189
+ }
190
+ /**
191
+ * Extract JSON-LD from HTML
192
+ */
193
+ function extractJsonLd(html) {
194
+ const schemas = [];
195
+ const regex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
196
+ let match;
197
+ while ((match = regex.exec(html)) !== null) {
198
+ try {
199
+ const parsed = JSON.parse(match[1]);
200
+ if (Array.isArray(parsed)) {
201
+ schemas.push(...parsed);
202
+ }
203
+ else {
204
+ schemas.push(parsed);
205
+ }
206
+ }
207
+ catch {
208
+ // Invalid JSON-LD, skip
209
+ }
210
+ }
211
+ return schemas;
212
+ }
213
+ /**
214
+ * Find schemas by type
215
+ */
216
+ function findInSchema(schemas, type) {
217
+ const results = [];
218
+ function search(obj) {
219
+ if (!obj || typeof obj !== 'object')
220
+ return;
221
+ if (Array.isArray(obj)) {
222
+ obj.forEach(search);
223
+ return;
224
+ }
225
+ if (obj['@type'] === type || (Array.isArray(obj['@type']) && obj['@type'].includes(type))) {
226
+ results.push(obj);
227
+ }
228
+ Object.values(obj).forEach(search);
229
+ }
230
+ schemas.forEach(search);
231
+ return results;
232
+ }
233
+ /**
234
+ * Get string length helper
235
+ */
236
+ function getStringLength(val) {
237
+ if (typeof val === 'string')
238
+ return val.length;
239
+ if (typeof val === 'object' && val !== null) {
240
+ if (val['@value'])
241
+ return String(val['@value']).length;
242
+ if (val.name)
243
+ return String(val.name).length;
244
+ }
245
+ return 0;
246
+ }
247
+ /**
248
+ * Check if value exists
249
+ */
250
+ function hasValue(val) {
251
+ if (val === null || val === undefined)
252
+ return false;
253
+ if (typeof val === 'string')
254
+ return val.trim().length > 0;
255
+ if (Array.isArray(val))
256
+ return val.length > 0;
257
+ if (typeof val === 'object')
258
+ return Object.keys(val).length > 0;
259
+ return true;
260
+ }
261
+ /**
262
+ * Validate product quality
263
+ */
264
+ function validateProductQuality(products) {
265
+ const issues = [];
266
+ const recommendations = [];
267
+ let totalCompleteness = 0;
268
+ products.forEach((product, idx) => {
269
+ let productScore = 0;
270
+ let maxScore = 0;
271
+ const productName = product.name || `Product ${idx + 1}`;
272
+ // Required fields (30 points each)
273
+ PRODUCT_FIELDS.required.forEach(field => {
274
+ maxScore += 30;
275
+ if (hasValue(product[field])) {
276
+ productScore += 30;
277
+ }
278
+ else {
279
+ issues.push({
280
+ severity: 'error',
281
+ code: `PRODUCT_MISSING_${field.toUpperCase()}`,
282
+ category: 'product_quality',
283
+ message: `Product "${productName}" missing required field: ${field}`,
284
+ });
285
+ }
286
+ });
287
+ // Recommended fields (15 points each)
288
+ PRODUCT_FIELDS.recommended.forEach(field => {
289
+ maxScore += 15;
290
+ if (hasValue(product[field])) {
291
+ productScore += 15;
292
+ }
293
+ else {
294
+ recommendations.push({
295
+ field,
296
+ message: `Add ${field} to improve AI product matching`,
297
+ priority: 'high'
298
+ });
299
+ }
300
+ });
301
+ // Optional fields (5 points each)
302
+ PRODUCT_FIELDS.optional.forEach(field => {
303
+ maxScore += 5;
304
+ if (hasValue(product[field])) {
305
+ productScore += 5;
306
+ }
307
+ });
308
+ // Description quality check
309
+ if (product.description) {
310
+ const descLength = getStringLength(product.description);
311
+ if (descLength < 50) {
312
+ issues.push({
313
+ severity: 'warn',
314
+ code: 'PRODUCT_SHORT_DESCRIPTION',
315
+ category: 'content_quality',
316
+ message: `Product "${productName}" has very short description (${descLength} chars)`,
317
+ hint: 'Descriptions under 50 chars may cause AI hallucinations. Aim for 150-300 chars.'
318
+ });
319
+ }
320
+ }
321
+ else {
322
+ issues.push({
323
+ severity: 'warn',
324
+ code: 'PRODUCT_NO_DESCRIPTION',
325
+ category: 'content_quality',
326
+ message: `Product "${productName}" has no description`,
327
+ hint: 'Missing descriptions increase risk of AI hallucinations'
328
+ });
329
+ }
330
+ // Image check
331
+ if (!product.image) {
332
+ issues.push({
333
+ severity: 'warn',
334
+ code: 'PRODUCT_NO_IMAGE',
335
+ category: 'content_quality',
336
+ message: `Product "${productName}" has no image`,
337
+ hint: 'Products without images may be deprioritized by AI agents'
338
+ });
339
+ }
340
+ // Validate offers
341
+ const offers = Array.isArray(product.offers) ? product.offers : (product.offers ? [product.offers] : []);
342
+ if (offers.length === 0) {
343
+ issues.push({
344
+ severity: 'error',
345
+ code: 'PRODUCT_NO_OFFERS',
346
+ category: 'product_quality',
347
+ message: `Product "${productName}" has no offers/pricing`,
348
+ });
349
+ }
350
+ else {
351
+ offers.forEach((offer) => {
352
+ if (!hasValue(offer.price)) {
353
+ issues.push({
354
+ severity: 'error',
355
+ code: 'OFFER_NO_PRICE',
356
+ category: 'product_quality',
357
+ message: `Product "${productName}" offer missing price`,
358
+ });
359
+ }
360
+ if (!hasValue(offer.priceCurrency)) {
361
+ issues.push({
362
+ severity: 'warn',
363
+ code: 'OFFER_NO_CURRENCY',
364
+ category: 'product_quality',
365
+ message: `Product "${productName}" offer missing priceCurrency`,
366
+ hint: 'Add ISO 4217 currency code (e.g., "USD", "EUR")'
367
+ });
368
+ }
369
+ // Shipping details validation
370
+ if (offer.shippingDetails) {
371
+ const shipping = Array.isArray(offer.shippingDetails) ? offer.shippingDetails[0] : offer.shippingDetails;
372
+ if (!shipping.shippingRate && !shipping.freeShippingThreshold) {
373
+ issues.push({
374
+ severity: 'warn',
375
+ code: 'SHIPPING_NO_RATE',
376
+ category: 'shipping_quality',
377
+ message: `Product "${productName}" shippingDetails missing shippingRate`,
378
+ hint: 'AI agents need shipping costs to complete purchases'
379
+ });
380
+ }
381
+ if (!shipping.deliveryTime) {
382
+ issues.push({
383
+ severity: 'warn',
384
+ code: 'SHIPPING_NO_DELIVERY_TIME',
385
+ category: 'shipping_quality',
386
+ message: `Product "${productName}" shippingDetails missing deliveryTime`,
387
+ hint: 'Delivery estimates help AI agents make purchase decisions'
388
+ });
389
+ }
390
+ }
391
+ // Return policy validation
392
+ if (offer.hasMerchantReturnPolicy) {
393
+ const policy = offer.hasMerchantReturnPolicy;
394
+ if (!policy.returnPolicyCategory) {
395
+ issues.push({
396
+ severity: 'warn',
397
+ code: 'RETURN_NO_CATEGORY',
398
+ category: 'return_policy',
399
+ message: `Return policy missing returnPolicyCategory`,
400
+ hint: 'Use MerchantReturnFiniteReturnWindow, MerchantReturnNotPermitted, etc.'
401
+ });
402
+ }
403
+ }
404
+ });
405
+ }
406
+ totalCompleteness += (productScore / maxScore) * 100;
407
+ });
408
+ const avgCompleteness = products.length > 0 ? Math.round(totalCompleteness / products.length) : 0;
409
+ // Dedupe recommendations
410
+ const uniqueRecs = [...new Map(recommendations.map(r => [r.field, r])).values()];
411
+ return {
412
+ issues,
413
+ recommendations: uniqueRecs,
414
+ completeness: avgCompleteness
415
+ };
416
+ }
417
+ /**
418
+ * Validate Schema.org markup
419
+ */
420
+ function validateSchema(schemas) {
421
+ const issues = [];
422
+ const recommendations = [];
423
+ const orgs = findInSchema(schemas, 'Organization');
424
+ const products = findInSchema(schemas, 'Product');
425
+ const returnPolicies = findInSchema(schemas, 'MerchantReturnPolicy');
426
+ // Check for Organization
427
+ if (orgs.length === 0) {
428
+ issues.push({
429
+ severity: 'warn',
430
+ code: 'SCHEMA_NO_ORG',
431
+ category: 'schema',
432
+ message: 'No Organization schema found',
433
+ hint: 'Add Organization schema for better AI recognition'
434
+ });
435
+ }
436
+ else {
437
+ const org = orgs[0];
438
+ if (!org.logo) {
439
+ recommendations.push({
440
+ field: 'logo',
441
+ message: 'Add logo to Organization schema',
442
+ priority: 'medium'
443
+ });
444
+ }
445
+ if (!org.contactPoint && !org.email && !org.telephone) {
446
+ recommendations.push({
447
+ field: 'contactPoint',
448
+ message: 'Add contact information to Organization',
449
+ priority: 'low'
450
+ });
451
+ }
452
+ }
453
+ // Check for MerchantReturnPolicy
454
+ const hasReturnPolicyInOffer = products.some((p) => p.offers?.hasMerchantReturnPolicy ||
455
+ (Array.isArray(p.offers) && p.offers.some((o) => o.hasMerchantReturnPolicy)));
456
+ if (returnPolicies.length === 0 && !hasReturnPolicyInOffer) {
457
+ issues.push({
458
+ severity: 'error',
459
+ code: 'SCHEMA_NO_RETURN_POLICY',
460
+ category: 'schema',
461
+ message: 'Missing MerchantReturnPolicy schema (required Jan 2026)',
462
+ hint: 'Add hasMerchantReturnPolicy to Product offers for AI commerce eligibility'
463
+ });
464
+ }
465
+ // Check for shippingDetails
466
+ const hasShippingInOffer = products.some((p) => p.offers?.shippingDetails ||
467
+ (Array.isArray(p.offers) && p.offers.some((o) => o.shippingDetails)));
468
+ if (!hasShippingInOffer && products.length > 0) {
469
+ issues.push({
470
+ severity: 'error',
471
+ code: 'SCHEMA_NO_SHIPPING',
472
+ category: 'schema',
473
+ message: 'Missing shippingDetails schema (required Jan 2026)',
474
+ hint: 'Add shippingDetails to Product offers for AI commerce eligibility'
475
+ });
476
+ }
477
+ // Validate product quality
478
+ const productQuality = products.length > 0 ? validateProductQuality(products) : { issues: [], recommendations: [], completeness: 0 };
479
+ issues.push(...productQuality.issues);
480
+ recommendations.push(...productQuality.recommendations);
481
+ return {
482
+ issues,
483
+ recommendations: [...new Map(recommendations.map(r => [r.field, r])).values()],
484
+ stats: {
485
+ orgs: orgs.length,
486
+ products: products.length,
487
+ returnPolicies: returnPolicies.length + (hasReturnPolicyInOffer ? 1 : 0),
488
+ productCompleteness: productQuality.completeness
489
+ }
490
+ };
491
+ }
492
+ /**
493
+ * Validate UCP profile structure
494
+ */
495
+ function validateUcpProfile(profile) {
496
+ const issues = [];
497
+ let version = null;
498
+ if (!profile) {
499
+ issues.push({
500
+ severity: 'error',
501
+ code: 'UCP_FETCH_FAILED',
502
+ category: 'ucp',
503
+ message: 'No UCP profile found at /.well-known/ucp or /.well-known/ucp.json',
504
+ hint: 'Create a UCP profile at /.well-known/ucp'
505
+ });
506
+ return { issues, version };
507
+ }
508
+ // Use proper validators from src/validator/ (updated per UCP Spec 2026-01-11)
509
+ const structuralIssues = (0, structural_validator_js_1.validateStructure)(profile);
510
+ const rulesIssues = (0, rules_validator_js_1.validateRules)(profile);
511
+ // Convert validator issues to service format (add category field)
512
+ const allValidatorIssues = [...structuralIssues, ...rulesIssues].map(issue => ({
513
+ severity: issue.severity,
514
+ code: issue.code,
515
+ category: 'ucp',
516
+ message: issue.message,
517
+ hint: issue.hint,
518
+ path: issue.path
519
+ }));
520
+ issues.push(...allValidatorIssues);
521
+ // Extract version if available
522
+ if (profile.ucp?.version) {
523
+ version = profile.ucp.version;
524
+ }
525
+ return { issues, version };
526
+ }
527
+ /**
528
+ * Calculate AI Readiness Score (0-100) - ADDITIVE MODEL
529
+ *
530
+ * Scale: 0 = Invisible to AI agents, 100 = Perfect AI commerce readiness
531
+ *
532
+ * NO UCP = 0 points (invisible to AI shopping agents)
533
+ * WITH UCP = earn points for each achievement:
534
+ * - UCP Profile: 50 points max (earn points for having UCP + valid structure)
535
+ * - Schema.org: 35 points max (earn points for required markup)
536
+ * - Performance: 15 points max (earn points for fast responses)
537
+ *
538
+ * Note: Products/catalog are NOT part of UCP spec. Product feeds are managed
539
+ * separately via Google Merchant Center. Use the Feed Analyzer for product validation.
540
+ */
541
+ function calculateReadinessScore(ucpIssues, schemaIssues, hasUcp, performanceScore = 100) {
542
+ if (!hasUcp) {
543
+ // NO UCP = 0 points - you're invisible to AI agents
544
+ return {
545
+ score: 0,
546
+ breakdown: {
547
+ ucp: { score: 0, maxScore: 50, status: 'Not found - install UCP to get discovered' },
548
+ schema: { score: 0, maxScore: 35, status: 'Requires UCP first' },
549
+ performance: { score: 0, maxScore: 15, status: 'Requires UCP first' },
550
+ total: 0,
551
+ maxTotal: 100
552
+ }
553
+ };
554
+ }
555
+ // ============ ADDITIVE SCORING ============
556
+ // UCP Profile: 50 points max
557
+ // +30 pts: UCP profile exists and is parseable (you're discoverable!)
558
+ // +7 pts: Has signing keys
559
+ // +7 pts: Valid capability structure
560
+ // +6 pts: No errors
561
+ let ucpScore = 30; // Base: UCP exists and is parseable
562
+ const ucpDetails = ['Discoverable (+30)'];
563
+ const hasSigningKeys = !ucpIssues.some(i => i.code === 'UCP_MISSING_SIGNING_KEYS');
564
+ if (hasSigningKeys) {
565
+ ucpScore += 7;
566
+ ucpDetails.push('Signing keys (+7)');
567
+ }
568
+ const hasValidCapabilities = !ucpIssues.some(i => i.code === 'UCP_INVALID_CAPABILITY');
569
+ if (hasValidCapabilities) {
570
+ ucpScore += 7;
571
+ ucpDetails.push('Valid capabilities (+7)');
572
+ }
573
+ const ucpErrors = ucpIssues.filter(i => i.severity === 'error').length;
574
+ if (ucpErrors === 0) {
575
+ ucpScore += 6;
576
+ ucpDetails.push('No errors (+6)');
577
+ }
578
+ const ucpStatus = ucpScore === 50 ? 'Perfect' : ucpDetails.join(', ');
579
+ // Schema.org: 35 points max
580
+ // +17 pts: Has MerchantReturnPolicy
581
+ // +18 pts: Has shippingDetails
582
+ let schemaScore = 0;
583
+ const schemaDetails = [];
584
+ const hasReturnPolicy = !schemaIssues.some(i => i.code === 'SCHEMA_NO_RETURN_POLICY');
585
+ if (hasReturnPolicy) {
586
+ schemaScore += 17;
587
+ schemaDetails.push('Return policy (+17)');
588
+ }
589
+ const hasShipping = !schemaIssues.some(i => i.code === 'SCHEMA_NO_SHIPPING');
590
+ if (hasShipping) {
591
+ schemaScore += 18;
592
+ schemaDetails.push('Shipping info (+18)');
593
+ }
594
+ const schemaStatus = schemaScore === 35 ? 'Complete' :
595
+ schemaScore === 0 ? 'Missing required markup' : schemaDetails.join(', ');
596
+ // Performance: 15 points max
597
+ // Points based on response time
598
+ let performancePoints = 0;
599
+ let perfStatus = 'Very slow';
600
+ if (performanceScore >= 90) {
601
+ performancePoints = 15;
602
+ perfStatus = 'Excellent (+15)';
603
+ }
604
+ else if (performanceScore >= 70) {
605
+ performancePoints = 12;
606
+ perfStatus = 'Good (+12)';
607
+ }
608
+ else if (performanceScore >= 50) {
609
+ performancePoints = 8;
610
+ perfStatus = 'Acceptable (+8)';
611
+ }
612
+ else if (performanceScore >= 30) {
613
+ performancePoints = 4;
614
+ perfStatus = 'Slow (+4)';
615
+ }
616
+ const totalScore = ucpScore + schemaScore + performancePoints;
617
+ return {
618
+ score: totalScore,
619
+ breakdown: {
620
+ ucp: { score: ucpScore, maxScore: 50, status: ucpStatus },
621
+ schema: { score: schemaScore, maxScore: 35, status: schemaStatus },
622
+ performance: { score: performancePoints, maxScore: 15, status: perfStatus },
623
+ total: totalScore,
624
+ maxTotal: 100
625
+ }
626
+ };
627
+ }
628
+ /**
629
+ * Get grade from score (0-100)
630
+ */
631
+ function getGrade(score) {
632
+ if (score >= 90)
633
+ return 'A';
634
+ if (score >= 75)
635
+ return 'B';
636
+ if (score >= 60)
637
+ return 'C';
638
+ if (score >= 40)
639
+ return 'D';
640
+ return 'F';
641
+ }
642
+ /**
643
+ * Get readiness level based on score and UCP presence
644
+ */
645
+ function getReadinessLevel(score, hasUcp, schemaIssues) {
646
+ if (!hasUcp) {
647
+ return { level: 'invisible', label: 'Invisible to AI Agents' };
648
+ }
649
+ const hasCriticalSchema = schemaIssues.some(i => i.code === 'SCHEMA_NO_RETURN_POLICY' || i.code === 'SCHEMA_NO_SHIPPING');
650
+ if (score >= 90 && !hasCriticalSchema) {
651
+ return { level: 'ready', label: 'AI Commerce Ready' };
652
+ }
653
+ if (score >= 75) {
654
+ return { level: 'good', label: 'Good - Minor Improvements Needed' };
655
+ }
656
+ if (score >= 60) {
657
+ return { level: 'partial', label: 'Partially Ready' };
658
+ }
659
+ if (score >= 40) {
660
+ return { level: 'limited', label: 'Limited Readiness' };
661
+ }
662
+ return { level: 'not_ready', label: 'Needs Work' };
663
+ }
664
+ /**
665
+ * Generate lint suggestions
666
+ */
667
+ function generateLintSuggestions(ucpIssues, schemaIssues, hasUcp, profile, schemaStats) {
668
+ const suggestions = [];
669
+ // UCP not found - critical
670
+ if (!hasUcp) {
671
+ suggestions.push({
672
+ severity: 'critical',
673
+ title: 'Create a UCP Profile',
674
+ code: 'UCP_FETCH_FAILED',
675
+ path: '$.well-known/ucp',
676
+ impact: 'AI shopping agents cannot discover your store without a UCP profile',
677
+ fix: 'Create a file at /.well-known/ucp with your store configuration',
678
+ codeSnippet: `{
679
+ "ucp": {
680
+ "version": "2026-05-01",
681
+ "services": {
682
+ "dev.ucp.shopping": {
683
+ "version": "1.0.0",
684
+ "spec": "https://ucp.dev/specs/shopping/1.0",
685
+ "rest": {
686
+ "schema": "https://yourstore.com/api/openapi.json",
687
+ "endpoint": "https://yourstore.com/api/v1"
688
+ }
689
+ }
690
+ },
691
+ "capabilities": [
692
+ {
693
+ "name": "dev.ucp.shopping.catalog",
694
+ "version": "1.0.0",
695
+ "spec": "https://ucp.dev/caps/catalog/1.0",
696
+ "schema": "https://ucp.dev/caps/catalog/1.0/schema.json"
697
+ }
698
+ ]
699
+ }
700
+ }`,
701
+ docLink: 'https://ucp.dev/docs/getting-started',
702
+ generatorLink: '/generate'
703
+ });
704
+ }
705
+ // Missing signing keys
706
+ if (ucpIssues.some(i => i.code === 'UCP_MISSING_SIGNING_KEYS')) {
707
+ suggestions.push({
708
+ severity: 'critical',
709
+ title: 'Add Signing Keys for Webhook Verification',
710
+ code: 'UCP_MISSING_SIGNING_KEYS',
711
+ path: '$.signing_keys',
712
+ impact: 'Required for secure webhook verification - AI agents cannot verify your responses',
713
+ fix: 'Add a signing_keys array with at least one JWK public key',
714
+ codeSnippet: `{
715
+ "signing_keys": [
716
+ {
717
+ "kid": "key-2026-01",
718
+ "kty": "EC",
719
+ "crv": "P-256",
720
+ "x": "YOUR_X_COORDINATE_BASE64URL",
721
+ "y": "YOUR_Y_COORDINATE_BASE64URL",
722
+ "use": "sig",
723
+ "alg": "ES256"
724
+ }
725
+ ]
726
+ }`,
727
+ docLink: 'https://ucp.dev/latest/specification/overview/',
728
+ generatorLink: '/generate?tab=keys'
729
+ });
730
+ }
731
+ // HTTP endpoint instead of HTTPS
732
+ if (ucpIssues.some(i => i.code === 'UCP_ENDPOINT_NOT_HTTPS')) {
733
+ const httpIssue = ucpIssues.find(i => i.code === 'UCP_ENDPOINT_NOT_HTTPS');
734
+ suggestions.push({
735
+ severity: 'critical',
736
+ title: 'Use HTTPS for All Endpoints',
737
+ code: 'UCP_ENDPOINT_NOT_HTTPS',
738
+ path: httpIssue?.path || '$.ucp.services',
739
+ impact: 'UCP requires all endpoints use HTTPS - AI agents will reject insecure connections',
740
+ fix: 'Change all endpoint URLs from http:// to https://',
741
+ codeSnippet: `// Change this:
742
+ "endpoint": "http://example.com/api/ucp"
743
+
744
+ // To this:
745
+ "endpoint": "https://example.com/api/ucp"`,
746
+ docLink: 'https://ucp.dev/latest/specification/overview/',
747
+ generatorLink: '/generate'
748
+ });
749
+ }
750
+ // Missing return policy
751
+ if (schemaIssues.some(i => i.code === 'SCHEMA_NO_RETURN_POLICY')) {
752
+ suggestions.push({
753
+ severity: 'critical',
754
+ title: 'Add MerchantReturnPolicy Schema',
755
+ code: 'SCHEMA_NO_RETURN_POLICY',
756
+ impact: 'Required for AI commerce eligibility (Jan 2026 deadline)',
757
+ fix: 'Add MerchantReturnPolicy to your product offers',
758
+ codeSnippet: `{
759
+ "@type": "Product",
760
+ "offers": {
761
+ "@type": "Offer",
762
+ "hasMerchantReturnPolicy": {
763
+ "@type": "MerchantReturnPolicy",
764
+ "applicableCountry": "US",
765
+ "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
766
+ "merchantReturnDays": 30,
767
+ "returnFees": "https://schema.org/FreeReturn"
768
+ }
769
+ }
770
+ }`,
771
+ docLink: 'https://schema.org/MerchantReturnPolicy',
772
+ generatorLink: '/generate?tab=schema'
773
+ });
774
+ }
775
+ // Missing shipping
776
+ if (schemaIssues.some(i => i.code === 'SCHEMA_NO_SHIPPING')) {
777
+ suggestions.push({
778
+ severity: 'critical',
779
+ title: 'Add OfferShippingDetails Schema',
780
+ code: 'SCHEMA_NO_SHIPPING',
781
+ impact: 'Required for AI commerce eligibility (Jan 2026 deadline)',
782
+ fix: 'Add shippingDetails to your product offers',
783
+ codeSnippet: `{
784
+ "@type": "Product",
785
+ "offers": {
786
+ "@type": "Offer",
787
+ "shippingDetails": {
788
+ "@type": "OfferShippingDetails",
789
+ "shippingRate": {
790
+ "@type": "MonetaryAmount",
791
+ "value": "5.99",
792
+ "currency": "USD"
793
+ },
794
+ "deliveryTime": {
795
+ "@type": "ShippingDeliveryTime",
796
+ "handlingTime": {
797
+ "@type": "QuantitativeValue",
798
+ "minValue": 1,
799
+ "maxValue": 2,
800
+ "unitCode": "d"
801
+ },
802
+ "transitTime": {
803
+ "@type": "QuantitativeValue",
804
+ "minValue": 3,
805
+ "maxValue": 5,
806
+ "unitCode": "d"
807
+ }
808
+ },
809
+ "shippingDestination": {
810
+ "@type": "DefinedRegion",
811
+ "addressCountry": "US"
812
+ }
813
+ }
814
+ }
815
+ }`,
816
+ docLink: 'https://schema.org/OfferShippingDetails',
817
+ generatorLink: '/generate?tab=schema'
818
+ });
819
+ }
820
+ // Missing payment handlers
821
+ if (ucpIssues.some(i => i.code === 'UCP_MISSING_PAYMENT_HANDLERS')) {
822
+ suggestions.push({
823
+ severity: 'critical',
824
+ title: 'Add Payment Handlers Configuration',
825
+ code: 'UCP_MISSING_PAYMENT_HANDLERS',
826
+ path: '$.payment',
827
+ impact: 'AI agents cannot complete purchases without payment configuration',
828
+ fix: 'Add payment handlers to specify supported payment methods',
829
+ codeSnippet: `{
830
+ "payment": {
831
+ "handlers": [
832
+ {
833
+ "id": "google_pay",
834
+ "name": "Google Pay",
835
+ "version": "2026-01-11",
836
+ "spec": "https://ucp.dev/handlers/google-pay/"
837
+ },
838
+ {
839
+ "id": "shop_pay",
840
+ "name": "Shop Pay",
841
+ "version": "2026-01-11",
842
+ "spec": "https://ucp.dev/handlers/shop-pay/"
843
+ }
844
+ ]
845
+ }
846
+ }`,
847
+ docLink: 'https://ucp.dev/latest/specification/overview/',
848
+ generatorLink: '/generate?tab=payment'
849
+ });
850
+ }
851
+ // Missing services
852
+ if (ucpIssues.some(i => i.code === 'UCP_MISSING_SERVICES')) {
853
+ suggestions.push({
854
+ severity: 'critical',
855
+ title: 'Add Services Configuration',
856
+ code: 'UCP_MISSING_SERVICES',
857
+ path: '$.ucp.services',
858
+ impact: 'No API endpoints defined - AI agents cannot interact with your store',
859
+ fix: 'Add at least one service with transport binding',
860
+ codeSnippet: `{
861
+ "ucp": {
862
+ "services": {
863
+ "dev.ucp.shopping": {
864
+ "version": "2026-01-11",
865
+ "spec": "https://ucp.dev/specification/shopping/",
866
+ "rest": {
867
+ "schema": "https://yourstore.com/api/openapi.json",
868
+ "endpoint": "https://yourstore.com/api/ucp"
869
+ }
870
+ }
871
+ }
872
+ }
873
+ }`,
874
+ docLink: 'https://ucp.dev/latest/specification/overview/',
875
+ generatorLink: '/generate'
876
+ });
877
+ }
878
+ // Missing capabilities
879
+ if (ucpIssues.some(i => i.code === 'UCP_MISSING_CAPABILITIES')) {
880
+ suggestions.push({
881
+ severity: 'critical',
882
+ title: 'Add Capabilities Array',
883
+ code: 'UCP_MISSING_CAPABILITIES',
884
+ path: '$.ucp.capabilities',
885
+ impact: 'AI agents cannot determine what actions are supported',
886
+ fix: 'Add capabilities to define supported checkout, order, and other features',
887
+ codeSnippet: `{
888
+ "ucp": {
889
+ "capabilities": [
890
+ {
891
+ "name": "dev.ucp.shopping.checkout",
892
+ "version": "2026-01-11",
893
+ "spec": "https://ucp.dev/specification/checkout/",
894
+ "schema": "https://ucp.dev/schemas/shopping/checkout.json"
895
+ },
896
+ {
897
+ "name": "dev.ucp.shopping.order",
898
+ "version": "2026-01-11",
899
+ "spec": "https://ucp.dev/specification/order/",
900
+ "schema": "https://ucp.dev/schemas/shopping/order.json"
901
+ }
902
+ ]
903
+ }
904
+ }`,
905
+ docLink: 'https://ucp.dev/latest/specification/overview/',
906
+ generatorLink: '/generate'
907
+ });
908
+ }
909
+ // REST transport missing schema or endpoint
910
+ if (ucpIssues.some(i => i.code === 'UCP_REST_MISSING_SCHEMA' || i.code === 'UCP_REST_MISSING_ENDPOINT')) {
911
+ suggestions.push({
912
+ severity: 'critical',
913
+ title: 'Complete REST Transport Configuration',
914
+ code: 'UCP_REST_MISSING_SCHEMA',
915
+ path: '$.ucp.services[].rest',
916
+ impact: 'REST API integration incomplete - AI agents cannot call your endpoints',
917
+ fix: 'Add both schema (OpenAPI) and endpoint URL to REST transport',
918
+ codeSnippet: `{
919
+ "rest": {
920
+ "schema": "https://yourstore.com/api/openapi.json",
921
+ "endpoint": "https://yourstore.com/api/v1"
922
+ }
923
+ }`,
924
+ docLink: 'https://ucp.dev/latest/specification/overview/',
925
+ generatorLink: '/generate'
926
+ });
927
+ }
928
+ // MCP transport missing schema or endpoint
929
+ if (ucpIssues.some(i => i.code === 'UCP_MCP_MISSING_SCHEMA' || i.code === 'UCP_MCP_MISSING_ENDPOINT')) {
930
+ suggestions.push({
931
+ severity: 'critical',
932
+ title: 'Complete MCP Transport Configuration',
933
+ code: 'UCP_MCP_MISSING_SCHEMA',
934
+ path: '$.ucp.services[].mcp',
935
+ impact: 'MCP integration incomplete - LLM tools cannot connect to your store',
936
+ fix: 'Add both schema (OpenRPC) and endpoint URL to MCP transport',
937
+ codeSnippet: `{
938
+ "mcp": {
939
+ "schema": "https://yourstore.com/api/openrpc.json",
940
+ "endpoint": "https://yourstore.com/api/mcp"
941
+ }
942
+ }`,
943
+ docLink: 'https://ucp.dev/latest/specification/overview/',
944
+ generatorLink: '/generate'
945
+ });
946
+ }
947
+ // Namespace origin mismatch
948
+ if (ucpIssues.some(i => i.code === 'UCP_NS_ORIGIN_MISMATCH')) {
949
+ const mismatchIssue = ucpIssues.find(i => i.code === 'UCP_NS_ORIGIN_MISMATCH');
950
+ suggestions.push({
951
+ severity: 'critical',
952
+ title: 'Fix Namespace Origin Mismatch',
953
+ code: 'UCP_NS_ORIGIN_MISMATCH',
954
+ path: mismatchIssue?.path || '$.ucp.capabilities',
955
+ impact: 'Security violation - spec/schema URLs must match capability namespace authority',
956
+ fix: 'Ensure spec and schema URLs come from the same domain as the capability namespace',
957
+ codeSnippet: `// For official UCP capabilities (dev.ucp.*):
958
+ {
959
+ "name": "dev.ucp.shopping.checkout",
960
+ "spec": "https://ucp.dev/specification/checkout/", // Must be ucp.dev
961
+ "schema": "https://ucp.dev/schemas/checkout.json" // Must be ucp.dev
962
+ }
963
+
964
+ // For custom capabilities (com.yourcompany.*):
965
+ {
966
+ "name": "com.yourcompany.custom",
967
+ "spec": "https://yourcompany.com/specs/custom/", // Must match namespace
968
+ "schema": "https://yourcompany.com/schemas/custom.json"
969
+ }`,
970
+ docLink: 'https://ucp.dev/latest/specification/overview/',
971
+ generatorLink: '/generate'
972
+ });
973
+ }
974
+ // Orphaned extension
975
+ if (ucpIssues.some(i => i.code === 'UCP_ORPHANED_EXTENSION')) {
976
+ const orphanIssue = ucpIssues.find(i => i.code === 'UCP_ORPHANED_EXTENSION');
977
+ suggestions.push({
978
+ severity: 'critical',
979
+ title: 'Fix Orphaned Extension',
980
+ code: 'UCP_ORPHANED_EXTENSION',
981
+ path: orphanIssue?.path || '$.ucp.capabilities',
982
+ impact: 'Extension references a parent capability that does not exist in your profile',
983
+ fix: 'Either add the parent capability or remove the extends field',
984
+ codeSnippet: `// If using an extension, parent must exist:
985
+ {
986
+ "capabilities": [
987
+ // Parent capability (REQUIRED)
988
+ {
989
+ "name": "dev.ucp.shopping.checkout",
990
+ "version": "2026-01-11",
991
+ "spec": "https://ucp.dev/specification/checkout/",
992
+ "schema": "https://ucp.dev/schemas/checkout.json"
993
+ },
994
+ // Extension (references parent via extends)
995
+ {
996
+ "name": "dev.ucp.shopping.fulfillment",
997
+ "version": "2026-01-11",
998
+ "spec": "https://ucp.dev/specification/fulfillment/",
999
+ "schema": "https://ucp.dev/schemas/fulfillment.json",
1000
+ "extends": "dev.ucp.shopping.checkout" // Parent must be in capabilities
1001
+ }
1002
+ ]
1003
+ }`,
1004
+ docLink: 'https://ucp.dev/latest/specification/overview/',
1005
+ generatorLink: '/generate'
1006
+ });
1007
+ }
1008
+ // Trailing slash in endpoint
1009
+ if (ucpIssues.some(i => i.code === 'UCP_ENDPOINT_TRAILING_SLASH')) {
1010
+ const slashIssue = ucpIssues.find(i => i.code === 'UCP_ENDPOINT_TRAILING_SLASH');
1011
+ suggestions.push({
1012
+ severity: 'warning',
1013
+ title: 'Remove Trailing Slash from Endpoint',
1014
+ code: 'UCP_ENDPOINT_TRAILING_SLASH',
1015
+ path: slashIssue?.path || '$.ucp.services',
1016
+ impact: 'Trailing slashes can cause URL concatenation issues with AI agents',
1017
+ fix: 'Remove the trailing slash from endpoint URLs',
1018
+ codeSnippet: `// Change this:
1019
+ "endpoint": "https://example.com/api/"
1020
+
1021
+ // To this:
1022
+ "endpoint": "https://example.com/api"`,
1023
+ docLink: 'https://ucp.dev/latest/specification/overview/'
1024
+ });
1025
+ }
1026
+ return suggestions;
1027
+ }
1028
+ /**
1029
+ * Main validation function
1030
+ */
1031
+ async function validateDomainReadiness(domain) {
1032
+ const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '').split('/')[0];
1033
+ // Fetch UCP profile, homepage, measure latency, and check ACP readiness in parallel
1034
+ const [ucpResult, homepageResult, latencyResult, acpResult] = await Promise.all([
1035
+ fetchProfile(cleanDomain),
1036
+ fetchHomepage(cleanDomain),
1037
+ (0, latency_js_1.quickLatencyCheck)(cleanDomain),
1038
+ (0, index_js_2.checkAcpReadiness)(cleanDomain, { timeoutMs: 15000 }).catch(() => null),
1039
+ ]);
1040
+ const { profile, profileUrl, error: ucpError } = ucpResult;
1041
+ const { html } = homepageResult;
1042
+ // Validate UCP
1043
+ const hasUcp = !!profile;
1044
+ const ucpValidation = validateUcpProfile(profile);
1045
+ const ucpIssues = ucpValidation.issues;
1046
+ const ucpVersion = ucpValidation.version;
1047
+ // Validate Schema.org
1048
+ let schemaIssues = [];
1049
+ let schemaRecommendations = [];
1050
+ let schemaStats = { orgs: 0, products: 0, returnPolicies: 0, productCompleteness: 0 };
1051
+ if (html) {
1052
+ const schemas = extractJsonLd(html);
1053
+ const schemaResult = validateSchema(schemas);
1054
+ schemaIssues = schemaResult.issues;
1055
+ schemaRecommendations = schemaResult.recommendations;
1056
+ schemaStats = schemaResult.stats;
1057
+ }
1058
+ else {
1059
+ schemaIssues.push({
1060
+ severity: 'warn',
1061
+ code: 'SCHEMA_FETCH_FAILED',
1062
+ category: 'schema',
1063
+ message: 'Could not fetch homepage to check schema',
1064
+ });
1065
+ }
1066
+ // Add performance warning for slow sites
1067
+ if (latencyResult.performanceScore < 60) {
1068
+ schemaIssues.push({
1069
+ severity: 'warn',
1070
+ code: 'PERFORMANCE_SLOW',
1071
+ category: 'performance',
1072
+ message: `Slow response time detected (${latencyResult.ucpLatencyMs || latencyResult.homepageLatencyMs}ms)`,
1073
+ hint: 'AI agents may timeout or deprioritize slow endpoints. Aim for <500ms response time.',
1074
+ });
1075
+ }
1076
+ // Calculate scores (including performance)
1077
+ // Note: Products removed from scoring - not part of UCP spec (use Feed Analyzer instead)
1078
+ const { score: readinessScore, breakdown: scoreBreakdown } = calculateReadinessScore(ucpIssues, schemaIssues, hasUcp, latencyResult.performanceScore);
1079
+ const grade = getGrade(readinessScore);
1080
+ const readiness = getReadinessLevel(readinessScore, hasUcp, schemaIssues);
1081
+ // Record to benchmark
1082
+ const benchmark = await recordAndGetBenchmark(readinessScore);
1083
+ // Generate lint suggestions
1084
+ const lintSuggestions = generateLintSuggestions(ucpIssues, schemaIssues, hasUcp, profile, schemaStats);
1085
+ // Calculate UCP score for backwards compatibility
1086
+ const ucpErrors = ucpIssues.filter(i => i.severity === 'error').length;
1087
+ const ucpScore = hasUcp ? Math.max(0, 100 - ucpErrors * 20 - ucpIssues.filter(i => i.severity === 'warn').length * 5) : 0;
1088
+ // Combine all issues
1089
+ const allIssues = [...ucpIssues, ...schemaIssues];
1090
+ return {
1091
+ ok: ucpErrors === 0 && hasUcp,
1092
+ domain: cleanDomain,
1093
+ profile_url: profileUrl || `https://${cleanDomain}/.well-known/ucp`,
1094
+ ucp_version: ucpVersion,
1095
+ ai_readiness: {
1096
+ score: readinessScore,
1097
+ grade: grade,
1098
+ level: readiness.level,
1099
+ label: readiness.label,
1100
+ },
1101
+ score_breakdown: scoreBreakdown,
1102
+ benchmark: benchmark ? {
1103
+ percentile: benchmark.percentile,
1104
+ comparison: `Your site scores better than ${benchmark.percentile}% of sites analyzed`,
1105
+ total_sites_analyzed: benchmark.total_validations,
1106
+ average_score: benchmark.avg_score,
1107
+ } : null,
1108
+ sdk_validation: {
1109
+ validated: true,
1110
+ sdk_version: '0.1.0',
1111
+ compliant: hasUcp && ucpErrors === 0,
1112
+ badge: hasUcp && ucpErrors === 0
1113
+ ? 'Validated using Official UCP SDK v0.1.0'
1114
+ : null,
1115
+ },
1116
+ ucp: {
1117
+ found: hasUcp,
1118
+ score: ucpScore,
1119
+ issues: ucpIssues.map(i => ({
1120
+ severity: i.severity,
1121
+ code: i.code,
1122
+ message: i.message,
1123
+ path: i.path,
1124
+ hint: i.hint,
1125
+ })),
1126
+ },
1127
+ schema: {
1128
+ checked: !!html,
1129
+ stats: schemaStats,
1130
+ issues: schemaIssues.filter(i => i.category === 'schema').map(i => ({
1131
+ severity: i.severity,
1132
+ code: i.code,
1133
+ message: i.message,
1134
+ path: i.path,
1135
+ hint: i.hint,
1136
+ })),
1137
+ },
1138
+ product_quality: {
1139
+ completeness: schemaStats.productCompleteness,
1140
+ issues: schemaIssues.filter(i => i.category === 'product_quality' || i.category === 'content_quality').map(i => ({
1141
+ severity: i.severity,
1142
+ code: i.code,
1143
+ message: i.message,
1144
+ hint: i.hint,
1145
+ })),
1146
+ recommendations: schemaRecommendations.slice(0, 10),
1147
+ },
1148
+ shipping: {
1149
+ issues: schemaIssues.filter(i => i.category === 'shipping_quality').map(i => ({
1150
+ severity: i.severity,
1151
+ code: i.code,
1152
+ message: i.message,
1153
+ hint: i.hint,
1154
+ })),
1155
+ },
1156
+ performance: {
1157
+ ucpLatencyMs: latencyResult.ucpLatencyMs,
1158
+ homepageLatencyMs: latencyResult.homepageLatencyMs,
1159
+ apiLatencyMs: null, // Only measured in detailed mode
1160
+ overallAvgMs: latencyResult.ucpLatencyMs || latencyResult.homepageLatencyMs || 0,
1161
+ performanceScore: latencyResult.performanceScore,
1162
+ performanceGrade: latencyResult.performanceGrade,
1163
+ },
1164
+ issues: allIssues.map(i => ({
1165
+ severity: i.severity,
1166
+ code: i.code,
1167
+ message: i.message,
1168
+ hint: i.hint,
1169
+ category: i.category,
1170
+ })),
1171
+ lint_suggestions: lintSuggestions.map(s => ({
1172
+ severity: s.severity,
1173
+ title: s.title,
1174
+ code: s.code,
1175
+ path: s.path,
1176
+ impact: s.impact,
1177
+ fix: s.fix,
1178
+ codeSnippet: s.codeSnippet,
1179
+ docLink: s.docLink,
1180
+ generatorLink: s.generatorLink,
1181
+ })),
1182
+ acp: acpResult || null,
1183
+ };
1184
+ }
1185
+ //# sourceMappingURL=validation.js.map