统一 AI 公司研究:ARR 分类表、名单补查与 CI 统计修复 #41
@@ -182,9 +182,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, 1155);
|
||||
assert.equal(research.companies.length, 1196);
|
||||
assert.equal(research.categories.length, 11);
|
||||
assert.equal(research.evidence.length, 3426);
|
||||
assert.equal(research.evidence.length, 4600);
|
||||
assert.equal(research.observations.length, 453);
|
||||
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 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import IncomeDistribution from "./IncomeDistribution";
|
||||
import ArrDisclosures from "./ArrDisclosures";
|
||||
import {
|
||||
categorySummary, evidenceUrl, technicalReviewLabels, reviewCompanyIds, type ResearchEvidence, type ResearchReview, type ResearchSnapshot,
|
||||
} from "@/lib/ai-research";
|
||||
@@ -154,6 +155,7 @@ export default function AICompanyResearch() {
|
||||
</>}
|
||||
{tab === "revenue" && <>
|
||||
<IncomeDistribution data={data} onSelect={setSelected} />
|
||||
<ArrDisclosures data={data} onSelect={setSelected} />
|
||||
<h3>企业披露与估算的历史里程碑</h3>
|
||||
<p className="research-notice">这一组包含 {disclosedCompanies} 家公司的披露或估算信号;企业披露含公司或交易当事方公开材料,披露主体与审计状态见证据。多为异时点历史里程碑,期间收入不直接年化,ARR 与年化收入口径仍待核对,暂不计算其同期领域均值。币种未确认的记录仅显示原值,不绘制美元长度条。上面的平台MRR统计与这一组分开。</p>
|
||||
<div className="research-revenue-chart" aria-label="不同时间的公开收入里程碑,线性长度,非同行排名">{observed.map((o) => <div key={o.id}>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { evidenceUrl, type ResearchSnapshot } from "@/lib/ai-research";
|
||||
import { arrDisclosureCoverage, arrDisclosures } from "@/lib/research-statistics";
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
signal: "企业或当事方披露", estimate: "第三方估算",
|
||||
historical: "历史披露", "historical-estimate": "历史估算",
|
||||
};
|
||||
|
||||
export default function ArrDisclosures({ data, onSelect }: { data: ResearchSnapshot; onSelect: (id: string) => void }) {
|
||||
const [category, setCategory] = useState("");
|
||||
const [year, setYear] = useState("");
|
||||
const rows = arrDisclosures(data, { category, foundedYear: year ? Number(year) : undefined });
|
||||
const coverage = arrDisclosureCoverage(data, year ? Number(year) : undefined);
|
||||
const companies = new Set(rows.map((row) => row.company.id));
|
||||
const categories = new Map(data.categories.map((item) => [item.id, item.name]));
|
||||
const evidence = new Map(data.evidence.map((item) => [item.id, item]));
|
||||
return <section aria-label="ARR披露分类表">
|
||||
<h3>2025—2026 年成立公司的 ARR 披露</h3>
|
||||
<p className="research-notice">这是跨时点证据表,共 {companies.size} 家公司、{rows.length} 条记录;同一公司的历史里程碑逐条保留,不能相加或视为独立公司。ARR 标签沿用原始披露,未证明经常性收入定义或审计状态;不纳入 MRR 图表,不计算同期行业均值或造假概率。仅收录明确标注的 ARR;定义冲突、目标、承诺、客户和前雇主收入排除,缺失不作零。</p>
|
||||
<div className="research-filters">
|
||||
<label>业务分类<select value={category} onChange={(event) => setCategory(event.target.value)}><option value="">全部分类</option>{data.categories.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}</select></label>
|
||||
<label>成立年份<select value={year} onChange={(event) => setYear(event.target.value)}><option value="">2025 与 2026</option><option value="2025">2025</option><option value="2026">2026</option></select></label>
|
||||
</div>
|
||||
<div className="research-table-wrap"><table>
|
||||
<caption>ARR 披露覆盖:按所选成立年份、每家公司计一次。点击分类筛选下方证据表。</caption>
|
||||
<thead><tr><th>业务分类</th><th>已采集公司</th><th>企业或当事方披露</th><th>仅第三方估算</th><th>缺少 ARR</th><th>披露覆盖率</th></tr></thead>
|
||||
<tbody>{coverage.filter((item) => item.companies > 0).map((item) => <tr key={item.id}>
|
||||
<td><button onClick={() => setCategory(item.id)} aria-pressed={category === item.id}>{item.name}</button></td>
|
||||
<td>{item.companies}</td><td>{item.reported}</td><td>{item.estimatedOnly}</td><td>{item.missing}</td>
|
||||
<td>{new Intl.NumberFormat("zh-CN", { style: "percent", maximumFractionDigits: 1 }).format(item.disclosed / item.companies)}</td>
|
||||
</tr>)}</tbody>
|
||||
</table></div>
|
||||
<p className="research-muted">分母含仅有目录资料的公司,覆盖率反映本次采集深度,不代表行业披露率。企业披露含历史里程碑,同一公司兼有披露和估算时归企业披露;缺少 ARR 的公司仍可能有其他收入。披露时点、币种与合同定义未对齐,不能将已披露少数公司的均值外推至整个分类,也不能由离散性判断财务造假。</p>
|
||||
<div className="research-table-wrap"><table>
|
||||
<caption>金额保留原币种、下界和估算身份;日期栏保留业务截至日缺项</caption>
|
||||
<thead><tr><th>公司 / 分类</th><th>收费与交付模式</th><th>原始 ARR 声明</th><th>时点</th><th>身份 / 限制</th><th>证据</th></tr></thead>
|
||||
<tbody>{rows.map(({ company, observation: item }) => {
|
||||
const source = evidence.get(item.evidence);
|
||||
const url = source && evidenceUrl(source.url);
|
||||
return <tr key={item.id}>
|
||||
<td><button onClick={() => onSelect(company.id)}>{company.name}</button><small>{categories.get(company.cat)} · {company.year}</small></td>
|
||||
<td>{company.businessModel ?? company.price}</td>
|
||||
<td>{item.bound !== "=" && item.bound}{new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 2 }).format(item.value)} {item.unit}<small>{item.kind}</small></td>
|
||||
<td>{item.timing}</td><td>{labels[item.selection]}<small>{item.reason}</small></td>
|
||||
<td>{url ? <a href={url} target="_blank" rel="noopener noreferrer">{item.evidence} ↗</a> : item.evidence}</td>
|
||||
</tr>;
|
||||
})}</tbody>
|
||||
</table></div>
|
||||
{!rows.length && <p>当前筛选没有符合条件的已记录 ARR 声明;不代表这些公司收入为零。</p>}
|
||||
</section>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ResearchSnapshot } from "@/lib/ai-research";
|
||||
import { groupIncome, platformIncomeSample } from "@/lib/research-statistics";
|
||||
import { compareIncomeGroups, groupIncome, platformIncomeSample } from "@/lib/research-statistics";
|
||||
|
||||
const usd = (value: number | null) => value === null ? "—" : new Intl.NumberFormat("zh-CN", {
|
||||
style: "currency", currency: "USD", maximumFractionDigits: 0,
|
||||
@@ -23,6 +23,7 @@ export default function IncomeDistribution({ data, onSelect }: {
|
||||
const categoryGroups = [...new Set(groups.map((group) => group.category))].map((category) => ({
|
||||
category, rows: groups.filter((group) => group.category === category),
|
||||
}));
|
||||
const comparisons = compareIncomeGroups(groups);
|
||||
const categories = new Map(data.categories.map((category) => [category.id, category.name]));
|
||||
const metric = annualize ? "MRR×12参考年化" : "平台MRR";
|
||||
const values = sample.points.map((point) => point.value);
|
||||
@@ -47,6 +48,20 @@ export default function IncomeDistribution({ data, onSelect }: {
|
||||
<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>
|
||||
{sample.points.length ? <>
|
||||
<aside aria-label="当前窗口样本解读" className="research-notice">
|
||||
<strong>当前窗口的样本解读</strong>
|
||||
{comparisons.length ? comparisons.map((comparison) => <div key={comparison.key}>
|
||||
<p>{comparison.provider} · {comparison.model}(仅比较各组 n≥3):
|
||||
{comparison.leastDispersed.stats.cv === comparison.mostDispersed.stats.cv
|
||||
? <>各组 CV 相同({comparison.leastDispersed.stats.cv?.toFixed(3)}),没有离散度高低差异。</>
|
||||
: <>{categories.get(comparison.mostDispersed.category)}更离散(CV {comparison.mostDispersed.stats.cv?.toFixed(3)}),{categories.get(comparison.leastDispersed.category)}较集中(CV {comparison.leastDispersed.stats.cv?.toFixed(3)})。</>}
|
||||
{comparison.leastSensitive.stats.largestRemovalDrop === comparison.mostSensitive.stats.largestRemovalDrop
|
||||
? <>各组移除最大值后的均值下降幅度相同({percent(comparison.leastSensitive.stats.largestRemovalDrop)})。</>
|
||||
: <>{categories.get(comparison.mostSensitive.category)}的均值更受最大值影响,移除后下降 {percent(comparison.mostSensitive.stats.largestRemovalDrop)};{categories.get(comparison.leastSensitive.category)}下降 {percent(comparison.leastSensitive.stats.largestRemovalDrop)}。</>}
|
||||
</p>
|
||||
</div>) : <p>当前没有至少两个可对照的统计组。需要相同支付平台、明确的收费模式标签,且各组至少 3 个产品;可调整同步窗口或创立年,不能将单个产品或两点均值当作领域典型。</p>}
|
||||
<p>这些是所选产品样本内的描述,较集中或较不敏感也不证明行业均值可靠。最后同步日并非统一财务结账日,产品年龄、主体独立性、成本和留存仍待核;参考年化也不是已披露 ARR。</p>
|
||||
</aside>
|
||||
<figure>
|
||||
<div className="research-table-wrap"><svg className="income-distribution-chart" viewBox={`0 0 900 ${groups.length * 70 + 65}`} role="group" aria-label={`${metric}分布,${logScale ? "对数" : "线性"}横轴;圆点为产品,竖线为均值,菱形为中位数`}>
|
||||
{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>)}
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
import type { ResearchCompany, ResearchObservation, ResearchSnapshot } from "./ai-research";
|
||||
|
||||
export type IncomePoint = { company: ResearchCompany; observation: ResearchObservation; value: number };
|
||||
|
||||
/** An evidence ledger, never a comparable sample: retain historical claims and estimates separately. */
|
||||
export function arrDisclosures(snapshot: ResearchSnapshot, options: { category?: string; foundedYear?: number } = {}) {
|
||||
const companies = new Map(snapshot.companies.map((company) => [company.id, company]));
|
||||
return snapshot.observations.flatMap((observation) => {
|
||||
const company = companies.get(observation.company);
|
||||
if (!company || ![2025, 2026].includes(company.year ?? 0) || observation.metric !== "arr" ||
|
||||
!["signal", "estimate", "historical", "historical-estimate"].includes(observation.selection) ||
|
||||
!Number.isFinite(observation.value) || observation.value < 0 ||
|
||||
(options.category && company.cat !== options.category) ||
|
||||
(options.foundedYear && company.year !== options.foundedYear)) return [];
|
||||
return [{ company, observation }];
|
||||
});
|
||||
}
|
||||
|
||||
/** Count companies once; missing ARR is a coverage gap, never zero revenue. */
|
||||
export function arrDisclosureCoverage(snapshot: ResearchSnapshot, foundedYear?: number) {
|
||||
const rows = arrDisclosures(snapshot, { foundedYear });
|
||||
return snapshot.categories.map((category) => {
|
||||
const companies = snapshot.companies.filter((company) => company.cat === category.id &&
|
||||
[2025, 2026].includes(company.year ?? 0) && (!foundedYear || company.year === foundedYear));
|
||||
const claims = rows.filter((row) => row.company.cat === category.id);
|
||||
const disclosed = new Set(claims.map((row) => row.company.id));
|
||||
const reported = new Set(claims.filter((row) =>
|
||||
["signal", "historical"].includes(row.observation.selection)).map((row) => row.company.id));
|
||||
return { id: category.id, name: category.name, companies: companies.length,
|
||||
disclosed: disclosed.size, reported: reported.size, estimatedOnly: disclosed.size - reported.size,
|
||||
missing: companies.length - disclosed.size, records: claims.length };
|
||||
});
|
||||
}
|
||||
export type IncomeStats = {
|
||||
n: number; mean: number; median: number; min: number; max: number;
|
||||
sampleDeviation: number | null; cv: number | null; meanMedianRatio: number | null;
|
||||
@@ -80,3 +110,24 @@ export function groupIncome(points: IncomePoint[], splitModels = true) {
|
||||
}
|
||||
return [...groups.entries()].map(([key, group]) => ({ key, ...group, stats: describeIncome(group.points.map((point) => point.value))! }));
|
||||
}
|
||||
|
||||
/** Compare descriptive statistics only within the same provider and delivery model. */
|
||||
export function compareIncomeGroups(groups: ReturnType<typeof groupIncome>) {
|
||||
const buckets = new Map<string, typeof groups>();
|
||||
for (const group of groups) {
|
||||
if (group.stats.n < 3 || group.stats.cv === null || group.stats.largestRemovalDrop === null ||
|
||||
group.model === "全部收费模式" || group.model === "收费模式未核验") continue;
|
||||
const key = JSON.stringify([group.provider, group.model]);
|
||||
const bucket = buckets.get(key) ?? [];
|
||||
bucket.push(group);
|
||||
buckets.set(key, bucket);
|
||||
}
|
||||
return [...buckets.entries()].flatMap(([key, rows]) => {
|
||||
if (rows.length < 2) return [];
|
||||
const byCV = [...rows].sort((a, b) => a.stats.cv! - b.stats.cv! || a.key.localeCompare(b.key));
|
||||
const bySensitivity = [...rows].sort((a, b) => a.stats.largestRemovalDrop! - b.stats.largestRemovalDrop! || a.key.localeCompare(b.key));
|
||||
return [{ key, provider: rows[0].provider, model: rows[0].model,
|
||||
leastDispersed: byCV[0], mostDispersed: byCV[byCV.length - 1],
|
||||
leastSensitive: bySensitivity[0], mostSensitive: bySensitivity[bySensitivity.length - 1] }];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,88 @@ import { openSiteDatabase } from "../src/lib/site-database";
|
||||
import { categorySummary, evidenceUrl, reviewCompanyIds, validateResearch, type ResearchSnapshot } from "../src/lib/ai-research";
|
||||
import { consumeCode, initializeStore } from "../src/lib/auth-store";
|
||||
import { newAuthConfig } from "../src/lib/auth";
|
||||
import { describeIncome, groupIncome, platformIncomeSample } from "../src/lib/research-statistics";
|
||||
import { arrDisclosureCoverage, arrDisclosures, compareIncomeGroups, describeIncome, groupIncome, platformIncomeSample } from "../src/lib/research-statistics";
|
||||
|
||||
test("income interpretations compare sufficient samples within a provider and known billing model", () => {
|
||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||
const makePoints = (cat: string, values: number[], provider = "Stripe", model = "软件订阅") => values.map((value, index) => ({
|
||||
company: { ...snapshot.companies[0], id: `${cat}-${provider}-${model}-${index}`, cat, businessModel: model },
|
||||
observation: { ...snapshot.observations[0], provider }, value,
|
||||
}));
|
||||
const points = [
|
||||
...makePoints("C04", [100, 100, 100]),
|
||||
...makePoints("C06", [10, 10, 1000]),
|
||||
...makePoints("C08", [1, 10000]),
|
||||
...makePoints("C09", [1, 10, 10000], "RevenueCat"),
|
||||
...makePoints("C10", [1, 10, 10000], "Stripe", "硬件服务"),
|
||||
...makePoints("C07", [0, 0, 0]),
|
||||
...makePoints("C01", [1, 10, 100], "Stripe", "收费模式未核验"),
|
||||
];
|
||||
const comparisons = compareIncomeGroups(groupIncome(points));
|
||||
assert.equal(comparisons.length, 1);
|
||||
assert.equal(comparisons[0].leastDispersed.category, "C04");
|
||||
assert.equal(comparisons[0].mostDispersed.category, "C06");
|
||||
assert.equal(comparisons[0].leastSensitive.category, "C04");
|
||||
assert.equal(comparisons[0].mostSensitive.category, "C06");
|
||||
assert.deepEqual(compareIncomeGroups(groupIncome(points, false)), [], "merged billing models are not comparable here");
|
||||
const annualized = compareIncomeGroups(groupIncome(points.map((point) => ({ ...point, value: point.value * 12 }))));
|
||||
assert.equal(annualized[0].mostSensitive.category, comparisons[0].mostSensitive.category);
|
||||
assert.ok(Math.abs(annualized[0].mostDispersed.stats.cv! - comparisons[0].mostDispersed.stats.cv!) < 1e-12);
|
||||
});
|
||||
|
||||
test("ARR coverage counts companies once and separates missing disclosures from zero income", () => {
|
||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||
snapshot.categories = [{ id: "C07", name: "行业应用", description: "" }];
|
||||
const template = snapshot.companies[0];
|
||||
snapshot.companies = [
|
||||
{ ...template, id: "reported", cat: "C07", year: 2025 },
|
||||
{ ...template, id: "estimated", cat: "C07", year: 2026 },
|
||||
{ ...template, id: "missing", cat: "C07", year: 2025 },
|
||||
{ ...template, id: "old", cat: "C07", year: 2024 },
|
||||
{ ...template, id: "undated", cat: "C07", year: null },
|
||||
];
|
||||
const observation = snapshot.observations[0];
|
||||
snapshot.observations = [
|
||||
{ ...observation, id: "zero", company: "reported", metric: "arr", selection: "signal", value: 0 },
|
||||
{ ...observation, id: "past", company: "reported", metric: "arr", selection: "historical", value: 10 },
|
||||
{ ...observation, id: "estimate", company: "reported", metric: "arr", selection: "estimate", value: 20 },
|
||||
{ ...observation, id: "only-estimate", company: "estimated", metric: "arr", selection: "historical-estimate", value: 30 },
|
||||
{ ...observation, id: "goal", company: "missing", metric: "arr", selection: "excluded", value: 40 },
|
||||
{ ...observation, id: "mrr", company: "missing", metric: "mrr", selection: "platform", value: 50 },
|
||||
{ ...observation, id: "old-arr", company: "old", metric: "arr", selection: "signal", value: 60 },
|
||||
];
|
||||
assert.deepEqual(arrDisclosureCoverage(snapshot), [{ id: "C07", name: "行业应用",
|
||||
companies: 3, disclosed: 2, reported: 1, estimatedOnly: 1, missing: 1, records: 4 }]);
|
||||
assert.deepEqual(arrDisclosureCoverage(snapshot, 2025), [{ id: "C07", name: "行业应用",
|
||||
companies: 2, disclosed: 1, reported: 1, estimatedOnly: 0, missing: 1, records: 3 }]);
|
||||
});
|
||||
|
||||
test("ARR ledger preserves historical estimates without promoting targets, run rates or outside-cohort claims", () => {
|
||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||
const rows = arrDisclosures(snapshot);
|
||||
const ids = new Set(rows.map((row) => row.observation.id));
|
||||
for (const id of ["O064", "O065", "O066", "O069", "O070", "O071", "O046"])
|
||||
assert.ok(ids.has(id), `${id}: historical claims, estimates and unknown currencies remain visible`);
|
||||
for (const id of ["O050", "O052", "O053", "O055", "SR-O-AI067-runrate", "ROCKET-O-arr", "AQ-O-RUNRATE"])
|
||||
assert.ok(!ids.has(id), `${id}: excluded or ambiguous records cannot enter the ARR ledger`);
|
||||
assert.equal(rows.find((row) => row.observation.id === "O066")?.observation.selection, "historical-estimate");
|
||||
assert.equal(rows.find((row) => row.observation.id === "O046")?.observation.unit, "原文$,币种待核");
|
||||
for (const id of ["O045", "O047"])
|
||||
assert.equal(rows.find((row) => row.observation.id === id)?.observation.unit, "原文$,币种待核",
|
||||
"current subscription prices cannot establish the currency of a historical ARR claim");
|
||||
const category = snapshot.companies.find((company) => company.id === "AI055")!.cat;
|
||||
const filtered = arrDisclosures(snapshot, { category, foundedYear: 2025 });
|
||||
assert.ok(filtered.length > 0);
|
||||
assert.ok(filtered.every((row) => row.company.cat === category && row.company.year === 2025));
|
||||
// A future mistakenly typed target still must not enter the ledger.
|
||||
const target = snapshot.observations.find((item) => item.id === "O052")!;
|
||||
target.metric = "arr";
|
||||
target.company = "AI055";
|
||||
assert.ok(!arrDisclosures(snapshot).some((row) => row.observation.id === target.id));
|
||||
const claim = snapshot.observations.find((item) => item.id === "O065")!;
|
||||
claim.value = Number.NaN;
|
||||
assert.ok(!arrDisclosures(snapshot).some((row) => row.observation.id === claim.id));
|
||||
});
|
||||
|
||||
test("research revisions share the authentication database and preserve replay limits", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "site-research-"));
|
||||
@@ -23,8 +104,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, 1155);
|
||||
assert.equal(initial.evidence.length, 3426);
|
||||
assert.equal(initial.companies.length, 1196);
|
||||
assert.equal(initial.evidence.length, 4600);
|
||||
assert.equal(initial.observations.length, 453);
|
||||
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");
|
||||
@@ -64,8 +145,8 @@ test("research validation rejects broken evidence and unsafe source links", () =
|
||||
|
||||
test("reviews link explicit companies without turning global or excluded cases into companies", () => {
|
||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "companies").length, 198);
|
||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 21);
|
||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "companies").length, 207);
|
||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 139);
|
||||
assert.equal(snapshot.reviews.filter((r) => r.scope === "excluded-candidate").length, 44);
|
||||
const review = snapshot.reviews.find((r) => r.id === "R4")!;
|
||||
assert.deepEqual(reviewCompanyIds(snapshot, review), ["AI007", "AI013", "AI021", "AI034"]);
|
||||
@@ -85,11 +166,11 @@ test("reviews link explicit companies without turning global or excluded cases i
|
||||
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), 1155);
|
||||
assert.equal(summary.reduce((n, c) => n + c.count, 0), 1196);
|
||||
assert.equal(summary.reduce((n, c) => n + c.self, 0), 58);
|
||||
assert.equal(summary.reduce((n, c) => n + c.estimate, 0), 2);
|
||||
assert.equal(summary.reduce((n, c) => n + c.platform, 0), 19);
|
||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 1075);
|
||||
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 1117);
|
||||
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 === "公司整体"));
|
||||
@@ -124,8 +205,8 @@ test("service run rates, legacy freight revenue and paid adoption do not become
|
||||
|
||||
test("technical reviews keep accessible materials separate from tested capabilities", () => {
|
||||
const snapshot = structuredClone(seed) as ResearchSnapshot;
|
||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview).length, 138);
|
||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 23);
|
||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview).length, 181);
|
||||
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 25);
|
||||
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.match(snapshot.companies.find((c) => c.id === "AI051")!.technicalReview!.testScope!, /Sep 2026/);
|
||||
@@ -406,7 +487,7 @@ test("unreconciled directory financials remain source reviews instead of income
|
||||
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, 43);
|
||||
assert.equal(snapshot.candidateBatches!.flatMap((b) => b.records).filter((r) => r.status === "pending-year").length, 125);
|
||||
});
|
||||
|
||||
test("product pivots and founder histories do not become new company years or current revenue", () => {
|
||||
@@ -498,9 +579,9 @@ test("a second uncertain cohort admits evidenced AI tasks while preserving auton
|
||||
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(24, 48);
|
||||
assert.equal(batch.complete, false);
|
||||
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 13);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 11);
|
||||
for (const name of ["Albacore Inc.", "Perseus Defense", "Munify", "Ruvo", "DeepGrove"]) {
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 14);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 10);
|
||||
for (const name of ["Albacore Inc.", "Perseus Defense", "Munify", "Ruvo"]) {
|
||||
const row = batch.records.find((r) => r.name === name)!;
|
||||
assert.equal(row.status, "pending-ai");
|
||||
assert.equal(row.companyId, undefined, "autonomy, a domain or finance automation alone is insufficient");
|
||||
@@ -528,9 +609,9 @@ test("the third uncertain cohort retains shutdowns, unknown scale and mixed data
|
||||
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(48, 72);
|
||||
assert.equal(batch.complete, false);
|
||||
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 12);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 12);
|
||||
for (const name of ["Voltair", "Mirabelle", "ZeroSettle", "Kyten Technologies"]) {
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 15);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 9);
|
||||
for (const name of ["Mirabelle", "Kyten Technologies"]) {
|
||||
assert.equal(batch.records.find((r) => r.name === name)!.companyId, undefined);
|
||||
}
|
||||
const klaus = snapshot.companies.find((c) => c.id === "YC31084")!;
|
||||
@@ -556,8 +637,8 @@ test("the fourth uncertain cohort distinguishes product pivots, managed labor an
|
||||
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(72, 96);
|
||||
assert.equal(batch.complete, false);
|
||||
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 13);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 11);
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 15);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 9);
|
||||
assert.equal(batch.records.find((r) => r.name === "Orthogonal")!.companyId, "AI036");
|
||||
assert.equal(snapshot.companies.filter((c) => c.url.endsWith("/companies/orthogonal")).length, 1);
|
||||
const fern = snapshot.companies.find((c) => c.id === "YC31152")!;
|
||||
@@ -584,8 +665,8 @@ test("the fifth uncertain cohort separates dated ARR, service run rates, booking
|
||||
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(96, 120);
|
||||
assert.equal(batch.complete, false);
|
||||
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 15);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 9);
|
||||
assert.equal(batch.records.filter((r) => r.status === "linked").length, 17);
|
||||
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 7);
|
||||
assert.equal(batch.records.find((r) => r.name === "primitive")!.companyId, "AI002");
|
||||
assert.equal(batch.records.find((r) => r.name === "Result")!.companyId, "AI001");
|
||||
const result = snapshot.companies.find((c) => c.id === "AI001")!;
|
||||
@@ -709,8 +790,8 @@ test("all original AI-unclear candidates retain a unique follow-up without count
|
||||
assert.equal(rows.length, 192);
|
||||
assert.equal(new Set(rows.map((r) => r.url)).size, 192);
|
||||
assert.deepEqual(rows.map((r) => r.url), original.map((r) => r.source));
|
||||
assert.equal(rows.filter((r) => r.status === "linked").length, 110);
|
||||
assert.equal(rows.filter((r) => r.status === "pending-ai").length, 82);
|
||||
assert.equal(rows.filter((r) => r.status === "linked").length, 118);
|
||||
assert.equal(rows.filter((r) => r.status === "pending-ai").length, 74);
|
||||
assert.equal(snapshot.directoryRechecks!.filter((r) => r.ycId === 36088).length, 1);
|
||||
const everyapp = rows.find((r) => r.name === "EveryApp")!;
|
||||
assert.equal(everyapp.companyId, "AI074");
|
||||
|
||||
+23915
-230
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"sourceVersion": "2026-10-31-missing-year-4",
|
||||
"sourceVersion": "2026-10-31-ph-all25-1",
|
||||
"asOf": "2026-10-31",
|
||||
"metric": "platform-mrr-usd",
|
||||
"sampleUnit": "product-or-business",
|
||||
|
||||
Reference in New Issue
Block a user