geo-ai-search-optimization 2.0.0 → 2.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.
@@ -0,0 +1,269 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { writeScanOutput } from "./scan.js";
4
+ import { auditProject } from "./audit.js";
5
+ import { analyzeCrawlers } from "./crawlers.js";
6
+ import { validateLlmsTxt } from "./validate-llms.js";
7
+ import { fullPageAudit } from "./full-page-audit.js";
8
+
9
+ async function runSafe(label, fn) {
10
+ try {
11
+ return { ok: true, data: await fn() };
12
+ } catch (err) {
13
+ return { ok: false, error: err.message, label };
14
+ }
15
+ }
16
+
17
+ async function findSpecialFile(root, filename) {
18
+ const candidates = [
19
+ path.join(root, filename),
20
+ path.join(root, "public", filename),
21
+ path.join(root, "static", filename),
22
+ path.join(root, "dist", filename),
23
+ path.join(root, "out", filename)
24
+ ];
25
+
26
+ for (const candidate of candidates) {
27
+ try {
28
+ await fs.access(candidate);
29
+ return candidate;
30
+ } catch {
31
+ // continue
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export async function fullAudit(input, options = {}) {
39
+ const root = path.resolve(input);
40
+
41
+ // Phase 1: Run base project audit
42
+ const baseAudit = await auditProject(root, options);
43
+
44
+ // Phase 2: Run infrastructure analyses in parallel
45
+ const robotsPath = await findSpecialFile(root, "robots.txt");
46
+ const llmsPath = await findSpecialFile(root, "llms.txt");
47
+
48
+ const [crawlersResult, llmsResult] = await Promise.all([
49
+ robotsPath
50
+ ? runSafe("crawlers", () => analyzeCrawlers(robotsPath))
51
+ : { ok: true, data: { score: 0, crawlers: [], summary: "No robots.txt found in project.", recommendation: "" } },
52
+ llmsPath
53
+ ? runSafe("validate-llms", () => validateLlmsTxt(llmsPath))
54
+ : { ok: true, data: { score: 0, found: false, summary: "No llms.txt found in project." } }
55
+ ]);
56
+
57
+ // Phase 3: Sample page audits (if URLs or HTML files provided)
58
+ const sampleUrls = options.sampleUrls || [];
59
+ const samplePages = [];
60
+
61
+ if (sampleUrls.length > 0) {
62
+ const concurrency = options.concurrency || 2;
63
+ const chunks = [];
64
+ for (let i = 0; i < sampleUrls.length; i += concurrency) {
65
+ chunks.push(sampleUrls.slice(i, i + concurrency));
66
+ }
67
+
68
+ for (const chunk of chunks) {
69
+ const results = await Promise.all(
70
+ chunk.map((url) => runSafe(`page:${url}`, () => fullPageAudit(url, options)))
71
+ );
72
+ for (const result of results) {
73
+ if (result.ok) {
74
+ samplePages.push(result.data);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ // Phase 4: Compute enhanced score
81
+ const infraScores = {
82
+ base: baseAudit.score,
83
+ crawlers: crawlersResult.ok ? crawlersResult.data.score : 0,
84
+ llmsValidation: llmsResult.ok ? llmsResult.data.score : 0
85
+ };
86
+
87
+ // Composite: base audit (60%) + crawler health (20%) + llms.txt quality (20%)
88
+ const infraComposite = Math.round(
89
+ infraScores.base * 0.6 +
90
+ infraScores.crawlers * 0.2 +
91
+ infraScores.llmsValidation * 0.2
92
+ );
93
+
94
+ // Page-level composite (average of sampled pages)
95
+ const pageComposite = samplePages.length > 0
96
+ ? Math.round(samplePages.reduce((sum, p) => sum + p.compositeScore, 0) / samplePages.length)
97
+ : null;
98
+
99
+ // Overall composite
100
+ const overallComposite = pageComposite !== null
101
+ ? Math.round(infraComposite * 0.5 + pageComposite * 0.5)
102
+ : infraComposite;
103
+
104
+ const overallLabel = overallComposite >= 80 ? "Strong"
105
+ : overallComposite >= 60 ? "Moderate"
106
+ : overallComposite >= 40 ? "Weak"
107
+ : "Very weak";
108
+
109
+ // Merge all recommendations
110
+ const allRecs = [];
111
+ if (baseAudit.actionPlan) {
112
+ allRecs.push(...baseAudit.actionPlan.map((t) => ({
113
+ priority: t.priority,
114
+ source: "project-audit",
115
+ action: t.action,
116
+ owner: t.owner,
117
+ area: t.area
118
+ })));
119
+ }
120
+ if (crawlersResult.ok && crawlersResult.data.blocked?.length > 0) {
121
+ allRecs.push({
122
+ priority: "P1",
123
+ source: "crawlers",
124
+ action: `Unblock ${crawlersResult.data.blocked.length} AI crawler(s): ${crawlersResult.data.blocked.join(", ")}`,
125
+ owner: "Engineering",
126
+ area: "AI Crawler Access"
127
+ });
128
+ }
129
+ if (llmsResult.ok && llmsResult.data.issues?.length > 0) {
130
+ allRecs.push({
131
+ priority: "P1",
132
+ source: "llms-validation",
133
+ action: `Fix ${llmsResult.data.issues.length} llms.txt issue(s): ${llmsResult.data.issues.map((i) => i.message).join("; ")}`,
134
+ owner: "Engineering / SEO",
135
+ area: "llms.txt"
136
+ });
137
+ }
138
+
139
+ // Page-level top issues
140
+ for (const page of samplePages) {
141
+ for (const rec of (page.recommendations || []).slice(0, 3)) {
142
+ allRecs.push({
143
+ priority: "P2",
144
+ source: `page:${page.input}`,
145
+ action: rec.rec,
146
+ owner: "Content / SEO",
147
+ area: rec.source
148
+ });
149
+ }
150
+ }
151
+
152
+ const errors = [];
153
+ if (!crawlersResult.ok) errors.push({ component: "crawlers", error: crawlersResult.error });
154
+ if (!llmsResult.ok) errors.push({ component: "validate-llms", error: llmsResult.error });
155
+
156
+ return {
157
+ kind: "geo-full-audit",
158
+ root,
159
+ overallScore: overallComposite,
160
+ overallLabel,
161
+ infraScore: infraComposite,
162
+ pageScore: pageComposite,
163
+ scores: {
164
+ base: infraScores.base,
165
+ crawlers: infraScores.crawlers,
166
+ llmsValidation: infraScores.llmsValidation,
167
+ pageAverage: pageComposite
168
+ },
169
+ baseAudit,
170
+ crawlers: crawlersResult.ok ? crawlersResult.data : null,
171
+ llmsValidation: llmsResult.ok ? llmsResult.data : null,
172
+ samplePages,
173
+ actionPlan: allRecs.slice(0, 20),
174
+ errors,
175
+ summary: `Full GEO Audit: ${overallComposite}/100 (${overallLabel}). Infrastructure: ${infraComposite}/100. ${samplePages.length > 0 ? `Page quality: ${pageComposite}/100 (${samplePages.length} pages sampled).` : "No page samples."}`
176
+ };
177
+ }
178
+
179
+ export function renderFullAuditMarkdown(report) {
180
+ const lines = [
181
+ "# GEO Full Audit Report",
182
+ "",
183
+ `- Project: \`${report.root}\``,
184
+ `- **Overall GEO Score: \`${report.overallScore}/100\` (${report.overallLabel})**`,
185
+ `- Infrastructure Score: \`${report.infraScore}/100\``,
186
+ report.pageScore !== null ? `- Page Quality Score: \`${report.pageScore}/100\` (${report.samplePages.length} pages)` : "- Page Quality: Not sampled",
187
+ `- Summary: ${report.summary}`,
188
+ "",
189
+ "## Score Breakdown",
190
+ "",
191
+ "| Component | Score | Weight |",
192
+ "|-----------|-------|--------|",
193
+ `| 🏗️ Base Project Audit | ${report.scores.base}/100 | 60% of infra |`,
194
+ `| 🤖 AI Crawler Access | ${report.scores.crawlers}/100 | 20% of infra |`,
195
+ `| 📄 llms.txt Validation | ${report.scores.llmsValidation}/100 | 20% of infra |`
196
+ ];
197
+
198
+ if (report.pageScore !== null) {
199
+ lines.push(`| 📊 Page Quality Average | ${report.scores.pageAverage}/100 | 50% of overall |`);
200
+ }
201
+
202
+ // Crawler summary
203
+ if (report.crawlers) {
204
+ lines.push("", "## AI Crawler Access", "");
205
+ const blocked = report.crawlers.crawlers?.filter((c) => c.status === "blocked") || [];
206
+ const allowed = report.crawlers.crawlers?.filter((c) => c.status === "allowed") || [];
207
+ lines.push(`- Allowed: \`${allowed.length}\` | Blocked: \`${blocked.length}\` | Score: \`${report.crawlers.score}/100\``);
208
+ if (blocked.length > 0) {
209
+ lines.push(`- ⚠️ Blocked crawlers: ${blocked.map((c) => `**${c.crawler}** (${c.engine})`).join(", ")}`);
210
+ }
211
+ }
212
+
213
+ // llms.txt summary
214
+ if (report.llmsValidation) {
215
+ lines.push("", "## llms.txt Status", "");
216
+ if (report.llmsValidation.found) {
217
+ lines.push(`- Score: \`${report.llmsValidation.score}/100\` (${report.llmsValidation.scoreLabel})`);
218
+ if (report.llmsValidation.issues?.length > 0) {
219
+ lines.push(`- Issues: ${report.llmsValidation.issues.map((i) => i.message).join("; ")}`);
220
+ }
221
+ } else {
222
+ lines.push("- ❌ No llms.txt found. Run `geo-ai-search-optimization init-llms` to create one.");
223
+ }
224
+ }
225
+
226
+ // Base audit areas
227
+ if (report.baseAudit?.areas) {
228
+ lines.push("", "## Issue Areas (Project Level)", "");
229
+ for (const area of report.baseAudit.areas) {
230
+ const icon = area.severity === "高" ? "🔴" : area.severity === "中" ? "🟡" : "🟢";
231
+ lines.push(`- ${icon} **${area.title}** — ${area.status}`);
232
+ if (area.issues.length > 0) {
233
+ lines.push(` ${area.summary}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ // Sample page results
239
+ if (report.samplePages.length > 0) {
240
+ lines.push("", "## Sampled Page Results", "", "| Page | Composite | Base | Citability | E-E-A-T | Readability |", "|------|-----------|------|------------|---------|-------------|");
241
+ for (const page of report.samplePages) {
242
+ const d = page.dimensions;
243
+ const shortInput = page.input.length > 40 ? `...${page.input.slice(-37)}` : page.input;
244
+ lines.push(`| ${shortInput} | **${page.compositeScore}** | ${d.base?.score ?? "—"} | ${d.citability?.score ?? "—"} | ${d.eeat?.score ?? "—"} | ${d.readability?.score ?? "—"} |`);
245
+ }
246
+ }
247
+
248
+ // Unified action plan
249
+ if (report.actionPlan.length > 0) {
250
+ lines.push("", "## Unified Action Plan", "");
251
+ for (const task of report.actionPlan) {
252
+ lines.push(`- **${task.priority}** [${task.source}] ${task.action}`);
253
+ }
254
+ }
255
+
256
+ if (report.errors.length > 0) {
257
+ lines.push("", "## Errors", "");
258
+ for (const err of report.errors) {
259
+ lines.push(`- ⚠️ ${err.component}: ${err.error}`);
260
+ }
261
+ }
262
+
263
+ lines.push("");
264
+ return lines.join("\n");
265
+ }
266
+
267
+ export async function writeFullAuditOutput(outputPath, content) {
268
+ return writeScanOutput(outputPath, content);
269
+ }
@@ -0,0 +1,273 @@
1
+ import { writeScanOutput } from "./scan.js";
2
+ import { auditPage } from "./page-audit.js";
3
+ import { analyzeCitability } from "./citability.js";
4
+ import { analyzeEeat } from "./eeat.js";
5
+ import { analyzeReadability } from "./readability.js";
6
+ import { analyzeHeadingStructure } from "./heading-structure.js";
7
+ import { analyzeInternalLinks } from "./internal-links.js";
8
+ import { analyzeSocialMeta } from "./social-meta.js";
9
+ import { analyzePlatformReadiness } from "./platform-ready.js";
10
+ import { validateSchema } from "./validate-schema.js";
11
+ import { analyzeFreshness } from "./freshness.js";
12
+ import { analyzeSecurity } from "./security.js";
13
+ import { analyzeTopics } from "./topics.js";
14
+
15
+ const DIMENSION_WEIGHTS = {
16
+ base: 0.15,
17
+ citability: 0.12,
18
+ eeat: 0.12,
19
+ readability: 0.08,
20
+ headingStructure: 0.06,
21
+ socialMeta: 0.05,
22
+ internalLinks: 0.05,
23
+ platformReady: 0.08,
24
+ schema: 0.07,
25
+ freshness: 0.08,
26
+ security: 0.07,
27
+ topics: 0.07
28
+ };
29
+
30
+ async function runSafe(label, fn) {
31
+ try {
32
+ return { ok: true, data: await fn() };
33
+ } catch (err) {
34
+ return { ok: false, error: err.message, label };
35
+ }
36
+ }
37
+
38
+ export async function fullPageAudit(input, options = {}) {
39
+ // Run all analyses in parallel (12 dimensions)
40
+ const [
41
+ baseResult,
42
+ citabilityResult,
43
+ eeatResult,
44
+ readabilityResult,
45
+ headingResult,
46
+ linksResult,
47
+ socialResult,
48
+ platformResult,
49
+ schemaResult,
50
+ freshnessResult,
51
+ securityResult,
52
+ topicsResult
53
+ ] = await Promise.all([
54
+ runSafe("base", () => auditPage(input, options)),
55
+ runSafe("citability", () => analyzeCitability(input)),
56
+ runSafe("eeat", () => analyzeEeat(input)),
57
+ runSafe("readability", () => analyzeReadability(input)),
58
+ runSafe("heading-structure", () => analyzeHeadingStructure(input)),
59
+ runSafe("internal-links", () => analyzeInternalLinks(input, { baseUrl: options.baseUrl })),
60
+ runSafe("social-meta", () => analyzeSocialMeta(input)),
61
+ runSafe("platform-ready", () => analyzePlatformReadiness(input)),
62
+ runSafe("validate-schema", () => validateSchema(input)),
63
+ runSafe("freshness", () => analyzeFreshness(input)),
64
+ runSafe("security", () => analyzeSecurity(input)),
65
+ runSafe("topics", () => analyzeTopics(input))
66
+ ]);
67
+
68
+ const dimensions = {};
69
+ const errors = [];
70
+
71
+ function addDimension(key, result, scoreField) {
72
+ if (result.ok) {
73
+ dimensions[key] = {
74
+ score: result.data[scoreField || "score"],
75
+ label: result.data.scoreLabel || result.data[scoreField + "Label"] || "",
76
+ summary: result.data.summary || ""
77
+ };
78
+ } else {
79
+ errors.push({ dimension: key, error: result.error });
80
+ dimensions[key] = { score: 0, label: "Error", summary: result.error };
81
+ }
82
+ }
83
+
84
+ // Base audit score
85
+ if (baseResult.ok) {
86
+ dimensions.base = {
87
+ score: baseResult.data.score.score,
88
+ label: baseResult.data.scoreLabel,
89
+ summary: baseResult.data.summary
90
+ };
91
+ } else {
92
+ errors.push({ dimension: "base", error: baseResult.error });
93
+ dimensions.base = { score: 0, label: "Error", summary: baseResult.error };
94
+ }
95
+
96
+ addDimension("citability", citabilityResult);
97
+ addDimension("eeat", eeatResult);
98
+ addDimension("readability", readabilityResult);
99
+ addDimension("headingStructure", headingResult);
100
+ addDimension("internalLinks", linksResult);
101
+ addDimension("socialMeta", socialResult);
102
+ addDimension("platformReady", platformResult, "overallScore");
103
+ addDimension("schema", schemaResult);
104
+ addDimension("freshness", freshnessResult);
105
+ addDimension("security", securityResult);
106
+ addDimension("topics", topicsResult);
107
+
108
+ // Compute composite score
109
+ let compositeScore = 0;
110
+ for (const [key, weight] of Object.entries(DIMENSION_WEIGHTS)) {
111
+ const dimScore = dimensions[key]?.score || 0;
112
+ compositeScore += dimScore * weight;
113
+ }
114
+ compositeScore = Math.round(compositeScore);
115
+
116
+ const compositeLabel = compositeScore >= 80 ? "Strong"
117
+ : compositeScore >= 60 ? "Moderate"
118
+ : compositeScore >= 40 ? "Weak"
119
+ : "Very weak";
120
+
121
+ // Build top issues across all dimensions
122
+ const topIssues = [];
123
+ if (baseResult.ok && baseResult.data.problemAreas) {
124
+ for (const area of baseResult.data.problemAreas) {
125
+ topIssues.push(...area.issues.map((i) => ({ source: "base", severity: area.severity, issue: i })));
126
+ }
127
+ }
128
+ if (eeatResult.ok && eeatResult.data.recommendations) {
129
+ for (const rec of eeatResult.data.recommendations.slice(0, 3)) {
130
+ topIssues.push({ source: "eeat", severity: "中", issue: rec });
131
+ }
132
+ }
133
+ if (citabilityResult.ok && citabilityResult.data.recommendations) {
134
+ for (const rec of citabilityResult.data.recommendations.slice(0, 3)) {
135
+ topIssues.push({ source: "citability", severity: "中", issue: rec });
136
+ }
137
+ }
138
+ if (socialResult.ok && socialResult.data.recommendations) {
139
+ for (const rec of socialResult.data.recommendations.slice(0, 2)) {
140
+ topIssues.push({ source: "social-meta", severity: "中", issue: rec });
141
+ }
142
+ }
143
+
144
+ // Build consolidated recommendations
145
+ const allRecs = [];
146
+ if (baseResult.ok) allRecs.push(...baseResult.data.recommendedBlocks.map((r) => ({ source: "base", rec: r })));
147
+ if (citabilityResult.ok) allRecs.push(...citabilityResult.data.recommendations.map((r) => ({ source: "citability", rec: r })));
148
+ if (eeatResult.ok) allRecs.push(...eeatResult.data.recommendations.map((r) => ({ source: "eeat", rec: r })));
149
+ if (readabilityResult.ok) allRecs.push(...readabilityResult.data.recommendations.map((r) => ({ source: "readability", rec: r })));
150
+ if (headingResult.ok) allRecs.push(...headingResult.data.recommendations.map((r) => ({ source: "heading", rec: r })));
151
+ if (socialResult.ok) allRecs.push(...socialResult.data.recommendations.map((r) => ({ source: "social", rec: r })));
152
+ if (freshnessResult.ok) allRecs.push(...freshnessResult.data.recommendations.map((r) => ({ source: "freshness", rec: r })));
153
+ if (securityResult.ok) allRecs.push(...securityResult.data.recommendations.map((r) => ({ source: "security", rec: r })));
154
+ if (topicsResult.ok) allRecs.push(...topicsResult.data.recommendations.map((r) => ({ source: "topics", rec: r })));
155
+
156
+ // Prioritize: dedupe and cap
157
+ const seenRecs = new Set();
158
+ const prioritizedRecs = allRecs.filter((r) => {
159
+ const key = r.rec.slice(0, 50);
160
+ if (seenRecs.has(key)) return false;
161
+ seenRecs.add(key);
162
+ return true;
163
+ }).slice(0, 15);
164
+
165
+ // Build platform readiness summary
166
+ const platformSummary = platformResult.ok
167
+ ? platformResult.data.platforms.map((p) => ({ platform: p.platform, score: p.score, readiness: p.readiness }))
168
+ : [];
169
+
170
+ return {
171
+ kind: "geo-full-page-audit",
172
+ input,
173
+ source: baseResult.ok ? baseResult.data.reference : input,
174
+ compositeScore,
175
+ compositeLabel,
176
+ dimensions,
177
+ platformSummary,
178
+ topIssues: topIssues.slice(0, 12),
179
+ recommendations: prioritizedRecs,
180
+ errors,
181
+ // Embed full sub-reports for JSON consumers
182
+ details: {
183
+ base: baseResult.ok ? baseResult.data : null,
184
+ citability: citabilityResult.ok ? citabilityResult.data : null,
185
+ eeat: eeatResult.ok ? eeatResult.data : null,
186
+ readability: readabilityResult.ok ? readabilityResult.data : null,
187
+ headingStructure: headingResult.ok ? headingResult.data : null,
188
+ internalLinks: linksResult.ok ? linksResult.data : null,
189
+ socialMeta: socialResult.ok ? socialResult.data : null,
190
+ platformReady: platformResult.ok ? platformResult.data : null,
191
+ schema: schemaResult.ok ? schemaResult.data : null,
192
+ freshness: freshnessResult.ok ? freshnessResult.data : null,
193
+ security: securityResult.ok ? securityResult.data : null,
194
+ topics: topicsResult.ok ? topicsResult.data : null
195
+ },
196
+ summary: `Composite GEO Score: ${compositeScore}/100 (${compositeLabel}). ${Object.entries(dimensions).filter(([, d]) => d.score < 40).length} dimension(s) need attention.`
197
+ };
198
+ }
199
+
200
+ export function renderFullPageAuditMarkdown(report) {
201
+ const lines = [
202
+ "# GEO Full Page Audit",
203
+ "",
204
+ `- Input: \`${report.input}\``,
205
+ `- Source: \`${report.source}\``,
206
+ `- **Composite GEO Score: \`${report.compositeScore}/100\` (${report.compositeLabel})**`,
207
+ `- Summary: ${report.summary}`,
208
+ "",
209
+ "## Dimension Scores",
210
+ "",
211
+ "| Dimension | Score | Weight | Label |",
212
+ "|-----------|-------|--------|-------|"
213
+ ];
214
+
215
+ const dimLabels = {
216
+ base: "Base Audit",
217
+ citability: "Citability",
218
+ eeat: "E-E-A-T",
219
+ readability: "Readability",
220
+ headingStructure: "Heading Structure",
221
+ internalLinks: "Internal Links",
222
+ socialMeta: "Social Meta",
223
+ platformReady: "Platform Readiness",
224
+ schema: "Schema Validation",
225
+ freshness: "Content Freshness",
226
+ security: "Security",
227
+ topics: "Topic Coverage"
228
+ };
229
+
230
+ for (const [key, weight] of Object.entries(DIMENSION_WEIGHTS)) {
231
+ const dim = report.dimensions[key] || { score: 0, label: "—" };
232
+ const icon = dim.score >= 70 ? "🟢" : dim.score >= 40 ? "🟡" : "🔴";
233
+ lines.push(`| ${icon} ${dimLabels[key] || key} | ${dim.score}/100 | ${Math.round(weight * 100)}% | ${dim.label} |`);
234
+ }
235
+
236
+ if (report.platformSummary.length > 0) {
237
+ lines.push("", "## Platform Readiness", "");
238
+ for (const p of report.platformSummary) {
239
+ const icon = p.readiness === "Ready" ? "✅" : p.readiness === "Partial" ? "⚠️" : "❌";
240
+ lines.push(`- ${icon} **${p.platform}**: ${p.score}/100 (${p.readiness})`);
241
+ }
242
+ }
243
+
244
+ if (report.topIssues.length > 0) {
245
+ lines.push("", "## Top Issues", "");
246
+ for (const issue of report.topIssues) {
247
+ const severity = issue.severity === "高" ? "🔴" : "🟡";
248
+ lines.push(`- ${severity} [${issue.source}] ${issue.issue}`);
249
+ }
250
+ }
251
+
252
+ if (report.recommendations.length > 0) {
253
+ lines.push("", "## Prioritized Recommendations", "");
254
+ for (let i = 0; i < report.recommendations.length; i++) {
255
+ const rec = report.recommendations[i];
256
+ lines.push(`${i + 1}. [${rec.source}] ${rec.rec}`);
257
+ }
258
+ }
259
+
260
+ if (report.errors.length > 0) {
261
+ lines.push("", "## Errors", "");
262
+ for (const err of report.errors) {
263
+ lines.push(`- ⚠️ ${err.dimension}: ${err.error}`);
264
+ }
265
+ }
266
+
267
+ lines.push("");
268
+ return lines.join("\n");
269
+ }
270
+
271
+ export async function writeFullPageAuditOutput(outputPath, content) {
272
+ return writeScanOutput(outputPath, content);
273
+ }