/** * Cheap, explainable "is this pin junk?" signals. * * Shared by the admin review queue and its Settings card so the two never drift. * These are HEURISTICS, not verdicts: every one of them is something a human can * check in about two seconds, and the action they drive (a reversible NIP-32 * label) is a suggestion-taking, never a deletion. */ import type { SkateSpot } from '@/lib/types'; /** Names that almost always mean a test entry or an off-topic pin. */ const SUSPICIOUS_TITLE = /(^|\b)(test|testing|asdf|asd|qwerty|foo|bar|untitled|demo|sample|example|secret|airport|my (house|home|flat|apartment))(\b|$)/i; /** The minimum any reviewable entity can tell us (any kind). */ export interface ContentQualityInput { title?: string; description?: string; /** How many pictures the entity carries (0 = none). */ imageCount?: number; /** Whether it has a usable location (spots/shops/events/tours). */ hasLocation?: boolean; } /** * Kind-agnostic junk signals, so the review queue can cover shops, events, * tours and listings without a copy of the logic per kind. */ export function contentFlags(input: ContentQualityInput): string[] { const flags: string[] = []; const title = (input.title ?? '').trim(); const description = (input.description ?? '').trim(); if (SUSPICIOUS_TITLE.test(title)) flags.push('suspicious name'); if (title.length <= 3) flags.push('name too short'); if ((input.imageCount ?? 0) === 0) flags.push('no photo'); if (description.length < 10) flags.push('no description'); if (input.hasLocation === false) flags.push('no location'); return flags; } export interface SpotQualitySignals { /** Mapper's web-of-trust score (0–100), when known. */ mapperTrust?: number; /** Status reports (kind 2259) from skaters — a real "someone was here" signal. */ communityStatusReports?: number; /** Community photos contributed by people other than the mapper. */ communityPhotos?: number; } /** Every flag that applies, as short human-readable labels. */ export function spotFlags(spot: SkateSpot, extra: SpotQualitySignals = {}): string[] { const flags = contentFlags({ title: spot.title, description: spot.description, imageCount: spot.images?.length ?? 0, hasLocation: !!spot.geohash, }); // Signals that need extra data (passed in by the review queue). if (typeof extra.mapperTrust === 'number' && extra.mapperTrust <= 0) { flags.push('mapper not in your web of trust'); } const communitySignals = (extra.communityStatusReports ?? 0) + (extra.communityPhotos ?? 0); if (communitySignals === 0 && flags.length > 0) { // Only meaningful alongside another flag: an unconfirmed spot with a real // name and a photo is just new, not junk. flags.push('no community signals'); } return flags; } export function isFlagged(spot: SkateSpot): boolean { return spotFlags(spot).length > 0; }