Expose ARR disclosure gaps before statistical comparison

This commit is contained in:
2026-10-31 23:36:33 +08:00
parent ae84304e16
commit cce2fd8581
3 changed files with 57 additions and 7 deletions
+23 -6
View File
@@ -1,26 +1,37 @@
import { useState } from "react";
import { evidenceUrl, type ResearchSnapshot } from "@/lib/ai-research";
import { arrDisclosureCoverage, arrDisclosures } from "@/lib/research-statistics";
import { arrDisclosureCoverage, arrDisclosures, arrFieldGaps, type ArrFieldGap } from "@/lib/research-statistics";
const labels: Record<string, string> = {
signal: "企业或当事方披露", estimate: "第三方估算",
historical: "历史披露", "historical-estimate": "历史估算",
};
const gapLabels: Record<ArrFieldGap, string> = {
date: "缺有效财务截至日", currency: "USD 未确认(含其他币种)",
amount: "下界、近似或非精确金额", 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 [gap, setGap] = useState<ArrFieldGap | "">("");
const claims = arrDisclosures(data, { category, foundedYear: year ? Number(year) : undefined });
const rows = claims.filter(({ observation }) => !gap || arrFieldGaps(observation, data.asOf).includes(gap));
const gapCounts = (Object.keys(gapLabels) as ArrFieldGap[]).map((id) => ({
id, count: claims.filter(({ observation }) => arrFieldGaps(observation, data.asOf).includes(id)).length,
}));
const completeFields = claims.filter(({ observation }) => !arrFieldGaps(observation, data.asOf).length).length;
const coverage = arrDisclosureCoverage(data, year ? Number(year) : undefined);
const companies = new Set(rows.map((row) => row.company.id));
const companies = new Set(claims.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>
<p className="research-notice">这是跨时点证据表,共 {companies.size} 家公司、{claims.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>
<label>补证项目<select value={gap} onChange={(event) => setGap(event.target.value as ArrFieldGap | "")}><option value="">全部记录</option>{gapCounts.map((item) => <option key={item.id} value={item.id}>{gapLabels[item.id]}({item.count} 条)</option>)}</select></label>
</div>
<div className="research-table-wrap"><table>
<caption>ARR 披露覆盖:按所选成立年份、每家公司计一次。点击分类筛选下方证据表。</caption>
@@ -32,6 +43,12 @@ export default function ArrDisclosures({ data, onSelect }: { data: ResearchSnaps
</tr>)}</tbody>
</table></div>
<p className="research-muted">分母含仅有目录资料的公司,覆盖率反映本次采集深度,不代表行业披露率。企业披露含历史里程碑,同一公司兼有披露和估算时归企业披露;缺少 ARR 的公司仍可能有其他收入。披露时点、币种与合同定义未对齐,不能将已披露少数公司的均值外推至整个分类,也不能由离散性判断财务造假。</p>
<aside className="research-notice" aria-label="ARR比较前的补证检查">
<strong>ARR 比较前的补证检查</strong>
<p>当前分类及成立年下有 {claims.length} 条披露,{completeFields} 条同时具有有效日期、明确 USD、精确金额且不是第三方估算。补证筛选后显示 {rows.length} 条。</p>
<div className="income-options">{gapCounts.map((item) => <button key={item.id} aria-pressed={gap === item.id} onClick={() => setGap(gap === item.id ? "" : item.id)}>{gapLabels[item.id]}:{item.count}</button>)}</div>
<p>各项可重叠,不能相加为独立公司数。日期必须是经营指标截至日,网页访问、发布或更新时间不能代替;其他币种保留原值,不擅自换算。基础字段齐全也不证明可比,还需对齐收入定义、业务期间、收费模式、主体与独立验证;这些缺项不作为造假判定。</p>
</aside>
<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>
@@ -42,11 +59,11 @@ export default function ArrDisclosures({ data, onSelect }: { data: ResearchSnaps
<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>{item.timing}</td><td>{labels[item.selection]}<small>{item.reason}</small><small>{arrFieldGaps(item, data.asOf).map((id) => gapLabels[id]).join(";") || "基础字段齐全;定义、期间与主体仍须核实"}</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>}
{!rows.length && <p>当前分类、成立年与补证筛选没有符合条件的已记录 ARR 声明;不代表这些公司收入为零。</p>}
</section>;
}
+14
View File
@@ -31,6 +31,20 @@ export function arrDisclosureCoverage(snapshot: ResearchSnapshot, foundedYear?:
missing: companies.length - disclosed.size, records: claims.length };
});
}
export type ArrFieldGap = "date" | "currency" | "amount" | "estimate";
/** Prerequisites for inspecting exact, dated USD disclosures; never a comparability or audit verdict. */
export function arrFieldGaps(observation: ResearchObservation, asOf: string): ArrFieldGap[] {
const gaps: ArrFieldGap[] = [];
const day = calendarDay(observation.observedAt);
const end = calendarDay(asOf);
if (day === null || end === null || day > end) gaps.push("date");
if (observation.unit !== "USD") gaps.push("currency");
if (observation.bound !== "=" || !Number.isFinite(observation.value) || observation.value < 0) gaps.push("amount");
if (["estimate", "historical-estimate"].includes(observation.selection)) gaps.push("estimate");
return gaps;
}
export type IncomeStats = {
n: number; mean: number; median: number; min: number; max: number;
sampleDeviation: number | null; cv: number | null; meanMedianRatio: number | null;
+20 -1
View File
@@ -11,7 +11,26 @@ 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 { arrDisclosureCoverage, arrDisclosures, compareIncomeGroups, describeIncome, groupIncome, platformIncomeSample } from "../src/lib/research-statistics";
import { arrDisclosureCoverage, arrDisclosures, arrFieldGaps, compareIncomeGroups, describeIncome, groupIncome, platformIncomeSample } from "../src/lib/research-statistics";
test("ARR field checks distinguish missing dates, other currencies, bounds and estimates", () => {
const observation = { ...seed.observations[0], metric: "arr", unit: "USD", value: 100,
bound: "=", selection: "signal", observedAt: "2026-10-30" } as ResearchSnapshot["observations"][number];
assert.deepEqual(arrFieldGaps(observation, "2026-10-31"), []);
for (const observedAt of [undefined, "2026-02-30", "2026-11-01"])
assert.deepEqual(arrFieldGaps({ ...observation, observedAt }, "2026-10-31"), ["date"]);
assert.deepEqual(arrFieldGaps({ ...observation, unit: "EUR" }, "2026-10-31"), ["currency"],
"known foreign currencies stay distinct without an invented USD conversion");
assert.deepEqual(arrFieldGaps({ ...observation, unit: "原文$,币种待核", bound: ">",
selection: "historical-estimate", observedAt: undefined }, "2026-10-31"), ["date", "currency", "amount", "estimate"]);
assert.deepEqual(arrFieldGaps({ ...observation, value: Number.NaN }, "2026-10-31"), ["amount"]);
assert.deepEqual(arrFieldGaps({ ...observation, value: -1 }, "2026-10-31"), ["amount"]);
assert.deepEqual(arrFieldGaps({ ...observation, value: 0 }, "2026-10-31"), [], "disclosed zero is a valid amount");
const rows = arrDisclosures(seed as ResearchSnapshot);
assert.ok(rows.length > 0);
assert.ok(rows.every(({ observation: claim }) => arrFieldGaps(claim, seed.asOf).length > 0),
"the current ledger cannot be described as a complete dated exact USD sample");
});
test("income interpretations compare sufficient samples within a provider and known billing model", () => {
const snapshot = structuredClone(seed) as ResearchSnapshot;