Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bba9fddef1 | |||
| 1302287205 | |||
| 59a1e4608b | |||
| ed99743730 | |||
| 74af205b29 | |||
| cffca6942b | |||
| bf58a27779 | |||
| 922835704b | |||
| 864a30356d | |||
| 9a026a08a5 | |||
| 62d3d498bb | |||
| 62c015fa9b | |||
| ec95f3be31 | |||
| 744e3314b8 | |||
| 79a6a8dbd7 | |||
| 0dcd178394 | |||
| dca7c81889 | |||
| f27b82b6a5 |
@@ -30,11 +30,11 @@ sudo /opt/endorphin-tabularium-docs/docs save < /private/work/purpose.json
|
|||||||
|
|
||||||
路径仅为操作示例,应选择实际存在且受保护的目录。运行成功后刷新网页即可看到正文、导航和搜索更新。版本冲突时重新读取并合并修改;不要把旧 revision 改成新数字强行覆盖。草案是现有展示标记,不是按分支或用户隔离的访问控制。
|
路径仅为操作示例,应选择实际存在且受保护的目录。运行成功后刷新网页即可看到正文、导航和搜索更新。版本冲突时重新读取并合并修改;不要把旧 revision 改成新数字强行覆盖。草案是现有展示标记,不是按分支或用户隔离的访问控制。
|
||||||
|
|
||||||
新增正文不提交回 Git。奇思妙想这类内容同样通过数据库写入;代码分支与数据发布是不同动作,不能因为迁移程序合并就自动发布其他未合并分支的内容。
|
新增正文不提交回 Git。奇思妙想这类内容同样通过数据库写入;本次用户明确要求汇总四个 PR,因此该草案已纳入首次迁移清单,保留草案状态。代码分支与数据发布是不同动作,其他分支的内容不会自动发布。
|
||||||
|
|
||||||
## 首次迁移与备份
|
## 首次迁移与备份
|
||||||
|
|
||||||
迁移前主线的 50 篇正文及目录可从固定历史提交 `25c5e83d08eae4277fe429a692955bd3bb2e717f` 完整导出:
|
本次汇总先合入 PR #14、#15、#16,再应用 PR #17 的数据库迁移。最新研究正文及奇思妙想草案合计 51 篇,连同目录可从固定历史提交 `cffca6942b1bc0005321e1685f38593fb0b853d5` 完整导出;其中 AI 公司研究三篇采用汇总期间追加的 `74af205b2959e1bb6a1fe9f567bc87069ac6d12c` 历史版本,保留 Wabi 更新:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 scripts/export-legacy-documents.py --output .artifacts/documents-migration.json
|
python3 scripts/export-legacy-documents.py --output .artifacts/documents-migration.json
|
||||||
@@ -104,11 +104,18 @@ sudo /opt/endorphin-tabularium-docs/auth verify
|
|||||||
同一版本内容变化会拒绝导入,避免静默读取旧数据。回退代码时继续读取对应旧版本。
|
同一版本内容变化会拒绝导入,避免静默读取旧数据。回退代码时继续读取对应旧版本。
|
||||||
所有修改都经过工作分支和 PR,部署仍等待人工合并主线。
|
所有修改都经过工作分支和 PR,部署仍等待人工合并主线。
|
||||||
|
|
||||||
当前资料包含 1,015 条公司/产品记录、10 类业务及待分类组、1,091 条证据记录和 183 条经营与技术观测。
|
当前资料包含 1,021 条公司/产品记录、10 类业务及待分类组、1,209 条证据记录和 189 条经营与技术观测。
|
||||||
其中保留 86 家个案资料、913 条目录基础资料,并新增 16 个平台产品/业务样本;目录项不等于完成财务和技术核验。
|
其中保留 93 家个案资料、912 条目录基础资料,并新增 16 个平台产品/业务样本;目录项不等于完成财务和技术核验。
|
||||||
|
Product Hunt两年档案各首批19张、合计38张发布/推广卡片以`candidateBatches`保存;公司成立年待核者不自动新增公司。其中18条仍待核成立年;排除依据区分正式成立字段与2025前已运营证据,精确注册日期未核者明确标注。候选元数据与其他研究记录同库持久化,覆盖报告逐条展示状态、缺项和公司关联。Kilo Code与KiloClaw两项映射一个已被收购的2025公司个案,采用规模、token吞吐及同名健康公司的收入不作为其ARR。
|
||||||
|
机器之心首批4条公司线索以独立候选批次记录:直达正文未读,经公司公开主页、官方代码或发起方公告补核PrismShadow与Stargate;SSI用2024官方历史更新排除,Utopai仍待核。不是两年中文来源全量。
|
||||||
|
TechCrunch栏目首批3条公司候选已追溯原始材料;Wabi据两个公开名录2025年份入表,精确注册日未核。官方条款称当前免费,收入仍未知,不填零。DreamHQ及Thrive Holdings成立年仍待核,后者2025公开发布公告另存。DreamHQ与名录同名摄像头公司区分,ADT拟收购尚待交割,融资、估值和目标公司业绩不当新AI公司的ARR。分类接口与RSS访问403及两年档案缺项保留。
|
||||||
|
Eloquent补按结果/用量混合收费与工作流自述;Peec补品牌套餐、招聘中的ARR里程碑和团队下界。招聘发布日期不当业务截止日,报价与客户降本不当供应商收入。Gojiberry报价保留原文$,未见币种代码不强转美元。平台MRR样本与统计不变。
|
||||||
|
Bessemer、Menlo、Stripe年度信与SaaS Capital的已核方法和访问限制另存为背景基准适用性表;不将高增长选样、采购支出、平台商户或增长率摘要当新AI公司ARR分布。SaaS Capital完整报告及各来源底表未取得者继续留缺。
|
||||||
补充ThinkWheel与中国NaiveAI;MiroMind成立年未核,公开研发年份单列。保留灵初智能成立年排除、深度原理日期待核和Naive近名实体复核记录。
|
补充ThinkWheel与中国NaiveAI;MiroMind成立年未核,公开研发年份单列。保留灵初智能成立年排除、深度原理日期待核和Naive近名实体复核记录。
|
||||||
新增7条Speedrun个案,保留标题/正文冲突、七位数数量级、币种不明、合同管线与履历归属排除记录;相对日期不充当同期收入时点。核查页优先按人工确认的公司ID关联明细,旧版本保留按证据归属回退;54条核查中43条指向具体公司、3条属于全局方法、8条是未纳入候选。明细反向列出关联核查;币种不明的声明保留原值,不绘制美元收入长度条。
|
新增7条Speedrun个案,保留标题/正文冲突、七位数数量级、币种不明、合同管线与履历归属排除记录;相对日期不充当同期收入时点。核查页优先按人工确认的公司ID关联明细,旧版本保留按证据归属回退;74条核查中54条指向具体公司、8条属于全局方法、12条是未纳入候选。明细反向列出关联核查;币种不明的声明保留原值,不绘制美元收入长度条。
|
||||||
|
Lightfield按Magical Tome主体与2025前历史业务列为CRM转型对照,成立年未知,不计新成立公司;Fundraisly成立年待核,其客户融资和创始人历史ARR不转移为公司收入。
|
||||||
Bud保留为2025成立且YC标记已收购的案例;Rocket.new按前身2021成立、2025产品上线的转型对照记录,历史ARR不混入新成立公司同期统计。
|
Bud保留为2025成立且YC标记已收购的案例;Rocket.new按前身2021成立、2025产品上线的转型对照记录,历史ARR不混入新成立公司同期统计。
|
||||||
|
原候选36088 EveryApp从AI不明经补证入表,`directoryRechecks`保存后续复核,原1,428条台账及928条原始新增计数不改。OpenSEO与EveryApp不重复计公司;定价、星标、客户数和数据成本加价均不转为ARR。
|
||||||
验收按2026-10-30确认的公开来源清单边界逐条核对并标明缺项;YC候选池有完整读取台账,其他来源目前为定向补充,不能把来源出现次数当作覆盖完成。收入披露不足且统计时点不统一,不代表全部 AI 公司。
|
验收按2026-10-30确认的公开来源清单边界逐条核对并标明缺项;YC候选池有完整读取台账,其他来源目前为定向补充,不能把来源出现次数当作覆盖完成。收入披露不足且统计时点不统一,不代表全部 AI 公司。
|
||||||
界面将公司自述、第三方估算、融资、客户收入和履历收入分开,未知收入不记为零。
|
界面将公司自述、第三方估算、融资、客户收入和履历收入分开,未知收入不记为零。
|
||||||
保留被撤回的声明作为核查记录,并排除收入比较;并购后业务、管理层预测与历史里程碑单列。
|
保留被撤回的声明作为核查记录,并排除收入比较;并购后业务、管理层预测与历史里程碑单列。
|
||||||
@@ -137,6 +144,7 @@ MRR×12仅作为显示单位转换,不改写为公司ARR;公司ARR、估算
|
|||||||
逐条读取对应 YC 官方公开页面,以页面中的成立年份、团队字段和业务描述筛选。
|
逐条读取对应 YC 官方公开页面,以页面中的成立年份、团队字段和业务描述筛选。
|
||||||
962 条初筛匹配,其中 34 条与原表 URL 重合,实际新增 928 条;268 条年份不符或缺失、
|
962 条初筛匹配,其中 34 条与原表 URL 重合,实际新增 928 条;268 条年份不符或缺失、
|
||||||
192 条 AI 业务待确认、6 条成立年份晚于批次年份未纳入本批新增。
|
192 条 AI 业务待确认、6 条成立年份晚于批次年份未纳入本批新增。
|
||||||
|
`content/research/ai-companies.sources.json` 保留最初12项来源与后续补充渠道、用途、覆盖边界及下一步;覆盖报告逐渠道复算证据、公司及观测数量。背景报告、入口可读和候选池逐条读取分别标记,零计数不表示没有公司。
|
||||||
`content/research/ai-companies.coverage.json` 保留逐条筛选结果、来源与响应摘要,
|
`content/research/ai-companies.coverage.json` 保留逐条筛选结果、来源与响应摘要,
|
||||||
数据快照中的 `coverage` 也随版本进入同一 SQLite 数据库。只覆盖该候选池,不能称全球全量。
|
数据快照中的 `coverage` 也随版本进入同一 SQLite 数据库。只覆盖该候选池,不能称全球全量。
|
||||||
|
|
||||||
@@ -149,7 +157,7 @@ MRR×12仅作为显示单位转换,不改写为公司ARR;公司ARR、估算
|
|||||||
仍有3条因简介不足保留待分类。人工复核分类不代表独立验证技术水平、客户效果或财务数据。
|
仍有3条因简介不足保留待分类。人工复核分类不代表独立验证技术水平、客户效果或财务数据。
|
||||||
AI 关键词可能出现在业务背景中,收入、买家和交付状态仍须逐家深查,不能把目录项的未知收入当作未披露。
|
AI 关键词可能出现在业务背景中,收入、买家和交付状态仍须逐家深查,不能把目录项的未知收入当作未披露。
|
||||||
技术专项评审用独立字段记录层级、日期、摘要与证据:未评审、已读技术自述、公开代码/模型材料可访问、有第三方独立评测记录。
|
技术专项评审用独立字段记录层级、日期、摘要与证据:未评审、已读技术自述、公开代码/模型材料可访问、有第三方独立评测记录。
|
||||||
当前7条完成该字段的专项记录,其中3条确认材料可访问、1条记录第三方已发表评测(Gradium TTS Sep 2026);本轮未自行运行模型测试。缺字段不表示无技术能力,代码或模型卡可读不证明性能。
|
当前14条完成该字段的专项记录,其中6条确认材料可访问、1条记录第三方已发表评测(Gradium TTS Sep 2026);本轮未自行运行模型测试。缺字段不表示无技术能力,代码或模型卡可读不证明性能。
|
||||||
界面支持按资料深度、记录团队规模和技术验证证据筛选;证据列表每页 30 条,避免一次渲染全部来源。
|
界面支持按资料深度、记录团队规模和技术验证证据筛选;证据列表每页 30 条,避免一次渲染全部来源。
|
||||||
|
|
||||||
`pnpm test:research` 在远程 Runner 上验证同库持久化、重复读取、版本导入与回退、
|
`pnpm test:research` 在远程 Runner 上验证同库持久化、重复读取、版本导入与回退、
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ async function main() {
|
|||||||
assert.equal(researchResponse.status, 200, "authenticated research API");
|
assert.equal(researchResponse.status, 200, "authenticated research API");
|
||||||
assert.ok(researchResponse.headers.get("cache-control")?.includes("no-store"));
|
assert.ok(researchResponse.headers.get("cache-control")?.includes("no-store"));
|
||||||
const research = await researchResponse.json();
|
const research = await researchResponse.json();
|
||||||
assert.equal(research.companies.length, 1015);
|
assert.equal(research.companies.length, 1021);
|
||||||
assert.equal(research.categories.length, 11);
|
assert.equal(research.categories.length, 11);
|
||||||
assert.equal(research.evidence.length, 1091);
|
assert.equal(research.evidence.length, 1209);
|
||||||
assert.equal(research.observations.length, 183);
|
assert.equal(research.observations.length, 189);
|
||||||
assert.equal((await request("/api/research/ai-companies", { method: "POST", headers })).status, 405);
|
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 });
|
const researchPage = await request("/docs/other-research/ai-company-research/overview", { headers });
|
||||||
assert.equal(researchPage.status, 200);
|
assert.equal(researchPage.status, 200);
|
||||||
@@ -221,7 +221,8 @@ async function main() {
|
|||||||
saveDocument({ ...first, slug: "endor/ci-live-document-renamed", content: "# Runtime document\n\nUpdated without rebuilding" }, first.revision);
|
saveDocument({ ...first, slug: "endor/ci-live-document-renamed", content: "# Runtime document\n\nUpdated without rebuilding" }, first.revision);
|
||||||
}
|
}
|
||||||
const stored = readDocument(sourceSlug);
|
const stored = readDocument(sourceSlug);
|
||||||
assert.ok(stored?.content.includes("Updated without rebuilding"), "DB content survives restart");
|
assert.ok(stored, "Smoke document must exist after creation or restart");
|
||||||
|
assert.ok(stored.content.includes("Updated without rebuilding"), "DB content survives restart");
|
||||||
const live = await request(`/docs/${stored.slug}`, { headers });
|
const live = await request(`/docs/${stored.slug}`, { headers });
|
||||||
assert.equal(live.status, 200);
|
assert.equal(live.status, 200);
|
||||||
assert.ok((await live.text()).includes("Updated without rebuilding"));
|
assert.ok((await live.text()).includes("Updated without rebuilding"));
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export default function IncomeDistribution({ data, onSelect }: {
|
|||||||
}), [data, dateWindow, year, annualize]);
|
}), [data, dateWindow, year, annualize]);
|
||||||
const groups = useMemo(() => groupIncome(sample.points, splitModels).sort((a, b) =>
|
const groups = useMemo(() => groupIncome(sample.points, splitModels).sort((a, b) =>
|
||||||
a.category.localeCompare(b.category) || a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model)), [sample.points, splitModels]);
|
a.category.localeCompare(b.category) || a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model)), [sample.points, splitModels]);
|
||||||
|
const categoryGroups = [...new Set(groups.map((group) => group.category))].map((category) => ({
|
||||||
|
category, rows: groups.filter((group) => group.category === category),
|
||||||
|
}));
|
||||||
const categories = new Map(data.categories.map((category) => [category.id, category.name]));
|
const categories = new Map(data.categories.map((category) => [category.id, category.name]));
|
||||||
const metric = annualize ? "MRR×12参考年化" : "平台MRR";
|
const metric = annualize ? "MRR×12参考年化" : "平台MRR";
|
||||||
const values = sample.points.map((point) => point.value);
|
const values = sample.points.map((point) => point.value);
|
||||||
@@ -41,6 +44,7 @@ export default function IncomeDistribution({ data, onSelect }: {
|
|||||||
<label>显示单位<select value={annualize ? "annualized" : "mrr"} onChange={(event) => setAnnualize(event.target.value === "annualized")}><option value="mrr">MRR(美元/月)</option><option value="annualized">MRR×12参考年化(美元/年)</option></select></label>
|
<label>显示单位<select value={annualize ? "annualized" : "mrr"} onChange={(event) => setAnnualize(event.target.value === "annualized")}><option value="mrr">MRR(美元/月)</option><option value="annualized">MRR×12参考年化(美元/年)</option></select></label>
|
||||||
</div>
|
</div>
|
||||||
<div className="income-options"><label><input type="checkbox" checked={splitModels} onChange={(event) => setSplitModels(event.target.checked)} /> 按收费与交付模式细分</label><label><input type="checkbox" checked={logScale} onChange={(event) => setLogScale(event.target.checked)} /> 对数横轴</label></div>
|
<div className="income-options"><label><input type="checkbox" checked={splitModels} onChange={(event) => setSplitModels(event.target.checked)} /> 按收费与交付模式细分</label><label><input type="checkbox" checked={logScale} onChange={(event) => setLogScale(event.target.checked)} /> 对数横轴</label></div>
|
||||||
|
<p>{splitModels ? "分组:业务类别 × 支付平台 × 收费与交付模式。同一类别下的多行是不同统计组,各自计算均值和中位数。" : "分组:业务类别 × 支付平台。同一平台内的收费模式已合并,不同支付平台仍分别统计。"}</p>
|
||||||
<p className="research-notice">资料截至 {data.asOf},纳入 {sample.points.length} 个产品,另有 {sample.excluded.length} 个 MRR 观测未纳入。{annualize ? "乘12只改变显示单位,不证明全年收入、续约或ARR;CV与相对敏感性不变。" : "MRR不等于近30日收款;平台数据连接也不等于财务审计。"}{dateWindow === "all" && " 当前包含不同时间的历史观测,不能称为同期对照。"}</p>
|
<p className="research-notice">资料截至 {data.asOf},纳入 {sample.points.length} 个产品,另有 {sample.excluded.length} 个 MRR 观测未纳入。{annualize ? "乘12只改变显示单位,不证明全年收入、续约或ARR;CV与相对敏感性不变。" : "MRR不等于近30日收款;平台数据连接也不等于财务审计。"}{dateWindow === "all" && " 当前包含不同时间的历史观测,不能称为同期对照。"}</p>
|
||||||
{sample.points.length ? <>
|
{sample.points.length ? <>
|
||||||
<figure>
|
<figure>
|
||||||
@@ -48,9 +52,16 @@ export default function IncomeDistribution({ data, onSelect }: {
|
|||||||
{ticks.map((tick) => <g key={tick}><line x1={x(tick)} x2={x(tick)} y1={15} y2={groups.length * 70 + 10} stroke="#dfe6e0" /><text x={x(tick)} y={groups.length * 70 + 35} textAnchor="middle" fontSize="11" fill="#637066">{usd(tick)}</text></g>)}
|
{ticks.map((tick) => <g key={tick}><line x1={x(tick)} x2={x(tick)} y1={15} y2={groups.length * 70 + 10} stroke="#dfe6e0" /><text x={x(tick)} y={groups.length * 70 + 35} textAnchor="middle" fontSize="11" fill="#637066">{usd(tick)}</text></g>)}
|
||||||
{groups.map((group, index) => {
|
{groups.map((group, index) => {
|
||||||
const y = index * 70 + 40;
|
const y = index * 70 + 40;
|
||||||
return <g key={group.key}>
|
const firstInCategory = index === 0 || groups[index - 1].category !== group.category;
|
||||||
<text x={8} y={y - 8} fontSize="12" fill="#202724">{categories.get(group.category)}</text>
|
const categoryRowCount = categoryGroups.find((entry) => entry.category === group.category)!.rows.length;
|
||||||
<text x={8} y={y + 10} fontSize="10" fill="#637066">{group.provider} · {group.model} · n={group.stats.n}</text>
|
return <g key={group.key} role="group" aria-label={`${categories.get(group.category)},${group.provider},${group.model},${group.stats.n}个产品`}>
|
||||||
|
{firstInCategory && <>
|
||||||
|
{index > 0 && <line x1={8} x2={right} y1={y - 34} y2={y - 34} stroke="#dfe6e0" />}
|
||||||
|
<text x={8} y={y - 8} fontSize="12" fontWeight="600" fill="#202724">{categories.get(group.category)}({categoryRowCount}组)</text>
|
||||||
|
{categoryRowCount > 1 && <line x1={10} x2={10} y1={y + 3} y2={y + (categoryRowCount - 1) * 70 + 12} stroke="#95b4a6" strokeWidth={2} />}
|
||||||
|
</>}
|
||||||
|
<text x={20} y={y + 10} fontSize="10" fill="#637066">{group.provider} · {group.model} · n={group.stats.n}</text>
|
||||||
|
{group.stats.n < 3 && <text x={20} y={y + 26} fontSize="9" fill="#637066">{group.stats.n === 1 ? "仅1个产品,无法判断组内离散度" : "仅2个产品,不代表领域典型收入"}</text>}
|
||||||
<line x1={x(group.stats.min)} x2={x(group.stats.max)} y1={y} y2={y} stroke="#95b4a6" strokeWidth={2} />
|
<line x1={x(group.stats.min)} x2={x(group.stats.max)} y1={y} y2={y} stroke="#95b4a6" strokeWidth={2} />
|
||||||
<line x1={x(group.stats.mean)} x2={x(group.stats.mean)} y1={y - 17} y2={y + 17} stroke="#b17725" strokeWidth={3}><title>样本均值 {usd(group.stats.mean)}</title></line>
|
<line x1={x(group.stats.mean)} x2={x(group.stats.mean)} y1={y - 17} y2={y + 17} stroke="#b17725" strokeWidth={3}><title>样本均值 {usd(group.stats.mean)}</title></line>
|
||||||
<path d={`M${x(group.stats.median)},${y - 7} l7,7 l-7,7 l-7,-7 Z`} fill="#8a78ad"><title>样本中位数 {usd(group.stats.median)}</title></path>
|
<path d={`M${x(group.stats.median)},${y - 7} l7,7 l-7,7 l-7,-7 Z`} fill="#8a78ad"><title>样本中位数 {usd(group.stats.median)}</title></path>
|
||||||
@@ -62,12 +73,13 @@ export default function IncomeDistribution({ data, onSelect }: {
|
|||||||
</g>;
|
</g>;
|
||||||
})}
|
})}
|
||||||
</svg></div>
|
</svg></div>
|
||||||
<figcaption>圆点:产品观测 竖线:样本均值 菱形:样本中位数。按业务类别与支付来源分组,点击点或下表产品名可看原始证据。</figcaption>
|
<figcaption>圆点:产品观测 竖线:样本均值 菱形:样本中位数。业务类别标题下各行按支付平台{splitModels && "及收费与交付模式"}分别统计;n为该组产品数。点击点或下表产品名可看原始证据。</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<div className="research-table-wrap"><table><caption>{metric}样本统计;金额按显示值计算,CV使用样本标准差</caption><thead><tr><th>类别 / 来源 / 模式</th><th>n</th><th>均值</th><th>中位数</th><th>均值/中位数</th><th>CV</th><th>去除最大值后的均值 / 下降幅度</th><th>产品与限制</th></tr></thead><tbody>{groups.map((group) => <tr key={group.key}>
|
<div className="research-table-wrap"><table><caption>{metric}样本统计;每行是类别下的一个统计组,CV使用样本标准差</caption><thead><tr><th scope="col">业务类别</th><th scope="col">支付平台 / 收费模式</th><th scope="col">n</th><th scope="col">均值</th><th scope="col">中位数</th><th scope="col">均值/中位数</th><th scope="col">CV</th><th scope="col">去除最大值后的均值 / 下降幅度</th><th scope="col">产品与限制</th></tr></thead>{categoryGroups.map(({ category, rows }) => <tbody key={category}>{rows.map((group, index) => <tr key={group.key}>
|
||||||
<td>{categories.get(group.category)}<small>{group.provider} · {group.model}</small></td><td>{group.stats.n}</td><td>{usd(group.stats.mean)}</td><td>{usd(group.stats.median)}</td><td>{group.stats.meanMedianRatio?.toFixed(2) ?? "—"}</td><td>{group.stats.cv?.toFixed(2) ?? "—"}</td><td>{usd(group.stats.meanWithoutLargest)}<small>{percent(group.stats.largestRemovalDrop)}</small></td>
|
{index === 0 && <th scope="rowgroup" rowSpan={rows.length}>{categories.get(category)}<small>{rows.length}个统计组</small></th>}
|
||||||
|
<td>{group.provider}<small>{group.model}</small></td><td>{group.stats.n}</td><td>{usd(group.stats.mean)}</td><td>{usd(group.stats.median)}</td><td>{group.stats.meanMedianRatio?.toFixed(2) ?? "—"}</td><td>{group.stats.cv?.toFixed(2) ?? "—"}</td><td>{usd(group.stats.meanWithoutLargest)}<small>{percent(group.stats.largestRemovalDrop)}</small></td>
|
||||||
<td>{group.points.map((point) => <span key={point.company.id}><button className="research-company-name" onClick={() => onSelect(point.company.id)}>{point.company.name}</button>{" "}</span>)}<small>{group.stats.n < 3 ? "样本少于3,不能据此判断领域典型收入。" : "自选的小样本,须结合产品年龄、成本与留存评估。"}</small></td>
|
<td>{group.points.map((point) => <span key={point.company.id}><button className="research-company-name" onClick={() => onSelect(point.company.id)}>{point.company.name}</button>{" "}</span>)}<small>{group.stats.n < 3 ? "样本少于3,不能据此判断领域典型收入。" : "自选的小样本,须结合产品年龄、成本与留存评估。"}</small></td>
|
||||||
</tr>)}</tbody></table></div>
|
</tr>)}</tbody>)}</table></div>
|
||||||
</> : <p className="research-message">当前窗口和创立年份没有符合条件的 MRR 记录。</p>}
|
</> : <p className="research-message">当前窗口和创立年份没有符合条件的 MRR 记录。</p>}
|
||||||
<details><summary>统计规则与解释</summary><p>每个产品只取符合条件的最新一条平台 MRR;币种为美元,要求有效数据日期、确定数值及支付来源。公司 ARR、估算、融资、目标和近30日收入均不进入这里。未知值不补零。</p><p>CV=样本标准差÷均值,标准差分母为 n−1;n=1 不算 CV。n=2 时均值必然等于中位数,不能据此认为均值有代表性。较大的均值/中位数差距、CV或移除最大值的变化,仅提示进一步核查样本构成。</p><p>支付商与平台的退款、试用、年付、币种及指标计算可能不同(<a href="https://trustmrr.com/faq" target="_blank" rel="noopener noreferrer">平台方法说明</a>)。不能将 MRR 与收款差异、单个高值或高离散度直接解释为财务造假。比较商业模式还需要毛利、获客成本、留存、人工交付和客户集中度。</p></details>
|
<details><summary>统计规则与解释</summary><p>每个产品只取符合条件的最新一条平台 MRR;币种为美元,要求有效数据日期、确定数值及支付来源。公司 ARR、估算、融资、目标和近30日收入均不进入这里。未知值不补零。</p><p>CV=样本标准差÷均值,标准差分母为 n−1;n=1 不算 CV。n=2 时均值必然等于中位数,不能据此认为均值有代表性。较大的均值/中位数差距、CV或移除最大值的变化,仅提示进一步核查样本构成。</p><p>支付商与平台的退款、试用、年付、币种及指标计算可能不同(<a href="https://trustmrr.com/faq" target="_blank" rel="noopener noreferrer">平台方法说明</a>)。不能将 MRR 与收款差异、单个高值或高离散度直接解释为财务造假。比较商业模式还需要毛利、获客成本、留存、人工交付和客户集中度。</p></details>
|
||||||
{sample.excluded.length > 0 && <details><summary>未纳入的 MRR 记录({sample.excluded.length})</summary><ul>{sample.excluded.map(({ observation, reason }) => <li key={observation.id}><button onClick={() => onSelect(observation.company)}>{observation.name}</button>:{usd(observation.value)} / 月,{observation.observedAt ?? "日期未知"};{reason}</li>)}</ul></details>}
|
{sample.excluded.length > 0 && <details><summary>未纳入的 MRR 记录({sample.excluded.length})</summary><ul>{sample.excluded.map(({ observation, reason }) => <li key={observation.id}><button onClick={() => onSelect(observation.company)}>{observation.name}</button>:{usd(observation.value)} / 月,{observation.observedAt ?? "日期未知"};{reason}</li>)}</ul></details>}
|
||||||
|
|||||||
@@ -43,11 +43,24 @@ export type ResearchCoverage = {
|
|||||||
otherOrMissingYear: number; unclearAI: number; dateConflicts: number; failedProfiles: number;
|
otherOrMissingYear: number; unclearAI: number; dateConflicts: number; failedProfiles: number;
|
||||||
discoverySha256: string;
|
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 = {
|
export type ResearchSnapshot = {
|
||||||
version: string; asOf: string; scope: string; limitations: string[];
|
version: string; asOf: string; scope: string; limitations: string[];
|
||||||
categories: ResearchCategory[]; companies: ResearchCompany[];
|
categories: ResearchCategory[]; companies: ResearchCompany[];
|
||||||
observations: ResearchObservation[]; evidence: ResearchEvidence[];
|
observations: ResearchObservation[]; evidence: ResearchEvidence[];
|
||||||
reviews: ResearchReview[]; coverage?: ResearchCoverage;
|
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 {
|
export function evidenceUrl(value: string): string | undefined {
|
||||||
@@ -98,7 +111,40 @@ export function validateResearch(snapshot: ResearchSnapshot) {
|
|||||||
!Number.isFinite(o.value))
|
!Number.isFinite(o.value))
|
||||||
throw new Error(`Invalid research observation: ${o.id}`);
|
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;
|
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 ||
|
if (coverage && (coverage.profilesRead + coverage.failedProfiles !== coverage.candidateRecords ||
|
||||||
coverage.matchingProfiles !== coverage.existingProfiles + coverage.newProfiles ||
|
coverage.matchingProfiles !== coverage.existingProfiles + coverage.newProfiles ||
|
||||||
coverage.matchingProfiles + coverage.otherOrMissingYear + coverage.unclearAI + coverage.dateConflicts !== coverage.profilesRead ||
|
coverage.matchingProfiles + coverage.otherOrMissingYear + coverage.unclearAI + coverage.dateConflicts !== coverage.profilesRead ||
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ test("research revisions share the authentication database and preserve replay l
|
|||||||
for (let i = 0; i < 20; i++) consumeCode(config, "invalid");
|
for (let i = 0; i < 20; i++) consumeCode(config, "invalid");
|
||||||
assert.equal(consumeCode(config, "invalid"), "limited");
|
assert.equal(consumeCode(config, "invalid"), "limited");
|
||||||
const initial = readResearchSnapshot();
|
const initial = readResearchSnapshot();
|
||||||
assert.equal(initial.companies.length, 1015);
|
assert.equal(initial.companies.length, 1021);
|
||||||
assert.equal(initial.evidence.length, 1091);
|
assert.equal(initial.evidence.length, 1209);
|
||||||
assert.equal(initial.observations.length, 183);
|
assert.equal(initial.observations.length, 189);
|
||||||
|
assert.deepEqual(initial.candidateBatches, seed.candidateBatches, "candidate gaps persist in the same database metadata");
|
||||||
|
assert.deepEqual(initial.directoryRechecks, seed.directoryRechecks, "follow-up classifications persist without rewriting the discovery audit");
|
||||||
assert.deepEqual(readResearchSnapshot(), initial, "reopening does not duplicate records");
|
assert.deepEqual(readResearchSnapshot(), initial, "reopening does not duplicate records");
|
||||||
const db = openSiteDatabase();
|
const db = openSiteDatabase();
|
||||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
||||||
@@ -62,9 +64,9 @@ test("research validation rejects broken evidence and unsafe source links", () =
|
|||||||
|
|
||||||
test("reviews link explicit companies without turning global or excluded cases into companies", () => {
|
test("reviews link explicit companies without turning global or excluded cases into companies", () => {
|
||||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "companies").length, 43);
|
assert.equal(snapshot.reviews.filter((r) => r.scope === "companies").length, 54);
|
||||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 3);
|
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 8);
|
||||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "excluded-candidate").length, 8);
|
assert.equal(snapshot.reviews.filter((r) => r.scope === "excluded-candidate").length, 12);
|
||||||
const review = snapshot.reviews.find((r) => r.id === "R4")!;
|
const review = snapshot.reviews.find((r) => r.id === "R4")!;
|
||||||
assert.deepEqual(reviewCompanyIds(snapshot, review), ["AI007", "AI013", "AI021", "AI034"]);
|
assert.deepEqual(reviewCompanyIds(snapshot, review), ["AI007", "AI013", "AI021", "AI034"]);
|
||||||
assert.deepEqual(reviewCompanyIds(snapshot, snapshot.reviews.find((r) => r.id === "CN-R-PSI")!), []);
|
assert.deepEqual(reviewCompanyIds(snapshot, snapshot.reviews.find((r) => r.id === "CN-R-PSI")!), []);
|
||||||
@@ -83,11 +85,11 @@ test("reviews link explicit companies without turning global or excluded cases i
|
|||||||
test("disclosure counts exclude client income, biographies, targets and duplicate milestones", () => {
|
test("disclosure counts exclude client income, biographies, targets and duplicate milestones", () => {
|
||||||
const snapshot = seed as ResearchSnapshot;
|
const snapshot = seed as ResearchSnapshot;
|
||||||
const summary = categorySummary(snapshot);
|
const summary = categorySummary(snapshot);
|
||||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 1015);
|
assert.equal(summary.reduce((n, c) => n + c.count, 0), 1021);
|
||||||
assert.equal(summary.reduce((n, c) => n + c.self, 0), 26);
|
assert.equal(summary.reduce((n, c) => n + c.self, 0), 26);
|
||||||
assert.equal(summary.reduce((n, c) => n + c.estimate, 0), 1);
|
assert.equal(summary.reduce((n, c) => n + c.estimate, 0), 1);
|
||||||
assert.equal(summary.reduce((n, c) => n + c.platform, 0), 16);
|
assert.equal(summary.reduce((n, c) => n + c.platform, 0), 16);
|
||||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 972);
|
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 978);
|
||||||
assert.equal(snapshot.companies.find((c) => c.name === "Rork")?.revenueStatus, "unknown");
|
assert.equal(snapshot.companies.find((c) => c.name === "Rork")?.revenueStatus, "unknown");
|
||||||
assert.equal(snapshot.companies.find((c) => c.name === "Result")?.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 === "公司整体"));
|
assert.ok(snapshot.observations.filter((o) => o.selection === "signal").every((o) => o.scope === "公司整体"));
|
||||||
@@ -95,8 +97,8 @@ test("disclosure counts exclude client income, biographies, targets and duplicat
|
|||||||
|
|
||||||
test("technical reviews keep accessible materials separate from tested capabilities", () => {
|
test("technical reviews keep accessible materials separate from tested capabilities", () => {
|
||||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview).length, 7);
|
assert.equal(snapshot.companies.filter((c) => c.technicalReview).length, 14);
|
||||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 3);
|
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 6);
|
||||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "independent-test").length, 1);
|
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "independent-test").length, 1);
|
||||||
assert.equal(snapshot.companies.find((c) => c.id === "AI051")?.revenueStatus, "unknown");
|
assert.equal(snapshot.companies.find((c) => c.id === "AI051")?.revenueStatus, "unknown");
|
||||||
assert.match(snapshot.companies.find((c) => c.id === "AI051")!.technicalReview!.testScope!, /Sep 2026/);
|
assert.match(snapshot.companies.find((c) => c.id === "AI051")!.technicalReview!.testScope!, /Sep 2026/);
|
||||||
@@ -255,3 +257,88 @@ test("distribution statistics match independently calculated snapshot references
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("product launch candidates preserve unresolved years and many launches per company", () => {
|
||||||
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
|
const batch = snapshot.candidateBatches![0];
|
||||||
|
assert.equal(batch.complete, false);
|
||||||
|
assert.equal(batch.records.length, 19);
|
||||||
|
const kilo = batch.records.filter((r) => r.companyId === "AI072");
|
||||||
|
assert.equal(kilo.length, 2, "IDE and hosted products resolve to one company");
|
||||||
|
assert.equal(snapshot.companies.filter((c) => c.id === "AI072").length, 1);
|
||||||
|
assert.ok(batch.records.some((r) => r.status === "pending-year" && !r.companyId));
|
||||||
|
assert.ok(batch.records.some((r) => r.status === "existing-outside-cohort" && r.companyId === "AI054"));
|
||||||
|
kilo[0].companyId = "missing-company";
|
||||||
|
assert.throws(() => validateResearch(snapshot), /research candidate/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("annual launch archives exclude older companies only with evidence and reuse existing records", () => {
|
||||||
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
|
const batch = snapshot.candidateBatches!.find((b) => b.id === "PH-2025-visible-1")!;
|
||||||
|
assert.equal(batch.complete, false);
|
||||||
|
assert.equal(batch.records.length, 19);
|
||||||
|
const older = batch.records.filter((r) => r.status === "outside-cohort");
|
||||||
|
assert.equal(older.length, 7);
|
||||||
|
assert.ok(older.every((r) => !r.companyId && r.evidenceIds?.length));
|
||||||
|
assert.equal(batch.records.find((r) => r.name === "Trace")?.companyId, "YC30687");
|
||||||
|
const trace = snapshot.companies.find((c) => c.id === "YC30687")!;
|
||||||
|
assert.equal(trace.profileOnly, false);
|
||||||
|
assert.equal(trace.directoryOrigin, "yc-2025-2026");
|
||||||
|
assert.equal(trace.revenueStatus, "unknown");
|
||||||
|
assert.ok(!snapshot.observations.some((o) => o.evidence === "TRACE-HOME"), "demo metrics must not become reported results");
|
||||||
|
older[0].evidenceIds = [];
|
||||||
|
assert.throws(() => validateResearch(snapshot), /research candidate/);
|
||||||
|
older[0].evidenceIds = ["missing-proof"];
|
||||||
|
assert.throws(() => validateResearch(snapshot), /research candidate/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unreconciled directory financials remain source reviews instead of income observations", () => {
|
||||||
|
const snapshot = seed as ResearchSnapshot;
|
||||||
|
const review = snapshot.reviews.find((r) => r.id === "PH-R-SOURCE-QUALITY")!;
|
||||||
|
assert.equal(review.scope, "dataset");
|
||||||
|
assert.ok(review.evidenceIds.includes("PH-TANA-DR"));
|
||||||
|
assert.equal(reviewCompanyIds(snapshot, review).length, 0);
|
||||||
|
assert.ok(!snapshot.observations.some((o) => o.evidence === "PH-TANA-DR"));
|
||||||
|
assert.equal(snapshot.candidateBatches!.flatMap((b) => b.records).filter((r) => r.status === "pending-year").length, 21);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("product pivots and founder histories do not become new company years or current revenue", () => {
|
||||||
|
const snapshot = seed as ResearchSnapshot;
|
||||||
|
const pivot = snapshot.companies.find((c) => c.id === "AI073")!;
|
||||||
|
assert.equal(pivot.year, null, "CRM launch is not a verified company founding year");
|
||||||
|
assert.equal(pivot.revenueStatus, "unknown");
|
||||||
|
const candidate = snapshot.candidateBatches!.flatMap((b) => b.records).find((r) => r.id === "PH26-11")!;
|
||||||
|
assert.equal(candidate.status, "existing-outside-cohort");
|
||||||
|
assert.equal(candidate.companyId, pivot.id);
|
||||||
|
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((point) => point.company.id === pivot.id));
|
||||||
|
assert.ok(!snapshot.observations.some((o) => ["PH-FUNDRAISLY-ABOUT", "LF-PIVOT", "LF-HISTORY"].includes(o.evidence)), "other ventures' ARR, users and funding must not transfer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("follow-up evidence can admit an uncertain directory candidate without rewriting its original status", () => {
|
||||||
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
|
const followup = snapshot.directoryRechecks!.find((row) => row.ycId === 36088)!;
|
||||||
|
const original = coverageAudit.records.find((row) => row.ycId === followup.ycId)!;
|
||||||
|
assert.equal(original.status, "ai-unclear");
|
||||||
|
assert.equal(followup.originalStatus, original.status);
|
||||||
|
assert.equal(snapshot.companies.find((c) => c.id === followup.companyId)?.year, 2026);
|
||||||
|
assert.equal(snapshot.candidateBatches!.flatMap((b) => b.records).find((r) => r.name === "OpenSEO")?.companyId, followup.companyId);
|
||||||
|
assert.ok(!snapshot.observations.some((o) => ["SEO-PRICE", "UP-YC", "PAZI-PRICE"].includes(o.evidence)), "pricing, other ventures' ARR and prepaid balances stay outside income observations");
|
||||||
|
followup.companyId = "missing-company";
|
||||||
|
assert.throws(() => validateResearch(snapshot), /directory recheck/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Chinese discovery entries preserve unread articles and use separate primary evidence", () => {
|
||||||
|
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||||
|
const batch = snapshot.candidateBatches!.find((b) => b.id === "JQ-2026-targeted-1")!;
|
||||||
|
assert.equal(batch.complete, false);
|
||||||
|
assert.equal(batch.records.length, 4);
|
||||||
|
assert.ok(batch.records.every((row) => row.kind === "company" && row.profileStatus === "unavailable"));
|
||||||
|
assert.equal(batch.records.filter((row) => row.status === "linked").length, 2);
|
||||||
|
assert.equal(batch.records.find((row) => row.name === "Utopai")?.status, "pending-year");
|
||||||
|
assert.ok(!snapshot.observations.some((o) => ["PS-CODE", "SG-START", "SG-APR", "SSI-HISTORY"].includes(o.evidence)), "task costs, investment, capacity and financing are not revenue");
|
||||||
|
const prism = snapshot.companies.find((c) => c.id === "AI075")!;
|
||||||
|
assert.equal(prism.team, null, "a team range or linked employees must not become exact headcount");
|
||||||
|
batch.records[0].url = null;
|
||||||
|
assert.throws(() => validateResearch(snapshot), /research candidate/);
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"version": "2026-10-30-10",
|
||||||
|
"scopeConfirmedAt": "2026-10-30",
|
||||||
|
"acceptance": "以确认的公开来源清单为边界,逐条核对并标明缺项。",
|
||||||
|
"provenance": "S01—S12恢复自已提交用户的《AI公司研究_来源与分类模板》来源清单;S13—S16单列后续已使用的补充证据渠道,不将使用次数当作全量覆盖。",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"id": "S01",
|
||||||
|
"name": "Y Combinator",
|
||||||
|
"url": "https://www.ycombinator.com/companies",
|
||||||
|
"role": "discovery",
|
||||||
|
"status": "candidate-audit",
|
||||||
|
"hosts": [
|
||||||
|
"www.ycombinator.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "官网目录可访问",
|
||||||
|
"boundary": "已读取八个2025—2026批次镜像候选池1428条,再核YC官方页面;不声称覆盖全部YC历年或未公开公司。",
|
||||||
|
"nextCheck": "继续复核规则初分和缺项;候选明细见逐条台账。",
|
||||||
|
"limitation": "批次不等于成立年;不代表全部公司"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S02",
|
||||||
|
"name": "TechCrunch AI",
|
||||||
|
"url": "https://techcrunch.com/category/artificial-intelligence/",
|
||||||
|
"role": "reporting",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"techcrunch.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "AI栏目可读取",
|
||||||
|
"boundary": "栏目首页选择的3条公司线索已追溯原始资料:Wabi据公开名录2025年份关联公司;DreamHQ与Thrive Holdings成立年待核,后者2025发布公告另存。不是首页全部条目或两年档案;公开分类接口与RSS普通HTTP请求返回403。",
|
||||||
|
"nextCheck": "补可公开取得的2025、2026档案及剩余分页;核DreamHQ、Thrive Holdings成立依据及Wabi精确注册、团队和付费口径。",
|
||||||
|
"limitation": "需追溯原始披露,部分文章受访问限制",
|
||||||
|
"accessRecheckedAt": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S03",
|
||||||
|
"name": "Dealroom",
|
||||||
|
"url": "https://dealroom.co/",
|
||||||
|
"role": "discovery",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"dealroom.co",
|
||||||
|
"app.dealroom.co"
|
||||||
|
],
|
||||||
|
"initialAccess": "官网可读取",
|
||||||
|
"boundary": "已读Tana与Wabi公开公司档案:前者财务字段不一致留缺,后者2025成立字段与官方业务关联入表;尚未取得两年成立筛选结果集,两个个案不等于目录全量。",
|
||||||
|
"nextCheck": "确认公开可取得的两年成立筛选清单;逐项追溯财务原始凭证及字段口径,受权限限制保留缺项。",
|
||||||
|
"limitation": "完整档案需权限;概览及财务表不能未经勾稽直接导入ARR",
|
||||||
|
"accessRecheckedAt": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S04",
|
||||||
|
"name": "Product Hunt",
|
||||||
|
"url": "https://www.producthunt.com/",
|
||||||
|
"role": "discovery",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"www.producthunt.com",
|
||||||
|
"producthunt.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "官网可读取",
|
||||||
|
"boundary": "2026与2025年度档案各登记首个公开响应19条,共38条候选/推广;2025另有一个仅图片位置未解析。已逐条保留关联、范围外与年份待核状态,未穷尽两年档案。",
|
||||||
|
"nextCheck": "继续补两年其余分页/日期、仅图片位置及18条年份待核候选;保留Pazi日期冲突,核清品牌与主体,避免转移历史业绩。",
|
||||||
|
"limitation": "上线时间不等于成立时间",
|
||||||
|
"accessRecheckedAt": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S05",
|
||||||
|
"name": "Sacra",
|
||||||
|
"url": "https://sacra.com/",
|
||||||
|
"role": "estimation",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"sacra.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "官网可读取",
|
||||||
|
"boundary": "已用于定向估算及日期口径交叉核查;无全站公司名录读取台账。",
|
||||||
|
"nextCheck": "保留估算与公司披露差异,核实历史截至日。",
|
||||||
|
"limitation": "部分付费;不能把估算当审计数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S06",
|
||||||
|
"name": "Bessemer",
|
||||||
|
"url": "https://www.bvp.com/atlas/the-state-of-ai-2025",
|
||||||
|
"role": "context",
|
||||||
|
"status": "context",
|
||||||
|
"hosts": [
|
||||||
|
"www.bvp.com",
|
||||||
|
"bvp.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "报告页面可读取",
|
||||||
|
"boundary": "主动选取20家高增长AI企业,含组合内外公司;非随机普查;已核方法与增长/毛利基准段;未取得20家逐公司底表。仅背景基准,不构成两年新公司名录。",
|
||||||
|
"nextCheck": "高增长选样和投资机构利益关系;不能把头部样本均值当各领域平均或新成立公司总体;需要公开可核底表时另补,不把缺项当已完成。",
|
||||||
|
"limitation": "投资机构选样偏差",
|
||||||
|
"accessRecheckedAt": "2026-10-30",
|
||||||
|
"methodology": {
|
||||||
|
"sample": "主动选取20家高增长AI企业,含组合内外公司;非随机普查",
|
||||||
|
"unit": "企业增长与毛利基准,按商业化后的第几年比较",
|
||||||
|
"period": "2025-08-13报告;商业化年数不等法律成立年",
|
||||||
|
"access": "已核方法与增长/毛利基准段;未取得20家逐公司底表",
|
||||||
|
"use": "用于提出留存、毛利、资本效率假设及分类线索",
|
||||||
|
"limit": "高增长选样和投资机构利益关系;不能把头部样本均值当各领域平均或新成立公司总体",
|
||||||
|
"evidenceIds": [
|
||||||
|
"CTX-BVP"
|
||||||
|
],
|
||||||
|
"checkedAt": "2026-10-30"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S07",
|
||||||
|
"name": "Menlo Ventures",
|
||||||
|
"url": "https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/",
|
||||||
|
"role": "context",
|
||||||
|
"status": "context",
|
||||||
|
"hosts": [
|
||||||
|
"menlovc.com",
|
||||||
|
"www.menlovc.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "报告页面可读取",
|
||||||
|
"boundary": "495名美国企业AI采购/开发决策者,企业已主动使用AI;已核公开方法、范围及限制;受访个体与公司估算底表未取得。仅背景基准,不构成两年新公司名录。",
|
||||||
|
"nextCheck": "仅美国企业;排除芯片、推理/模型服务及既有软件内嵌AI功能,不能与包含这些项目的总量直接比较或分摊为单家公司ARR;需要公开可核底表时另补,不把缺项当已完成。",
|
||||||
|
"limitation": "口径与样本需查看方法说明",
|
||||||
|
"accessRecheckedAt": "2026-10-30",
|
||||||
|
"methodology": {
|
||||||
|
"sample": "495名美国企业AI采购/开发决策者,企业已主动使用AI",
|
||||||
|
"unit": "采购调查与自下而上的市场支出估算,非公司逐项ARR",
|
||||||
|
"period": "2025-11-07至11-25调查,市场判断截至2025-12",
|
||||||
|
"access": "已核公开方法、范围及限制;受访个体与公司估算底表未取得",
|
||||||
|
"use": "用于买家、采购方式和任务方向假设",
|
||||||
|
"limit": "仅美国企业;排除芯片、推理/模型服务及既有软件内嵌AI功能,不能与包含这些项目的总量直接比较或分摊为单家公司ARR",
|
||||||
|
"evidenceIds": [
|
||||||
|
"CTX-MENLO"
|
||||||
|
],
|
||||||
|
"checkedAt": "2026-10-30"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S08",
|
||||||
|
"name": "Stripe",
|
||||||
|
"url": "https://stripe.com/annual-updates/2025",
|
||||||
|
"role": "context",
|
||||||
|
"status": "context",
|
||||||
|
"hosts": [
|
||||||
|
"stripe.com",
|
||||||
|
"www.stripe.com",
|
||||||
|
"assets.stripeassets.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "年度信可读取",
|
||||||
|
"boundary": "2025年开始使用Stripe的商户分组,另有Atlas注册企业分组;已读并视觉核对PDF第1、3页;未取得商户明细与完整分组计算底表。仅背景基准,不构成两年新公司名录。",
|
||||||
|
"nextCheck": "开始使用平台不等成立年,也非AI公司专属样本;交易量不等Stripe收入,门槛数量翻倍不等ARR均值翻倍;不与TrustMRR公开者样本混算;需要公开可核底表时另补,不把缺项当已完成。",
|
||||||
|
"limitation": "平台样本不能代表全部AI公司",
|
||||||
|
"accessRecheckedAt": "2026-10-30",
|
||||||
|
"methodology": {
|
||||||
|
"sample": "2025年开始使用Stripe的商户分组,另有Atlas注册企业分组",
|
||||||
|
"unit": "平台交易量、商户增长及达到收入门槛的数量;分母不同",
|
||||||
|
"period": "2025年度;2026-02-24年度信",
|
||||||
|
"access": "已读并视觉核对PDF第1、3页;未取得商户明细与完整分组计算底表",
|
||||||
|
"use": "用于支付、全球市场及首次收费速度假设",
|
||||||
|
"limit": "开始使用平台不等成立年,也非AI公司专属样本;交易量不等Stripe收入,门槛数量翻倍不等ARR均值翻倍;不与TrustMRR公开者样本混算",
|
||||||
|
"evidenceIds": [
|
||||||
|
"CTX-STRIPE"
|
||||||
|
],
|
||||||
|
"checkedAt": "2026-10-30"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S09",
|
||||||
|
"name": "SaaS Capital",
|
||||||
|
"url": "https://www.saas-capital.com/research/",
|
||||||
|
"role": "context",
|
||||||
|
"status": "context",
|
||||||
|
"hosts": [
|
||||||
|
"www.saas-capital.com",
|
||||||
|
"saas-capital.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "研究页可读取",
|
||||||
|
"boundary": "第15次年度调查,超过1000家私营B2B SaaS公司;仅读公开摘要;完整下载需填写表单,未提交表单或取得完整底表。仅背景基准,不构成两年新公司名录。",
|
||||||
|
"nextCheck": "样本不是两年新AI公司;增长率中位数不能当ARR中位数;完整抽样与指标细节仍缺,不外推至硬件、项目服务或所有AI模式;需要公开可核底表时另补,不把缺项当已完成。",
|
||||||
|
"limitation": "不直接套用硬件与项目制业务",
|
||||||
|
"accessRecheckedAt": "2026-10-30",
|
||||||
|
"methodology": {
|
||||||
|
"sample": "第15次年度调查,超过1000家私营B2B SaaS公司",
|
||||||
|
"unit": "2025年度增长率与留存等公开摘要;非ARR金额分布",
|
||||||
|
"period": "2026报告回顾2025增长;不把报告年当公司成立年",
|
||||||
|
"access": "仅读公开摘要;完整下载需填写表单,未提交表单或取得完整底表",
|
||||||
|
"use": "用于B2B订阅软件的规模、融资和留存分层假设",
|
||||||
|
"limit": "样本不是两年新AI公司;增长率中位数不能当ARR中位数;完整抽样与指标细节仍缺,不外推至硬件、项目服务或所有AI模式",
|
||||||
|
"evidenceIds": [
|
||||||
|
"CTX-SAASCAP"
|
||||||
|
],
|
||||||
|
"checkedAt": "2026-10-30"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S10",
|
||||||
|
"name": "机器之心",
|
||||||
|
"url": "https://www.jiqizhixin.com/",
|
||||||
|
"role": "reporting",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"www.jiqizhixin.com",
|
||||||
|
"jiqizhixin.com",
|
||||||
|
"pro.jiqizhixin.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "首页可读取",
|
||||||
|
"boundary": "已登记4篇定向检索线索;三篇直达仅登录外壳,一篇读取失败。2项经公开原始资料补证关联公司,1项范围外,1项仍待核;未阅读全文或穷尽两年档案。",
|
||||||
|
"nextCheck": "补公开可读文章、2025档案及Utopai原始成立材料;受限正文留缺,不以搜索摘录代全文。",
|
||||||
|
"limitation": "全文访问受限;仅作发现时明确标识,技术和财务另追原始资料",
|
||||||
|
"accessRecheckedAt": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S11",
|
||||||
|
"name": "36氪",
|
||||||
|
"url": "https://www.36kr.com/",
|
||||||
|
"role": "reporting",
|
||||||
|
"status": "access-limited",
|
||||||
|
"hosts": [
|
||||||
|
"www.36kr.com",
|
||||||
|
"36kr.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "安全检测,内容未核验",
|
||||||
|
"boundary": "2026-10-30仍返回安全检测页面,未把首页作为已读文章。",
|
||||||
|
"nextCheck": "使用可公开读取的具体文章或其他原始来源;不绕过访问保护。",
|
||||||
|
"limitation": "暂不作为已读取内容证据",
|
||||||
|
"accessRecheckedAt": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S12",
|
||||||
|
"name": "公司官网与官方博客",
|
||||||
|
"url": null,
|
||||||
|
"role": "primary",
|
||||||
|
"status": "per-company",
|
||||||
|
"hosts": [],
|
||||||
|
"initialAccess": "按公司逐一核验",
|
||||||
|
"boundary": "公司官网和官方材料逐条绑定证据;不是可预先列尽的全站候选池。",
|
||||||
|
"nextCheck": "核定价、主体、交付与收入定义,自述与独立验证分开。",
|
||||||
|
"limitation": "自述不等于独立审计"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S13",
|
||||||
|
"name": "TrustMRR",
|
||||||
|
"url": "https://trustmrr.com/",
|
||||||
|
"role": "platform",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"trustmrr.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "见具体证据访问记录",
|
||||||
|
"boundary": "已采集产品与支付快照;公开者自选,不是全部商户或独立财务审计。",
|
||||||
|
"nextCheck": "核同步日、连接失效、支付平台公式及产品/法人归属。",
|
||||||
|
"limitation": "来源可读不等于声明已独立验证。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S14",
|
||||||
|
"name": "a16z Speedrun",
|
||||||
|
"url": "https://speedrun.a16z.com/",
|
||||||
|
"role": "investor",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"speedrun.a16z.com"
|
||||||
|
],
|
||||||
|
"initialAccess": "见具体证据访问记录",
|
||||||
|
"boundary": "投资机构公司介绍定向样本,收入数字仍多来自公司。",
|
||||||
|
"nextCheck": "核币种、相对时间及管线/履历;未读取的页面保持缺项。",
|
||||||
|
"limitation": "来源可读不等于声明已独立验证。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S15",
|
||||||
|
"name": "Artificial Analysis",
|
||||||
|
"url": "https://artificialanalysis.ai/",
|
||||||
|
"role": "technical",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [
|
||||||
|
"artificialanalysis.ai"
|
||||||
|
],
|
||||||
|
"initialAccess": "见具体证据访问记录",
|
||||||
|
"boundary": "第三方已发表技术评测,仅覆盖所列模型版本和任务。",
|
||||||
|
"nextCheck": "核榜单版本、测评方法及置信区间,不将排名变成企业整体能力。",
|
||||||
|
"limitation": "来源可读不等于声明已独立验证。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S16",
|
||||||
|
"name": "其他公开补充资料",
|
||||||
|
"url": null,
|
||||||
|
"role": "supplement",
|
||||||
|
"status": "targeted",
|
||||||
|
"hosts": [],
|
||||||
|
"initialAccess": "见具体证据访问记录",
|
||||||
|
"boundary": "未归入上述来源的公开材料按原始证据链接逐条保留;不视为新增已批准的完整候选池。",
|
||||||
|
"nextCheck": "逐项核来源归属、可靠性及可读正文,不能将搜索摘要当全文。",
|
||||||
|
"limitation": "来源可读不等于声明已独立验证。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sourceVersion": "2026-10-30-depth-4",
|
"sourceVersion": "2026-10-30-context-1",
|
||||||
"asOf": "2026-10-30",
|
"asOf": "2026-10-30",
|
||||||
"metric": "platform-mrr-usd",
|
"metric": "platform-mrr-usd",
|
||||||
"sampleUnit": "product-or-business",
|
"sampleUnit": "product-or-business",
|
||||||
|
|||||||
@@ -9,11 +9,142 @@ from urllib.parse import urlsplit
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text())
|
DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text())
|
||||||
AUDIT = json.loads((ROOT / 'content/research/ai-companies.coverage.json').read_text())
|
AUDIT = json.loads((ROOT / 'content/research/ai-companies.coverage.json').read_text())
|
||||||
|
SOURCES = json.loads((ROOT / 'content/research/ai-companies.sources.json').read_text())
|
||||||
TARGET = ROOT / '.artifacts/reports/02-coverage-and-gaps.md'
|
TARGET = ROOT / '.artifacts/reports/02-coverage-and-gaps.md'
|
||||||
SOURCE_SLUG = 'ai-company-research/02-coverage-and-gaps'
|
SOURCE_SLUG = 'ai-company-research/02-coverage-and-gaps'
|
||||||
|
|
||||||
BASE = '/docs/other-research/ai-company-research/overview'
|
BASE = '/docs/other-research/ai-company-research/overview'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def source_register():
|
||||||
|
sources = SOURCES['sources']
|
||||||
|
ids = {s['id'] for s in sources}
|
||||||
|
assert len(ids) == len(sources) and ids == {f'S{i:02}' for i in range(1, 17)}
|
||||||
|
assert SOURCES['scopeConfirmedAt'] <= DATA['asOf']
|
||||||
|
host_map = {}
|
||||||
|
for source in sources:
|
||||||
|
assert source['status'] in {'candidate-audit', 'targeted', 'pending', 'context', 'access-limited', 'per-company'}
|
||||||
|
assert source['boundary'] and source['nextCheck']
|
||||||
|
if source['url']:
|
||||||
|
assert urlsplit(source['url']).scheme in {'http', 'https'} and urlsplit(source['url']).netloc
|
||||||
|
for host in source['hosts']:
|
||||||
|
assert host not in host_map, f'Duplicate source host: {host}'
|
||||||
|
host_map[host] = source['id']
|
||||||
|
assigned = {key: [] for key in ids}
|
||||||
|
for evidence in DATA['evidence']:
|
||||||
|
owner = host_map.get(urlsplit(evidence['url']).netloc)
|
||||||
|
if owner is None:
|
||||||
|
owner = 'S12' if any(word in evidence['kind'] for word in ('官方', '公司自述', '公司披露')) else 'S16'
|
||||||
|
assigned[owner].append(evidence)
|
||||||
|
assert sum(len(v) for v in assigned.values()) == len(DATA['evidence'])
|
||||||
|
statuses = {'candidate-audit': '候选池逐条已读', 'targeted': '定向使用,未穷尽', 'pending': '候选台账待建',
|
||||||
|
'context': '背景研究', 'access-limited': '访问受限', 'per-company': '随公司逐条核对'}
|
||||||
|
lines = ['', '## 来源清单与验收边界', '', SOURCES['provenance'], '',
|
||||||
|
'每条证据在下表只计入一个渠道:先按所列域名归属,再将类型明确标为官方/公司自述/公司披露的剩余项计入S12,其余计入S16。公司可跨渠道出现,所以公司数不可跨行相加。零条证据表示尚未进入研究表,不表示该来源没有相关公司。背景报告不承担公司发现全集的角色。', '',
|
||||||
|
'| 来源 | 当前状态 | 证据 / 公司 / 观测数 | 已核对范围 | 剩余核查 |', '| --- | --- | --- | --- | --- |']
|
||||||
|
for source in sources:
|
||||||
|
evidence = assigned[source['id']]
|
||||||
|
evidence_ids = {e['id'] for e in evidence}
|
||||||
|
company_ids = {e['companyId'] for e in evidence if e['companyId']}
|
||||||
|
observations = sum(o['evidence'] in evidence_ids for o in DATA['observations'])
|
||||||
|
name = f"[{source['name']}]({source['url']})" if source['url'] else source['name']
|
||||||
|
lines.append(f"| {source['id']} {name} | {statuses[source['status']]} | {len(evidence)} / {len(company_ids)} / {observations} | {source['boundary']} | {source['nextCheck']} |")
|
||||||
|
lines += ['', 'Dealroom、Product Hunt、机器之心与36氪入口于2026-10-30重新访问。官网首页可读不证明已取得完整筛选结果;安全检测页面不作为文章内容。候选池之外的来源边界尚需建立可回查条目,完成前不宣布来源清单已全部覆盖。', '']
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def context_methods():
|
||||||
|
evidence = {e['id']: e for e in DATA['evidence']}
|
||||||
|
rows = [s for s in SOURCES['sources'] if s.get('methodology')]
|
||||||
|
assert {s['id'] for s in rows} == {'S06', 'S07', 'S08', 'S09'}
|
||||||
|
lines = ['', '## 背景基准适用性与缺项', '',
|
||||||
|
'以下核对背景资料的方法及适用范围,不是公司名录的完整覆盖。原始方法记录与全局核查随研究版本进入同一数据库;没有把行业汇总值新增为任何公司的收入观测。', '',
|
||||||
|
'| 来源与证据 | 样本与统计单位 | 时间 | 本次读取范围 | 可用于什么 | 主要限制 |',
|
||||||
|
'| --- | --- | --- | --- | --- | --- |']
|
||||||
|
for source in rows:
|
||||||
|
m = source['methodology']
|
||||||
|
assert m['checkedAt'] <= DATA['asOf']
|
||||||
|
assert all(m.get(key) for key in ('sample', 'unit', 'period', 'access', 'use', 'limit', 'evidenceIds'))
|
||||||
|
assert all(eid in evidence and evidence[eid]['companyId'] is None for eid in m['evidenceIds'])
|
||||||
|
assert not any(o['evidence'] in m['evidenceIds'] for o in DATA['observations'])
|
||||||
|
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in m['evidenceIds'])
|
||||||
|
lines.append(f"| {source['id']} {source['name']}:{proof} | {m['sample']};{m['unit']} | {m['period']} | {m['access']} | {m['use']} | {m['limit']} |")
|
||||||
|
lines += ['', '本表的应用解释属于研究判断。选样、分母、时间或指标不同会造成表观差异;这些差异不单独证明财务造假,也不能作为给未知公司填补收入的依据。', '']
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_register():
|
||||||
|
batches = DATA.get('candidateBatches', [])
|
||||||
|
companies = {c['id']: c for c in DATA['companies']}
|
||||||
|
categories = {c['id']: c['name'] for c in DATA['categories']}
|
||||||
|
known_sources = {s['id'] for s in SOURCES['sources']}
|
||||||
|
evidence = {e['id']: e for e in DATA['evidence']}
|
||||||
|
assert len({b['id'] for b in batches}) == len(batches)
|
||||||
|
all_ids = [r['id'] for b in batches for r in b['records']]
|
||||||
|
assert len(set(all_ids)) == len(all_ids)
|
||||||
|
lines = []
|
||||||
|
statuses = {'pending-year': '成立年待核', 'linked': '已关联公司',
|
||||||
|
'existing-outside-cohort': '已有关联,年份范围外',
|
||||||
|
'outside-cohort': '成立年范围外,不新增公司', 'promotion': '推广活动,非公司新增'}
|
||||||
|
for batch in batches:
|
||||||
|
assert batch['sourceId'] in known_sources
|
||||||
|
assert batch['accessedAt'] <= DATA['asOf']
|
||||||
|
assert any(e['url'] == batch['url'] for e in DATA['evidence'])
|
||||||
|
rows = batch['records']
|
||||||
|
counts = Counter(r['status'] for r in rows)
|
||||||
|
assert sum(counts.values()) == len(rows)
|
||||||
|
lines += ['', f"## {batch['sourceId']} 候选核对({batch['id']})", '',
|
||||||
|
f"[发现页面]({batch['url']}),读取于{batch['accessedAt']}。{batch['scope']}", '',
|
||||||
|
f"本批{len(rows)}条:{counts['linked']}条关联公司、{counts['existing-outside-cohort']}条关联范围外对照、{counts['outside-cohort']}条范围外不新增公司、{counts['pending-year']}条成立年待核、{counts['promotion']}条推广活动。完整来源覆盖标记:{'已完成' if batch['complete'] else '未完成'}。主营或产品任务的初分类不计入主公司表数量,只有已关联的公司记录进入对应公司明细。候选与缺项随研究版本保存在文档站同一SQLite的研究元数据中。", '',
|
||||||
|
'| 发现条目 | 主营或产品任务初分 | 核对状态 | 关联公司 | 核对证据 | 限制 |', '| --- | --- | --- | --- | --- | --- |']
|
||||||
|
for row in rows:
|
||||||
|
assert row['categoryId'] in categories
|
||||||
|
assert row['status'] in statuses
|
||||||
|
assert all(eid in evidence for eid in row.get('evidenceIds', []))
|
||||||
|
if row['status'] == 'outside-cohort':
|
||||||
|
assert row.get('evidenceIds') and not row.get('companyId')
|
||||||
|
name = f"[{row['name']}]({row['url']})" if row['url'] else row['name']
|
||||||
|
company = '—'
|
||||||
|
if row.get('companyId'):
|
||||||
|
assert row['companyId'] in companies
|
||||||
|
c = companies[row['companyId']]
|
||||||
|
company = f"[{c['name']}]({BASE}?company={c['id']}#research-company-detail)"
|
||||||
|
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in row.get('evidenceIds', [])) or '成立证据待补,产品页读取状态见候选记录'
|
||||||
|
category = f"[{categories[row['categoryId']]}]({BASE}?category={row['categoryId']})"
|
||||||
|
lines.append(f"| {name} | {category} | {statuses[row['status']]} | {company} | {proof} | {row['note']} |")
|
||||||
|
if batch['sourceId'] == 'S04':
|
||||||
|
lines += ['', '产品页可读只表示有可访问的资料;本批未将所有产品页完成公司、技术或财务深查。既有直接HTTP采集返回403,当前采用可读的公开页面响应,不声称取得底层全量列表;后续公开档案与未读页面继续保留缺项。', '']
|
||||||
|
else:
|
||||||
|
lines += ['', '发现条目和可读正文分开记录;登录外壳、读取失败或搜索摘录不充当已阅读全文。关联公司以列出的原始材料补证,仍保留技术、主体与收入未核事项。', '']
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def directory_rechecks():
|
||||||
|
rows = DATA.get('directoryRechecks', [])
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
original = {row['ycId']: row for row in AUDIT['records']}
|
||||||
|
companies = {c['id']: c for c in DATA['companies']}
|
||||||
|
evidence = {e['id']: e for e in DATA['evidence']}
|
||||||
|
assert len({row['ycId'] for row in rows}) == len(rows)
|
||||||
|
lines = ['', '## 原候选池的后续复核', '',
|
||||||
|
'原始1,428条读取快照与初筛计数保持不变;下表记录当时未入表、后续补足证据后进入公司表的项目。初筛未通过不是永久排除。后续入表不回写为当时已匹配,也不重复计入928条原始新增目录项。', '',
|
||||||
|
'| 原候选 | 原始状态 | 后续公司记录 | 复核证据 | 处理与限制 |', '| --- | --- | --- | --- | --- |']
|
||||||
|
for row in rows:
|
||||||
|
baseline = original[row['ycId']]
|
||||||
|
assert baseline['status'] == row['originalStatus']
|
||||||
|
assert row['reviewedAt'] <= DATA['asOf'] and row['reason']
|
||||||
|
assert row['originalStatus'] in {'ai-unclear', 'year-outside-or-missing', 'date-conflict'}
|
||||||
|
company = companies[row['companyId']]
|
||||||
|
assert row['evidenceIds'] and all(eid in evidence for eid in row['evidenceIds'])
|
||||||
|
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in row['evidenceIds'])
|
||||||
|
lines.append(f"| [{baseline['name']} · YC{row['ycId']}]({baseline['source']}) | {row['originalStatus']} | [{company['name']}]({BASE}?company={company['id']}#research-company-detail) | {proof} | {row['reviewedAt']}:{row['reason']} |")
|
||||||
|
lines += ['', f"已记录{len(rows)}条后续复核入表;其余未入表候选仍按原状态保留缺项,不能把这一批复核视为完成全部模糊项。", '']
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def render():
|
def render():
|
||||||
cov = DATA['coverage']
|
cov = DATA['coverage']
|
||||||
companies = DATA['companies']
|
companies = DATA['companies']
|
||||||
@@ -86,6 +217,10 @@ def render():
|
|||||||
lines.append(f"| {name} | {len(entries)} | {len({e['url'] for e in entries})} | {note} |")
|
lines.append(f"| {name} | {len(entries)} | {len({e['url'] for e in entries})} | {note} |")
|
||||||
others = [e for e in DATA['evidence'] if urlsplit(e['url']).netloc not in included]
|
others = [e for e in DATA['evidence'] if urlsplit(e['url']).netloc not in included]
|
||||||
lines.append(f"| 其他官网、评测及公开资料 | {len(others)} | {len({e['url'] for e in others})} | 按每条来源限制理解,不能统一当作独立证据 |")
|
lines.append(f"| 其他官网、评测及公开资料 | {len(others)} | {len({e['url'] for e in others})} | 按每条来源限制理解,不能统一当作独立证据 |")
|
||||||
|
lines += directory_rechecks()
|
||||||
|
lines += source_register()
|
||||||
|
lines += context_methods()
|
||||||
|
lines += candidate_register()
|
||||||
lines += ['', '## 尚未满足的研究条件', '',
|
lines += ['', '## 尚未满足的研究条件', '',
|
||||||
'行业ARR比较仍缺统一业务截止日、币种、经常性定义和业务边界。现有平台MRR可描述已采集产品,不足以代表全部新公司。技术评测只覆盖所列版本与任务,不宜跨领域汇总为统一强弱分数。', '',
|
'行业ARR比较仍缺统一业务截止日、币种、经常性定义和业务边界。现有平台MRR可描述已采集产品,不足以代表全部新公司。技术评测只覆盖所列版本与任务,不宜跨领域汇总为统一强弱分数。', '',
|
||||||
'下一轮优先补收入线索的日期及合同定义、非YC和非英文来源、规则初分的人工复核,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。', '',
|
'下一轮优先补收入线索的日期及合同定义、非YC和非英文来源、规则初分的人工复核,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。', '',
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
BASELINE = '25c5e83d08eae4277fe429a692955bd3bb2e717f' # Last main revision before database-backed documents.
|
BASELINE = 'cffca6942b1bc0005321e1685f38593fb0b853d5' # Integration snapshot before database migration.
|
||||||
|
RESEARCH_REVISION = '74af205b2959e1bb6a1fe9f567bc87069ac6d12c' # Final PR 14 update received during integration.
|
||||||
|
|
||||||
|
|
||||||
def export(ref):
|
def export(ref):
|
||||||
commit = subprocess.check_output(['git', 'rev-parse', '--verify', ref + '^{commit}'], cwd=ROOT, text=True).strip()
|
commit = subprocess.check_output(['git', 'rev-parse', '--verify', ref + '^{commit}'], cwd=ROOT, text=True).strip()
|
||||||
|
|
||||||
def read(name):
|
def read(name, revision=commit):
|
||||||
return subprocess.check_output(['git', 'show', f'{commit}:{name}'], cwd=ROOT).decode()
|
return subprocess.check_output(['git', 'show', f'{revision}:{name}'], cwd=ROOT).decode()
|
||||||
|
|
||||||
catalog = json.loads(read('content/catalog.json'))
|
catalog = json.loads(read('content/catalog.json'))
|
||||||
names = subprocess.check_output(['git', 'ls-tree', '-r', '--name-only', commit, '--', 'content/docs'], cwd=ROOT, text=True).splitlines()
|
names = subprocess.check_output(['git', 'ls-tree', '-r', '--name-only', commit, '--', 'content/docs'], cwd=ROOT, text=True).splitlines()
|
||||||
@@ -23,7 +24,8 @@ def export(ref):
|
|||||||
source = name.removeprefix('content/docs/').removesuffix('.md')
|
source = name.removeprefix('content/docs/').removesuffix('.md')
|
||||||
metadata = catalog['documents'][source]
|
metadata = catalog['documents'][source]
|
||||||
collection = next(c for c in catalog['collections'] if not c['path'] or source.startswith(c['path'] + '/'))
|
collection = next(c for c in catalog['collections'] if not c['path'] or source.startswith(c['path'] + '/'))
|
||||||
content = read(name)
|
revision = RESEARCH_REVISION if commit == BASELINE and source.startswith("ai-company-research/") else commit
|
||||||
|
content = read(name, revision)
|
||||||
title = re.search(r'^# (.+)$', content, re.M)
|
title = re.search(r'^# (.+)$', content, re.M)
|
||||||
documents.append(dict(slug=metadata['slug'], sourceSlug=source, category=metadata['category'],
|
documents.append(dict(slug=metadata['slug'], sourceSlug=source, category=metadata['category'],
|
||||||
title=title[1].strip() if title else source, content=content,
|
title=title[1].strip() if title else source, content=content,
|
||||||
|
|||||||
Reference in New Issue
Block a user