135 lines
7.2 KiB
TypeScript
135 lines
7.2 KiB
TypeScript
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 };
|
|
});
|
|
}
|