@ucptools/validator 1.0.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 (121) hide show
  1. package/CLAUDE.md +109 -0
  2. package/CONTRIBUTING.md +113 -0
  3. package/LICENSE +21 -0
  4. package/README.md +203 -0
  5. package/api/analyze-feed.js +140 -0
  6. package/api/badge.js +185 -0
  7. package/api/benchmark.js +177 -0
  8. package/api/directory-stats.ts +29 -0
  9. package/api/directory.ts +73 -0
  10. package/api/generate-compliance.js +143 -0
  11. package/api/generate-schema.js +457 -0
  12. package/api/generate.js +132 -0
  13. package/api/security-scan.js +133 -0
  14. package/api/simulate.js +187 -0
  15. package/api/tsconfig.json +10 -0
  16. package/api/validate.js +1351 -0
  17. package/apify-actor/.actor/actor.json +68 -0
  18. package/apify-actor/.actor/input_schema.json +32 -0
  19. package/apify-actor/APIFY-STORE-LISTING.md +412 -0
  20. package/apify-actor/Dockerfile +8 -0
  21. package/apify-actor/README.md +166 -0
  22. package/apify-actor/main.ts +111 -0
  23. package/apify-actor/package.json +17 -0
  24. package/apify-actor/src/main.js +199 -0
  25. package/docs/BRAND-IDENTITY.md +238 -0
  26. package/docs/BRAND-STYLE-GUIDE.md +356 -0
  27. package/drizzle/0000_black_king_cobra.sql +39 -0
  28. package/drizzle/meta/0000_snapshot.json +309 -0
  29. package/drizzle/meta/_journal.json +13 -0
  30. package/drizzle.config.ts +10 -0
  31. package/examples/full-profile.json +70 -0
  32. package/examples/minimal-profile.json +23 -0
  33. package/package.json +69 -0
  34. package/public/.well-known/ucp +25 -0
  35. package/public/android-chrome-192x192.png +0 -0
  36. package/public/android-chrome-512x512.png +0 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/brand.css +321 -0
  39. package/public/directory.html +701 -0
  40. package/public/favicon-16x16.png +0 -0
  41. package/public/favicon-32x32.png +0 -0
  42. package/public/favicon.ico +0 -0
  43. package/public/guides/bigcommerce.html +743 -0
  44. package/public/guides/fastucp.html +838 -0
  45. package/public/guides/magento.html +779 -0
  46. package/public/guides/shopify.html +726 -0
  47. package/public/guides/squarespace.html +749 -0
  48. package/public/guides/wix.html +747 -0
  49. package/public/guides/woocommerce.html +733 -0
  50. package/public/index.html +3835 -0
  51. package/public/learn.html +396 -0
  52. package/public/logo.jpeg +0 -0
  53. package/public/og-image-icon.png +0 -0
  54. package/public/og-image.png +0 -0
  55. package/public/robots.txt +6 -0
  56. package/public/site.webmanifest +31 -0
  57. package/public/sitemap.xml +69 -0
  58. package/public/social/linkedin-banner-1128x191.png +0 -0
  59. package/public/social/temp.PNG +0 -0
  60. package/public/social/x-header-1500x500.png +0 -0
  61. package/public/verify.html +410 -0
  62. package/scripts/generate-favicons.js +44 -0
  63. package/scripts/generate-ico.js +23 -0
  64. package/scripts/generate-og-image.js +45 -0
  65. package/scripts/reset-db.ts +77 -0
  66. package/scripts/seed-db.ts +71 -0
  67. package/scripts/setup-benchmark-db.js +70 -0
  68. package/src/api/server.ts +266 -0
  69. package/src/cli/index.ts +302 -0
  70. package/src/compliance/compliance-generator.ts +452 -0
  71. package/src/compliance/index.ts +28 -0
  72. package/src/compliance/templates.ts +338 -0
  73. package/src/compliance/types.ts +170 -0
  74. package/src/db/index.ts +28 -0
  75. package/src/db/schema.ts +84 -0
  76. package/src/feed-analyzer/feed-analyzer.ts +726 -0
  77. package/src/feed-analyzer/index.ts +34 -0
  78. package/src/feed-analyzer/types.ts +354 -0
  79. package/src/generator/index.ts +7 -0
  80. package/src/generator/key-generator.ts +124 -0
  81. package/src/generator/profile-builder.ts +402 -0
  82. package/src/hosting/artifacts-generator.ts +679 -0
  83. package/src/hosting/index.ts +6 -0
  84. package/src/index.ts +105 -0
  85. package/src/security/index.ts +15 -0
  86. package/src/security/security-scanner.ts +604 -0
  87. package/src/security/types.ts +55 -0
  88. package/src/services/directory.ts +434 -0
  89. package/src/simulator/agent-simulator.ts +941 -0
  90. package/src/simulator/index.ts +7 -0
  91. package/src/simulator/types.ts +170 -0
  92. package/src/types/generator.ts +140 -0
  93. package/src/types/index.ts +7 -0
  94. package/src/types/ucp-profile.ts +140 -0
  95. package/src/types/validation.ts +89 -0
  96. package/src/validator/index.ts +194 -0
  97. package/src/validator/network-validator.ts +417 -0
  98. package/src/validator/rules-validator.ts +297 -0
  99. package/src/validator/sdk-validator.ts +330 -0
  100. package/src/validator/structural-validator.ts +476 -0
  101. package/tests/fixtures/non-compliant-profile.json +25 -0
  102. package/tests/fixtures/official-sample-profile.json +75 -0
  103. package/tests/integration/benchmark.test.ts +207 -0
  104. package/tests/integration/database.test.ts +163 -0
  105. package/tests/integration/directory-api.test.ts +268 -0
  106. package/tests/integration/simulate-api.test.ts +230 -0
  107. package/tests/integration/validate-api.test.ts +269 -0
  108. package/tests/setup.ts +15 -0
  109. package/tests/unit/agent-simulator.test.ts +575 -0
  110. package/tests/unit/compliance-generator.test.ts +374 -0
  111. package/tests/unit/directory-service.test.ts +272 -0
  112. package/tests/unit/feed-analyzer.test.ts +517 -0
  113. package/tests/unit/lint-suggestions.test.ts +423 -0
  114. package/tests/unit/official-samples.test.ts +211 -0
  115. package/tests/unit/pdf-report.test.ts +390 -0
  116. package/tests/unit/sdk-validator.test.ts +531 -0
  117. package/tests/unit/security-scanner.test.ts +410 -0
  118. package/tests/unit/validation.test.ts +390 -0
  119. package/tsconfig.json +20 -0
  120. package/vercel.json +34 -0
  121. package/vitest.config.ts +22 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Unit Tests for PDF Report Generation (Issue #7)
3
+ * Tests the data transformation and formatting logic for PDF reports
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+
8
+ // Types matching the validation API response
9
+ interface ValidationData {
10
+ domain?: string;
11
+ ucp?: {
12
+ found: boolean;
13
+ issues: Array<{ code: string; message: string; severity: 'error' | 'warn' }>;
14
+ };
15
+ schema?: {
16
+ found: boolean;
17
+ issues: Array<{ code: string; message: string; severity: 'error' | 'warn'; category?: string }>;
18
+ stats: {
19
+ products: number;
20
+ returnPolicies: number;
21
+ organizations?: number;
22
+ };
23
+ };
24
+ ai_readiness?: {
25
+ score: number;
26
+ grade: string;
27
+ label: string;
28
+ };
29
+ benchmark?: {
30
+ percentile: number;
31
+ total_sites_analyzed: number;
32
+ avg_score?: number;
33
+ };
34
+ product_quality?: {
35
+ completeness: number;
36
+ };
37
+ lint_suggestions?: Array<{
38
+ severity: string;
39
+ title: string;
40
+ code: string;
41
+ impact: string;
42
+ fix?: string;
43
+ codeSnippet?: string;
44
+ docLink?: string;
45
+ }>;
46
+ }
47
+
48
+ // Helper functions that mirror the PDF generation logic
49
+ function getGradeColor(grade: string): [number, number, number] {
50
+ const gradeColors: Record<string, [number, number, number]> = {
51
+ 'A': [22, 163, 74], // Green
52
+ 'B': [37, 99, 235], // Blue
53
+ 'C': [202, 138, 4], // Yellow
54
+ 'D': [234, 88, 12], // Orange
55
+ 'F': [220, 38, 38] // Red
56
+ };
57
+ return gradeColors[grade] || gradeColors['F'];
58
+ }
59
+
60
+ function countIssuesBySeverity(issues: Array<{ severity: string }>): { errors: number; warnings: number } {
61
+ return {
62
+ errors: issues.filter(i => i.severity === 'error').length,
63
+ warnings: issues.filter(i => i.severity === 'warn').length
64
+ };
65
+ }
66
+
67
+ function truncateText(text: string, maxLength: number): string {
68
+ if (text.length <= maxLength) return text;
69
+ return text.substring(0, maxLength) + '...';
70
+ }
71
+
72
+ function generateFilename(domain: string): string {
73
+ return `ucp-report-${domain.replace(/\./g, '-')}.pdf`;
74
+ }
75
+
76
+ function extractReportData(data: ValidationData, domain: string) {
77
+ return {
78
+ domain,
79
+ score: data.ai_readiness?.score ?? 0,
80
+ grade: data.ai_readiness?.grade ?? 'F',
81
+ readinessLabel: data.ai_readiness?.label ?? 'Not Ready',
82
+ ucpFound: data.ucp?.found ?? false,
83
+ ucpIssues: data.ucp?.issues ?? [],
84
+ schemaStats: data.schema?.stats ?? { products: 0, returnPolicies: 0 },
85
+ schemaIssues: data.schema?.issues ?? [],
86
+ productQuality: data.product_quality?.completeness ?? null,
87
+ benchmark: data.benchmark ?? null,
88
+ suggestions: data.lint_suggestions ?? [],
89
+ };
90
+ }
91
+
92
+ describe('PDF Report Generation (Issue #7)', () => {
93
+ describe('Grade Color Mapping', () => {
94
+ it('should return green for grade A', () => {
95
+ const color = getGradeColor('A');
96
+ expect(color).toEqual([22, 163, 74]);
97
+ });
98
+
99
+ it('should return blue for grade B', () => {
100
+ const color = getGradeColor('B');
101
+ expect(color).toEqual([37, 99, 235]);
102
+ });
103
+
104
+ it('should return yellow for grade C', () => {
105
+ const color = getGradeColor('C');
106
+ expect(color).toEqual([202, 138, 4]);
107
+ });
108
+
109
+ it('should return orange for grade D', () => {
110
+ const color = getGradeColor('D');
111
+ expect(color).toEqual([234, 88, 12]);
112
+ });
113
+
114
+ it('should return red for grade F', () => {
115
+ const color = getGradeColor('F');
116
+ expect(color).toEqual([220, 38, 38]);
117
+ });
118
+
119
+ it('should default to red for unknown grade', () => {
120
+ const color = getGradeColor('X');
121
+ expect(color).toEqual([220, 38, 38]);
122
+ });
123
+ });
124
+
125
+ describe('Issue Counting', () => {
126
+ it('should correctly count errors and warnings', () => {
127
+ const issues = [
128
+ { severity: 'error', code: 'ERR1', message: 'Error 1' },
129
+ { severity: 'error', code: 'ERR2', message: 'Error 2' },
130
+ { severity: 'warn', code: 'WARN1', message: 'Warning 1' },
131
+ ];
132
+ const counts = countIssuesBySeverity(issues);
133
+
134
+ expect(counts.errors).toBe(2);
135
+ expect(counts.warnings).toBe(1);
136
+ });
137
+
138
+ it('should return zeros for empty array', () => {
139
+ const counts = countIssuesBySeverity([]);
140
+
141
+ expect(counts.errors).toBe(0);
142
+ expect(counts.warnings).toBe(0);
143
+ });
144
+
145
+ it('should handle only errors', () => {
146
+ const issues = [
147
+ { severity: 'error', code: 'ERR1', message: 'Error 1' },
148
+ { severity: 'error', code: 'ERR2', message: 'Error 2' },
149
+ ];
150
+ const counts = countIssuesBySeverity(issues);
151
+
152
+ expect(counts.errors).toBe(2);
153
+ expect(counts.warnings).toBe(0);
154
+ });
155
+
156
+ it('should handle only warnings', () => {
157
+ const issues = [
158
+ { severity: 'warn', code: 'WARN1', message: 'Warning 1' },
159
+ { severity: 'warn', code: 'WARN2', message: 'Warning 2' },
160
+ ];
161
+ const counts = countIssuesBySeverity(issues);
162
+
163
+ expect(counts.errors).toBe(0);
164
+ expect(counts.warnings).toBe(2);
165
+ });
166
+ });
167
+
168
+ describe('Text Truncation', () => {
169
+ it('should not truncate short text', () => {
170
+ const result = truncateText('Short text', 70);
171
+ expect(result).toBe('Short text');
172
+ });
173
+
174
+ it('should truncate long text with ellipsis', () => {
175
+ const longText = 'This is a very long message that should be truncated because it exceeds the maximum allowed length for display';
176
+ const result = truncateText(longText, 70);
177
+
178
+ expect(result.length).toBe(73); // 70 + '...'
179
+ expect(result.endsWith('...')).toBe(true);
180
+ });
181
+
182
+ it('should handle exact length text', () => {
183
+ const text = 'A'.repeat(70);
184
+ const result = truncateText(text, 70);
185
+
186
+ expect(result.length).toBe(70);
187
+ expect(result.endsWith('...')).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe('Filename Generation', () => {
192
+ it('should replace dots with dashes in domain', () => {
193
+ const filename = generateFilename('example.com');
194
+ expect(filename).toBe('ucp-report-example-com.pdf');
195
+ });
196
+
197
+ it('should handle subdomains', () => {
198
+ const filename = generateFilename('shop.example.com');
199
+ expect(filename).toBe('ucp-report-shop-example-com.pdf');
200
+ });
201
+
202
+ it('should handle multiple dots', () => {
203
+ const filename = generateFilename('www.shop.example.co.uk');
204
+ expect(filename).toBe('ucp-report-www-shop-example-co-uk.pdf');
205
+ });
206
+ });
207
+
208
+ describe('Report Data Extraction', () => {
209
+ it('should extract all fields from complete data', () => {
210
+ const data: ValidationData = {
211
+ ai_readiness: { score: 85, grade: 'B', label: 'Partially Ready' },
212
+ ucp: { found: true, issues: [{ code: 'TEST', message: 'Test', severity: 'error' }] },
213
+ schema: {
214
+ found: true,
215
+ issues: [{ code: 'SCHEMA_TEST', message: 'Schema test', severity: 'warn' }],
216
+ stats: { products: 10, returnPolicies: 2 }
217
+ },
218
+ product_quality: { completeness: 78 },
219
+ benchmark: { percentile: 75, total_sites_analyzed: 1000 },
220
+ lint_suggestions: [{ severity: 'critical', title: 'Test', code: 'TEST', impact: 'Test impact' }]
221
+ };
222
+
223
+ const result = extractReportData(data, 'example.com');
224
+
225
+ expect(result.domain).toBe('example.com');
226
+ expect(result.score).toBe(85);
227
+ expect(result.grade).toBe('B');
228
+ expect(result.readinessLabel).toBe('Partially Ready');
229
+ expect(result.ucpFound).toBe(true);
230
+ expect(result.ucpIssues).toHaveLength(1);
231
+ expect(result.schemaStats.products).toBe(10);
232
+ expect(result.schemaStats.returnPolicies).toBe(2);
233
+ expect(result.schemaIssues).toHaveLength(1);
234
+ expect(result.productQuality).toBe(78);
235
+ expect(result.benchmark?.percentile).toBe(75);
236
+ expect(result.suggestions).toHaveLength(1);
237
+ });
238
+
239
+ it('should provide defaults for missing data', () => {
240
+ const data: ValidationData = {};
241
+
242
+ const result = extractReportData(data, 'test.com');
243
+
244
+ expect(result.domain).toBe('test.com');
245
+ expect(result.score).toBe(0);
246
+ expect(result.grade).toBe('F');
247
+ expect(result.readinessLabel).toBe('Not Ready');
248
+ expect(result.ucpFound).toBe(false);
249
+ expect(result.ucpIssues).toEqual([]);
250
+ expect(result.schemaStats).toEqual({ products: 0, returnPolicies: 0 });
251
+ expect(result.schemaIssues).toEqual([]);
252
+ expect(result.productQuality).toBeNull();
253
+ expect(result.benchmark).toBeNull();
254
+ expect(result.suggestions).toEqual([]);
255
+ });
256
+
257
+ it('should handle partial data', () => {
258
+ const data: ValidationData = {
259
+ ai_readiness: { score: 45, grade: 'F', label: 'Not Ready' },
260
+ ucp: { found: false, issues: [] },
261
+ };
262
+
263
+ const result = extractReportData(data, 'partial.com');
264
+
265
+ expect(result.score).toBe(45);
266
+ expect(result.ucpFound).toBe(false);
267
+ expect(result.schemaStats).toEqual({ products: 0, returnPolicies: 0 });
268
+ });
269
+ });
270
+
271
+ describe('PDF Content Sections', () => {
272
+ it('should limit displayed issues to prevent overflow', () => {
273
+ const issues = Array.from({ length: 20 }, (_, i) => ({
274
+ code: `ERR${i}`,
275
+ message: `Error message ${i}`,
276
+ severity: 'error' as const
277
+ }));
278
+
279
+ // PDF logic limits to 8 issues
280
+ const displayedIssues = issues.slice(0, 8);
281
+ expect(displayedIssues).toHaveLength(8);
282
+ });
283
+
284
+ it('should limit displayed suggestions to prevent overflow', () => {
285
+ const suggestions = Array.from({ length: 10 }, (_, i) => ({
286
+ severity: 'warning',
287
+ title: `Suggestion ${i}`,
288
+ code: `SUGG${i}`,
289
+ impact: `Impact ${i}`
290
+ }));
291
+
292
+ // PDF logic limits to 5 suggestions
293
+ const displayedSuggestions = suggestions.slice(0, 5);
294
+ expect(displayedSuggestions).toHaveLength(5);
295
+ });
296
+ });
297
+
298
+ describe('Validation Data Requirements', () => {
299
+ it('should validate that data is available before generating', () => {
300
+ const data = null;
301
+ const domain = null;
302
+
303
+ const isValid = data !== null && domain !== null;
304
+ expect(isValid).toBe(false);
305
+ });
306
+
307
+ it('should accept valid data for PDF generation', () => {
308
+ const data: ValidationData = {
309
+ ai_readiness: { score: 70, grade: 'C', label: 'Partially Ready' }
310
+ };
311
+ const domain = 'test.com';
312
+
313
+ const isValid = data !== null && domain !== null;
314
+ expect(isValid).toBe(true);
315
+ });
316
+ });
317
+
318
+ describe('Brand Colors', () => {
319
+ it('should use correct brand blue', () => {
320
+ const brandBlue: [number, number, number] = [46, 134, 171];
321
+ expect(brandBlue).toEqual([46, 134, 171]);
322
+ });
323
+
324
+ it('should use correct brand teal', () => {
325
+ const brandTeal: [number, number, number] = [54, 181, 162];
326
+ expect(brandTeal).toEqual([54, 181, 162]);
327
+ });
328
+
329
+ it('should use correct severity colors', () => {
330
+ const severityColors = {
331
+ error: [220, 38, 38],
332
+ warn: [202, 138, 4]
333
+ };
334
+
335
+ expect(severityColors.error).toEqual([220, 38, 38]);
336
+ expect(severityColors.warn).toEqual([202, 138, 4]);
337
+ });
338
+ });
339
+ });
340
+
341
+ describe('PDF Report Integration Requirements', () => {
342
+ describe('Data Storage', () => {
343
+ it('should store validation data in expected format', () => {
344
+ // Simulates window.lastValidationData structure
345
+ const storedData: ValidationData = {
346
+ ai_readiness: { score: 85, grade: 'B', label: 'Partially Ready' },
347
+ ucp: { found: true, issues: [] },
348
+ schema: { found: true, issues: [], stats: { products: 5, returnPolicies: 1 } },
349
+ lint_suggestions: []
350
+ };
351
+
352
+ expect(storedData.ai_readiness).toBeDefined();
353
+ expect(storedData.ucp).toBeDefined();
354
+ expect(storedData.schema).toBeDefined();
355
+ expect(storedData.lint_suggestions).toBeDefined();
356
+ });
357
+
358
+ it('should store domain for filename generation', () => {
359
+ // Simulates window.lastValidatedDomain
360
+ const storedDomain = 'example.com';
361
+
362
+ expect(storedDomain).toBeDefined();
363
+ expect(typeof storedDomain).toBe('string');
364
+ expect(storedDomain.length).toBeGreaterThan(0);
365
+ });
366
+ });
367
+
368
+ describe('Download Trigger', () => {
369
+ it('should generate correct filename from domain', () => {
370
+ const domain = 'shop.example.com';
371
+ const filename = `ucp-report-${domain.replace(/\./g, '-')}.pdf`;
372
+
373
+ expect(filename).toBe('ucp-report-shop-example-com.pdf');
374
+ });
375
+ });
376
+
377
+ describe('Analytics Event', () => {
378
+ it('should include required tracking fields', () => {
379
+ const eventData = {
380
+ domain: 'example.com',
381
+ score: 85,
382
+ grade: 'B'
383
+ };
384
+
385
+ expect(eventData).toHaveProperty('domain');
386
+ expect(eventData).toHaveProperty('score');
387
+ expect(eventData).toHaveProperty('grade');
388
+ });
389
+ });
390
+ });