@rankcli/agent-runtime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,299 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ runAuditWithFixes,
4
+ createAuditPR,
5
+ AuditRunnerOptions,
6
+ AuditRunnerResult,
7
+ } from './runner.js';
8
+ import type { AuditReport } from './types.js';
9
+
10
+ // Mock the audit engine
11
+ vi.mock('./engine.js', () => ({
12
+ runFullAudit: vi.fn(),
13
+ }));
14
+
15
+ // Mock the fix generators
16
+ vi.mock('./fixes/index.js', () => ({
17
+ generateAllFixes: vi.fn(),
18
+ }));
19
+
20
+ // Mock the PR helper
21
+ vi.mock('../git/pr-helper.js', () => ({
22
+ generatePRDescription: vi.fn(),
23
+ createPullRequest: vi.fn(),
24
+ }));
25
+
26
+ describe('audit-runner', () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ describe('runAuditWithFixes', () => {
32
+ it('runs audit and returns report with fixes', async () => {
33
+ const { runFullAudit } = await import('./engine.js');
34
+ const { generateAllFixes } = await import('./fixes/index.js');
35
+
36
+ const mockReport: AuditReport = {
37
+ url: 'https://example.com',
38
+ domain: 'example.com',
39
+ timestamp: '2024-01-01T00:00:00Z',
40
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
41
+ healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 70, social: 60, localSeo: 50 },
42
+ issues: [
43
+ { code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title tag', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://example.com'] },
44
+ { code: 'OG_IMAGE_MISSING', severity: 'warning', category: 'social', title: 'Missing OG image', description: 'No og:image', impact: 'Social shares will not display a preview image.', howToFix: 'Add og:image meta tag.', affectedUrls: ['https://example.com'] },
45
+ ],
46
+ pages: [],
47
+ summary: { errors: 1, warnings: 1, notices: 0, passed: 80 },
48
+ };
49
+
50
+ const mockFixes = [
51
+ { category: 'on-page', issues: ['TITLE_MISSING'], files: [{ path: 'index.html', content: '<title>Example</title>' }] },
52
+ { category: 'social', issues: ['OG_IMAGE_MISSING'], files: [{ path: 'index.html', content: '<meta property="og:image">' }] },
53
+ ];
54
+
55
+ vi.mocked(runFullAudit).mockResolvedValue(mockReport);
56
+ vi.mocked(generateAllFixes).mockReturnValue(mockFixes);
57
+
58
+ const result = await runAuditWithFixes({ url: 'https://example.com' });
59
+
60
+ expect(result.report).toEqual(mockReport);
61
+ expect(result.fixes).toEqual(mockFixes);
62
+ expect(result.score).toBe(65);
63
+ expect(runFullAudit).toHaveBeenCalledWith(expect.objectContaining({ url: 'https://example.com' }));
64
+ });
65
+
66
+ it('passes through audit options', async () => {
67
+ const { runFullAudit } = await import('./engine.js');
68
+ const { generateAllFixes } = await import('./fixes/index.js');
69
+
70
+ vi.mocked(runFullAudit).mockResolvedValue({
71
+ url: 'https://example.com',
72
+ domain: 'example.com',
73
+ timestamp: '2024-01-01T00:00:00Z',
74
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
75
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
76
+ issues: [],
77
+ pages: [],
78
+ summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
79
+ });
80
+ vi.mocked(generateAllFixes).mockReturnValue([]);
81
+
82
+ await runAuditWithFixes({
83
+ url: 'https://example.com',
84
+ checkBrokenLinks: true,
85
+ checkHreflangUrls: true,
86
+ });
87
+
88
+ expect(runFullAudit).toHaveBeenCalledWith(expect.objectContaining({
89
+ url: 'https://example.com',
90
+ checkBrokenLinks: true,
91
+ checkHreflangUrls: true,
92
+ }));
93
+ });
94
+
95
+ it('skips fix generation when disabled', async () => {
96
+ const { runFullAudit } = await import('./engine.js');
97
+ const { generateAllFixes } = await import('./fixes/index.js');
98
+
99
+ vi.mocked(runFullAudit).mockResolvedValue({
100
+ url: 'https://example.com',
101
+ domain: 'example.com',
102
+ timestamp: '2024-01-01T00:00:00Z',
103
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
104
+ healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
105
+ issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
106
+ pages: [],
107
+ summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
108
+ });
109
+
110
+ const result = await runAuditWithFixes({
111
+ url: 'https://example.com',
112
+ generateFixes: false,
113
+ });
114
+
115
+ expect(result.fixes).toEqual([]);
116
+ expect(generateAllFixes).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it('handles audit errors gracefully', async () => {
120
+ const { runFullAudit } = await import('./engine.js');
121
+
122
+ vi.mocked(runFullAudit).mockRejectedValue(new Error('Network error'));
123
+
124
+ await expect(runAuditWithFixes({ url: 'https://example.com' }))
125
+ .rejects.toThrow('Network error');
126
+ });
127
+ });
128
+
129
+ describe('createAuditPR', () => {
130
+ it('creates PR with audit results', async () => {
131
+ const { generatePRDescription, createPullRequest } = await import('../git/pr-helper.js');
132
+
133
+ const mockResult: AuditRunnerResult = {
134
+ report: {
135
+ url: 'https://example.com',
136
+ domain: 'example.com',
137
+ timestamp: '2024-01-01T00:00:00Z',
138
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
139
+ healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
140
+ issues: [],
141
+ pages: [],
142
+ summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
143
+ },
144
+ fixes: [
145
+ { category: 'on-page', issues: ['TITLE_MISSING'], files: [{ path: 'index.html', content: '' }] },
146
+ ],
147
+ score: 82,
148
+ previousScore: 65,
149
+ };
150
+
151
+ vi.mocked(generatePRDescription).mockReturnValue({
152
+ title: 'fix(meta): SEO improvements',
153
+ body: '## Summary',
154
+ labels: ['seo'],
155
+ });
156
+
157
+ vi.mocked(createPullRequest).mockResolvedValue({
158
+ success: true,
159
+ url: 'https://github.com/owner/repo/pull/123',
160
+ number: 123,
161
+ });
162
+
163
+ const result = await createAuditPR(mockResult, {
164
+ baseBranch: 'main',
165
+ headBranch: 'seo-fixes',
166
+ });
167
+
168
+ expect(result.success).toBe(true);
169
+ expect(result.prUrl).toBe('https://github.com/owner/repo/pull/123');
170
+ expect(generatePRDescription).toHaveBeenCalledWith(
171
+ expect.arrayContaining([expect.objectContaining({ category: 'on-page' })]),
172
+ expect.objectContaining({ scoreBefore: 65, scoreAfter: 82 })
173
+ );
174
+ });
175
+
176
+ it('returns error when no fixes available', async () => {
177
+ const mockResult: AuditRunnerResult = {
178
+ report: {
179
+ url: 'https://example.com',
180
+ domain: 'example.com',
181
+ timestamp: '2024-01-01T00:00:00Z',
182
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
183
+ healthScore: { overall: 95, crawlability: 95, indexability: 95, onPage: 95, content: 95, links: 95, performance: 95, security: 95, aiReadiness: 95, social: 95, localSeo: 95 },
184
+ issues: [],
185
+ pages: [],
186
+ summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
187
+ },
188
+ fixes: [],
189
+ score: 95,
190
+ };
191
+
192
+ const result = await createAuditPR(mockResult, {
193
+ baseBranch: 'main',
194
+ headBranch: 'seo-fixes',
195
+ });
196
+
197
+ expect(result.success).toBe(false);
198
+ expect(result.error).toContain('No fixes');
199
+ });
200
+
201
+ it('supports dry run mode', async () => {
202
+ const { createPullRequest } = await import('../git/pr-helper.js');
203
+
204
+ const mockResult: AuditRunnerResult = {
205
+ report: {
206
+ url: 'https://example.com',
207
+ domain: 'example.com',
208
+ timestamp: '2024-01-01T00:00:00Z',
209
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
210
+ healthScore: { overall: 82, crawlability: 80, indexability: 80, onPage: 85, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
211
+ issues: [],
212
+ pages: [],
213
+ summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
214
+ },
215
+ fixes: [
216
+ { category: 'on-page', issues: ['TITLE_MISSING'], files: [{ path: 'index.html', content: '' }] },
217
+ ],
218
+ score: 82,
219
+ };
220
+
221
+ vi.mocked(createPullRequest).mockResolvedValue({
222
+ success: true,
223
+ dryRun: true,
224
+ });
225
+
226
+ const result = await createAuditPR(mockResult, {
227
+ baseBranch: 'main',
228
+ headBranch: 'seo-fixes',
229
+ dryRun: true,
230
+ });
231
+
232
+ expect(result.success).toBe(true);
233
+ expect(result.dryRun).toBe(true);
234
+ });
235
+ });
236
+
237
+ describe('integration scenarios', () => {
238
+ it('full audit to PR workflow', async () => {
239
+ const { runFullAudit } = await import('./engine.js');
240
+ const { generateAllFixes } = await import('./fixes/index.js');
241
+ const { generatePRDescription, createPullRequest } = await import('../git/pr-helper.js');
242
+
243
+ // Mock audit result
244
+ vi.mocked(runFullAudit).mockResolvedValue({
245
+ url: 'https://myapp.com',
246
+ domain: 'myapp.com',
247
+ timestamp: '2024-01-01T00:00:00Z',
248
+ crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
249
+ healthScore: { overall: 45, crawlability: 60, indexability: 50, onPage: 30, content: 40, links: 50, performance: 60, security: 70, aiReadiness: 50, social: 40, localSeo: 45 },
250
+ issues: [
251
+ { code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://myapp.com'] },
252
+ { code: 'META_DESC_MISSING', severity: 'error', category: 'on-page', title: 'Missing description', description: 'No meta desc', impact: 'Search results will show auto-generated snippets.', howToFix: 'Add a meta description.', affectedUrls: ['https://myapp.com'] },
253
+ { code: 'OG_TITLE_MISSING', severity: 'warning', category: 'social', title: 'Missing OG title', description: 'No og:title', impact: 'Social shares will not display properly.', howToFix: 'Add og:title meta tag.', affectedUrls: ['https://myapp.com'] },
254
+ { code: 'SCHEMA_MISSING', severity: 'warning', category: 'structured-data', title: 'No schema', description: 'Missing JSON-LD', impact: 'Missing rich snippet opportunities.', howToFix: 'Add structured data markup.', affectedUrls: ['https://myapp.com'] },
255
+ ],
256
+ pages: [],
257
+ summary: { errors: 2, warnings: 2, notices: 0, passed: 81 },
258
+ });
259
+
260
+ // Mock fix generation
261
+ vi.mocked(generateAllFixes).mockReturnValue([
262
+ { category: 'on-page', issues: ['TITLE_MISSING', 'META_DESC_MISSING'], files: [{ path: 'index.html', content: '<title>...</title>' }] },
263
+ { category: 'social', issues: ['OG_TITLE_MISSING'], files: [{ path: 'index.html', content: '<meta property="og:title">' }] },
264
+ { category: 'structured-data', issues: ['SCHEMA_MISSING'], files: [{ path: 'index.html', content: '<script type="application/ld+json">' }] },
265
+ ]);
266
+
267
+ // Mock PR creation
268
+ vi.mocked(generatePRDescription).mockReturnValue({
269
+ title: 'fix(seo): SEO improvements - 4 issues fixed',
270
+ body: '## Summary\nFixes 4 SEO issues...',
271
+ labels: ['seo', 'automated'],
272
+ });
273
+
274
+ vi.mocked(createPullRequest).mockResolvedValue({
275
+ success: true,
276
+ url: 'https://github.com/owner/myapp/pull/47',
277
+ number: 47,
278
+ });
279
+
280
+ // Run audit
281
+ const auditResult = await runAuditWithFixes({
282
+ url: 'https://myapp.com',
283
+ previousScore: 45,
284
+ });
285
+
286
+ expect(auditResult.score).toBe(45);
287
+ expect(auditResult.fixes).toHaveLength(3);
288
+
289
+ // Create PR
290
+ const prResult = await createAuditPR(auditResult, {
291
+ baseBranch: 'main',
292
+ headBranch: 'seo-fixes-20240101',
293
+ });
294
+
295
+ expect(prResult.success).toBe(true);
296
+ expect(prResult.prUrl).toContain('/pull/47');
297
+ });
298
+ });
299
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Audit Runner
3
+ *
4
+ * High-level API for running SEO audits with automatic fix generation
5
+ * and PR creation. Orchestrates the audit engine, fix generators, and
6
+ * git helpers.
7
+ */
8
+
9
+ import { runFullAudit, AuditOptions } from './engine.js';
10
+ import { generateAllFixes, FixResult } from './fixes/index.js';
11
+ import {
12
+ generatePRDescription,
13
+ createPullRequest,
14
+ SEOFixSummary,
15
+ PRResult,
16
+ } from '../git/pr-helper.js';
17
+ import type { AuditReport } from './types.js';
18
+
19
+ export interface AuditRunnerOptions extends Partial<AuditOptions> {
20
+ url: string;
21
+ generateFixes?: boolean;
22
+ previousScore?: number;
23
+ }
24
+
25
+ export interface AuditRunnerResult {
26
+ report: AuditReport;
27
+ fixes: FixResult[];
28
+ score: number;
29
+ previousScore?: number;
30
+ }
31
+
32
+ export interface CreateAuditPROptions {
33
+ baseBranch: string;
34
+ headBranch: string;
35
+ dryRun?: boolean;
36
+ reviewers?: string[];
37
+ assignees?: string[];
38
+ draft?: boolean;
39
+ }
40
+
41
+ export interface AuditPRResult {
42
+ success: boolean;
43
+ prUrl?: string;
44
+ prNumber?: number;
45
+ error?: string;
46
+ dryRun?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Runs a full SEO audit and generates fixes for identified issues
51
+ */
52
+ export async function runAuditWithFixes(
53
+ options: AuditRunnerOptions
54
+ ): Promise<AuditRunnerResult> {
55
+ const { url, generateFixes = true, previousScore, ...auditOptions } = options;
56
+
57
+ // Run the audit
58
+ const report = await runFullAudit({ url, ...auditOptions });
59
+
60
+ // Generate fixes if enabled
61
+ let fixes: FixResult[] = [];
62
+ if (generateFixes && report.issues.length > 0) {
63
+ fixes = generateAllFixes(report);
64
+ }
65
+
66
+ return {
67
+ report,
68
+ fixes,
69
+ score: report.healthScore.overall,
70
+ previousScore,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Creates a pull request with audit fixes
76
+ */
77
+ export async function createAuditPR(
78
+ result: AuditRunnerResult,
79
+ options: CreateAuditPROptions
80
+ ): Promise<AuditPRResult> {
81
+ // Check if there are any fixes
82
+ if (result.fixes.length === 0) {
83
+ return {
84
+ success: false,
85
+ error: 'No fixes available to create PR',
86
+ };
87
+ }
88
+
89
+ // Convert fixes to SEOFixSummary format for PR description
90
+ const fixSummaries: SEOFixSummary[] = result.fixes.map((fix) => ({
91
+ category: fix.category,
92
+ issues: fix.issues,
93
+ filesCount: fix.files.length,
94
+ }));
95
+
96
+ // Generate PR description
97
+ const prDescription = generatePRDescription(fixSummaries, {
98
+ scoreBefore: result.previousScore,
99
+ scoreAfter: result.score,
100
+ reviewers: options.reviewers,
101
+ assignees: options.assignees,
102
+ });
103
+
104
+ // Create the PR
105
+ const prResult = await createPullRequest({
106
+ title: prDescription.title,
107
+ body: prDescription.body,
108
+ baseBranch: options.baseBranch,
109
+ headBranch: options.headBranch,
110
+ labels: prDescription.labels,
111
+ reviewers: prDescription.reviewers,
112
+ assignees: prDescription.assignees,
113
+ draft: options.draft,
114
+ dryRun: options.dryRun,
115
+ });
116
+
117
+ if (!prResult.success) {
118
+ return {
119
+ success: false,
120
+ error: prResult.error,
121
+ };
122
+ }
123
+
124
+ return {
125
+ success: true,
126
+ prUrl: prResult.url,
127
+ prNumber: prResult.number,
128
+ dryRun: prResult.dryRun,
129
+ };
130
+ }