export type ResearchCategory = { id: string; name: string; description: string }; export const technicalReviewLabels = { unreviewed: "技术证据尚未专项评审", description: "已读技术自述", "public-artifact": "公开代码/模型材料可访问", "independent-test": "有第三方独立评测记录", }; export type ResearchCompany = { id: string; name: string; url: string; cat: string; year: number | null; team: number | null; teamBound?: ">" | "≥"; location: string | null; business: string; buyer: string; delivery: string; notes: string; cohort: string; teamBasis: string | null; price: string; stage: string; sourceType: string; evidenceIds: string[]; revenueStatus: "self" | "estimate" | "platform" | "unknown"; extraUrl?: string; pricingEvidence?: string; businessModel?: string; entityBasis?: string; profileOnly?: boolean; classificationBasis?: string; directoryTags?: string[]; directoryOrigin?: "yc-2025-2026"; technicalReview?: { level: "description" | "public-artifact" | "independent-test"; reviewedAt: string; summary: string; evidenceIds: string[]; testScope?: string; }; }; export type ResearchObservation = { id: string; company: string; name: string; kind: string; value: number; unit: string; timing: string; evidence: string; scope: string; selection: string; reason: string; bound: string; comparisonGroup: string | null; metric?: "mrr" | "arr" | "revenue-30d" | "annualized-revenue"; observedAt?: string; provider?: string; connectionExpired?: boolean; origin?: "payment-platform"; }; export type ResearchEvidence = { id: string; companyId: string | null; title: string; url: string; publishedAt: string | null; accessedAt: string; kind: string; summary: string; limitation: string; }; export type ResearchReview = { id: string; type: string; company: string; summary: string; evidenceIds: string[]; handling: string; scope?: "companies" | "dataset" | "excluded-candidate"; companyIds?: string[]; }; export type ResearchCoverage = { source: string; accessedAt: string; candidateRecords: number; profilesRead: number; matchingProfiles: number; existingProfiles: number; newProfiles: number; otherOrMissingYear: number; unclearAI: number; dateConflicts: number; failedProfiles: number; discoverySha256: string; }; export type ResearchSnapshot = { version: string; asOf: string; scope: string; limitations: string[]; categories: ResearchCategory[]; companies: ResearchCompany[]; observations: ResearchObservation[]; evidence: ResearchEvidence[]; reviews: ResearchReview[]; coverage?: ResearchCoverage; }; export function evidenceUrl(value: string): string | undefined { try { const url = new URL(value); return ["https:", "http:"].includes(url.protocol) ? url.href : undefined; } catch { return undefined; } } export function validateResearch(snapshot: ResearchSnapshot) { const unique = (values: string[], label: string) => { if (new Set(values).size !== values.length || values.some((v) => !v)) throw new Error(`Duplicate or missing research ${label}`); return new Set(values); }; if (!snapshot.version || !/^\d{4}-\d{2}-\d{2}$/.test(snapshot.asOf)) throw new Error("Invalid research dataset metadata"); const categories = unique(snapshot.categories.map((c) => c.id), "categories"); const companies = unique(snapshot.companies.map((c) => c.id), "companies"); const evidence = unique(snapshot.evidence.map((e) => e.id), "evidence"); unique(snapshot.observations.map((o) => o.id), "observations"); unique(snapshot.reviews.map((r) => r.id), "reviews"); for (const c of snapshot.companies) { const technical = c.technicalReview; if (technical && (!["description", "public-artifact", "independent-test"].includes(technical.level) || !/^\d{4}-\d{2}-\d{2}$/.test(technical.reviewedAt) || Number.isNaN(Date.parse(technical.reviewedAt)) || new Date(technical.reviewedAt).toISOString().slice(0, 10) !== technical.reviewedAt || technical.reviewedAt > snapshot.asOf || !technical.summary.trim() || !technical.evidenceIds.length || technical.evidenceIds.some((id) => !evidence.has(id)) || (technical.level === "independent-test" && !technical.testScope?.trim()))) throw new Error(`Invalid technical review: ${c.id}`); if (c.profileOnly && (c.revenueStatus !== "unknown" || !c.classificationBasis)) throw new Error(`Invalid profile-only research record: ${c.id}`); if (c.directoryOrigin && (c.directoryOrigin !== "yc-2025-2026" || !/^YC\d+$/.test(c.id))) throw new Error(`Invalid research directory origin: ${c.id}`); if (!categories.has(c.cat) || !evidenceUrl(c.url) || c.evidenceIds.some((id) => !evidence.has(id)) || (c.pricingEvidence && !evidence.has(c.pricingEvidence))) throw new Error(`Invalid research company reference: ${c.id}`); } for (const e of snapshot.evidence) { if (!evidenceUrl(e.url) || (e.companyId && !companies.has(e.companyId))) throw new Error(`Invalid research evidence: ${e.id}`); } for (const o of snapshot.observations) { if (!companies.has(o.company) || !evidence.has(o.evidence) || !Number.isFinite(o.value)) throw new Error(`Invalid research observation: ${o.id}`); } const coverage = snapshot.coverage; if (coverage && (coverage.profilesRead + coverage.failedProfiles !== coverage.candidateRecords || coverage.matchingProfiles !== coverage.existingProfiles + coverage.newProfiles || coverage.matchingProfiles + coverage.otherOrMissingYear + coverage.unclearAI + coverage.dateConflicts !== coverage.profilesRead || snapshot.companies.filter((c) => c.profileOnly || c.directoryOrigin === "yc-2025-2026").length !== coverage.newProfiles || !evidenceUrl(coverage.source))) throw new Error("Invalid research coverage accounting"); for (const r of snapshot.reviews) if (r.evidenceIds.some((id) => !evidence.has(id)) || (r.companyIds && (new Set(r.companyIds).size !== r.companyIds.length || r.companyIds.some((id) => !companies.has(id)))) || (r.scope && !["companies", "dataset", "excluded-candidate"].includes(r.scope)) || (r.scope === "companies" && !r.companyIds?.length) || (r.scope && r.scope !== "companies" && !!r.companyIds?.length)) throw new Error(`Invalid research review: ${r.id}`); } /** Explicit reviewed associations take priority; older revisions use evidence ownership. */ export function reviewCompanyIds(snapshot: ResearchSnapshot, review: ResearchReview): string[] { if (review.companyIds) return review.companyIds; const evidenceIds = new Set(review.evidenceIds); return [...new Set(snapshot.evidence.flatMap((e) => e.companyId && evidenceIds.has(e.id) ? [e.companyId] : []))]; } export function categorySummary(snapshot: ResearchSnapshot) { return snapshot.categories.map((category) => { const companies = snapshot.companies.filter((c) => c.cat === category.id); return { ...category, count: companies.length, self: companies.filter((c) => c.revenueStatus === "self").length, platform: companies.filter((c) => c.revenueStatus === "platform").length, estimate: companies.filter((c) => c.revenueStatus === "estimate").length, unknown: companies.filter((c) => c.revenueStatus === "unknown").length }; }); }