@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.
- package/dist/auth/config.d.ts +20 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +114 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +17 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +45 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +170 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/service.d.ts +80 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +298 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mock-server.d.ts +20 -0
- package/dist/cli/mock-server.d.ts.map +1 -0
- package/dist/cli/mock-server.js +261 -0
- package/dist/cli/mock-server.js.map +1 -0
- package/dist/db/index.d.ts +8 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +22 -5
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.d.ts +3570 -128
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +377 -17
- package/dist/db/schema.js.map +1 -1
- package/dist/db/utils.d.ts +252 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +295 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -1
- package/dist/feed-analyzer/feed-analyzer.js +218 -4
- package/dist/feed-analyzer/feed-analyzer.js.map +1 -1
- package/dist/feed-analyzer/types.d.ts +82 -1
- package/dist/feed-analyzer/types.d.ts.map +1 -1
- package/dist/feed-analyzer/types.js +13 -0
- package/dist/feed-analyzer/types.js.map +1 -1
- package/dist/generator/profile-builder.d.ts.map +1 -1
- package/dist/generator/profile-builder.js +158 -115
- package/dist/generator/profile-builder.js.map +1 -1
- package/dist/lib/analytics.d.ts +349 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/analytics.js +198 -0
- package/dist/lib/analytics.js.map +1 -0
- package/dist/security/security-scanner.d.ts.map +1 -1
- package/dist/security/security-scanner.js +130 -2
- package/dist/security/security-scanner.js.map +1 -1
- package/dist/security/types.d.ts +32 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/security/types.js.map +1 -1
- package/dist/services/analytics.d.ts +114 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/analytics.js +862 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/badge.d.ts +31 -0
- package/dist/services/badge.d.ts.map +1 -0
- package/dist/services/badge.js +152 -0
- package/dist/services/badge.js.map +1 -0
- package/dist/services/cron.d.ts +127 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +693 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/directory.d.ts +2 -0
- package/dist/services/directory.d.ts.map +1 -1
- package/dist/services/directory.js +45 -27
- package/dist/services/directory.js.map +1 -1
- package/dist/services/email.d.ts +127 -0
- package/dist/services/email.d.ts.map +1 -0
- package/dist/services/email.js +876 -0
- package/dist/services/email.js.map +1 -0
- package/dist/services/hosted-profiles.d.ts +77 -0
- package/dist/services/hosted-profiles.d.ts.map +1 -0
- package/dist/services/hosted-profiles.js +433 -0
- package/dist/services/hosted-profiles.js.map +1 -0
- package/dist/services/latency.d.ts +67 -0
- package/dist/services/latency.d.ts.map +1 -0
- package/dist/services/latency.js +274 -0
- package/dist/services/latency.js.map +1 -0
- package/dist/services/manifest-compliance.d.ts +64 -0
- package/dist/services/manifest-compliance.d.ts.map +1 -0
- package/dist/services/manifest-compliance.js +271 -0
- package/dist/services/manifest-compliance.js.map +1 -0
- package/dist/services/monitoring-diff.d.ts +31 -0
- package/dist/services/monitoring-diff.d.ts.map +1 -0
- package/dist/services/monitoring-diff.js +189 -0
- package/dist/services/monitoring-diff.js.map +1 -0
- package/dist/services/notifications.d.ts +46 -0
- package/dist/services/notifications.d.ts.map +1 -0
- package/dist/services/notifications.js +88 -0
- package/dist/services/notifications.js.map +1 -0
- package/dist/services/posthog.d.ts +43 -0
- package/dist/services/posthog.d.ts.map +1 -0
- package/dist/services/posthog.js +110 -0
- package/dist/services/posthog.js.map +1 -0
- package/dist/services/stripe.d.ts +93 -0
- package/dist/services/stripe.d.ts.map +1 -0
- package/dist/services/stripe.js +490 -0
- package/dist/services/stripe.js.map +1 -0
- package/dist/services/validation-history.d.ts +99 -0
- package/dist/services/validation-history.d.ts.map +1 -0
- package/dist/services/validation-history.js +344 -0
- package/dist/services/validation-history.js.map +1 -0
- package/dist/services/validation-logging.d.ts +103 -0
- package/dist/services/validation-logging.d.ts.map +1 -0
- package/dist/services/validation-logging.js +210 -0
- package/dist/services/validation-logging.js.map +1 -0
- package/dist/services/validation.d.ts +119 -0
- package/dist/services/validation.d.ts.map +1 -0
- package/dist/services/validation.js +1185 -0
- package/dist/services/validation.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -1
- package/dist/simulator/agent-simulator.js +229 -9
- package/dist/simulator/agent-simulator.js.map +1 -1
- package/dist/simulator/types.d.ts +26 -0
- package/dist/simulator/types.d.ts.map +1 -1
- package/dist/simulator/types.js.map +1 -1
- package/dist/types/acp-validation.d.ts +87 -0
- package/dist/types/acp-validation.d.ts.map +1 -0
- package/dist/types/acp-validation.js +40 -0
- package/dist/types/acp-validation.js.map +1 -0
- package/dist/types/analytics.d.ts +182 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/analytics.js +7 -0
- package/dist/types/analytics.js.map +1 -0
- package/dist/types/generator.d.ts +4 -0
- package/dist/types/generator.d.ts.map +1 -1
- package/dist/types/ucp-profile.d.ts +32 -2
- package/dist/types/ucp-profile.d.ts.map +1 -1
- package/dist/types/ucp-profile.js +31 -1
- package/dist/types/ucp-profile.js.map +1 -1
- package/dist/types/validation.d.ts +14 -0
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/types/validation.js +19 -0
- package/dist/types/validation.js.map +1 -1
- package/dist/validator/acp/index.d.ts +31 -0
- package/dist/validator/acp/index.d.ts.map +1 -0
- package/dist/validator/acp/index.js +574 -0
- package/dist/validator/acp/index.js.map +1 -0
- package/dist/validator/network-validator.d.ts.map +1 -1
- package/dist/validator/network-validator.js +23 -13
- package/dist/validator/network-validator.js.map +1 -1
- package/dist/validator/rules-validator.d.ts +8 -0
- package/dist/validator/rules-validator.d.ts.map +1 -1
- package/dist/validator/rules-validator.js +159 -43
- package/dist/validator/rules-validator.js.map +1 -1
- package/dist/validator/structural-validator.d.ts.map +1 -1
- package/dist/validator/structural-validator.js +283 -53
- package/dist/validator/structural-validator.js.map +1 -1
- package/dist/validator/utils.d.ts +62 -0
- package/dist/validator/utils.d.ts.map +1 -0
- package/dist/validator/utils.js +151 -0
- package/dist/validator/utils.js.map +1 -0
- package/package.json +45 -12
- package/.claude/settings.local.json +0 -60
- package/.vercel/README.txt +0 -11
- package/.vercel/project.json +0 -1
- package/publish-output.txt +0 -0
- 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
|