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 ResearchCandidateBatch = { id: string; sourceId: string; url: string; accessedAt: string; scope: string; complete: boolean; records: { id: string; name: string; url: string | null; categoryId: string; kind: "product" | "company" | "promotion"; profileStatus: "readable" | "unavailable" | "archive-only"; status: "pending-year" | "linked" | "existing-outside-cohort" | "outside-cohort" | "promotion"; companyId?: string; evidenceIds?: string[]; note: string; }[]; }; export type ResearchSnapshot = { version: string; asOf: string; scope: string; limitations: string[]; categories: ResearchCategory[]; companies: ResearchCompany[]; observations: ResearchObservation[]; evidence: ResearchEvidence[]; reviews: ResearchReview[]; coverage?: ResearchCoverage; candidateBatches?: ResearchCandidateBatch[]; directoryRechecks?: { ycId: number; originalStatus: "ai-unclear" | "year-outside-or-missing" | "date-conflict"; companyId: string; reviewedAt: string; evidenceIds: string[]; reason: string; }[]; }; 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}`); } unique((snapshot.candidateBatches ?? []).map((b) => b.id), "candidate batches"); unique((snapshot.candidateBatches ?? []).flatMap((b) => b.records.map((r) => r.id)), "candidates"); for (const batch of snapshot.candidateBatches ?? []) { if (!/^S\d{2}$/.test(batch.sourceId) || !evidenceUrl(batch.url) || !batch.scope.trim() || typeof batch.complete !== "boolean" || !/^\d{4}-\d{2}-\d{2}$/.test(batch.accessedAt) || Number.isNaN(Date.parse(batch.accessedAt)) || new Date(batch.accessedAt).toISOString().slice(0, 10) !== batch.accessedAt || batch.accessedAt > snapshot.asOf) throw new Error(`Invalid research candidate batch: ${batch.id}`); for (const row of batch.records) { const linked = ["linked", "existing-outside-cohort"].includes(row.status); if (!row.name.trim() || !row.note.trim() || !categories.has(row.categoryId) || !["product", "company", "promotion"].includes(row.kind) || !["readable", "unavailable", "archive-only"].includes(row.profileStatus) || !["pending-year", "linked", "existing-outside-cohort", "outside-cohort", "promotion"].includes(row.status) || (row.kind !== "promotion" && (!row.url || !evidenceUrl(row.url))) || (row.url !== null && !evidenceUrl(row.url)) || linked !== !!row.companyId || (row.companyId && !companies.has(row.companyId)) || (row.kind === "promotion") !== (row.status === "promotion") || (row.evidenceIds ?? []).some((id) => !evidence.has(id)) || (row.status === "outside-cohort" && !row.evidenceIds?.length)) throw new Error(`Invalid research candidate: ${row.id}`); } } const coverage = snapshot.coverage; unique((snapshot.directoryRechecks ?? []).map((row) => String(row.ycId)), "directory rechecks"); for (const row of snapshot.directoryRechecks ?? []) { if (!Number.isSafeInteger(row.ycId) || row.ycId <= 0 || !companies.has(row.companyId) || !["ai-unclear", "year-outside-or-missing", "date-conflict"].includes(row.originalStatus) || !/^\d{4}-\d{2}-\d{2}$/.test(row.reviewedAt) || Number.isNaN(Date.parse(row.reviewedAt)) || new Date(row.reviewedAt).toISOString().slice(0, 10) !== row.reviewedAt || row.reviewedAt > snapshot.asOf || !row.reason.trim() || !row.evidenceIds.length || row.evidenceIds.some((id) => !evidence.has(id))) throw new Error(`Invalid directory recheck: ${row.ycId}`); } 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 }; }); }