Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/guidance/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { GuidanceContext } from "./types";

export type InspiredesignGuidanceQualitySource = {
rankedReferenceCount: number;
rankedReferenceUrls?: string[];
rejectedReferenceCount: number;
topReferenceScore?: number;
topReferenceConfidence?: number;
Expand All @@ -24,6 +25,7 @@ export type InspiredesignGuidanceSource = {
acceptedUrls: string[];
failure?: string;
failures: number;
hardFailureReasonCodes?: string[];
};
metrics: {
referenceCount: number;
Expand All @@ -38,7 +40,55 @@ export type InspiredesignGuidanceSource = {
};
};

const HARD_PROVIDER_FAILURE_REASON_CODES = new Set([
"auth_required",
"challenge_detected",
"policy_blocked",
"rate_limited",
"token_required"
]);

const normalizeComparableUrl = (value: string): string | null => {
try {
const url = new URL(value);
url.protocol = "https:";
url.hostname = url.hostname.toLowerCase().replace(/^www\./, "");
url.hash = "";
url.search = "";
return url.toString();
} catch {
return null;
}
};

const hasAcceptedUserSuppliedSiteRecipeReferenceUrl = (source: InspiredesignGuidanceSource): boolean => {
if (source.metrics.referenceCount === 0 || source.quality.rankedReferenceCount === 0) return false;
const rankedReferenceUrls = new Set((source.quality.rankedReferenceUrls ?? [])
.map(normalizeComparableUrl)
.filter((url): url is string => typeof url === "string"));
if (rankedReferenceUrls.size === 0) return false;
const requestedSiteRecipeIds = new Set(source.requestedProviders
.map((providerId) => resolveSiteRecipeForProvider(providerId)?.id)
.filter((recipeId): recipeId is string => typeof recipeId === "string"));
return (source.urls ?? []).some((url) => {
const recipeId = resolveSiteRecipeForUrl(url)?.id;
if (!recipeId) return false;
if (requestedSiteRecipeIds.size > 0 && !requestedSiteRecipeIds.has(recipeId)) return false;
const normalizedUrl = normalizeComparableUrl(url);
return normalizedUrl !== null && rankedReferenceUrls.has(normalizedUrl);
});
};

const hasHardProviderFailureSignal = (source: InspiredesignGuidanceSource): boolean => (
(
!hasAcceptedUserSuppliedSiteRecipeReferenceUrl(source)
&& (source.discovery.hardFailureReasonCodes ?? []).some((reasonCode) => HARD_PROVIDER_FAILURE_REASON_CODES.has(reasonCode))
)
|| (source.primaryConstraint?.reasonCode ? HARD_PROVIDER_FAILURE_REASON_CODES.has(source.primaryConstraint.reasonCode) : false)
);

const hasProviderUnavailableSignal = (source: InspiredesignGuidanceSource): boolean => {
if (hasHardProviderFailureSignal(source)) return true;
if (source.metrics.referenceCount > 0 && source.quality.rankedReferenceCount > 0) return false;
if (source.discovery.requested && source.discovery.acceptedUrls.length === 0 && source.discovery.failures > 0) return true;
if (source.discovery.failure && source.discovery.acceptedUrls.length === 0) return true;
Expand Down Expand Up @@ -111,6 +161,7 @@ export const createInspiredesignGuidanceContext = (
details: {
brief: source.brief,
discoveryFailure: source.discovery.failure ?? "",
hardFailureReasonCodes: source.discovery.hardFailureReasonCodes ?? [],
primaryConstraintSummary: source.primaryConstraint?.summary ?? ""
}
};
Expand Down
49 changes: 42 additions & 7 deletions src/guidance/recipes/pinterest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,24 @@ const pinterestGuidance: NextStepGuidance = {
]
};

const RESERVED_PINTEREST_BOARD_PATHS = new Set(["about", "business", "ideas", "login", "pin", "search", "settings", "today"]);
const PINTEREST_PIN_ID_PATTERN = /^\d+$/;
const RESERVED_PINTEREST_BOARD_PATHS = new Set([
"about",
"board",
"business",
"create",
"explore",
"ideas",
"login",
"messages",
"notifications",
"pin",
"search",
"settings",
"shopping",
"today"
]);
const RESERVED_PINTEREST_IDEA_PATHS = new Set(["create", "edit", "search"]);
const RESERVED_PINTEREST_PROFILE_TABS = new Set([
"activity",
"comments",
Expand All @@ -63,18 +80,35 @@ const RESERVED_PINTEREST_PROFILE_TABS = new Set([
"tried"
]);

const normalizePinterestCandidateUrl = (value: string): string | null => {
export const isAllowedPinterestReferenceHost = (hostname: string): boolean => (
hostname === "pinterest.com"
|| hostname === "www.pinterest.com"
|| /^[a-z]{2}\.pinterest\.com$/.test(hostname)
);

export const normalizePinterestReferenceUrl = (value: string): string | null => {
const trimmed = value.trim();
const absolute = trimmed.startsWith("/")
? `https://www.pinterest.com${trimmed}`
: trimmed;
try {
const url = new URL(absolute);
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
url.protocol = "https:";
const hostname = url.hostname.toLowerCase();
if (hostname !== "pinterest.com" && !hostname.endsWith(".pinterest.com")) return null;
if (!isAllowedPinterestReferenceHost(hostname)) return null;
const pathSegments = url.pathname.split("/").filter(Boolean);
const isPin = pathSegments[0] === "pin" && pathSegments.length === 2;
const isIdea = pathSegments[0] === "ideas" && pathSegments.length >= 2;
const isPin = (
pathSegments[0] === "pin"
&& pathSegments.length === 2
&& PINTEREST_PIN_ID_PATTERN.test(pathSegments[1] ?? "")
);
const isIdea = (
pathSegments[0] === "ideas"
&& pathSegments.length >= 3
&& !RESERVED_PINTEREST_IDEA_PATHS.has(pathSegments[1] ?? "")
&& PINTEREST_PIN_ID_PATTERN.test(pathSegments[pathSegments.length - 1] ?? "")
);
const isBoard = pathSegments.length === 2
&& !RESERVED_PINTEREST_BOARD_PATHS.has(pathSegments[0] ?? "")
&& !RESERVED_PINTEREST_PROFILE_TABS.has(pathSegments[1] ?? "")
Expand All @@ -91,7 +125,7 @@ const normalizePinterestCandidateUrl = (value: string): string | null => {
const extractPinterestUrlsFromText = (value: string): string[] => {
const candidates = value.match(/(?:https?:\/\/(?:(?:www|[a-z]{2})\.)?pinterest\.com\/(?:pin\/[a-zA-Z0-9_-]+|ideas\/[a-zA-Z0-9/_-]+|[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+)|(?<![A-Za-z0-9.])\/(?:pin\/[a-zA-Z0-9_-]+|ideas\/[a-zA-Z0-9/_-]+|[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+))\/?/g) ?? [];
return candidates
.map(normalizePinterestCandidateUrl)
.map(normalizePinterestReferenceUrl)
.filter((url): url is string => url !== null);
};

Expand All @@ -102,7 +136,8 @@ const buildPinterestSearchUrl = (query: string): string => {

const extractPinterestReferenceUrls = (candidate: SiteRecipeReferenceCandidate): string[] => {
return [
normalizePinterestCandidateUrl(candidate.url ?? ""),
normalizePinterestReferenceUrl(candidate.url ?? ""),
...(candidate.links ?? []).map(normalizePinterestReferenceUrl),
...extractPinterestUrlsFromText(candidate.content ?? ""),
...extractPinterestUrlsFromText(candidate.html ?? "")
].filter((url): url is string => url !== null);
Expand Down
10 changes: 7 additions & 3 deletions src/guidance/recipes/site-registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pinterestSiteRecipe } from "./pinterest";
import { isAllowedPinterestReferenceHost, pinterestSiteRecipe } from "./pinterest";
import type { SiteRecipe } from "../types";

const freezeRecipeValue = <T>(value: T): T => {
Expand All @@ -22,14 +22,18 @@ export const resolveSiteRecipeForProvider = (providerId: string): SiteRecipe | u
export const resolveSiteRecipeForUrl = (url: string): SiteRecipe | undefined => {
let hostname: string;
try {
hostname = normalizeHostname(new URL(url).hostname);
hostname = new URL(url).hostname.toLowerCase();
} catch {
return undefined;
}
return SITE_RECIPES.find((recipe) => {
if (recipe.id === "social/pinterest") {
return isAllowedPinterestReferenceHost(hostname);
}
const normalizedHost = normalizeHostname(hostname);
return recipe.hostnames.some((candidate) => {
const normalized = normalizeHostname(candidate);
return hostname === normalized || hostname.endsWith(`.${normalized}`);
return normalizedHost === normalized || normalizedHost.endsWith(`.${normalized}`);
});
});
};
1 change: 1 addition & 0 deletions src/guidance/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export type SiteRecipeReferenceCandidate = {
url?: string;
content?: string;
html?: string;
links?: string[];
};

export type SiteRecipeBrowserNativeDiscovery = {
Expand Down
Loading
Loading