Externalize audit pipeline services
This commit is contained in:
@@ -60,6 +60,7 @@ export type AuditEvidenceInput = {
|
||||
observedUxSignals: string[];
|
||||
observedContentSignals: string[];
|
||||
observedTechnicalSignals: string[];
|
||||
externalMarkdown?: string;
|
||||
screenshotReferences: Array<{
|
||||
storageId: string;
|
||||
sourceUrl: string;
|
||||
@@ -80,6 +81,7 @@ export type AuditEvidenceInputArgs = {
|
||||
screenshots?: readonly AuditScreenshotEvidence[];
|
||||
pageSpeedInputs?: readonly PageSpeedMinimalAuditResult[];
|
||||
skillRegistry?: readonly SkillRegistryEntryEvidence[];
|
||||
externalMarkdown?: string;
|
||||
};
|
||||
|
||||
const COMPANY_CONTEXT_LIMIT = 8;
|
||||
@@ -90,6 +92,20 @@ const TECHNICAL_SIGNAL_LIMIT = 6;
|
||||
const PAGESPEED_SIGNAL_LIMIT = 8;
|
||||
const SCREENSHOT_REFERENCE_LIMIT = 8;
|
||||
const SELECTED_SKILLS_LIMIT = 6;
|
||||
const EXTERNAL_MARKDOWN_LIMIT = 4_000;
|
||||
const V3_LOCAL_AUDIT_PRIORITY = new Map(
|
||||
[
|
||||
"visual-design",
|
||||
"contact-conversion",
|
||||
"local-seo-basics",
|
||||
"performance-experience",
|
||||
"mobile-usability",
|
||||
"conversion-copy",
|
||||
"first-impression-clarity",
|
||||
"trust-signals",
|
||||
"accessibility-basics",
|
||||
].map((id, index) => [id, index] as const),
|
||||
);
|
||||
|
||||
const URL_PATTERN = /\bhttps?:\/\/[^\s<>"']+/i;
|
||||
const JSON_BRACKET_PATTERN = /\{[^}]*\}|\[[^\]]*\]/;
|
||||
@@ -140,6 +156,19 @@ function sanitizeCustomerText(value: unknown, maxLength = 180): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
function sanitizeExternalMarkdown(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const markdown = value.replace(/\s+/g, " ").trim();
|
||||
if (!markdown) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return markdown.slice(0, EXTERNAL_MARKDOWN_LIMIT);
|
||||
}
|
||||
|
||||
function addUniqueCapped(
|
||||
bucket: string[],
|
||||
input: string,
|
||||
@@ -233,6 +262,77 @@ function selectTopSkill(
|
||||
return toAuditUsedSkill(scored[0]!.candidate);
|
||||
}
|
||||
|
||||
type SkillInputAvailability = {
|
||||
websiteExists: boolean;
|
||||
hasDesktopScreenshot: boolean;
|
||||
hasMobileScreenshot: boolean;
|
||||
hasMarkdown: boolean;
|
||||
hasPageSpeed: boolean;
|
||||
hasDom: boolean;
|
||||
};
|
||||
|
||||
function hasRequiredV3Input(input: string, availability: SkillInputAvailability) {
|
||||
switch (input) {
|
||||
case "desktop_screenshot":
|
||||
return availability.hasDesktopScreenshot;
|
||||
case "mobile_screenshot":
|
||||
return availability.hasMobileScreenshot;
|
||||
case "markdown":
|
||||
return availability.hasMarkdown;
|
||||
case "pagespeed":
|
||||
return availability.hasPageSpeed;
|
||||
case "dom":
|
||||
return availability.hasDom;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function v3SkillApplies(
|
||||
skill: SkillRegistryEntryEvidence,
|
||||
availability: SkillInputAvailability,
|
||||
) {
|
||||
const appliesWhen = skill.appliesWhen ?? "website_exists";
|
||||
const applies =
|
||||
appliesWhen === "always" ||
|
||||
(appliesWhen === "website_exists" && availability.websiteExists) ||
|
||||
(appliesWhen === "has_mobile_screenshot" &&
|
||||
availability.hasMobileScreenshot) ||
|
||||
(appliesWhen === "has_pagespeed" && availability.hasPageSpeed);
|
||||
|
||||
if (!applies) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (skill.inputs ?? []).every((input) =>
|
||||
hasRequiredV3Input(input, availability),
|
||||
);
|
||||
}
|
||||
|
||||
function selectV3Skills(
|
||||
skillRegistry: readonly SkillRegistryEntryEvidence[],
|
||||
availability: SkillInputAvailability,
|
||||
) {
|
||||
return skillRegistry
|
||||
.map((skill, registryIndex) => ({ skill, registryIndex }))
|
||||
.filter(({ skill }) => skill.id && !skill.category)
|
||||
.filter(({ skill }) => v3SkillApplies(skill, availability))
|
||||
.sort((a, b) => {
|
||||
// Keep core local-audit coverage inside the cap; otherwise preserve registry order.
|
||||
const aPriority = V3_LOCAL_AUDIT_PRIORITY.get(a.skill.id ?? "");
|
||||
const bPriority = V3_LOCAL_AUDIT_PRIORITY.get(b.skill.id ?? "");
|
||||
if (aPriority !== undefined || bPriority !== undefined) {
|
||||
return (
|
||||
(aPriority ?? Number.POSITIVE_INFINITY) -
|
||||
(bPriority ?? Number.POSITIVE_INFINITY)
|
||||
);
|
||||
}
|
||||
return a.registryIndex - b.registryIndex;
|
||||
})
|
||||
.slice(0, SELECTED_SKILLS_LIMIT)
|
||||
.map(({ skill }) => toAuditUsedSkill(skill));
|
||||
}
|
||||
|
||||
function buildObservedSignals(
|
||||
crawlPages: readonly AuditCrawlPageEvidence[],
|
||||
technicalChecks: readonly AuditTechnicalCheckEvidence[],
|
||||
@@ -403,8 +503,12 @@ function extractSkills(
|
||||
marketing: boolean;
|
||||
offer: boolean;
|
||||
},
|
||||
availability: SkillInputAvailability,
|
||||
): AuditUsedSkill[] {
|
||||
const selected: AuditUsedSkill[] = [];
|
||||
const selected: AuditUsedSkill[] = selectV3Skills(
|
||||
skillRegistry,
|
||||
availability,
|
||||
);
|
||||
const categoryOrder = ["design", "ux", "copy", "seo", "marketing", "offer"] as const;
|
||||
const evidenceText = {
|
||||
design:
|
||||
@@ -450,6 +554,7 @@ export function buildAuditEvidenceInput(
|
||||
const screenshots = args.screenshots ?? [];
|
||||
const pageSpeedInputs = args.pageSpeedInputs ?? [];
|
||||
const skillRegistry = args.skillRegistry ?? [];
|
||||
const externalMarkdown = sanitizeExternalMarkdown(args.externalMarkdown);
|
||||
|
||||
const companyContext: string[] = [];
|
||||
const checkedPages: string[] = [];
|
||||
@@ -542,6 +647,26 @@ export function buildAuditEvidenceInput(
|
||||
...signals.evidenceText,
|
||||
marketing: false,
|
||||
offer: false,
|
||||
}, {
|
||||
websiteExists:
|
||||
Boolean(lead.websiteDomain || lead.websiteUrl) ||
|
||||
crawlPages.length > 0 ||
|
||||
screenshots.length > 0,
|
||||
hasDesktopScreenshot: screenshots.some(
|
||||
(screenshot) => screenshot.viewport === "desktop",
|
||||
),
|
||||
hasMobileScreenshot: screenshots.some(
|
||||
(screenshot) => screenshot.viewport === "mobile",
|
||||
),
|
||||
hasMarkdown:
|
||||
Boolean(externalMarkdown) ||
|
||||
crawlPages.some((page) =>
|
||||
Boolean(page.visibleText || page.visibleTextExcerpt),
|
||||
),
|
||||
hasPageSpeed:
|
||||
pageSpeedInputsOutput.customerImplications.length > 0 ||
|
||||
pageSpeedInputs.some((input) => input.status === "succeeded"),
|
||||
hasDom: crawlPages.length > 0 || technicalChecks.length > 0,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -550,6 +675,7 @@ export function buildAuditEvidenceInput(
|
||||
observedUxSignals: signals.ux,
|
||||
observedContentSignals: signals.content,
|
||||
observedTechnicalSignals: signals.technical,
|
||||
...(externalMarkdown ? { externalMarkdown } : {}),
|
||||
screenshotReferences: screenshotReferences.map((reference) => ({
|
||||
...reference,
|
||||
width: Math.max(reference.width, 0),
|
||||
|
||||
Reference in New Issue
Block a user