From cce2fd858137576fb6ab63caa3b3397d1a8f65c3 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sat, 31 Oct 2026 23:36:33 +0800 Subject: [PATCH] Expose ARR disclosure gaps before statistical comparison --- apps/docs/src/components/ArrDisclosures.tsx | 29 ++++++++++++++++----- apps/docs/src/lib/research-statistics.ts | 14 ++++++++++ apps/docs/tests/research.test.ts | 21 ++++++++++++++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/apps/docs/src/components/ArrDisclosures.tsx b/apps/docs/src/components/ArrDisclosures.tsx index 1bd564f..adb1627 100644 --- a/apps/docs/src/components/ArrDisclosures.tsx +++ b/apps/docs/src/components/ArrDisclosures.tsx @@ -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 = { signal: "企业或当事方披露", estimate: "第三方估算", historical: "历史披露", "historical-estimate": "历史估算", }; +const gapLabels: Record = { + 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(""); + 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

2025—2026 年成立公司的 ARR 披露

-

这是跨时点证据表,共 {companies.size} 家公司、{rows.length} 条记录;同一公司的历史里程碑逐条保留,不能相加或视为独立公司。ARR 标签沿用原始披露,未证明经常性收入定义或审计状态;不纳入 MRR 图表,不计算同期行业均值或造假概率。仅收录明确标注的 ARR;定义冲突、目标、承诺、客户和前雇主收入排除,缺失不作零。

+

这是跨时点证据表,共 {companies.size} 家公司、{claims.length} 条记录;同一公司的历史里程碑逐条保留,不能相加或视为独立公司。ARR 标签沿用原始披露,未证明经常性收入定义或审计状态;不纳入 MRR 图表,不计算同期行业均值或造假概率。仅收录明确标注的 ARR;定义冲突、目标、承诺、客户和前雇主收入排除,缺失不作零。

+
@@ -32,6 +43,12 @@ export default function ArrDisclosures({ data, onSelect }: { data: ResearchSnaps )}
ARR 披露覆盖:按所选成立年份、每家公司计一次。点击分类筛选下方证据表。

分母含仅有目录资料的公司,覆盖率反映本次采集深度,不代表行业披露率。企业披露含历史里程碑,同一公司兼有披露和估算时归企业披露;缺少 ARR 的公司仍可能有其他收入。披露时点、币种与合同定义未对齐,不能将已披露少数公司的均值外推至整个分类,也不能由离散性判断财务造假。

+
@@ -42,11 +59,11 @@ export default function ArrDisclosures({ data, onSelect }: { data: ResearchSnaps - + ; })}
金额保留原币种、下界和估算身份;日期栏保留业务截至日缺项
公司 / 分类收费与交付模式原始 ARR 声明时点身份 / 限制证据
{categories.get(company.cat)} · {company.year} {company.businessModel ?? company.price} {item.bound !== "=" && item.bound}{new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 2 }).format(item.value)} {item.unit}{item.kind}{item.timing}{labels[item.selection]}{item.reason}{item.timing}{labels[item.selection]}{item.reason}{arrFieldGaps(item, data.asOf).map((id) => gapLabels[id]).join(";") || "基础字段齐全;定义、期间与主体仍须核实"} {url ? {item.evidence} ↗ : item.evidence}
- {!rows.length &&

当前筛选没有符合条件的已记录 ARR 声明;不代表这些公司收入为零。

} + {!rows.length &&

当前分类、成立年与补证筛选没有符合条件的已记录 ARR 声明;不代表这些公司收入为零。

}
; } diff --git a/apps/docs/src/lib/research-statistics.ts b/apps/docs/src/lib/research-statistics.ts index 2ad13d6..55dbf1b 100644 --- a/apps/docs/src/lib/research-statistics.ts +++ b/apps/docs/src/lib/research-statistics.ts @@ -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; diff --git a/apps/docs/tests/research.test.ts b/apps/docs/tests/research.test.ts index 9ddad38..a136e99 100644 --- a/apps/docs/tests/research.test.ts +++ b/apps/docs/tests/research.test.ts @@ -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;