扩展 AI 公司目录覆盖并保留逐条核查台账
Verify and deploy Tabularium / verify-and-deploy (pull_request) Successful in 11m10s
Verify and deploy Tabularium / verify-and-deploy (pull_request) Successful in 11m10s
This commit is contained in:
@@ -98,13 +98,27 @@ AUTH_CONFIG_FILE=/private/tmp/tabularium-auth/config.json AUTH_COOKIE_SECURE=fal
|
||||
同一版本内容变化会拒绝导入,避免静默读取旧数据。回退代码时继续读取对应旧版本。
|
||||
所有修改都经过工作分支和 PR,部署仍等待人工合并主线。
|
||||
|
||||
首批资料包含 58 家公司、10 类业务、100 条证据记录和 75 条经营观测。
|
||||
当前资料包含 986 条公司记录、10 类业务及待分类组、1,035 条证据记录和 75 条经营观测。
|
||||
其中保留 58 家个案资料,新增 928 条目录基础资料;目录项不等于完成财务和技术核验。
|
||||
它是定向试样,收入披露不足且统计时点不统一,不代表全部 AI 公司。
|
||||
界面将公司自述、第三方估算、融资、客户收入和履历收入分开,未知收入不记为零。
|
||||
保留被撤回的声明作为核查记录,并排除收入比较;并购后业务、管理层预测与历史里程碑单列。
|
||||
团队规模下界在列表中显示比较符,商业成熟度展示公开交付证据,不等于技术能力排名。
|
||||
当前尚不能可靠计算领域 ARR 均值、离散排名或财务造假概率。
|
||||
|
||||
覆盖核查:从 yc-oss 非官方镜像发现 2025—2026 八个 YC 批次的 1,428 条候选,
|
||||
逐条读取对应 YC 官方公开页面,以页面中的成立年份、团队字段和业务描述筛选。
|
||||
962 条初筛匹配,其中 34 条与原表 URL 重合,实际新增 928 条;268 条年份不符或缺失、
|
||||
192 条 AI 业务待确认、6 条成立年份晚于批次年份未纳入本批新增。
|
||||
`content/research/ai-companies.coverage.json` 保留逐条筛选结果、来源与响应摘要,
|
||||
数据快照中的 `coverage` 也随版本进入同一 SQLite 数据库。只覆盖该候选池,不能称全球全量。
|
||||
|
||||
新增业务分类优先匹配主营简介,再参考目录标签;未匹配者进入待分类组。
|
||||
初分规则位于 `scripts/classify-ai-profiles.py`,只生成待评审标签,不执行抓取或构建。
|
||||
所有新增项均标注规则初分,保留 `classificationBasis` 和 `directoryTags` 供复核。
|
||||
AI 关键词可能出现在业务背景中,收入、买家和交付状态仍须逐家深查,不能把目录项的未知收入当作未披露。
|
||||
界面支持按资料深度、记录团队规模筛选;证据列表每页 30 条,避免一次渲染全部来源。
|
||||
|
||||
`pnpm test:research` 在远程 Runner 上验证同库持久化、重复读取、版本导入与回退、
|
||||
登录限流保留、引用完整性及收入排除规则。镜像冒烟检查会验证研究页面与 API 的认证、
|
||||
返回记录数量、只读方法和容器重启后的读取结果。页面筛选交互仍需实际浏览器验证。
|
||||
|
||||
@@ -185,9 +185,9 @@ async function main() {
|
||||
assert.equal(researchResponse.status, 200, "authenticated research API");
|
||||
assert.ok(researchResponse.headers.get("cache-control")?.includes("no-store"));
|
||||
const research = await researchResponse.json();
|
||||
assert.equal(research.companies.length, 58);
|
||||
assert.equal(research.categories.length, 10);
|
||||
assert.equal(research.evidence.length, 100);
|
||||
assert.equal(research.companies.length, 986);
|
||||
assert.equal(research.categories.length, 11);
|
||||
assert.equal(research.evidence.length, 1035);
|
||||
assert.equal(research.observations.length, 75);
|
||||
assert.equal((await request("/api/research/ai-companies", { method: "POST", headers })).status, 405);
|
||||
const researchPage = await request("/docs/other-research/ai-company-research/overview", { headers });
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from "@/lib/ai-research";
|
||||
|
||||
const pageSize = 12;
|
||||
const evidencePageSize = 30;
|
||||
const revenueLabels = { self: "公司披露,待核口径", estimate: "仅第三方估算", unknown: "暂无可用收入数字" };
|
||||
const useLabels: Record<string, string> = {
|
||||
signal: "公司披露信号", estimate: "第三方估算", historical: "历史里程碑",
|
||||
@@ -36,6 +37,9 @@ export default function AICompanyResearch() {
|
||||
const [cohort, setCohort] = useState("");
|
||||
const [income, setIncome] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [sourcePage, setSourcePage] = useState(0);
|
||||
const [depth, setDepth] = useState("");
|
||||
const [teamBand, setTeamBand] = useState("");
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [tab, setTab] = useState<"companies" | "revenue" | "sources" | "reviews">("companies");
|
||||
const companySection = useRef<HTMLElement>(null);
|
||||
@@ -55,9 +59,12 @@ export default function AICompanyResearch() {
|
||||
const filtered = useMemo(() => data?.companies.filter((c) => {
|
||||
const search = `${c.name} ${c.business} ${c.location ?? ""} ${c.buyer}`.toLocaleLowerCase();
|
||||
const yearMatch = !cohort || (cohort === "other" ? ![2025, 2026].includes(c.year ?? 0) : c.year === Number(cohort));
|
||||
return (!category || c.cat === category) && yearMatch && (!income || c.revenueStatus === income)
|
||||
const depthMatch = !depth || (depth === "profile" ? c.profileOnly : !c.profileOnly);
|
||||
const teamMatch = !teamBand || (teamBand === "unknown" ? c.team === null || (!!c.teamBound && c.team < 51)
|
||||
: c.team !== null && (teamBand === "xl" ? c.team >= 51 : !c.teamBound && (teamBand === "small" ? c.team <= 5 : teamBand === "medium" ? c.team >= 6 && c.team <= 20 : c.team >= 21 && c.team <= 50)));
|
||||
return depthMatch && teamMatch && (!category || c.cat === category) && yearMatch && (!income || c.revenueStatus === income)
|
||||
&& search.includes(query.trim().toLocaleLowerCase());
|
||||
}) ?? [], [data, query, category, cohort, income]);
|
||||
}) ?? [], [data, query, category, cohort, income, depth, teamBand]);
|
||||
if (error) return <div className="research-message" role="alert">研究数据库暂时不可用。<button onClick={() => { setError(false); setAttempt((n) => n + 1); }}>重新读取</button></div>;
|
||||
if (!data) return <p className="research-message" role="status">正在读取研究数据库…</p>;
|
||||
const categories = new Map(data.categories.map((c) => [c.id, c]));
|
||||
@@ -83,6 +90,7 @@ export default function AICompanyResearch() {
|
||||
<div><strong>{data.evidence.length}</strong><span>证据记录</span></div>
|
||||
</div>
|
||||
<div className="research-scope"><p>{data.scope}</p>
|
||||
{data.coverage && <p>YC 候选池 {data.coverage.candidateRecords} 条,官方页面读取 {data.coverage.profilesRead} 条;初筛匹配 {data.coverage.matchingProfiles} 条,新增 {data.coverage.newProfiles} 条目录记录。年份不符或缺失 {data.coverage.otherOrMissingYear} 条、AI 业务待确认 {data.coverage.unclearAI} 条、日期冲突 {data.coverage.dateConflicts} 条。目录基础资料尚未逐家核验收入和交付能力。</p>}
|
||||
<details><summary>数据口径与覆盖限制</summary><ul>{data.limitations.map((note) => <li key={note}>{note}</li>)}</ul>
|
||||
<p>来源以 YC、公司官网、投资机构名录与经营案例为主。同一披露的转载不构成独立验证。团队人数包含公司自报的 Team Size,与正式员工口径可能不同。</p>
|
||||
</details>
|
||||
@@ -106,14 +114,16 @@ export default function AICompanyResearch() {
|
||||
<label className="research-query">搜索公司<input type="search" value={query} placeholder="名称、业务、地区或买家" onChange={(e) => { setQuery(e.target.value); setPage(0); }} /></label>
|
||||
<label>业务类别<select value={category} onChange={(e) => { setCategory(e.target.value); setPage(0); }}><option value="">全部类别</option>{data.categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}</select></label>
|
||||
<label>成立或发布<select value={cohort} onChange={(e) => { setCohort(e.target.value); setPage(0); }}><option value="">全部组别</option><option value="2025">2025 年成立</option><option value="2026">2026 年成立</option><option value="other">公开发布与转型对照</option></select></label>
|
||||
<label>资料深度<select value={depth} onChange={(e) => { setDepth(e.target.value); setPage(0); }}><option value="">全部资料</option><option value="case">原有个案研究</option><option value="profile">目录基础资料</option></select></label>
|
||||
<label>记录团队规模<select value={teamBand} onChange={(e) => { setTeamBand(e.target.value); setPage(0); }}><option value="">全部规模</option><option value="small">1—5 人</option><option value="medium">6—20 人</option><option value="large">21—50 人</option><option value="xl">至少 51 人</option><option value="unknown">未披露或分组不确定</option></select></label>
|
||||
<label>收入资料<select value={income} onChange={(e) => { setIncome(e.target.value); setPage(0); }}><option value="">全部状态</option><option value="self">找到公司披露</option><option value="estimate">仅第三方估算</option><option value="unknown">暂无可用数字</option></select></label>
|
||||
</div>
|
||||
<div className="research-result-count" role="status">{filtered.length} 家匹配公司<button onClick={() => { setCategory(""); setQuery(""); setCohort(""); setIncome(""); setPage(0); }}>清除筛选</button></div>
|
||||
<div className="research-result-count" role="status">{filtered.length} 家匹配公司<button onClick={() => { setCategory(""); setQuery(""); setCohort(""); setIncome(""); setDepth(""); setTeamBand(""); setPage(0); }}>清除筛选</button></div>
|
||||
<div className="research-table-wrap"><table><caption className="sr-only">公司分类与经营资料。选择公司名称查看指标和原始证据。</caption><thead><tr><th>公司 / 地区</th><th>业务与买家</th><th>成立 / 发布组别</th><th>团队规模</th><th>商业成熟度</th><th>收入资料</th></tr></thead><tbody>
|
||||
{filtered.slice(page * pageSize, (page + 1) * pageSize).map((c) => <tr key={c.id} data-selected={selected === c.id || undefined}>
|
||||
<td><button className="research-company-name" aria-expanded={selected === c.id} aria-controls="research-company-detail" onClick={() => setSelected(c.id)}>{c.name}</button><small>{c.location ?? "地区未注明"}</small></td>
|
||||
<td><span className="research-tag">{categories.get(c.cat)?.name}</span><p>{c.business}</p><small>买家:{c.buyer}</small></td>
|
||||
<td>{c.cohort}</td><td>{c.team !== null && c.teamBound}{c.team ?? "未披露"}{c.team !== null && " 人"}</td><td>{c.stage}</td><td>{revenueLabels[c.revenueStatus]}</td>
|
||||
<td><span className="research-tag">{categories.get(c.cat)?.name}</span>{c.profileOnly && <small>规则初分 · 待复核</small>}<p>{c.business}</p><small>买家:{c.buyer}</small></td>
|
||||
<td>{c.cohort}</td><td>{c.team !== null && c.teamBound}{c.team ?? "未披露"}{c.team !== null && " 人"}</td><td>{c.stage}</td><td>{c.profileOnly ? "收入尚未逐一核验" : revenueLabels[c.revenueStatus]}</td>
|
||||
</tr>)}
|
||||
</tbody></table></div>
|
||||
{!filtered.length && <p className="research-message">没有匹配公司,请调整筛选条件。</p>}
|
||||
@@ -126,16 +136,16 @@ export default function AICompanyResearch() {
|
||||
<span className="research-bar" data-estimate={o.selection === "estimate" || undefined} aria-hidden="true"><i style={{width: `${o.value / Math.max(...observed.map((v) => v.value)) * 100}%`}} /></span>
|
||||
<small>{o.kind} · {useLabels[o.selection]} · {o.timing}</small>
|
||||
</div>)}</div>
|
||||
<div className="research-table-wrap"><table><caption>各领域收入可得性;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>公司披露数</th><th>仅估算数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
|
||||
<div className="research-table-wrap"><table><caption>当前研究数据中的收入可得性,目录项未逐一检索;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>公司披露数</th><th>仅估算数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
|
||||
<p className="research-muted">离散度用于筛选值得核查的异常,还需排除公司年龄、口径和披露偏差;不能据此认定财务造假。</p>
|
||||
</>}
|
||||
{tab === "sources" && <><p className="research-muted">来源链接保留原始位置。相同 URL 的记录可能属于同一披露,不能按记录数计算独立证据数量。</p><ul className="research-evidence-list">{data.evidence.map((e) => <Evidence key={e.id} item={e} />)}</ul></>}
|
||||
{tab === "sources" && <><p className="research-muted">来源链接保留原始位置。相同 URL 的记录可能属于同一披露,不能按记录数计算独立证据数量。</p><ul className="research-evidence-list">{data.evidence.slice(sourcePage * evidencePageSize, (sourcePage + 1) * evidencePageSize).map((e) => <Evidence key={e.id} item={e} />)}</ul>{data.evidence.length > evidencePageSize && <nav className="research-pagination" aria-label="原始证据分页"><button disabled={sourcePage === 0} onClick={() => setSourcePage((n) => n - 1)}>上一页</button><span>{sourcePage + 1} / {Math.ceil(data.evidence.length / evidencePageSize)}</span><button disabled={(sourcePage + 1) * evidencePageSize >= data.evidence.length} onClick={() => setSourcePage((n) => n + 1)}>下一页</button></nav>}</>}
|
||||
{tab === "reviews" && <ul className="research-review-list">{data.reviews.map((r) => <li key={r.id}><span className="research-tag">{r.type}</span><h3>{r.company}</h3><p>{r.summary}</p><p>{r.handling}</p><div>{r.evidenceIds.map((id) => { const e=data.evidence.find((item) => item.id === id); return e ? <External key={id} url={e.url}>{id}</External> : null; })}</div></li>)}</ul>}
|
||||
</section>
|
||||
{selectedCompany && <section id="research-company-detail" ref={detailSection} tabIndex={-1} className="research-detail research-section" aria-label={`${selectedCompany.name}公司明细`}>
|
||||
<div className="research-detail-heading"><h2>{selectedCompany.name}</h2><button onClick={() => setSelected(null)} aria-label="关闭公司明细">关闭</button></div>
|
||||
<p><External url={selectedCompany.url}>公司资料入口</External> {selectedCompany.extraUrl && <External url={selectedCompany.extraUrl}>额外核验入口</External>}</p>
|
||||
<dl className="research-company-facts"><div><dt>主营业务</dt><dd>{selectedCompany.business}</dd></div><div><dt>样本组别</dt><dd>{selectedCompany.cohort}</dd></div><div><dt>买家</dt><dd>{selectedCompany.buyer}</dd></div><div><dt>收费方式</dt><dd>{selectedCompany.price}</dd></div><div><dt>交付证据</dt><dd>{selectedCompany.delivery}</dd></div><div><dt>团队口径</dt><dd>{selectedCompany.teamBasis ?? "本轮未核验人数"}</dd></div><div><dt>技术水平</dt><dd>尚未独立测试,不据融资或收入作能力排名。</dd></div><div><dt>身份与限制</dt><dd>{selectedCompany.notes}</dd></div></dl>
|
||||
<dl className="research-company-facts"><div><dt>主营业务</dt><dd>{selectedCompany.business}</dd></div><div><dt>样本组别</dt><dd>{selectedCompany.cohort}</dd></div><div><dt>买家</dt><dd>{selectedCompany.buyer}</dd></div><div><dt>收费方式</dt><dd>{selectedCompany.price}</dd></div><div><dt>交付证据</dt><dd>{selectedCompany.delivery}</dd></div><div><dt>团队口径</dt><dd>{selectedCompany.teamBasis ?? "本轮未核验人数"}</dd></div><div><dt>分类依据</dt><dd>{selectedCompany.classificationBasis ?? "原有个案分类;随研究复核"}</dd></div><div><dt>技术水平</dt><dd>尚未独立测试,不据融资或收入作能力排名。</dd></div><div><dt>身份与限制</dt><dd>{selectedCompany.notes}</dd></div></dl>
|
||||
<h3>经营指标与排除记录</h3>
|
||||
{selectedObservations.length ? <div className="research-table-wrap"><table><thead><tr><th>指标 / 归属</th><th>原始数值</th><th>数据时点</th><th>用途及限制</th><th>证据</th></tr></thead><tbody>{selectedObservations.map((o) => <tr key={o.id}><td>{o.kind}<small>{o.scope}</small></td><td>{o.bound !== "=" && o.bound}{money(o.value,o.unit)}</td><td>{o.timing}</td><td>{useLabels[o.selection] ?? o.selection}<small>{o.reason}</small></td><td><a href={`#detail-evidence-${o.evidence}`}>{o.evidence}</a></td></tr>)}</tbody></table></div> : <p>本轮尚未找到可记录的量化经营指标。收入保持未知。</p>}
|
||||
<h3>原始证据</h3><ul className="research-evidence-list">{selectedEvidence.map((e) => <Evidence key={e.id} prefix="detail-evidence" item={e} />)}</ul>
|
||||
|
||||
@@ -6,6 +6,7 @@ export type ResearchCompany = {
|
||||
price: string; stage: string; sourceType: string; evidenceIds: string[];
|
||||
revenueStatus: "self" | "estimate" | "unknown";
|
||||
extraUrl?: string; pricingEvidence?: string;
|
||||
profileOnly?: boolean; classificationBasis?: string; directoryTags?: string[];
|
||||
};
|
||||
export type ResearchObservation = {
|
||||
id: string; company: string; name: string; kind: string; value: number;
|
||||
@@ -21,11 +22,17 @@ export type ResearchReview = {
|
||||
id: string; type: string; company: string; summary: string;
|
||||
evidenceIds: string[]; handling: 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[];
|
||||
reviews: ResearchReview[]; coverage?: ResearchCoverage;
|
||||
};
|
||||
|
||||
export function evidenceUrl(value: string): string | undefined {
|
||||
@@ -49,6 +56,8 @@ export function validateResearch(snapshot: ResearchSnapshot) {
|
||||
unique(snapshot.observations.map((o) => o.id), "observations");
|
||||
unique(snapshot.reviews.map((r) => r.id), "reviews");
|
||||
for (const c of snapshot.companies) {
|
||||
if (c.profileOnly && (c.revenueStatus !== "unknown" || !c.classificationBasis))
|
||||
throw new Error(`Invalid profile-only research record: ${c.id}`);
|
||||
if (!categories.has(c.cat) || !evidenceUrl(c.url) ||
|
||||
c.evidenceIds.some((id) => !evidence.has(id)) ||
|
||||
(c.pricingEvidence && !evidence.has(c.pricingEvidence)))
|
||||
@@ -63,6 +72,12 @@ export function validateResearch(snapshot: ResearchSnapshot) {
|
||||
!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).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)))
|
||||
throw new Error(`Invalid research review: ${r.id}`);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import seed from "../../../content/research/ai-companies.seed.json";
|
||||
import coverageAudit from "../../../content/research/ai-companies.coverage.json";
|
||||
import { readResearchSnapshot } from "../src/lib/ai-research-store";
|
||||
import { openSiteDatabase } from "../src/lib/site-database";
|
||||
import { categorySummary, evidenceUrl, validateResearch, type ResearchSnapshot } from "../src/lib/ai-research";
|
||||
@@ -20,8 +21,8 @@ test("research revisions share the authentication database and preserve replay l
|
||||
for (let i = 0; i < 20; i++) consumeCode(config, "invalid");
|
||||
assert.equal(consumeCode(config, "invalid"), "limited");
|
||||
const initial = readResearchSnapshot();
|
||||
assert.equal(initial.companies.length, 58);
|
||||
assert.equal(initial.evidence.length, 100);
|
||||
assert.equal(initial.companies.length, 986);
|
||||
assert.equal(initial.evidence.length, 1035);
|
||||
assert.equal(initial.observations.length, 75);
|
||||
assert.deepEqual(readResearchSnapshot(), initial, "reopening does not duplicate records");
|
||||
const db = openSiteDatabase();
|
||||
@@ -60,10 +61,10 @@ test("research validation rejects broken evidence and unsafe source links", () =
|
||||
test("disclosure counts exclude client income, biographies, targets and duplicate milestones", () => {
|
||||
const snapshot = seed as ResearchSnapshot;
|
||||
const summary = categorySummary(snapshot);
|
||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 58);
|
||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 986);
|
||||
assert.equal(summary.reduce((n, c) => n + c.self, 0), 8);
|
||||
assert.equal(summary.reduce((n, c) => n + c.estimate, 0), 1);
|
||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 49);
|
||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 977);
|
||||
assert.equal(snapshot.companies.find((c) => c.name === "Rork")?.revenueStatus, "unknown");
|
||||
assert.equal(snapshot.companies.find((c) => c.name === "Result")?.revenueStatus, "unknown");
|
||||
assert.ok(snapshot.observations.filter((o) => o.selection === "signal").every((o) => o.scope === "公司整体"));
|
||||
@@ -85,3 +86,27 @@ test("retracted ARR and projected margins remain excluded", () => {
|
||||
assert.equal(snapshot.observations.filter((o) => o.company === peec.id && o.selection === "signal").length, 1);
|
||||
assert.ok(snapshot.observations.some((o) => o.company === peec.id && o.selection === "historical-estimate"));
|
||||
});
|
||||
|
||||
test("directory coverage reconciles every candidate and keeps profile-only income unknown", () => {
|
||||
const snapshot = seed as ResearchSnapshot;
|
||||
const coverage = snapshot.coverage!;
|
||||
assert.equal(coverageAudit.records.length, coverage.candidateRecords);
|
||||
assert.equal(new Set(coverageAudit.records.map((r) => r.ycId)).size, coverage.candidateRecords);
|
||||
const count = (status: string) => coverageAudit.records.filter((r) => r.status === status).length;
|
||||
assert.equal(count("imported"), coverage.newProfiles);
|
||||
assert.equal(count("existing"), coverage.existingProfiles);
|
||||
assert.equal(count("date-conflict"), coverage.dateConflicts);
|
||||
assert.equal(count("year-outside-or-missing"), coverage.otherOrMissingYear);
|
||||
assert.equal(count("ai-unclear"), coverage.unclearAI);
|
||||
for (const row of coverageAudit.records.filter((r) => r.status === "imported")) {
|
||||
const company = snapshot.companies.find((c) => c.id === `YC${row.ycId}`)!;
|
||||
assert.ok(company.profileOnly);
|
||||
assert.equal(company.url, row.source);
|
||||
assert.equal(company.revenueStatus, "unknown");
|
||||
assert.equal(company.year, row.foundedYear);
|
||||
assert.ok([2025, 2026].includes(company.year!));
|
||||
}
|
||||
const invalid = structuredClone(snapshot);
|
||||
invalid.coverage!.newProfiles += 1;
|
||||
assert.throws(() => validateResearch(invalid), /coverage accounting/);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
"""Draft business labels for public company profiles, never financial verification.
|
||||
|
||||
Input is a directory of JSON records with a `company` object containing
|
||||
one_liner and tags, as retained by the official-profile research snapshot.
|
||||
Output is for review; unmatched records remain C00 rather than being forced.
|
||||
This tool does not fetch sources, modify the site database or run a build.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def match(pattern,text):return bool(re.search(pattern,text,re.I))
|
||||
def classify(c):
|
||||
short=c.get('one_liner') or '';tags=' '.join(c.get('tags') or [])
|
||||
rules=[
|
||||
('C10',r'\brobot(?:s|ics|ic)?\b|embodied|physical intelligence|autonomous (?:driving|vehicle)|drones?|manufacturing hardware'),
|
||||
('C02',r'context compression|compression middleware|RNA intelligence|audio data for AI|synthetic data|data quality|\bGPU\b|\bGPUs\b|\bchips?\b|semiconductor|data centers?|datacenters?|inference|compute|training data|data labeling|data labelling|RL environments|reinforcement learning|model training'),
|
||||
('C07',r'precision medicine|ambulance|clinic|biology|manufacturers|distributors|fabrication|people-intelligence|deskless workers|health|medical|clinical|dental|physician|hospital|veterinar|therap|radiolog|pharma|legal|law firm|lawyer|litigation|accountant|accounting|audit\b|insurance|underwrit|financ|hedge funds?|bank|wealth|investment|investing|real estate|property|properties|construction|building (?:plan|code)|engineering projects|\bCAD\b|\bEDA\b|chip design|architecture|architects|logistics|supply chain|freight|global trade|compliance|government|govtech|lobby|energy|utilities|\bfleets?\b|education|edtech|\btutor|schools?|exam prep|tax\b|recruit|hiring|human resources|procurement|retail|e-commerce|ecommerce|merchants?|industrial'),
|
||||
('C03',r'AI evaluations|AI governance|voice agents|chat AI agents|harnesses|agent.{0,25}(?:infrastructure|tools|identity|identities|memory|evaluation|eval|observability|browser|payment|communication)|(?:infrastructure|tools|identity|identities|evaluation|eval|observability|browsers?|payments?).{0,25}agents?|\bMCP\b|agentmail|API.{0,20}agents?|developer.{0,15}agents?'),
|
||||
('C04',r'firmware|internal apps|coding|software engineer|code\b|codebase|developer|programming|app(?:lication)? (?:builder|creation|generation)|(?:build|building|make|create|generat).{0,25}(?:apps|applications|software)|\bDevOps\b|product docs|documentation engineer|\bIDE\b|logs and fix|testing|debug'),
|
||||
('C06',r'\bsales\b|marketing|customer support|customer service|call center|call centre|\bGTM\b|\bCRM\b|\bSEO\b|\bGEO\b|\bAEO\b|growth engine|outbound|telemarketing|advertis|\bads\b|contact center|customer success'),
|
||||
('C08',r'\bvideo\b|\bimage\b|\bphoto\b|\bfilm\b|\bdesign\b|animation|creative|creators?|content creation|\bmusic\b|media|audio edit|voiceover'),
|
||||
('C01',r'foundation models?|frontier models?|large language models?|\bLLMs?\b|voice model|speech model|world model'),
|
||||
('C09',r'consumer|personal|companions?|\bstor(?:y|ies)\b|entertainment|\bgam(?:e|es|ing)\b|social network|\bphone\b|smartphone|keyboard|your digital life'),
|
||||
('C05',r'\bB2B\b|enterprise|workflow|workspace|productivity|operations|\bSaaS\b|\bSOP\b|Word editor|spreadsheet|knowledge|company OS|company brain|work\b|teams?|business|employee|meeting|security|cyber|search'),
|
||||
]
|
||||
for label,text in [('主营简介',short),('目录标签',tags)]:
|
||||
for cat,pattern in rules:
|
||||
if match(pattern,text):return cat,label+'的规则初分'
|
||||
return 'C00','简介不足以确定主业务,待复核'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
result = []
|
||||
for path in sorted(args.input.glob("*.json")):
|
||||
record = json.loads(path.read_text())
|
||||
company = record["company"]
|
||||
category, basis = classify(company)
|
||||
result.append({"ycId": company["id"], "category": category,
|
||||
"basis": basis, "status": "provisional"})
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user