Compare commits

...

174 Commits

Author SHA1 Message Date
codex-bot 34f7cae0fe Distinguish live ARR, contracted demand and professional service run rates 2026-11-01 01:09:45 +08:00
codex-bot 76aa80fedc Classify SR007 deployments and assess local free to cloud subscription economics 2026-11-01 01:03:48 +08:00
codex-bot 7901ab940e Reconcile SR007 AI scope and separate travel revenue from ARR claims 2026-11-01 00:56:54 +08:00
codex-bot d52dac64e5 Classify six SR007 companies and exclude projected revenue and client assets 2026-11-01 00:50:40 +08:00
codex-bot b3fcb9ad3a Separate contracted pipeline, pilot demand and cloud usage in SR007 research 2026-11-01 00:40:39 +08:00
codex-bot 91d6c22e6b Classify six SR007 follow-ups and preserve revenue attribution limits 2026-11-01 00:31:49 +08:00
Zayden-Jung e04ab96187 Merge pull request '统一 AI 公司研究:ARR 补证、商业模式初评与来源核查' (#42) from codex/ai-research-ph-all26 into main
Verify and deploy Tabularium / verify-and-deploy (push) Has been cancelled
Reviewed-on: #42
2026-10-31 16:30:24 +00:00
codex-bot eb7e758570 Register public SR007 roster and verify three new AI companies 2026-11-01 00:25:46 +08:00
codex-bot 5ba790eec2 Reconcile current YC coverage accounting with source boundaries 2026-11-01 00:14:51 +08:00
codex-bot ac9ddd9c27 Expose source candidates and unresolved classification gaps 2026-11-01 00:10:23 +08:00
codex-bot ea31287924 Keep webpage maintenance dates out of ARR financial cutoffs 2026-11-01 00:06:41 +08:00
codex-bot 5b31fa5b77 Connect evidence-backed business model assessments to company categories 2026-11-01 00:00:35 +08:00
codex-bot c833aa56c1 Trace DUIX and EZsite legacy businesses and licensing boundaries 2026-10-31 23:55:03 +08:00
codex-bot 0a640d75d4 Resolve Ada company registration and shared ChartGen identity 2026-10-31 23:50:49 +08:00
codex-bot 0bd2ce6b02 Cross-check Sacra candidates and classify General Intuition 2026-10-31 23:45:58 +08:00
codex-bot cce2fd8581 Expose ARR disclosure gaps before statistical comparison 2026-10-31 23:36:33 +08:00
codex-bot ae84304e16 Keep explicit ARR labels while guarding ambiguous income samples 2026-10-31 23:31:29 +08:00
codex-bot c6e5a7b05f Complete Product Hunt All snapshot and classify StoreClaw 2026-10-31 23:28:03 +08:00
Zayden-Jung 9bfd6891c3 Merge pull request '统一 AI 公司研究:ARR 分类表、名单补查与 CI 统计修复' (#41) from codex/ai-research-consolidated into main
Verify and deploy Tabularium / verify-and-deploy (push) Failing after 4m0s
Reviewed-on: #41
2026-10-31 15:25:36 +00:00
codex-bot 3557ee7e87 Audit Product Hunt All archive and preserve company boundaries 2026-10-31 23:18:18 +08:00
codex-bot cc6993de2a Separate asset returns, funding and material pilot evidence 2026-10-31 23:10:05 +08:00
codex-bot 462fe01df0 Clarify game tooling, wallet fees and hardware evidence 2026-10-31 23:05:52 +08:00
codex-bot 838c9f1bad Audit teleoperation, product continuity and trading fee boundaries 2026-10-31 22:56:48 +08:00
codex-bot 0d7c24c529 Separate research, prototypes and current AI product evidence 2026-10-31 22:52:07 +08:00
codex-bot 74cd21c6fa Verify HEVN AI assistant and separate mixed service revenue 2026-10-31 22:46:33 +08:00
codex-bot 3c14c6fdca Audit Andustry sourcing AI and distinguish transaction fee models 2026-10-31 22:36:42 +08:00
codex-bot a1587e86bb Audit physical product stages and SpotPay entity boundaries 2026-10-31 22:29:58 +08:00
codex-bot 17d6589db1 Classify Fort and Sequence with training and revenue boundaries 2026-10-31 22:25:18 +08:00
codex-bot 708f136163 Verify ZeroSettle hosted AI and separate mixed billing economics 2026-10-31 22:20:06 +08:00
codex-bot f74b268da4 research: retain unqualified MRR claims and robotics product-stage gaps 2026-10-31 22:10:17 +08:00
codex-bot 2f525f97e3 research: link Voltair inspection service and review post-acquisition billing 2026-10-31 22:05:44 +08:00
codex-bot 92f7bcffbd research: link Sava trust workflows and review service revenue boundaries 2026-10-31 21:59:37 +08:00
codex-bot 1ca8ab72e8 research: separate trial evidence capacity and payment pricing from ARR 2026-10-31 21:54:50 +08:00
codex-bot 2c1f35f000 research: link DeepGrove model artifacts and separate candidate revenue boundaries 2026-10-31 21:51:18 +08:00
codex-bot 5cbaa013a7 research: distinguish infrastructure delivery and customer earnings 2026-10-31 21:46:35 +08:00
codex-bot f72043a4ed research: revisit AI scope and corporate lineage for YC candidates 2026-10-31 21:43:16 +08:00
codex-bot 21fe8ded32 feat: explain income dispersion within comparable sample groups 2026-10-31 21:34:38 +08:00
codex-bot 3c631a43a6 research: distinguish Chinese startup and institution evidence 2026-10-31 21:31:41 +08:00
codex-bot 53df8a4a2b research: link ARR definitions to subscription economics 2026-10-31 21:27:13 +08:00
codex-bot 5b86f08959 research: verify ARR currency and current contract models 2026-10-31 21:23:02 +08:00
codex-bot 4330817138 feat: show ARR disclosure coverage by business category 2026-10-31 21:18:11 +08:00
codex-bot 72f0928efa research: verify Perfingo historical roster identity 2026-10-31 21:15:24 +08:00
codex-bot e113b74c41 research: resolve Nunatak unlabeled roster link 2026-10-31 21:11:24 +08:00
codex-bot a1184b31b8 research: reconcile all 2026 named roster URLs 2026-10-31 21:08:50 +08:00
codex-bot 14e0c8ac76 research: distinguish treatment, diagnostic and audio revenue 2026-10-31 21:05:14 +08:00
codex-bot ea3433bd60 research: separate health analysis fees and clinic collections 2026-10-31 21:01:57 +08:00
codex-bot dc17a4d802 research: verify health devices and bereavement AI boundaries 2026-10-31 20:59:22 +08:00
codex-bot f49b5b96e7 research: audit first four 2026 health roster entries 2026-10-31 20:56:05 +08:00
codex-bot f3d83901a1 research: audit 2026 biotech roster and evidence boundaries 2026-10-31 20:47:46 +08:00
codex-bot cb803662c3 research: audit 2026 manufacturing roster and delivery gaps 2026-10-31 20:39:27 +08:00
codex-bot 7fc6742129 research: audit 2026 space roster and delivery evidence 2026-10-31 20:33:14 +08:00
codex-bot 91d4734763 research: audit 2026 energy roster and industrial revenue scope 2026-10-31 20:27:48 +08:00
codex-bot 4269d05b2e research: audit 2026 consumer roster and hardware revenue scope 2026-10-31 20:23:38 +08:00
codex-bot 4be984fa0a research: audit 2026 education roster and revenue boundaries 2026-10-31 20:18:17 +08:00
codex-bot 643c0df7aa research: audit 2026 infrastructure and social impact roster 2026-10-31 20:11:19 +08:00
codex-bot 8eb3b888d1 补核2026零售媒体八项并隔离实物佣金与按篇服务收入 2026-10-31 20:02:39 +08:00
codex-bot e9f48d6d9c 补核2026营销八项并拆分公关合作成果与软件净收入 2026-10-31 19:57:28 +08:00
codex-bot a67a3b3b81 补核2026房地产六项并保留成立冲突与机器人交付边界 2026-10-31 19:51:15 +08:00
codex-bot d7251e892b 补核2026物流六项并拆分运输硬件与软件净收入 2026-10-31 19:45:30 +08:00
codex-bot c0f9ee50c1 Merge remote-tracking branch 'origin/main' into codex/ai-research-consolidated 2026-10-31 19:39:46 +08:00
codex-bot ad682df1cc 补核2026人才招聘五项并区分按次评估与订阅收入 2026-10-31 19:39:46 +08:00
codex-bot cf47c1af31 补核2026法律公共事务七项并保留主体与签名验证缺项 2026-10-31 19:34:59 +08:00
codex-bot 06a6883be3 补核2026金融十项并拆分客户资产与软件净收入 2026-10-31 19:24:13 +08:00
codex-bot 9c08d9dc0f 补核2026安全组13项并核实 HODOR 法国登记 2026-10-31 19:17:04 +08:00
codex-bot c0924df7c4 补齐2026企业组候选并核实 Optise 登记与收入边界 2026-10-31 19:09:34 +08:00
codex-bot 0e1e127d4b 补核2026企业组第二批并区分成果合同与平台净费 2026-10-31 19:00:57 +08:00
codex-bot a9e5ac6a87 补核2026企业组八项并拆分任务交易与软件收入 2026-10-31 18:54:23 +08:00
codex-bot b81e1ad76e 补核2026汽车交通六项及软硬件与租赁收入边界 2026-10-31 18:46:19 +08:00
codex-bot 1f87628076 建立2026竞赛名单对账并补核农业食品七项 2026-10-31 18:42:59 +08:00
codex-bot a6a1860ef1 核实七项原网址对应并保留 SaveFirst 沿革缺项 2026-10-31 18:37:54 +08:00
codex-bot 94dcac4e95 补齐健康最后八项并对账2025总入口候选 2026-10-31 18:33:58 +08:00
codex-bot ec0d9abb34 补核健康七项及设备会员与原始验证边界 2026-10-31 18:29:50 +08:00
codex-bot d2e83cd33b 补核健康第三批及研究验证与收购边界 2026-10-31 18:25:30 +08:00
codex-bot 69c3cc8e64 补核健康名单第二批及 Eos 既有主体关联 2026-10-31 18:21:57 +08:00
codex-bot cdb84376cb 补核健康名单首六项及软硬件收入口径 2026-10-31 18:17:42 +08:00
codex-bot fabfce1f4c 补核娱乐物流制造24项并收录 CosmicBrain 2026-10-31 18:10:52 +08:00
codex-bot 142fa62d10 补核清洁能源22项并对账2025竞赛总名单 2026-10-31 18:00:37 +08:00
codex-bot 868594c79a 补核航天防务7项与 HANCE 后继 Rapidly 主体 2026-10-31 17:50:19 +08:00
codex-bot 83f91f1b1e 补核农业食品14项并收录按店计费的 Clave 2026-10-31 17:42:18 +08:00
codex-bot f99a354db2 增加按分类与成立年份浏览的 ARR 披露表 2026-10-31 17:36:03 +08:00
codex-bot 6410535ed9 补核TechCrunch消费教育26项并拆分交易硬件与软件收入 2026-10-31 17:27:07 +08:00
codex-bot db3b21bf54 补核TechCrunch安全名单9项并区分旧主体与收入口径 2026-10-31 17:14:55 +08:00
codex-bot 7140ea0469 完成54项缺年池补查并按最新官方字段纳入Graphide 2026-10-31 17:07:43 +08:00
codex-bot 241c82aa88 同步研究快照的核查与技术统计测试断言 2026-10-31 16:47:31 +08:00
codex-bot 5d62dee4e9 补核6项缺年候选并拆分推理代收与客户价值 2026-10-31 16:44:22 +08:00
Zayden-Jung 543d6ed527 Merge pull request '补核18项AI公司缺年候选、主体沿革与收费边界' (#40) from codex/ai-research-consolidated into main
Verify and deploy Tabularium / verify-and-deploy (push) Failing after 4m1s
Reviewed-on: #40
2026-10-31 08:43:48 +00:00
codex-bot 445bfb37d0 补核6项缺年候选并区分旧业务与资金示例 2026-10-31 16:41:30 +08:00
codex-bot baaea4fc61 补核6项缺年候选并以登记证据纳入Zymbly 2026-10-31 16:38:01 +08:00
codex-bot ac04839571 补核6项缺年候选并保留旧品牌与服务收费边界 2026-10-31 16:34:12 +08:00
Zayden-Jung 7cecb9f753 Merge pull request '补核12项缺年AI候选,区分旧主体与成立缺项' (#39) from codex/ai-research-consolidated into main
Verify and deploy Tabularium / verify-and-deploy (push) Failing after 4m15s
Reviewed-on: #39
2026-10-31 08:33:16 +00:00
codex-bot a2a5958fc4 补核12项缺年候选并关联官方主体登记 2026-10-31 16:29:35 +08:00
Zayden-Jung 7cc2397103 Merge pull request '统一 AI 公司研究:504项主营、192项AI候选与6项成立冲突补核' (#38) from codex/ai-research-consolidated into main
Verify and deploy Tabularium / verify-and-deploy (push) Failing after 5m35s
Reviewed-on: #38
2026-10-31 08:27:07 +00:00
codex-bot feb4de220d 补核6项成立冲突并拆分旧年份与缺年候选 2026-10-31 04:48:47 +08:00
codex-bot 652b0fd539 完成192项AI不明名单补核并核对预订与交付口径 2026-10-31 04:37:52 +08:00
codex-bot 5ee04f26ae 补核24项AI候选并区分实体销售与预付收入 2026-10-31 04:26:29 +08:00
codex-bot 812749823b 补核24项AI不明候选并区分同名主体与流失收入 2026-10-31 04:16:25 +08:00
codex-bot 8a4df5c7ba Review fifth uncertain AI cohort and distinguish ARR from service and contract claims 2026-10-31 04:03:33 +08:00
codex-bot 8d886ad3cc Review fourth uncertain AI cohort and separate managed services from transaction flows 2026-10-31 03:51:44 +08:00
codex-bot 790dda09de Review third uncertain AI cohort and retain shutdown and mixed service evidence 2026-10-31 03:44:51 +08:00
codex-bot 5182eca961 Review second uncertain AI cohort and separate client and legacy financials 2026-10-31 03:38:16 +08:00
codex-bot 5cd8f831be Review uncertain AI candidates and preserve unresolved business scope 2026-10-31 03:31:34 +08:00
codex-bot a520694f9e Merge developer tag review into consolidated AI research 2026-10-31 03:17:15 +08:00
codex-bot dc373bad09 Merge model tag review into consolidated AI research 2026-10-31 03:17:08 +08:00
codex-bot dc2c3a22f3 Merge industry review into consolidated AI research 2026-10-31 03:17:02 +08:00
codex-bot fefa7c5ebd Merge enterprise review into consolidated AI research 2026-10-31 03:16:57 +08:00
codex-bot 5c2d9fd06c Complete remaining tag review and distinguish intent from revenue 2026-10-31 03:13:45 +08:00
codex-bot 85a0c534aa Review mixed directory tags and separate historical Mixy income 2026-10-31 03:06:48 +08:00
codex-bot d4736f7477 Complete enterprise-tag review and exclude prospective income claims 2026-10-31 02:59:56 +08:00
codex-bot f41d9e0d53 Review first enterprise-tag cohort and separate client metrics 2026-10-31 02:51:26 +08:00
codex-bot 1736912ecb Complete developer tag review and distinguish agent hosting cost models 2026-10-31 02:39:58 +08:00
codex-bot 45f1d1dba3 Review developer tag candidates and separate runtime and compute businesses 2026-10-31 02:32:24 +08:00
codex-bot 8d7b10df19 Review model tag candidates and separate licensing and revenue evidence 2026-10-31 02:24:38 +08:00
codex-bot d9d675ce56 Complete industry tag review and distinguish service revenue run rates 2026-10-31 02:17:19 +08:00
codex-bot b8a82359e3 Review 20 industry tag candidates and Litmus historical ARR 2026-10-31 02:05:45 +08:00
codex-bot 4682e3513e Review 20 tag-classified industry candidates and current AI relevance 2026-10-31 02:00:16 +08:00
codex-bot c0ff99e640 Complete remaining 39 industry classifications and revenue boundary checks 2026-10-31 01:53:42 +08:00
codex-bot b5e4f817fb Review 20 industry companies and separate services from model claims 2026-10-31 01:44:23 +08:00
codex-bot df227df18c Review 30 industry companies and distinguish revenue commitments 2026-10-31 01:37:48 +08:00
codex-bot 1e81887682 research: review regulated services and separate client assets from income 2026-10-31 01:23:41 +08:00
codex-bot c25fb79eee research: separate industry software from brokerage and operations services 2026-10-31 01:18:08 +08:00
codex-bot a8b054381f research: distinguish industry services and model training inputs 2026-10-31 01:12:51 +08:00
codex-bot 8e5df06fdd research: review industry workflows and historical ARR boundaries 2026-10-31 01:07:22 +08:00
codex-bot 4942b86a4a research: finish enterprise classification and separate Tasklet product history 2026-10-31 00:57:16 +08:00
codex-bot 7e4f4fe3e6 research: separate enterprise workflows and speech model evidence 2026-10-31 00:37:13 +08:00
codex-bot 1e716c36b2 research: review enterprise workflows and product pivots 2026-10-31 00:29:52 +08:00
Zayden-Jung ba042ed5ae Merge pull request '扩展 AI 公司研究并修复收入统计表布局' (#31) from codex/ai-research-cn-sme into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 9m0s
Reviewed-on: #31
2026-10-30 16:11:48 +00:00
codex-bot 0cc3815bd8 Review sales delivery models and Native founding conflict 2026-10-31 00:11:28 +08:00
codex-bot f49aac2778 Review creative and consumer classifications and historical income 2026-10-31 00:00:28 +08:00
codex-bot d588f2e1fb Review remaining robotics classifications and pilot contract boundaries 2026-10-30 23:49:14 +08:00
codex-bot ad1f351a6b Review software development classifications and delivery models 2026-10-30 23:40:12 +08:00
codex-bot f73d2631fb Review remaining model infrastructure classifications 2026-10-30 23:31:15 +08:00
codex-bot 6dca32dc66 Review compute infrastructure category boundaries 2026-10-30 23:21:43 +08:00
codex-bot 992275560f Resolve candidate years and preserve seller and legacy business boundaries 2026-10-30 23:12:15 +08:00
codex-bot 74c74ddda9 Expand platform income samples and derive report conclusions from current data 2026-10-30 22:58:47 +08:00
codex-bot 61201c1bb0 Review agent infrastructure classifications and financial service boundaries 2026-10-30 22:47:04 +08:00
codex-bot 4eb08c0857 Research 2025 Speedrun companies and separate pilot and client revenue 2026-10-30 22:35:50 +08:00
codex-bot 3e7792191e Audit biotech candidates and separate research milestones from revenue 2026-10-30 22:19:52 +08:00
codex-bot fa09a01421 Audit financial and real-estate candidates without treating asset values as ARR 2026-10-30 22:12:07 +08:00
Zayden-Jung 87987f6cc6 Merge pull request '支持基于内部摘要的章节级文章编辑' (#30) from codex/document-sections into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 8m39s
Reviewed-on: #30
2026-10-30 14:05:53 +00:00
codex-bot c9497ccc6b Audit visible government and legal startup candidates with source gaps 2026-10-30 22:04:11 +08:00
codex-bot 05b500e911 Refine physical industry classifications and preserve pivot revenue limits 2026-10-30 21:54:16 +08:00
codex-bot ea09b414a2 Merge income statistics layout fix into AI research delivery 2026-10-30 21:40:21 +08:00
codex-bot 945e7eadb2 Keep income statistics columns readable with grouped rows 2026-10-30 21:36:38 +08:00
codex-bot f86a4a75a0 Expand Speedrun companies and separate contract revenue claims 2026-10-30 21:20:12 +08:00
codex-bot ccba2c546a Expand Chinese SME AI delivery models and physical-AI candidates 2026-10-30 21:11:14 +08:00
Zayden-Jung 56b0ee60c0 Merge pull request '扩展中英文 AI 公司分类与收入核查' (#29) from codex/ai-research-source-lineage into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 6m33s
Reviewed-on: #29
2026-10-30 13:05:46 +00:00
codex-bot f7cdf16933 Audit four further TechCrunch candidates and bundled-service boundaries 2026-10-30 20:58:01 +08:00
codex-bot deadd24dce Verify AiMOGA founding and preserve interim revenue disclosure 2026-10-30 20:50:58 +08:00
codex-bot 4f1d3caafd Expand Chinese candidates and audit data-vendor revenue evidence 2026-10-30 20:40:17 +08:00
codex-bot 28ced713dc Preserve unspecified period of the AfterQuery run-rate claim 2026-10-30 20:22:00 +08:00
codex-bot c8c8bad563 Separate revenue run-rate claims from recurring contract evidence 2026-10-30 20:19:03 +08:00
codex-bot 13f2b31e8a Review model-category business boundaries and reconcile Eka history 2026-10-30 20:08:28 +08:00
codex-bot 9ee2bd219a Add chapter editing API with internal summaries and revision control 2026-10-30 19:58:40 +08:00
codex-bot 20e5097783 Record Nirva founder-reported team size and planned price 2026-10-30 19:55:45 +08:00
codex-bot ae4bd203a1 Add three model and wearable companies from archive discovery 2026-10-30 19:50:05 +08:00
codex-bot 5b6d3d9bfd Reconcile four pre-2025 businesses with original source evidence 2026-10-30 19:36:51 +08:00
Zayden-Jung 6b4fa711c1 Merge pull request '补核六项既有业务并保留 Sponstar 年份冲突' (#28) from codex/ai-research-year-proofs into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 8m34s
Reviewed-on: #28
2026-10-30 11:32:34 +00:00
codex-bot 76e32490e9 Verify six company histories and retain conflicting founding dates 2026-10-30 19:27:55 +08:00
Zayden-Jung fa0bcb15fb Merge pull request '补齐32项企业竞赛登记并核查19项既有业务' (#26) from codex/ai-research-2025-coverage into main
Verify and deploy Tabularium / verify-and-deploy (push) Has been cancelled
Reviewed-on: #26
2026-10-30 11:20:47 +00:00
codex-bot 12123c278f Resolve fourteen older Battlefield businesses with source evidence 2026-10-30 19:17:43 +08:00
codex-bot 4273dd77e0 Register all 32 enterprise Battlefield discovery entries 2026-10-30 19:09:13 +08:00
Zayden-Jung 3ab2f8e122 Merge pull request '汇总 AI 公司核查更新与文档上传 API' (#25) from codex/research-api-integration into main
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 7m33s
Reviewed-on: #25
2026-10-30 10:28:45 +00:00
codex-bot 15024c3908 merge: include latest company research updates from PR #23 2026-10-30 18:20:34 +08:00
codex-bot 770be83692 Backfill 2025 discovery ledger and verify public archive scope 2026-10-30 18:19:29 +08:00
codex-bot 083f532609 merge: integrate document upload API from PR #24 2026-10-30 18:17:53 +08:00
codex-bot 2b3706c90d merge: integrate company categorization and deployment fixes from PR #23 2026-10-30 18:17:53 +08:00
codex-bot f156406265 Classify Thrive services rollup and exclude Brila existing operator 2026-10-30 18:12:52 +08:00
codex-bot e65d90c78b Resolve Utopai founding history and preserve Fundraisly entity gaps 2026-10-30 18:10:35 +08:00
codex-bot 96f1417cda Add Goldfish with investor-backed founding-year evidence 2026-10-30 18:08:01 +08:00
codex-bot 347b9587c6 Reconcile Tapflow history and Atoms company-year evidence 2026-10-30 18:04:11 +08:00
codex-bot 677cd73945 Fix Peec team bounds regression assertion 2026-10-30 17:54:08 +08:00
codex-bot e075eee8a5 Separate legacy launches from unresolved Happycapy entity lineage 2026-10-30 17:41:15 +08:00
codex-bot d971362a97 Resolve four launch candidates through operator and app history evidence 2026-10-30 17:31:44 +08:00
codex-bot 0e5ccafa30 Classify remaining directory profiles using official business evidence 2026-10-30 17:21:06 +08:00
24 changed files with 87740 additions and 6076 deletions
+1
View File
@@ -4,6 +4,7 @@
- Web 技术栈为 TypeScript、React、Tailwind CSS、Next.js Pages Router;不要切换到 App Router。
- 使用 pnpm 管理 JavaScript workspace,使用 uv 管理 `tools/figures` 的 Python 项目,保留两套锁文件。
- 文档正文、目录元数据与修订记录以站点现有 SQLite 数据库为唯一权威源,使用文档 API 或 docs 管理工具直接更新;不得把新正文、目录 JSON、数据库文件或日常内容快照提交到仓库。历史 Git 文档只用于显式的一次性迁移,不作为运行时种子。Python 图形源码放在 `tools/figures/src/endor_figures`,可复现的 SVG 输出放在 `apps/docs/public/figures`。
- 编辑已有文章时优先使用章节 API:先读取章节目录与内部摘要,据此选择需要读取正文的章节,再提交单章或多章操作。摘要缺失时先读对应正文并补充;修改观点前必须核对该章原文,检查读取版本与提交 revision 一致。摘要只写入内部章节数据,不放入网页正文、目录摘要或 Git;改写正文时同步更新摘要。完整操作与 Python 客户端用法见 README 的「先读摘要,再按章节编辑」。
- 未确认的架构提议明确标注为草案,不能表述为现有实现。
- 本地开发与预览只监听 `127.0.0.1`。部署容器监听 `0.0.0.0:3000`,服务器入口默认 18135,可通过独立 production.env 配置。
- 对 gitea.entroia.com 仅使用 codex-bot,提交身份为 Codex Bot <codex-bot@gitea.entroia.local>。SSH 使用隔离配置:`ssh -F /dev/null -o IdentitiesOnly=yes -i /Users/zzyang/.codex/credentials/gitea-entroia/codex_entroia_ed25519 -p 22222`。
+50
View File
@@ -75,6 +75,10 @@ sudo /opt/endorphin-tabularium-docs/docs save < "$doc_work_dir/document.json"
| `GET /api/documents/item/<slug>?revision=1` | 读取指定修订的完整正文 |
| `POST /api/documents` | 保存一个完整文档 JSON;字段与 `docs save` 相同 |
| `POST /api/documents/batch` | 保存 `{ "documents": [...] }`,每个请求 1—100 篇 |
| `GET /api/documents/sections/<slug>` | 读取章节 ID、标题和内部摘要,不返回正文 |
| `GET /api/documents/sections/<slug>?section=<id>` | 读取选中章节正文;重复 `section` 可选多章 |
| `GET /api/documents/sections/<slug>?revision=2` | 读取对应修订的章节目录;也可搭配 `section` |
| `POST /api/documents/sections/<slug>` | 原子执行章节新增、更新、删除和移动 |
写入使用 `Authorization: Bearer <文档 API Key>`。新建传 `revision: 0`;更新先读取当前全文,再使用读到的 revision。每篇正文最多 2,000,000 个 JavaScript 字符单位;每次请求最多 8 MiB。目录和修订分页的 `limit` 范围为 1—100,响应包含 `total` 和 `nextOffset`。
@@ -82,6 +86,52 @@ sudo /opt/endorphin-tabularium-docs/docs save < "$doc_work_dir/document.json"
建议写入时提供 `Idempotency-Key`,由 1—128 个字母、数字或 `._:-` 组成。同一个 Key、相同请求 ID 和相同输入重试时,返回原回执并标记 `replayed: true`,不追加修订;相同 ID 改换输入会冲突。回执描述原请求,不代表之后没有其他修改;需要当前内容时重新读取文档。改过正文或 revision 后,应使用新的请求 ID。
### 先读摘要,再按章节编辑
章节接口使用相同的文档 Key 和读写权限。编辑流程为:先读章节目录及摘要,选择需要深入的章节,再读取这些章节的正文,最后提交一组操作。不要只凭摘要修改未读正文;目录与正文响应的 `revision` 应一致,否则重新读取。接口不调用模型生成摘要:由编辑 Agent 阅读正文、撰写摘要,并在编辑请求中保存。
章节按行首 Markdown ATX 二级标题(`##`)划分,三级至六级标题随所属章节一起编辑;代码围栏内的标题不会切章。二级标题之前的文档标题及导言属于固定 ID `preamble`,可以修改但不能删除或移动。Setext 标题、带缩进或引用、列表标记的标题不作为章节边界。正文无行首二级标题时,全文作为导言处理。拆分及组合保持换行和未编辑内容原样,不自动格式化 Markdown。
目录中的每章包含 `id`、`title`、`position`、`summary`、`summaryStatus` 和 `contentHash`。摘要为编辑内部数据,单独保存在 SQLite 的 `document_sections` 和 `document_section_revisions` 中,不加入普通文档响应、网页 props、搜索索引或页面正文。完整数据库备份包含这些表;`docs export` 仍只导出当前文档,不包含内部摘要和修订。
旧文章无需重写,首次读取即可按原文返回目录,初始摘要为空,`summaryStatus` 为 `missing`。Agent 应按需读取这些章节,生成摘要并通过 `update` 保存;保存后状态为 `current`。这里的 `current` 仅表示摘要与该版正文一起保存,不保证模型的总结正确。全文接口仍兼容;未变化章节保留摘要与 ID,正文变化的章节清空摘要,等待更新。全文改名且改写章节时可能产生新 ID,因此全文保存后应重新读取目录。
章节编辑请求示例(示例 ID 需要替换为目录实际返回的值):
```json
{
"revision": 3,
"operations": [
{
"op": "update",
"id": "s_existing",
"content": "## 原有章节\n\n修改后的正文。\n\n",
"summary": "本章说明修改后的核心观点与边界。"
},
{
"op": "insert",
"after": "s_existing",
"content": "## 新章节\n\n新增论证。\n\n",
"summary": "补充上一章尚未覆盖的论证。"
}
]
}
```
`update` 可只改 `summary`,也可改正文;正文改变时若不提供新摘要,旧摘要会清空。`insert` 必须提供正文和非空摘要;摘要最多 8000 个 JavaScript 字符单位。`delete` 使用 `{ "op": "delete", "id": "s_existing" }`;`move` 使用 `{ "op": "move", "id": "s_existing", "after": "preamble" }`。按数组顺序执行,最多 100 个操作;新章 ID 由服务器生成,在回执目录中返回。章正文应从自己的 `##` 标题开始,只包含一个二级标题,并保留与下一章之间的换行;跨章代码围栏或章节边界改变会被拒绝,拆章请使用多个操作。
章节变更、内部摘要、组合后的全文和历史快照在同一事务内提交。每批操作只增加一次文章 `revision`;摘要单独更新也增加版本号。后续操作失败或全文版本过期时全部回滚。写入沿用幂等规则,回执包含新版本、章节目录和 `replayed`;章节接口与全文接口的幂等记录分开。修改章节不会改变其 ID,移动亦保留 ID;删除后的正文和摘要仍可从对应的旧修订读取。启用前的历史修订只有原文,摘要显示缺失。
Python 客户端对应命令:
```sh
python3 scripts/documents-client.py outline endor/endor-purpose-draft
python3 scripts/documents-client.py sections endor/endor-purpose-draft preamble s_existing
python3 scripts/documents-client.py edit-sections endor/endor-purpose-draft /private/path/edit.json --request-id chapter-edit-1
```
`outline` 和 `sections` 支持 `--revision`;请求文件只包含 `revision` 和 `operations`,不要把内部摘要文件提交到 Git。
### 创建、查看与撤销 Key
管理员只需通过已验证的服务器连接进行 Key 管理:
+3 -3
View File
@@ -182,10 +182,10 @@ 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, 1021);
assert.equal(research.companies.length, 1232);
assert.equal(research.categories.length, 11);
assert.equal(research.evidence.length, 1215);
assert.equal(research.observations.length, 189);
assert.equal(research.evidence.length, 4797);
assert.equal(research.observations.length, 475);
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 });
assert.equal(researchPage.status, 200);
+14 -8
View File
@@ -1,15 +1,18 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/router";
import IncomeDistribution from "./IncomeDistribution";
import ArrDisclosures from "./ArrDisclosures";
import BusinessModelAssessment from "./BusinessModelAssessment";
import ResearchCandidates from "./ResearchCandidates";
import {
categorySummary, evidenceUrl, technicalReviewLabels, reviewCompanyIds, type ResearchEvidence, type ResearchReview, type ResearchSnapshot,
} from "@/lib/ai-research";
const pageSize = 12;
const evidencePageSize = 30;
const revenueLabels = { self: "公司披露,待核口径", estimate: "仅第三方估算", platform: "仅支付平台记录", unknown: "暂无可用收入数字" };
const revenueLabels = { self: "企业披露,待核口径", estimate: "仅第三方估算", platform: "仅支付平台记录", unknown: "暂无可用收入数字" };
const useLabels: Record<string, string> = {
platform: "支付平台记录", signal: "公司披露信号", estimate: "第三方估算", historical: "历史里程碑",
platform: "支付平台记录", signal: "企业披露信号", estimate: "第三方估算", historical: "历史里程碑",
"historical-estimate": "其他时点估算", excluded: "排除收入比较", scale: "团队规模", adoption: "采用规模",
funding: "融资", technical: "第三方技术评测", crosscheck: "同一里程碑的其他资料",
};
@@ -55,7 +58,7 @@ export default function AICompanyResearch() {
: router.isReady && typeof linkedCompany === "string" && data?.companies.some((c) => c.id === linkedCompany)
? linkedCompany : null;
function setSelected(id: string | null) { setSelection({ route: router.asPath, id }); }
const [tab, setTab] = useState<"companies" | "revenue" | "sources" | "reviews">("companies");
const [tab, setTab] = useState<"companies" | "candidates" | "revenue" | "models" | "sources" | "reviews">("companies");
const companySection = useRef<HTMLElement>(null);
const detailSection = useRef<HTMLElement>(null);
useEffect(() => {
@@ -127,7 +130,7 @@ export default function AICompanyResearch() {
<section ref={companySection} className="research-section" aria-labelledby="research-records-title">
<h2 id="research-records-title">研究数据</h2>
<div className="research-tabs" role="group" aria-label="研究数据视图">
{([['companies','公司与产品'],['revenue','收入观测'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) =>
{([['companies','公司与产品'],['candidates','候选与缺项'],['revenue','收入观测'],['models','商业模式初评'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) =>
<button key={id} aria-pressed={tab === id} onClick={() => setTab(id)}>{label}</button>)}
</div>
{tab === "companies" && <>
@@ -138,7 +141,7 @@ export default function AICompanyResearch() {
<label>资料深度<select value={depth} onChange={(e) => { setDepth(e.target.value); setPage(0); }}><option value="">全部资料</option><option value="case">原有个案研究</option><option value="profile">目录基础资料</option></select></label>
<label>记录团队规模<select value={teamBand} onChange={(e) => { setTeamBand(e.target.value); setPage(0); }}><option value="">全部规模</option><option value="small">1—5 人</option><option value="medium">6—20 人</option><option value="large">21—50 人</option><option value="xl">至少 51 人</option><option value="unknown">未披露或分组不确定</option></select></label>
<label>技术验证证据<select value={technicalLevel} onChange={(e) => { setTechnicalLevel(e.target.value); setPage(0); }}><option value="">全部验证层级</option>{Object.entries(technicalReviewLabels).map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></label>
<label>收入资料<select value={income} onChange={(e) => { setIncome(e.target.value); setPage(0); }}><option value="">全部状态</option><option value="self">找到公司披露</option><option value="estimate">仅第三方估算</option><option value="platform">仅支付平台记录</option><option value="unknown">暂无可用数字</option></select></label>
<label>收入资料<select value={income} onChange={(e) => { setIncome(e.target.value); setPage(0); }}><option value="">全部状态</option><option value="self">找到企业披露</option><option value="estimate">仅第三方估算</option><option value="platform">仅支付平台记录</option><option value="unknown">暂无可用数字</option></select></label>
</div>
<p className="research-muted">技术验证层级记录我们查到了什么,不是能力强弱评分。技术专项评审与业务分类复核分别记录。</p>
<div className="research-result-count" role="status">{filtered.length} 条匹配记录<button onClick={() => { setCategory(""); setQuery(""); setCohort(""); setIncome(""); setDepth(""); setTeamBand(""); setTechnicalLevel(""); setPage(0); }}>清除筛选</button></div>
@@ -152,16 +155,19 @@ export default function AICompanyResearch() {
{!filtered.length && <p className="research-message">没有匹配公司,请调整筛选条件。</p>}
{filtered.length > pageSize && <nav className="research-pagination" aria-label="公司数据分页"><button disabled={page === 0} onClick={() => setPage((n) => n - 1)}>上一页</button><span>{page + 1} / {Math.ceil(filtered.length / pageSize)}</span><button disabled={(page + 1) * pageSize >= filtered.length} onClick={() => setPage((n) => n + 1)}>下一页</button></nav>}
</>}
{tab === "models" && <BusinessModelAssessment data={data} onSelect={setSelected} onChooseCategory={chooseCategory} />}
{tab === "candidates" && <ResearchCandidates data={data} onSelect={setSelected} onChooseCategory={chooseCategory} />}
{tab === "revenue" && <>
<IncomeDistribution data={data} onSelect={setSelected} />
<h3>公司披露与估算的历史里程碑</h3>
<p className="research-notice">这一组包含 {disclosedCompanies} 家公司的披露或估算信号,多为异时点历史里程碑,ARR 与年化收入口径仍待核对,暂不计算其同期领域均值。币种未确认的记录仅显示原值,不绘制美元长度条。上面的平台MRR统计与这一组分开。</p>
<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}>
<div><button onClick={() => setSelected(o.company)}>{o.name}</button><strong>{o.bound !== "=" && o.bound}{money(o.value, o.unit)}</strong></div>
{o.unit === "USD" && <span className="research-bar" data-estimate={o.selection === "estimate" || undefined} aria-hidden="true"><i style={{width: `${o.value / Math.max(1, ...observed.filter((v) => v.unit === "USD").map((v) => v.value)) * 100}%`}} /></span>}
<small>{o.kind} · {useLabels[o.selection]} · {o.timing}</small>
</div>)}</div>
<div className="research-table-wrap"><table><caption>当前研究数据中的收入可得性,目录项未逐一检索;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>公司披露数</th><th>仅估算数</th><th>仅平台数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.platform}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
<div className="research-table-wrap"><table><caption>当前研究数据中的收入可得性,目录项未逐一检索;未建立同期、同龄、同口径比较组</caption><thead><tr><th>业务分类</th><th>企业披露数</th><th>仅估算数</th><th>仅平台数</th><th>暂无可用数字</th><th>同期均值 / 离散度</th></tr></thead><tbody>{summaries.map((c) => <tr key={c.id}><td><button onClick={() => chooseCategory(c.id)}>{c.name}</button></td><td>{c.self}</td><td>{c.estimate}</td><td>{c.platform}</td><td>{c.unknown}</td><td>条件不足</td></tr>)}</tbody></table></div>
<p className="research-muted">离散度用于筛选值得核查的异常,还需排除公司年龄、口径和披露偏差;不能据此认定财务造假。</p>
</>}
{tab === "sources" && <><p className="research-muted">来源链接保留原始位置。相同 URL 的记录可能属于同一披露,不能按记录数计算独立证据数量。</p><ul className="research-evidence-list">{data.evidence.slice(sourcePage * evidencePageSize, (sourcePage + 1) * evidencePageSize).map((e) => <Evidence key={e.id} item={e} />)}</ul>{data.evidence.length > evidencePageSize && <nav className="research-pagination" aria-label="原始证据分页"><button disabled={sourcePage === 0} onClick={() => setSourcePage((n) => n - 1)}>上一页</button><span>{sourcePage + 1} / {Math.ceil(data.evidence.length / evidencePageSize)}</span><button disabled={(sourcePage + 1) * evidencePageSize >= data.evidence.length} onClick={() => setSourcePage((n) => n + 1)}>下一页</button></nav>}</>}
@@ -0,0 +1,69 @@
import { useState } from "react";
import { evidenceUrl, type ResearchSnapshot } from "@/lib/ai-research";
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 [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(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} 家公司、{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>
<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>
<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>
<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><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>}
</section>;
}
@@ -0,0 +1,32 @@
import { businessModelReviewType, evidenceUrl, reviewCompanyIds, type ResearchSnapshot } from "@/lib/ai-research";
export default function BusinessModelAssessment({ data, onSelect, onChooseCategory }: {
data: ResearchSnapshot; onSelect: (id: string) => void; onChooseCategory: (id: string) => void;
}) {
const reviews = data.reviews.filter((review) => review.type === businessModelReviewType && review.scope === "companies");
const evidence = new Map(data.evidence.map((item) => [item.id, item]));
return <section aria-labelledby="research-models-title">
<h3 id="research-models-title">商业模式初评</h3>
<p>从已核个案提炼收费方式、成本约束与下一步验证条件。已知事实与研究判断分列;同一种收费方式可以跨业务分类,以下个案不能代表整个行业。</p>
<p className="research-muted">这是基于当前证据的初步评估,不是收入预测或盈利排名。判断能否用于我们的业务,还需用实际客户任务、付费留存与交付成本验证;ARR 或平台 MRR 单独不能证明利润。</p>
{reviews.length ? <div className="research-table-wrap"><table>
<caption>收费模式与业务分类、公司明细及原始证据相互关联</caption>
<thead><tr><th scope="col">模式 / 业务分类</th><th scope="col">已核事实</th><th scope="col">初步判断 / 验证条件</th><th scope="col">参考个案与证据</th></tr></thead>
<tbody>{reviews.map((review) => {
const companyIds = new Set(reviewCompanyIds(data, review));
const companies = data.companies.filter((company) => companyIds.has(company.id));
const categories = data.categories.filter((category) => companies.some((company) => company.cat === category.id));
return <tr key={review.id}>
<td><strong>{review.company}</strong><div>{categories.map((category) => <button key={category.id} onClick={() => onChooseCategory(category.id)}>{category.name}</button>)}</div></td>
<td>{review.summary}</td><td>{review.handling}</td>
<td><div>{companies.map((company) => <button key={company.id} className="research-company-name" onClick={() => onSelect(company.id)}>{company.name}</button>)}</div>
<div>{review.evidenceIds.map((id) => {
const item = evidence.get(id); const url = item && evidenceUrl(item.url);
return item && url ? <p key={id}><a href={url} target="_blank" rel="noopener noreferrer">{item.title} ↗</a></p> : null;
})}</div>
</td>
</tr>;
})}</tbody>
</table></div> : <p>当前数据版本尚未记录商业模式初评。</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>)}
@@ -75,7 +90,7 @@ export default function IncomeDistribution({ data, onSelect }: {
</svg></div>
<figcaption>圆点:产品观测 竖线:样本均值 菱形:样本中位数。业务类别标题下各行按支付平台{splitModels && "及收费与交付模式"}分别统计;n为该组产品数。点击点或下表产品名可看原始证据。</figcaption>
</figure>
<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}>
<div className="research-table-wrap"><table className="income-statistics"><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}>
{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>
@@ -0,0 +1,75 @@
import { useMemo, useState } from "react";
import { evidenceUrl, type ResearchCandidateBatch, type ResearchSnapshot } from "@/lib/ai-research";
type CandidateStatus = ResearchCandidateBatch["records"][number]["status"];
const statusLabels: Record<CandidateStatus, string> = {
"pending-year": "成立年或主体待核", "pending-ai": "AI 主营待核", linked: "已关联公司",
"existing-outside-cohort": "已关联范围外对照", "outside-cohort": "范围外候选", promotion: "推广活动",
};
const pageSize = 20;
const sourceNames: Record<string, string> = {
S01: "Y Combinator", S02: "TechCrunch AI", S04: "Product Hunt", S05: "Sacra",
S10: "机器之心", S11: "36氪", S13: "TrustMRR", S14: "a16z Speedrun",
};
const sourceLabel = (id: string) => `${id} · ${sourceNames[id] ?? "其他来源"}`;
function SourceLink({ url, label }: { url: string | null; label: string }) {
const safe = url && evidenceUrl(url);
return safe ? <a href={safe} target="_blank" rel="noopener noreferrer">{label} ↗</a> : <span>{label}(链接未取得)</span>;
}
export default function ResearchCandidates({ data, onSelect, onChooseCategory }: {
data: ResearchSnapshot; onSelect: (id: string) => void; onChooseCategory: (id: string) => void;
}) {
const [source, setSource] = useState("");
const [batchId, setBatchId] = useState("");
const [category, setCategory] = useState("");
const [status, setStatus] = useState<CandidateStatus | "pending" | "">("pending");
const [query, setQuery] = useState("");
const [page, setPage] = useState(0);
const all = useMemo(() => (data.candidateBatches ?? []).flatMap((batch) => batch.records.map((record) => ({ batch, record }))), [data.candidateBatches]);
const sources = [...new Set(all.map(({ batch }) => batch.sourceId))].sort();
const batches = (data.candidateBatches ?? []).filter((batch) => !source || batch.sourceId === source);
const matched = all.filter(({ batch, record }) => (!source || batch.sourceId === source)
&& (!batchId || batch.id === batchId) && (!category || record.categoryId === category)
&& `${record.name} ${record.id} ${record.note}`.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
const pending = (value: CandidateStatus) => value === "pending-year" || value === "pending-ai";
const rows = matched.filter(({ record }) => !status || (status === "pending" ? pending(record.status) : record.status === status));
const companies = new Map(data.companies.map((company) => [company.id, company]));
const evidence = new Map(data.evidence.map((item) => [item.id, item]));
const categories = new Map(data.categories.map((item) => [item.id, item.name]));
return <section aria-labelledby="research-candidates-title">
<h3 id="research-candidates-title">候选与缺项</h3>
<p>展示已登记的有限来源批次及后续核查。默认显示成立年、主体或 AI 主营待核的条目;待核不是确认排除,也不计入公司表。每行是一次发现位置,同一公司或产品可以重复出现。</p>
<p className="research-muted">候选初分类与公司表数量分开,来源批次不等全年或全站名单。原始发现范围及后续结论见每行说明;暂无收入资料不作零,推广活动也不算公司。</p>
<div className="research-filters">
<label className="research-query">搜索候选<input type="search" value={query} placeholder="名称、编号或核查说明" onChange={(event) => { setQuery(event.target.value); setPage(0); }} /></label>
<label>发现来源<select value={source} onChange={(event) => { setSource(event.target.value); setBatchId(""); setPage(0); }}><option value="">全部已登记来源</option>{sources.map((id) => <option key={id} value={id}>{sourceLabel(id)}</option>)}</select></label>
<label>发现批次<select value={batchId} onChange={(event) => { setBatchId(event.target.value); setPage(0); }}><option value="">全部批次</option>{batches.map((batch) => <option key={batch.id} value={batch.id}>{batch.id}</option>)}</select></label>
<label>候选初分类<select value={category} onChange={(event) => { setCategory(event.target.value); setPage(0); }}><option value="">全部分类</option>{data.categories.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}</select></label>
<label>核查状态<select value={status} onChange={(event) => { setStatus(event.target.value as typeof status); setPage(0); }}>
<option value="">全部状态({matched.length})</option><option value="pending">全部待核({matched.filter(({ record }) => pending(record.status)).length})</option>
{Object.entries(statusLabels).map(([id, label]) => <option key={id} value={id}>{label}({matched.filter(({ record }) => record.status === id).length})</option>)}
</select></label>
</div>
<p role="status">{rows.length} 个发现位置匹配;不是独立公司数。</p>
<div className="research-table-wrap"><table>
<caption>有限来源候选与处理记录;选择关联公司查看主数据,原始证据直接链接来源</caption>
<thead><tr><th scope="col">候选</th><th scope="col">初分类 / 状态</th><th scope="col">来源及范围</th><th scope="col">核查结论 / 缺项</th><th scope="col">关联公司 / 证据</th></tr></thead>
<tbody>{rows.slice(page * pageSize, (page + 1) * pageSize).map(({ batch, record }) => {
const company = record.companyId && companies.get(record.companyId);
return <tr key={record.id}>
<td><SourceLink url={record.url} label={record.name} /><small>{record.id}</small></td>
<td><button onClick={() => onChooseCategory(record.categoryId)} aria-label={`查看${categories.get(record.categoryId)}分类内的公司`}>{categories.get(record.categoryId)}</button><small>{statusLabels[record.status]}</small></td>
<td><SourceLink url={batch.url} label={sourceLabel(batch.sourceId)} /><small>{batch.id} · 读取 {batch.accessedAt}</small><details><summary>发现批次范围</summary><p>{batch.scope}</p><p>{batch.complete ? "该批次标记已完成;不代表来源全站覆盖。" : "该批次仍保留覆盖缺项。"}</p></details></td>
<td>{record.note}</td>
<td>{company ? <button className="research-company-name" onClick={() => onSelect(company.id)}>{company.name}</button> : <small>尚未关联公司主记录</small>}
{record.evidenceIds?.map((id) => { const item = evidence.get(id); return item ? <p key={id}><SourceLink url={item.url} label={id} /></p> : null; })}
</td>
</tr>;
})}</tbody>
</table></div>
{!rows.length && <p>当前筛选没有已登记的候选记录;不代表该来源或分类没有公司。</p>}
{rows.length > pageSize && <nav className="research-pagination" aria-label="候选与缺项分页"><button disabled={page === 0} onClick={() => setPage((value) => value - 1)}>上一页</button><span>{page + 1} / {Math.ceil(rows.length / pageSize)}</span><button disabled={(page + 1) * pageSize >= rows.length} onClick={() => setPage((value) => value + 1)}>下一页</button></nav>}
</section>;
}
+4 -3
View File
@@ -1,4 +1,5 @@
export type ResearchCategory = { id: string; name: string; description: string };
export const businessModelReviewType = "商业模式初评";
export const technicalReviewLabels = {
unreviewed: "技术证据尚未专项评审",
description: "已读技术自述",
@@ -48,7 +49,7 @@ export type ResearchCandidateBatch = {
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";
status: "pending-year" | "pending-ai" | "linked" | "existing-outside-cohort" | "outside-cohort" | "promotion";
companyId?: string; evidenceIds?: string[]; note: string;
}[];
};
@@ -125,13 +126,13 @@ export function validateResearch(snapshot: ResearchSnapshot) {
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) ||
!["pending-year", "pending-ai", "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))
(["outside-cohort", "pending-ai"].includes(row.status) && !row.evidenceIds?.length))
throw new Error(`Invalid research candidate: ${row.id}`);
}
}
+75 -5
View File
@@ -1,5 +1,9 @@
import { randomUUID, createHash } from "node:crypto";
import { openSiteDatabase } from "./site-database";
import { DocumentValidationError, DocumentConflictError } from "./document-errors";
import { sectionSchema, loadSections, reconcileSections, persistSections, splitSections, outline,
applySectionOperations, parseSectionEdit, type DocumentSection } from "./document-sections";
export { DocumentValidationError, DocumentConflictError } from "./document-errors";
export type DocLink = {
slug: string; sourceSlug: string; category: string; title: string; searchText?: string;
@@ -9,8 +13,6 @@ export type DocLink = {
export type Doc = DocLink & { content: string };
export type DocInput = Omit<Doc, "revision" | "searchText">;
export type DocumentExport = { format: "tabularium-documents-v1"; documents: Doc[] };
export class DocumentValidationError extends Error {}
export class DocumentConflictError extends Error {}
export type DocumentReceipt = Pick<Doc, "slug" | "sourceSlug" | "revision">;
export const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
@@ -36,6 +38,7 @@ function database() {
digest TEXT PRIMARY KEY, document_count INTEGER NOT NULL, created_at TEXT NOT NULL
);
`);
sectionSchema(db);
return db;
}
@@ -57,12 +60,13 @@ export function validateDocument(input: DocInput): void {
throw new DocumentValidationError("Use compressed WebP images instead of original rasters");
}
function write(db: ReturnType<typeof database>, input: DocInput, expectedRevision: number): Doc {
function write(db: ReturnType<typeof database>, input: DocInput, expectedRevision: number, editedSections?: DocumentSection[]): Doc {
validateDocument(input);
const existing = db.prepare("SELECT id, revision FROM documents WHERE source_slug=?")
.get(input.sourceSlug) as { id: string; revision: number } | undefined;
const existing = db.prepare("SELECT id, revision, payload FROM documents WHERE source_slug=?")
.get(input.sourceSlug) as { id: string; revision: number; payload: string } | undefined;
if ((existing?.revision ?? 0) !== expectedRevision) throw new DocumentConflictError("Document revision conflict");
const id = existing?.id ?? randomUUID();
const previous = existing ? loadSections(db, id, (JSON.parse(existing.payload) as Doc).content) : [];
const doc: Doc = { slug: input.slug, sourceSlug: input.sourceSlug, category: input.category,
title: input.title, group: input.group, collection: input.collection, description: input.description,
draft: input.draft, featured: input.featured, content: input.content, revision: expectedRevision + 1 };
@@ -80,9 +84,75 @@ function write(db: ReturnType<typeof database>, input: DocInput, expectedRevisio
db.prepare("INSERT OR IGNORE INTO document_aliases VALUES (?,?)").run(alias, id);
db.prepare("INSERT INTO document_revisions VALUES (?,?,?,?)")
.run(id, doc.revision, payload, new Date().toISOString());
persistSections(db, id, doc.revision, editedSections ?? reconcileSections(doc.content, previous));
return doc;
}
// Internal summaries never enter Doc, catalog, search or browser page props.
export function readDocumentSections(slug: string, revision?: number) {
if (!validSlug.test(slug)) throw new DocumentValidationError("Invalid document slug");
if (revision !== undefined && (!Number.isSafeInteger(revision) || revision < 1)) throw new DocumentValidationError("Invalid revision");
const db = database();
try {
db.exec("BEGIN");
const row = db.prepare(`SELECT d.id,d.payload FROM documents d JOIN document_aliases a
ON a.document_id=d.id WHERE a.alias=?`).get(slug) as { id: string; payload: string } | undefined;
if (!row) { db.exec("COMMIT"); return null; }
let doc = JSON.parse(row.payload) as Doc;
let sections: DocumentSection[];
if (revision === undefined || revision === doc.revision) sections = loadSections(db, row.id, doc.content);
else {
const prior = db.prepare("SELECT payload FROM document_revisions WHERE document_id=? AND revision=?")
.get(row.id, revision) as { payload: string } | undefined;
if (!prior) { db.exec("COMMIT"); return null; }
doc = JSON.parse(prior.payload) as Doc;
const snapshot = db.prepare("SELECT payload FROM document_section_revisions WHERE document_id=? AND revision=?")
.get(row.id, revision) as { payload: string } | undefined;
sections = snapshot ? JSON.parse(snapshot.payload) as DocumentSection[] : splitSections(doc.content);
}
db.exec("COMMIT");
return { slug: doc.slug, sourceSlug: doc.sourceSlug, title: doc.title, revision: doc.revision, sections };
} finally { db.close(); }
}
export function editDocumentSections(slug: string, value: unknown, request?: { namespace: string; id: string }) {
if (!validSlug.test(slug)) throw new DocumentValidationError("Invalid document slug");
const edit = parseSectionEdit(value);
if (request && (!request.namespace || !/^[A-Za-z0-9._:-]{1,128}$/.test(request.id))) throw new DocumentValidationError("Invalid Idempotency-Key");
const digest = createHash("sha256").update(JSON.stringify({ slug, edit })).digest("hex");
const namespace = request ? request.namespace + ":sections" : undefined;
const db = database();
try {
db.exec("BEGIN IMMEDIATE");
try {
if (request) {
const prior = db.prepare("SELECT digest,response FROM document_write_requests WHERE namespace=? AND request_id=?")
.get(namespace!, request.id) as { digest: string; response: string } | undefined;
if (prior) {
if (prior.digest !== digest) throw new DocumentConflictError("Idempotency-Key was used with different content");
db.exec("COMMIT");
return { ...JSON.parse(prior.response) as { slug: string; sourceSlug: string; revision: number; sections: ReturnType<typeof outline> }, replayed: true };
}
}
const row = db.prepare(`SELECT d.id,d.payload FROM documents d JOIN document_aliases a
ON a.document_id=d.id WHERE a.alias=?`).get(slug) as { id: string; payload: string } | undefined;
if (!row) { db.exec("COMMIT"); return null; }
const doc = JSON.parse(row.payload) as Doc;
if (doc.revision !== edit.revision) throw new DocumentConflictError("Document revision conflict");
const previous = loadSections(db, row.id, doc.content);
const sections = applySectionOperations(previous, edit.operations);
// Preserve the initial outline when upgrading an existing document for the first time.
db.prepare("INSERT OR IGNORE INTO document_section_revisions VALUES (?,?,?)").run(row.id, doc.revision, JSON.stringify(previous));
const saved = write(db, { ...doc, content: sections.map((s) => s.content).join("") }, doc.revision, sections);
const receipt = { slug: saved.slug, sourceSlug: saved.sourceSlug, revision: saved.revision, sections: outline(sections) };
if (request) db.prepare("INSERT INTO document_write_requests VALUES (?,?,?,?)")
.run(namespace!, request.id, digest, JSON.stringify(receipt));
db.exec("COMMIT");
return { ...receipt, replayed: false };
} catch (error) { db.exec("ROLLBACK"); throw error; }
} finally { db.close(); }
}
export function saveDocument(input: DocInput, expectedRevision: number): Doc {
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) throw new DocumentValidationError("Provide expected revision");
const db = database();
+2
View File
@@ -0,0 +1,2 @@
export class DocumentValidationError extends Error {}
export class DocumentConflictError extends Error {}
+144
View File
@@ -0,0 +1,144 @@
import { createHash, randomUUID } from "node:crypto";
import type { DatabaseSync } from "node:sqlite";
import { DocumentValidationError } from "./document-errors";
export type DocumentSection = {
id: string; title: string; content: string; summary: string;
};
export type SectionOperation =
| { op: "update"; id: string; content?: string; summary?: string }
| { op: "insert"; after: string; content: string; summary: string }
| { op: "delete"; id: string }
| { op: "move"; id: string; after: string };
export type SectionEdit = { revision: number; operations: SectionOperation[] };
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
// Preserve every byte, including CRLF and separators. H3-H6 belong to their H2 chapter.
export function splitSections(content: string): DocumentSection[] {
const starts: { offset: number; title: string }[] = [{ offset: 0, title: "导言" }];
let offset = 0;
let fence: { marker: string; length: number } | undefined;
for (const line of content.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
const opening = line.match(/^ {0,3}(`{3,}|~{3,})(.*?)(?:\r?\n)?$/);
if (fence) {
if (new RegExp(`^ {0,3}${fence.marker}{${fence.length},}[ \\t]*\\r?\\n?$`).test(line)) fence = undefined;
} else if (opening && !(opening[1][0] === "`" && opening[2].includes("`"))) {
fence = { marker: opening[1][0], length: opening[1].length };
} else {
const heading = line.match(/^##(?:[ \t]+(.*?))?[ \t]*\r?\n?$/);
if (heading) starts.push({ offset, title: (heading[1] ?? "").replace(/[ \t]+#+[ \t]*$/, "").trim() });
}
offset += line.length;
}
const seen = new Map<string, number>();
return starts.map((start, index) => {
const occurrence = seen.get(start.title) ?? 0;
seen.set(start.title, occurrence + 1);
return { id: index === 0 ? "preamble" : "s_" + hash(`${start.title}\0${occurrence}`).slice(0, 24),
title: start.title, content: content.slice(start.offset, starts[index + 1]?.offset), summary: "" };
});
}
export function sectionSchema(db: DatabaseSync) {
db.exec(`CREATE TABLE IF NOT EXISTS document_sections (
document_id TEXT PRIMARY KEY REFERENCES documents(id), payload TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS document_section_revisions (
document_id TEXT NOT NULL REFERENCES documents(id), revision INTEGER NOT NULL,
payload TEXT NOT NULL, PRIMARY KEY(document_id, revision)
);`);
}
export function loadSections(db: DatabaseSync, documentId: string, content: string): DocumentSection[] {
const row = db.prepare("SELECT payload FROM document_sections WHERE document_id=?")
.get(documentId) as { payload: string } | undefined;
if (!row) return splitSections(content);
const sections = JSON.parse(row.payload) as DocumentSection[];
// A legacy writer must not leave an apparently current outline over different Markdown.
return sections.map((s) => s.content).join("") === content ? sections : reconcileSections(content, sections);
}
export function reconcileSections(content: string, previous: DocumentSection[]): DocumentSection[] {
const unused = new Set(previous);
return splitSections(content).map((section) => {
const prior = section.id === "preamble" ? previous.find((p) => p.id === "preamble") :
previous.find((p) => p.id !== "preamble" && unused.has(p) && p.content === section.content) ??
previous.find((p) => p.id !== "preamble" && unused.has(p) && p.title === section.title);
if (prior) unused.delete(prior);
return { ...section, id: prior?.id ?? (section.id === "preamble" ? "preamble" : "s_" + randomUUID()),
summary: prior?.content === section.content ? prior.summary : "" };
});
}
export function persistSections(db: DatabaseSync, documentId: string, revision: number, sections: DocumentSection[]) {
const payload = JSON.stringify(sections);
db.prepare(`INSERT INTO document_sections VALUES (?,?) ON CONFLICT(document_id)
DO UPDATE SET payload=excluded.payload`).run(documentId, payload);
db.prepare("INSERT INTO document_section_revisions VALUES (?,?,?)").run(documentId, revision, payload);
}
export function outline(sections: DocumentSection[]) {
return sections.map(({ content, ...section }, position) => ({ ...section, position,
contentHash: hash(content), summaryStatus: section.summary.trim() ? "current" : "missing" }));
}
export function parseSectionEdit(value: unknown): SectionEdit {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new DocumentValidationError("Expected section edit object");
const row = value as Record<string, unknown>;
if (Object.keys(row).some((key) => !["revision", "operations"].includes(key)) ||
!Number.isSafeInteger(row.revision) || (row.revision as number) < 1 || !Array.isArray(row.operations) ||
!row.operations.length || row.operations.length > 100) throw new DocumentValidationError("Provide revision and 1 to 100 operations");
for (const raw of row.operations) {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new DocumentValidationError("Invalid operation");
const op = raw as Record<string, unknown>;
const fields: Record<string, string[]> = { update: ["op", "id", "content", "summary"],
insert: ["op", "after", "content", "summary"], delete: ["op", "id"], move: ["op", "id", "after"] };
if (typeof op.op !== "string" || !Object.hasOwn(fields, op.op) || Object.keys(op).some((key) => !fields[op.op as string].includes(key)))
throw new DocumentValidationError("Unknown operation or field");
for (const key of fields[op.op].filter((key) => !["content", "summary", "op"].includes(key)))
if (typeof op[key] !== "string" || !(op[key] as string).length) throw new DocumentValidationError("Missing section identifier");
for (const key of ["content", "summary"])
if (op[key] !== undefined && typeof op[key] !== "string") throw new DocumentValidationError(`Invalid ${key}`);
if (op.op === "update" && op.content === undefined && op.summary === undefined) throw new DocumentValidationError("Empty update");
if (op.op === "insert" && (typeof op.content !== "string" || typeof op.summary !== "string" || !op.summary.trim()))
throw new DocumentValidationError("Insert requires content and summary");
if (typeof op.summary === "string" && (!op.summary.trim() || op.summary.length > 8000))
throw new DocumentValidationError("Summary must contain 1 to 8000 characters");
}
return { revision: row.revision as number, operations: row.operations as SectionOperation[] };
}
export function applySectionOperations(previous: DocumentSection[], operations: SectionOperation[]) {
const sections = previous.map((section) => ({ ...section }));
function index(id: string) {
const result = sections.findIndex((s) => s.id === id);
if (result < 0) throw new DocumentValidationError("Unknown section id");
return result;
}
for (const op of operations) {
if (op.op === "insert") {
sections.splice(index(op.after) + 1, 0, { id: "s_" + randomUUID(), title: "", content: op.content, summary: op.summary });
} else {
const at = index(op.id);
if (op.op === "update") {
if (op.content !== undefined && op.content !== sections[at].content) sections[at].summary = "";
if (op.content !== undefined) sections[at].content = op.content;
if (op.summary !== undefined) sections[at].summary = op.summary;
} else {
if (op.id === "preamble") throw new DocumentValidationError("Cannot delete or move the preamble");
if (op.op === "delete") sections.splice(at, 1);
else {
if (op.id === op.after) throw new DocumentValidationError("Cannot move section after itself");
index(op.after);
const [section] = sections.splice(at, 1);
sections.splice(index(op.after) + 1, 0, section);
}
}
}
}
const content = sections.map((s) => s.content).join("");
const parsed = splitSections(content);
if (parsed.length !== sections.length || parsed.some((s, i) => s.content !== sections[i].content))
throw new DocumentValidationError("Each chapter must start with one H2 heading; preserve newline boundaries and balanced fences");
return sections.map((s, i) => ({ ...s, title: parsed[i].title }));
}
+65
View File
@@ -1,6 +1,50 @@
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 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;
@@ -80,3 +124,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] }];
});
}
@@ -0,0 +1,37 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { DocumentValidationError, readDocumentSections, editDocumentSections } from "@/lib/docs";
import { outline } from "@/lib/document-sections";
import { authorizeDocuments, documentApiError, idempotency } from "@/lib/documents-api";
export const config = { api: { bodyParser: { sizeLimit: "8mb" } } };
export default function handler(request: NextApiRequest, response: NextApiResponse) {
try {
const key = authorizeDocuments(request, response, request.method === "POST");
if (!key) return;
if (request.method !== "GET" && request.method !== "POST") {
response.setHeader("Allow", "GET, POST");
return response.status(405).end();
}
if (!Array.isArray(request.query.slug)) throw new DocumentValidationError("Invalid document slug");
const slug = request.query.slug.join("/");
if (Object.keys(request.query).some((k) => !["slug", "section", "revision"].includes(k)))
throw new DocumentValidationError("Unknown section query");
if (request.method === "POST") {
if (request.query.section !== undefined || request.query.revision !== undefined)
throw new DocumentValidationError("POST uses revision and operations in the body");
const saved = editDocumentSections(slug, request.body, idempotency(request, key.id));
return saved ? response.status(200).json(saved) : response.status(404).json({ code: "not_found" });
}
const revision = request.query.revision;
if (revision !== undefined && (typeof revision !== "string" || !/^[1-9]\d*$/.test(revision)))
throw new DocumentValidationError("Invalid revision");
const doc = readDocumentSections(slug, revision === undefined ? undefined : Number(revision));
if (!doc) return response.status(404).json({ code: "not_found" });
const selected = request.query.section;
if (selected === undefined) return response.status(200).json({ ...doc, sections: outline(doc.sections) });
const ids = typeof selected === "string" ? [selected] : selected;
if (!ids.length || ids.length > 100 || ids.some((id) => !id)) throw new DocumentValidationError("Select 1 to 100 sections");
if (ids.some((id) => !doc.sections.some((s) => s.id === id))) return response.status(404).json({ code: "not_found" });
return response.status(200).json({ ...doc, sections: doc.sections.filter((s) => ids.includes(s.id)) });
} catch (error) { return documentApiError(response, error); }
}
+7
View File
@@ -1473,6 +1473,13 @@ svg { flex-shrink: 0; }
.research-distribution { margin: 24px 0 38px; }
.research-distribution figure { margin: 20px 0; }
.ai-research .income-statistics { min-width: 1200px; }
/* Row-spanning category headings shift cell indexes between rows. */
.ai-research .income-statistics td { min-width: 0; }
.ai-research .income-statistics th[scope="rowgroup"] { min-width: 150px; white-space: nowrap; }
.ai-research .income-statistics th[scope="rowgroup"] small { display: block; margin-top: 5px; }
.ai-research .income-statistics td:not(:last-child) { white-space: nowrap; }
.ai-research .income-statistics td:last-child { min-width: 220px; }
.income-distribution-chart { display: block; width: 100%; min-width: 900px; height: auto; }
.income-distribution-chart [role="button"] { cursor: pointer; outline: none; }
.income-distribution-chart [role="button"]:focus circle:last-child { stroke: #b17725; stroke-width: 3; }
+123 -1
View File
@@ -7,8 +7,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import documents from "../src/pages/api/documents/index";
import batch from "../src/pages/api/documents/batch";
import item from "../src/pages/api/documents/item/[...slug]";
import sections from "../src/pages/api/documents/sections/[...slug]";
import { splitSections } from "../src/lib/document-sections";
import { openSiteDatabase } from "../src/lib/site-database";
import { createDocumentKey, authenticateDocumentKey, revokeDocumentKey, listDocumentKeys } from "../src/lib/document-keys";
import { allDocuments, documentHistory, readDocument, saveDocument, type Doc } from "../src/lib/docs";
import { allDocuments, documentHistory, readDocument, saveDocument, readDocumentSections, type Doc } from "../src/lib/docs";
import { newAuthConfig, signSession } from "../src/lib/auth";
const input: Doc = { slug: "endor/example", sourceSlug: "00-example", title: "Example", category: "Endor",
@@ -134,3 +137,122 @@ test("invalid batch members, duplicate identifiers, raw images and malformed fie
assert.equal(call(documents, { method: "DELETE", headers }).status, 405);
assert.equal(call(item, { headers, query: { slug: ["missing"] } }).status, 404);
}));
const chapterContent = "# Example\n\nIntroduction.\n\n## First\n\nFirst body.\n\n### Detail\n\nChild stays in chapter.\n\n## Second\n\nSecond body.\n";
type SectionResponse = { revision: number; replayed?: boolean;
sections: { id: string; title: string; summary: string; summaryStatus?: string; content?: string }[] };
const chapterQuery = { slug: input.slug.split("/") };
test("chapter parser preserves bytes, fences, duplicate headings and CRLF", () => {
const content = "# Title\r\n\r\n## Repeated\r\n~~~text\r\n## Inside fence\r\n~~~\r\n### Nested\r\n```js\r\n## Also inside\r\n````\r\n\r\n## Repeated\r\nTail";
const parts = splitSections(content);
assert.equal(parts.length, 3);
assert.notEqual(parts[1].id, parts[2].id);
assert.equal(parts.map((p) => p.content).join(""), content);
assert.equal(parts[1].title, "Repeated");
assert.ok(parts[1].content.includes("### Nested"));
assert.equal(splitSections("").length, 1);
assert.equal(splitSections("# Title\n\n- Item\n ## List detail\n\n> ## Quoted\n").length, 1);
});
test("outline reads existing Markdown without changing it and returns only selected bodies", () => isolated(({ headers }) => {
saveDocument({ ...input, content: chapterContent }, 0);
const db = openSiteDatabase();
db.exec("DELETE FROM document_sections; DELETE FROM document_section_revisions;"); db.close();
const result = call(sections, { headers, query: chapterQuery });
assert.equal(result.status, 200);
const outline = result.body as SectionResponse;
assert.equal(outline.revision, 1); assert.equal(outline.sections.length, 3);
assert.deepEqual(outline.sections.map((s) => s.title), ["导言", "First", "Second"]);
assert.ok(outline.sections.every((s) => s.content === undefined && s.summaryStatus === "missing"));
const selected = call(sections, { headers, query: { ...chapterQuery, section: outline.sections[1].id } }).body as SectionResponse;
assert.equal(selected.sections.length, 1);
assert.ok(selected.sections[0].content!.includes("### Detail"));
assert.equal(readDocument(input.slug)!.content, chapterContent);
assert.equal(call(sections, { headers, query: { ...chapterQuery, section: "missing" } }).status, 404);
assert.equal(call(sections, { headers, query: { ...chapterQuery, revision: "0" } }).status, 400);
}));
test("internal summaries persist separately and chapter updates leave other bytes untouched", () => isolated(({ headers }) => {
saveDocument({ ...input, content: chapterContent }, 0);
const before = readDocumentSections(input.slug)!;
const first = before.sections[1]; const second = before.sections[2];
const changed = first.content.replace("First body.", "Revised body.");
const edited = call(sections, { method: "POST", headers, query: chapterQuery, body: { revision: 1, operations: [
{ op: "update", id: first.id, content: changed, summary: "INTERNAL_SUMMARY_FIRST" },
{ op: "update", id: second.id, summary: "INTERNAL_SUMMARY_SECOND" },
] } });
assert.equal(edited.status, 200);
const after = readDocumentSections(input.slug)!;
assert.equal(after.revision, 2); assert.equal(after.sections[1].id, first.id);
assert.equal(after.sections[0].content, before.sections[0].content);
assert.equal(after.sections[2].content, before.sections[2].content);
assert.equal(after.sections[1].summary, "INTERNAL_SUMMARY_FIRST");
assert.equal(readDocument(input.slug)!.content, chapterContent.replace("First body.", "Revised body."));
assert.ok(!JSON.stringify(allDocuments()).includes("INTERNAL_SUMMARY"));
assert.ok(!JSON.stringify(call(item, { headers, query: chapterQuery }).body).includes("INTERNAL_SUMMARY"));
assert.ok(!JSON.stringify(call(documents, { headers }).body).includes("INTERNAL_SUMMARY"));
const history = readDocumentSections(input.slug, 1)!;
assert.equal(history.sections[1].content, first.content); assert.equal(history.sections[1].summary, "");
assert.equal(readDocumentSections(input.slug, 2)!.sections[1].summary, "INTERNAL_SUMMARY_FIRST");
}));
test("insert, move and delete chapters commit together with replay and conflict protection", () => isolated(({ headers }) => {
saveDocument({ ...input, content: chapterContent }, 0);
const before = readDocumentSections(input.slug)!;
const first = before.sections[1]; const second = before.sections[2];
const body = { revision: 1, operations: [
{ op: "move", id: second.id, after: "preamble" },
{ op: "delete", id: first.id },
{ op: "insert", after: second.id, content: "## New chapter\n\nNew body.\n", summary: "New material" },
] };
const options = { method: "POST", headers: { ...headers, "idempotency-key": "chapters-1" }, query: chapterQuery, body };
assert.equal(call(sections, options).status, 200);
const repeated = call(sections, options);
assert.equal(repeated.status, 200); assert.equal((repeated.body as SectionResponse).replayed, true);
const after = readDocumentSections(input.slug)!;
assert.deepEqual(after.sections.map((s) => s.title), ["导言", "Second", "New chapter"]);
assert.equal(after.sections[1].content, second.content);
assert.equal(after.revision, 2); assert.equal(documentHistory(input.sourceSlug).length, 2);
assert.equal(call(sections, { ...options, body: { ...body, revision: 2 } }).status, 409);
assert.equal(call(sections, { ...options, headers }).status, 409);
assert.equal(saveDocument({ ...readDocument(input.slug)!, description: "Changed metadata" }, 2).revision, 3);
assert.equal(readDocumentSections(input.slug)!.sections[2].summary, "New material");
}));
test("invalid late operation rolls back content, summary and history; read-only keys cannot edit", () => isolated(({ headers }) => {
saveDocument({ ...input, content: chapterContent }, 0);
const before = readDocumentSections(input.slug)!;
const invalid = [
[{ op: "update", id: before.sections[1].id, summary: "Should roll back" }, { op: "delete", id: "missing" }],
[{ op: "delete", id: "preamble" }],
[{ op: "move", id: before.sections[1].id, after: before.sections[1].id }],
[{ op: "update", id: before.sections[1].id, content: "## First\n\n## Extra\n" }],
[{ op: "update", id: before.sections[1].id, content: "## First\n![raw](/figures/raw.png)\n" }],
[{ op: "insert", after: "preamble", content: "## Missing summary\n" }],
[{ op: "delete", id: before.sections[1].id, unexpected: true }],
];
for (const operations of invalid) {
assert.equal(call(sections, { method: "POST", headers, query: chapterQuery, body: { revision: 1, operations } }).status, 400);
assert.deepEqual(readDocumentSections(input.slug), before);
assert.equal(documentHistory(input.sourceSlug).length, 1);
}
assert.equal(call(sections, { query: chapterQuery }).status, 401);
const reader = createDocumentKey("chapter reader", "read");
const readHeaders = { authorization: "Bearer " + reader.token };
assert.equal(call(sections, { headers: readHeaders, query: chapterQuery }).status, 200);
assert.equal(call(sections, { method: "POST", headers: readHeaders, query: chapterQuery, body: { revision: 1, operations: [] } }).status, 403);
}));
test("full-document saves invalidate changed chapter summaries and preserve unaffected summaries and ids", () => isolated(({ headers }) => {
saveDocument({ ...input, content: chapterContent }, 0);
const before = readDocumentSections(input.slug)!;
assert.equal(call(sections, { method: "POST", headers, query: chapterQuery, body: { revision: 1, operations:
before.sections.map((s) => ({ op: "update", id: s.id, summary: "Summary of " + s.title })) } }).status, 200);
saveDocument({ ...readDocument(input.slug)!, content: chapterContent.replace("First body.", "Changed externally.") }, 2);
const after = readDocumentSections(input.slug)!;
assert.equal(after.sections[1].id, before.sections[1].id);
assert.equal(after.sections[1].summary, "");
assert.equal(after.sections[2].summary, "Summary of Second");
assert.equal(readDocumentSections(input.slug, 2)!.sections[1].summary, "Summary of First");
}));
+662 -26
View File
@@ -8,10 +8,110 @@ import coverageAudit from "../../../content/research/ai-companies.coverage.json"
import incomeReference from "../../../content/research/ai-income-analysis.json";
import { readResearchSnapshot } from "../src/lib/ai-research-store";
import { openSiteDatabase } from "../src/lib/site-database";
import { categorySummary, evidenceUrl, reviewCompanyIds, validateResearch, type ResearchSnapshot } from "../src/lib/ai-research";
import { businessModelReviewType, 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, 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;
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,9 +123,14 @@ 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, 1021);
assert.equal(initial.evidence.length, 1215);
assert.equal(initial.observations.length, 189);
assert.equal(initial.companies.length, 1232);
assert.equal(initial.evidence.length, 4797);
assert.equal(initial.observations.length, 475);
const businessReviews = initial.reviews.filter((review) => review.type === businessModelReviewType);
assert.equal(businessReviews.length, 6);
assert.deepEqual(businessReviews, seed.reviews.filter((review) => review.type === businessModelReviewType)
.sort((a, b) => a.id.localeCompare(b.id)),
"business model assessments and company/evidence links persist in the shared database");
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");
@@ -64,9 +169,9 @@ 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, 54);
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 8);
assert.equal(snapshot.reviews.filter((r) => r.scope === "excluded-candidate").length, 16);
assert.equal(snapshot.reviews.filter((r) => r.scope === "companies").length, 234);
assert.equal(snapshot.reviews.filter((r) => r.scope === "dataset").length, 145);
assert.equal(snapshot.reviews.filter((r) => r.scope === "excluded-candidate").length, 53);
const review = snapshot.reviews.find((r) => r.id === "R4")!;
assert.deepEqual(reviewCompanyIds(snapshot, review), ["AI007", "AI013", "AI021", "AI034"]);
assert.deepEqual(reviewCompanyIds(snapshot, snapshot.reviews.find((r) => r.id === "CN-R-PSI")!), []);
@@ -85,20 +190,47 @@ 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), 1021);
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.platform, 0), 16);
assert.equal(summary.reduce((n, c) => n + c.unknown, 0), 978);
assert.equal(summary.reduce((n, c) => n + c.count, 0), 1232);
assert.equal(summary.reduce((n, c) => n + c.self, 0), 62);
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), 1149);
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 === "公司整体"));
const arena = snapshot.observations.find((o) => o.id === "COARENA-O-CUMULATIVE")!;
assert.equal(arena.selection, "historical", "cumulative launch revenue is not a recurring company-wide signal");
assert.equal(arena.comparisonGroup, null);
assert.ok(arena.metric !== "mrr" && arena.metric !== "arr", "a two-week cumulative figure cannot become MRR or ARR");
});
test("service run rates, legacy freight revenue and paid adoption do not become platform ARR", () => {
const snapshot = seed as ResearchSnapshot;
const runRate = snapshot.observations.find((o) => o.id === "INDUSTRY11-O-LOCKE-RUNRATE")!;
assert.equal(runRate.metric, "annualized-revenue");
assert.equal(runRate.selection, "historical");
assert.equal(runRate.observedAt, undefined);
assert.equal(runRate.comparisonGroup, null);
assert.equal(snapshot.companies.find((c) => c.id === "YC33283")?.revenueStatus, "self");
for (const id of ["TRUEMILE-OLD-REV", "VELTHA-DEMO", "LANTERN-OLD-REV"]) {
const observation = snapshot.observations.find((o) => o.id === `INDUSTRY11-O-${id}`)!;
assert.equal(observation.selection, "excluded");
assert.equal(observation.metric, undefined);
assert.equal(observation.comparisonGroup, null);
}
for (const [id, company] of [["VERDANT-DEPLOY", "YC32189"], ["SUTRIX-PAID", "YC36576"]]) {
const observation = snapshot.observations.find((o) => o.id === `INDUSTRY11-O-${id}`)!;
assert.equal(observation.selection, "adoption");
assert.equal(observation.metric, undefined);
assert.equal(snapshot.companies.find((c) => c.id === company)?.revenueStatus, "unknown");
}
assert.ok(platformIncomeSample(snapshot, { windowDays: 90 }).points.every((point) => !point.observation.id.startsWith("INDUSTRY11-")));
});
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, 14);
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 6);
assert.equal(snapshot.companies.filter((c) => c.technicalReview).length, 217);
assert.equal(snapshot.companies.filter((c) => c.technicalReview?.level === "public-artifact").length, 28);
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/);
@@ -137,6 +269,49 @@ test("ambiguous ARR claims retain currency, precision and attribution limits", (
assert.equal(snapshot.companies.find((c) => c.id === "AI070")?.revenueStatus, "unknown");
assert.equal(snapshot.observations.filter((o) => o.company === "AI070").length, 0,
"unreadable announcement summaries do not become numeric ARR");
const waitlist = snapshot.observations.find((o) => o.id === "SR25-O-OMI-WAITLIST")!;
assert.equal(waitlist.selection, "excluded", "waitlist conversions are not paid recurring revenue");
assert.equal(snapshot.companies.find((c) => c.id === waitlist.company)?.revenueStatus, "unknown");
const bluma = snapshot.observations.find((o) => o.id === "BLUMA-O-HISTORICAL-MRR")!;
assert.equal(bluma.selection, "historical");
assert.ok(bluma.unit !== "USD" && !bluma.observedAt && bluma.comparisonGroup === null);
const native = snapshot.observations.find((o) => o.id === "NATIVE-O-ARR-CLAIM")!;
assert.equal(native.selection, "crosscheck");
assert.ok(!native.metric && !native.observedAt && native.comparisonGroup === null);
const marketVolume = snapshot.observations.find((o) => o.id === "PALISADE-O-GMV")!;
assert.equal(marketVolume.selection, "excluded", "customer GMV must not establish company revenue or ARR");
assert.equal(snapshot.companies.find((c) => c.id === marketVolume.company)?.revenueStatus, "unknown");
const pingo = snapshot.observations.find((o) => o.id === "PINGO-O-MRR-JUL25")!;
assert.equal(pingo.selection, "historical", "a July 2025 self-report is not a current payment-platform sample");
assert.ok(pingo.unit !== "USD" && !pingo.observedAt && pingo.comparisonGroup === null);
const designService = snapshot.observations.find((o) => o.id === "SEMIOTIC-O-LAUNCH-REVENUE")!;
assert.equal(designService.selection, "historical");
assert.ok(!designService.metric && designService.comparisonGroup === null, "four-week service revenue is not MRR or ARR");
const restaurantAdoption = snapshot.observations.filter((o) => o.company === "YC32815");
assert.ok(restaurantAdoption.length > 0 && restaurantAdoption.every((o) => o.selection === "adoption" && !o.metric),
"restaurant count and list price must not produce an invented MRR observation");
const fishPilot = snapshot.observations.find((o) => o.id === "OCTAPULSE-O-CONTRACT")!;
assert.equal(fishPilot.selection, "excluded", "a paid pilot or annual contract does not establish recognized recurring revenue");
assert.equal(snapshot.companies.find((c) => c.id === fishPilot.company)?.revenueStatus, "unknown");
assert.equal(fishPilot.comparisonGroup, null);
assert.notEqual(fishPilot.unit, "USD", "six-figure wording does not establish the currency");
assert.ok(!fishPilot.metric && !fishPilot.observedAt, "the publication date is not the financial reporting date");
const cedar = snapshot.observations.filter((o) => o.company === "AI102");
assert.ok(cedar.every((o) => o.selection !== "signal" && o.comparisonGroup === null),
"closed and contracted amounts without recognition or period do not establish ARR");
assert.equal(snapshot.observations.find((o) => o.id === "SR25-O-LOOPS-CUSTOMER-SALES")?.selection, "excluded");
const portfolio = snapshot.observations.filter((o) => o.company === "AI100");
assert.deepEqual(portfolio.filter((o) => o.selection === "signal").map((o) => o.id), ["SR25-O-DRS-PORTFOLIO"],
"a product milestone must not be added to its company portfolio as another company");
const speedrun2025 = snapshot.observations.filter((o) => o.id.startsWith("SR25-O-"));
assert.ok(speedrun2025.every((o) =>
o.unit !== "USD" && !o.observedAt && o.comparisonGroup === null),
"access dates and dollar symbols must not invent dated USD income samples");
assert.deepEqual(speedrun2025.filter((o) => o.metric).map((o) => [o.id, o.metric]).sort(),
[["SR25-O-DRS-PORTFOLIO", "arr"], ["SR25-O-LOOPS-ARR", "arr"]],
"explicit company ARR claims retain their metric without inferring currency or reporting dates");
assert.ok(platformIncomeSample(snapshot, { windowDays: null }).points.every((point) =>
!point.observation.id.startsWith("SR25-O-")), "undated ARR claims never enter platform MRR comparisons");
});
// Retractions and forecasts must stay visible for review without becoming revenue signals.
@@ -151,7 +326,14 @@ test("retracted ARR and projected margins remain excluded", () => {
const forecast = snapshot.observations.find((o) => o.kind === "非GAAP毛利率预期")!;
assert.equal(forecast.selection, "excluded");
const peec = snapshot.companies.find((c) => c.name === "Peec AI")!;
assert.equal(peec.teamBound, ">");
assert.equal(peec.team, 100);
assert.equal(peec.teamBound, "≥");
const currentTeam = snapshot.observations.find((o) => o.id === "PEEC-JOB-TEAM")!;
assert.equal(currentTeam.value, peec.team);
assert.equal(currentTeam.bound, peec.teamBound);
const historicalTeam = snapshot.observations.find((o) => o.id === "O067")!;
assert.equal(historicalTeam.value, 70);
assert.equal(historicalTeam.bound, ">");
assert.equal(snapshot.observations.filter((o) => o.company === peec.id && o.selection === "signal").length, 1);
assert.ok(snapshot.observations.some((o) => o.company === peec.id && o.selection === "historical-estimate"));
});
@@ -172,8 +354,22 @@ test("directory coverage reconciles every candidate and keeps profile-only incom
assert.ok(company.profileOnly || company.directoryOrigin === "yc-2025-2026");
assert.equal(company.url, row.source);
if (company.profileOnly) assert.equal(company.revenueStatus, "unknown");
assert.equal(company.year, row.foundedYear);
assert.ok([2025, 2026].includes(company.year!));
if (company.id === "YC31370") {
assert.equal(row.foundedYear, 2025, "original discovery remains unchanged");
assert.equal(company.year, null, "a pivot year cannot settle the company founding year");
assert.ok(snapshot.reviews.some((review) => review.id === "DAYJOB-R-COHORT" && review.companyIds?.includes(company.id)));
} else if (company.id === "YC36279") {
assert.equal(row.foundedYear, 2026, "original Native discovery remains unchanged");
assert.equal(company.year, null, "a model launch and YC date cannot settle the older business founding year");
assert.ok(snapshot.reviews.some((review) => review.id === "NATIVE-R-COHORT" && review.companyIds?.includes(company.id)));
} else if (company.id === "YC31470") {
assert.equal(row.foundedYear, 2025, "original Tasklet discovery remains unchanged");
assert.equal(company.year, null, "a new product cannot establish a new company founding year");
assert.ok(snapshot.reviews.some((review) => review.id === "ENTERPRISE3-R-TASKLET" && review.companyIds?.includes(company.id)));
} else {
assert.equal(company.year, row.foundedYear);
assert.ok([2025, 2026].includes(company.year!));
}
}
const invalid = structuredClone(snapshot);
invalid.coverage!.newProfiles += 1;
@@ -187,6 +383,20 @@ test("directory coverage reconciles every candidate and keeps profile-only incom
assert.throws(() => validateResearch(lostOrigin), /coverage accounting/);
});
test("business pivots and procurement volume do not become comparable startup ARR", () => {
const snapshot = seed as ResearchSnapshot;
const dayjob = snapshot.companies.find((company) => company.id === "YC31370")!;
assert.equal(dayjob.year, null);
assert.equal(dayjob.directoryOrigin, "yc-2025-2026");
const milestones = snapshot.observations.filter((observation) => observation.company === dayjob.id);
assert.deepEqual(milestones.filter((observation) => observation.selection === "crosscheck").map((observation) => observation.value).sort(), [496000, 653000]);
assert.ok(milestones.every((observation) => observation.comparisonGroup === null && !observation.metric && !observation.observedAt));
assert.ok(milestones.filter((observation) => [150000, 800000].includes(observation.value)).every((observation) => observation.selection === "excluded"));
const procurement = snapshot.observations.find((observation) => observation.id === "SAUDARA-O-SOURCING")!;
assert.equal(procurement.selection, "excluded");
assert.equal(procurement.comparisonGroup, null);
});
test("income descriptions report sample dispersion and maximum-value sensitivity", () => {
const stats = describeIncome([100, 200, 300])!;
assert.equal(stats.mean, 200);
@@ -208,14 +418,21 @@ test("income descriptions report sample dispersion and maximum-value sensitivity
test("platform samples exclude stale data and keep ARR, estimates and collections separate", () => {
const snapshot = seed as ResearchSnapshot;
const recent = platformIncomeSample(snapshot, { windowDays: 7 });
assert.equal(recent.points.length, 14);
assert.equal(recent.excluded.length, 3);
assert.equal(recent.points.length, 16);
assert.equal(recent.excluded.length, 4);
assert.ok(recent.points.every((p) => p.observation.metric === "mrr" && p.observation.selection === "platform"));
assert.deepEqual(new Set(recent.excluded.map((p) => p.observation.company)), new Set(["AI016", "MR013", "MR014"]));
assert.deepEqual(new Set(recent.excluded.map((p) => p.observation.company)), new Set(["AI016", "MR013", "MR014", "MR017"]));
assert.ok(recent.points.some((p) => p.company.name === "TrackAI" && p.observation.connectionExpired), "dated recent value is not a claim of active connection");
const older = platformIncomeSample(snapshot, { windowDays: 90 });
assert.equal(older.points.length, 15);
assert.equal(older.points.length, 18);
assert.ok(older.points.some((p) => p.company.id === "AI016"));
assert.ok(!recent.points.some((p) => p.company.id === "MR017"));
assert.ok(older.points.some((p) => p.company.id === "MR017" && p.observation.connectionExpired),
"an expired connection retains its historical observation date in the wider window");
assert.equal(recent.points.find((p) => p.company.id === "MR018")?.value, 67864,
"an older Markdown cache must not replace or duplicate the later web snapshot");
assert.equal(recent.points.find((p) => p.company.id === "MR019")?.value, 65431,
"period-table revenue and revenue snapshots must not replace MRR");
const scaled = platformIncomeSample(snapshot, { windowDays: 7, annualize: true });
const monthlyGroups = groupIncome(recent.points);
const annualGroups = groupIncome(scaled.points);
@@ -227,7 +444,7 @@ test("platform samples exclude stale data and keep ARR, estimates and collection
const invalid = structuredClone(snapshot);
invalid.observations = invalid.observations.filter((o) => o.company === "MR001" && o.metric === "mrr");
const first = invalid.observations[0];
invalid.observations.push({ ...first, id: "future", observedAt: "2026-11-01", value: 999999 });
invalid.observations.push({ ...first, id: "future", observedAt: "2026-11-02", value: 999999 });
invalid.observations.push({ ...first, id: "bad-date", observedAt: "2026-02-30", value: 999999 });
invalid.observations.push({ ...first, id: "currency", unit: "EUR", value: 999999 });
invalid.observations.push({ ...first, id: "older", observedAt: "2026-10-28", value: 1 });
@@ -273,13 +490,92 @@ test("product launch candidates preserve unresolved years and many launches per
assert.throws(() => validateResearch(snapshot), /research candidate/);
});
test("the public SR007 roster preserves unique entries and Alven's older operator conflict", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((item) => item.id === "SR007-public-roster-2026-1")!;
assert.equal(batch.complete, false, "one public default cohort is not all source coverage");
assert.equal(batch.records.length, 79);
assert.equal(new Set(batch.records.map((row) => row.url)).size, 79);
const alven = batch.records.find((row) => row.name === "Alven")!;
assert.equal(alven.status, "pending-year");
assert.equal(alven.companyId, undefined);
assert.ok(alven.evidenceIds?.includes("SR007R-ALVEN-REG"));
assert.ok(!snapshot.observations.some((item) => item.evidence.startsWith("SR007R-")),
"roster claims, deployment plans and per-unit prices are not new financial observations");
const coherence = batch.records.find((row) => row.name === "Coherence Health")!;
assert.equal(coherence.status, "pending-ai", "a new coaching business still needs current AI delivery evidence");
assert.equal(coherence.companyId, undefined);
const followup = snapshot.observations.filter((item) => item.id.startsWith("SR007F2-"));
assert.equal(followup.length, 4);
assert.ok(followup.every((item) => ["excluded", "adoption"].includes(item.selection) && !item.metric && !item.observedAt));
assert.ok(!arrDisclosures(snapshot).some(({ observation }) => observation.id.startsWith("SR007F2-")),
"contracted ARR and pipeline are not live ARR disclosures");
const thirdFollowup = snapshot.observations.filter((item) => item.id.startsWith("SR007F3-"));
assert.equal(thirdFollowup.length, 6);
assert.equal(thirdFollowup.filter((item) => item.selection === "excluded").length, 3);
assert.equal(thirdFollowup.filter((item) => item.selection === "adoption").length, 3);
assert.ok(thirdFollowup.every((item) => !item.metric && !item.observedAt));
assert.ok(!arrDisclosures(snapshot).some(({ observation }) => observation.id.startsWith("SR007F3-")),
"projected ARR, connected customer assets and prospective contracts are not realized ARR");
assert.equal(snapshot.companies.find((item) => item.id === "AI121")!.cat, "C02",
"training data and environments are infrastructure rather than demonstrated foundation models");
const embrasure = snapshot.companies.find((item) => item.id === "AI122")!;
assert.equal(embrasure.technicalReview!.level, "public-artifact");
assert.ok(embrasure.technicalReview!.evidenceIds.includes("SR007F3-EMBRASURE-FLOW"),
"the public connector supports artifact availability, not independently reproduced warehouse performance");
for (const name of ["KIRA", "Leonidas", "Mecone", "PhotonSpear"]) {
const candidate = batch.records.find((row) => row.name === name)!;
assert.equal(candidate.status, "pending-ai",
"serving AI customers or using software controls is not evidence of an AI business");
assert.equal(candidate.companyId, undefined);
}
const maestroClaims = arrDisclosures(snapshot).filter(({ company }) => company.id === "AI130");
assert.equal(maestroClaims.length, 2, "keep the two original ARR milestones in the evidence ledger");
assert.ok(maestroClaims.every(({ observation }) => observation.selection === "historical" &&
arrFieldGaps(observation, snapshot.asOf).includes("date") &&
arrFieldGaps(observation, snapshot.asOf).includes("currency")));
const cumulative = snapshot.observations.find((item) => item.id === "SR007F4-O-MAESTRO-CUMULATIVE")!;
assert.equal(cumulative.selection, "historical");
assert.equal(cumulative.metric, undefined, "nine-month cumulative revenue cannot become ARR");
const contracted = snapshot.observations.find((item) => item.id === "SR007F4-O-HOID-CONTRACT")!;
assert.equal(contracted.selection, "excluded");
assert.equal(contracted.metric, undefined, "contracted run rate is not realized ARR");
const openwhispr = snapshot.companies.find((item) => item.id === "AI135")!;
assert.equal(openwhispr.technicalReview!.level, "public-artifact");
assert.ok(openwhispr.technicalReview!.evidenceIds.includes("SR007F5-OPENWHISPR-REPO"));
const voiceArr = arrDisclosures(snapshot).find(({ company }) => company.id === openwhispr.id)!.observation;
assert.equal(voiceArr.value, 120000);
assert.equal(voiceArr.bound, "≥");
assert.ok(arrFieldGaps(voiceArr, snapshot.asOf).includes("date"));
assert.ok(arrFieldGaps(voiceArr, snapshot.asOf).includes("currency"),
"current plan prices cannot supply the currency of an undated investor ARR claim");
const voiceUsers = snapshot.observations.find((item) => item.id === "SR007F5-O-OPENWHISPR-DAU")!;
assert.equal(voiceUsers.selection, "adoption", "daily active users are not paying subscribers");
assert.equal(voiceUsers.metric, undefined);
const salesLive = snapshot.observations.find((item) => item.id === "SR007F6-O-OTHELLO-LIVE")!;
assert.equal(salesLive.metric, "arr");
assert.equal(salesLive.value, 600000, "live ARR must not include contracted ARR or pipeline");
for (const suffix of ["CONTRACT", "PIPELINE"]) {
const prospective = snapshot.observations.find((item) => item.id === `SR007F6-O-OTHELLO-${suffix}`)!;
assert.equal(prospective.selection, "excluded");
assert.equal(prospective.metric, undefined);
}
const professionalService = snapshot.observations.find((item) => item.id === "SR007F6-O-PARCA-RUNRATE")!;
assert.equal(professionalService.metric, "annualized-revenue");
assert.equal(professionalService.selection, "historical");
assert.ok(!arrDisclosures(snapshot).some(({ observation }) => observation.id === professionalService.id),
"annualized professional-service revenue is not recurring subscription ARR");
assert.equal(snapshot.companies.find((item) => item.id === "AI137")!.technicalReview!.level, "description",
"a preliminary external ranking with unresolved methodology is not complete independent verification");
});
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, 10);
assert.equal(older.length, 16);
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")!;
@@ -300,7 +596,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, 17);
assert.equal(snapshot.candidateBatches!.flatMap((b) => b.records).filter((r) => r.status === "pending-year").length, 153);
});
test("product pivots and founder histories do not become new company years or current revenue", () => {
@@ -313,6 +609,15 @@ test("product pivots and founder histories do not become new company years or cu
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");
const candidates = snapshot.candidateBatches!.flatMap((b) => b.records);
assert.equal(candidates.find((r) => r.id === "PH26-12")?.status, "outside-cohort",
"a brand iteration with an earlier business chain is not a new company");
const happycapy = candidates.find((r) => r.id === "PH26-04")!;
assert.ok(happycapy.evidenceIds?.includes("FOLLOWUP-CAPY-ACRA"));
assert.equal(happycapy.status, "pending-year", "a seller's registration does not establish a new global business");
assert.equal(happycapy.companyId, undefined);
assert.ok(!snapshot.observations.some((o) => o.evidence.startsWith("FOLLOWUP-")),
"registration, funding, app-store prices and client amounts are not company income");
});
test("follow-up evidence can admit an uncertain directory candidate without rewriting its original status", () => {
@@ -328,6 +633,337 @@ test("follow-up evidence can admit an uncertain directory candidate without rewr
assert.throws(() => validateResearch(snapshot), /directory recheck/);
});
test("AI relevance follow-ups retain unresolved businesses and reuse existing company identities", () => {
const snapshot = structuredClone(seed) as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-1")!;
assert.equal(batch.complete, false);
assert.equal(batch.records.length, 24);
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(0, 24);
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
const linked = batch.records.filter((r) => r.status === "linked");
assert.equal(linked.length, 17);
assert.equal(linked.filter((r) => r.companyId!.startsWith("YC")).length, 14);
for (const [name, id] of [["Bud", "AI070"], ["Prism", "AI030"], ["Parametric", "AI014"]]) {
assert.equal(linked.find((r) => r.name === name)?.companyId, id);
}
assert.equal(snapshot.companies.filter((c) => c.profileOnly || c.directoryOrigin === "yc-2025-2026").length, 928);
for (const row of linked) {
const ycId = Number(row.id.replace("YC-AI1-", ""));
assert.equal(snapshot.directoryRechecks!.find((r) => r.ycId === ycId)?.companyId, row.companyId);
}
const pending = batch.records.filter((r) => r.status === "pending-ai");
assert.equal(pending.length, 7);
assert.ok(pending.every((r) => !r.companyId && r.evidenceIds!.length && /2025/.test(r.note)));
assert.match(pending.find((r) => r.name === "Better Auth")!.note, /AI.*模块/);
validateResearch(snapshot);
pending[0].companyId = "AI014";
assert.throws(() => validateResearch(snapshot), /research candidate/);
delete pending[0].companyId;
pending[0].evidenceIds = [];
assert.throws(() => validateResearch(snapshot), /research candidate/);
});
test("historical monthly revenue and customer examples do not enter recurring platform comparisons", () => {
const snapshot = seed as ResearchSnapshot;
const monthly = snapshot.observations.find((o) => o.id === "UNCLEAR1-O-SCALAR-MONTHLY")!;
assert.equal(monthly.value, 74000);
assert.equal(monthly.selection, "historical");
assert.equal(monthly.metric, undefined, "monthly revenue alone does not establish MRR or ARR");
assert.equal(monthly.comparisonGroup, null);
assert.equal(snapshot.companies.find((c) => c.id === "YC30514")!.revenueStatus, "self");
for (const id of ["AMBY-CLIENT", "NIXO-EXAMPLE", "AUTOSTEP-WASTE"]) {
assert.equal(snapshot.observations.find((o) => o.id === `UNCLEAR1-O-${id}`)!.selection, "excluded");
}
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR1-")));
const cua = snapshot.companies.find((c) => c.id === "YC30420")!;
assert.equal(cua.cat, "C03");
assert.equal(cua.revenueStatus, "unknown");
assert.match(cua.businessModel!, /自备计算机/);
assert.equal(cua.technicalReview!.level, "public-artifact");
});
test("a second uncertain cohort admits evidenced AI tasks while preserving autonomy and finance gaps", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-2")!;
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, 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");
assert.ok(row.evidenceIds!.length);
}
const signals = snapshot.companies.find((c) => c.id === "YC30639")!;
assert.equal(signals.year, 2026, "retain the company year instead of assigning the batch's majority year");
assert.equal(signals.cat, "C06");
assert.equal(snapshot.companies.find((c) => c.id === "YC30846")!.cat, "C03");
assert.match(snapshot.companies.find((c) => c.id === "YC30821")!.business, /远端推理/);
assert.equal(snapshot.companies.find((c) => c.id === "YC30933")!.technicalReview!.level, "description",
"vendor clinical agreement is not an independent validation");
const amounts = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR2-"));
assert.equal(amounts.length, 8);
assert.ok(amounts.every((o) => ["excluded", "adoption"].includes(o.selection) && !o.metric && o.comparisonGroup === null));
assert.equal(amounts.find((o) => o.id.endsWith("WARDSTONE-GRANT"))!.selection, "excluded");
assert.equal(amounts.find((o) => o.id.endsWith("SOLVA-OLD-PREMIUM"))!.selection, "excluded");
assert.ok(batch.records.filter((r) => r.companyId).every((r) => snapshot.companies.find((c) => c.id === r.companyId)!.revenueStatus === "unknown"));
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR2-")));
});
test("the third uncertain cohort retains shutdowns, unknown scale and mixed data-service economics", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-3")!;
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, 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")!;
assert.equal(klaus.revenueStatus, "unknown", "a shutdown must not manufacture a zero revenue report");
assert.match(klaus.stage, /2026-09-01停服/);
assert.ok(!snapshot.observations.some((o) => o.company === klaus.id));
const openspec = snapshot.companies.find((c) => c.id === "YC31116")!;
assert.equal(openspec.team, null, "missing staff count stays unknown");
assert.equal(openspec.year, 2025, "a license copyright line alone is not a company founding date");
assert.equal(openspec.technicalReview!.level, "public-artifact");
assert.match(snapshot.companies.find((c) => c.id === "YC31095")!.businessModel!, /清洁/);
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR3-"));
assert.equal(figures.length, 8);
assert.ok(figures.every((o) => ["excluded", "adoption"].includes(o.selection) && !o.metric && o.comparisonGroup === null));
assert.equal(figures.find((o) => o.id.endsWith("SHORTKIT-OLD-ARR"))!.selection, "excluded");
assert.equal(figures.find((o) => o.id.endsWith("KALPA-TRAIN-COST"))!.selection, "excluded");
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR3-")));
});
test("the fourth uncertain cohort distinguishes product pivots, managed labor and pass-through money", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-4")!;
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, 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")!;
assert.equal(fern.cat, "C02");
assert.match(fern.notes, /会议/);
const mocha = snapshot.companies.find((c) => c.id === "YC31269")!;
assert.equal(mocha.year, 2026);
assert.match(mocha.businessModel!, /人工/);
assert.match(mocha.price, /Platform.*Managed/);
assert.match(mocha.notes, /Coming soon/);
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR4-"));
assert.equal(figures.length, 7);
assert.ok(figures.every((o) => ["excluded", "adoption"].includes(o.selection) && !o.metric && o.comparisonGroup === null));
for (const suffix of ["LAURENCE-ADSPEND", "EKPA-CONNECTED-ASSETS", "REGBASE-CLIENT-GRANTS", "MOCHA-FUNDING"]) {
assert.equal(figures.find((o) => o.id.endsWith(suffix))!.selection, "excluded");
}
assert.equal(snapshot.companies.find((c) => c.id === "YC31199")!.technicalReview!.level, "public-artifact");
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR4-")));
});
test("the fifth uncertain cohort separates dated ARR, service run rates, bookings and previous ventures", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-5")!;
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, 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")!;
assert.equal(result.cat, "C09");
assert.equal(result.revenueStatus, "unknown");
assert.match(result.notes, /280万美元ARR属于用户/);
assert.ok(result.evidenceIds.includes("UNCLEAR5-31438-TERMS"));
const andco = snapshot.companies.find((c) => c.id === "YC31411")!;
assert.equal(andco.year, 2026, "a founder's previous venture must not replace the new company's stated year");
assert.match(andco.notes, /Layup/);
assert.equal(snapshot.evidence.find((e) => e.id === "UNCLEAR5-31411-LAUNCH6")!.companyId, null);
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR5-"));
assert.equal(figures.length, 18);
const dated = figures.find((o) => o.id.endsWith("TRELLIS-ARR-AUG"))!;
assert.equal(dated.metric, "arr");
assert.equal(dated.observedAt, undefined, "a page maintenance date is not a financial cutoff");
assert.ok(arrFieldGaps(dated, snapshot.asOf).includes("date"));
assert.equal(dated.bound, "≥");
assert.equal(figures.find((o) => o.id.endsWith("INSTA-RUNRATE"))!.metric, "annualized-revenue");
assert.equal(figures.find((o) => o.id.endsWith("INSTA-LAUNCH-ARR"))!.metric, "arr");
for (const suffix of ["PARROT-CONTRACT", "KINRO-OLD-CONTRACT", "BENTO-OLD-EMPLOYER", "TRELLIS-OLD-MGMT"]) {
assert.equal(figures.find((o) => o.id.endsWith(suffix))!.selection, "excluded");
}
assert.equal(snapshot.companies.find((c) => c.id === "YC31423")!.technicalReview!.level, "description",
"an internal benchmark subset is not an independent leaderboard result");
assert.ok(figures.every((o) => o.comparisonGroup === null));
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR5-")));
});
test("the sixth uncertain cohort preserves namesakes, churn and historical robotics MRR", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-6")!;
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(120, 144);
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);
const hera = snapshot.companies.find((c) => c.id === "YC32123")!;
const videoHera = snapshot.companies.find((c) => c.id === "AI025")!;
assert.equal(hera.cat, "C07");
assert.equal(hera.year, 2026);
assert.equal(videoHera.cat, "C08");
assert.equal(videoHera.year, 2025);
assert.notEqual(hera.url, videoHera.url, "same-name products with different YC pages must not be merged");
for (const name of ["Aernata", "Maquoketa Research", "Fire Wallet", "Eden Robotics", "Edgerun"]) {
const row = batch.records.find((r) => r.name === name)!;
assert.equal(row.status, "pending-ai");
assert.equal(row.companyId, undefined, "historical AI products, agent features and autonomous hardware need current-core evidence");
}
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR6-"));
assert.equal(figures.length, 13);
const churn = figures.find((o) => o.id.endsWith("ARDENT-CHURN"))!;
assert.equal(churn.selection, "excluded");
assert.equal(churn.metric, undefined, "lost ARR is not a positive current ARR observation");
assert.equal(snapshot.companies.find((c) => c.id === "YC31475")!.revenueStatus, "unknown");
const mrr = figures.find((o) => o.id.endsWith("AVEA-MRR"))!;
assert.equal(mrr.value, 18000);
assert.equal(mrr.metric, "mrr");
assert.equal(mrr.selection, "historical");
assert.match(mrr.unit, /无币种/);
assert.match(snapshot.companies.find((c) => c.id === "YC31530")!.notes, /Acquired/);
for (const suffix of ["PLAN0-PROJECTS-YC", "PLAN0-PROJECTS-LAUNCH", "SILMARIL-DAMAGE-YC", "SILMARIL-DAMAGE-CASE1", "SILMARIL-DAMAGE-CASE2", "EXPANSE-WASTE"]) {
assert.equal(figures.find((o) => o.id.endsWith(suffix))!.selection, "excluded");
}
const openwork = snapshot.companies.find((c) => c.id === "YC31505")!;
assert.equal(openwork.technicalReview!.level, "public-artifact");
assert.ok(openwork.technicalReview!.evidenceIds.includes("UNCLEAR6-31505-EE-LICENSE"));
assert.match(openwork.notes, /桌面MIT与ee目录/);
assert.ok(figures.every((o) => o.comparisonGroup === null));
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR6-")));
});
test("the seventh uncertain cohort separates hardware sales, prepayments and bounded verification", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-7")!;
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear").slice(144, 168);
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, 16);
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 8);
const robotNori = snapshot.companies.find((c) => c.id === "YC32553")!;
const healthNori = snapshot.companies.find((c) => c.id === "YC30657")!;
assert.equal(robotNori.cat, "C10");
assert.equal(healthNori.cat, "C09");
assert.notEqual(robotNori.url, healthNori.url, "same-name hardware and health companies retain distinct identities");
assert.equal(batch.records.find((r) => r.name === "Nori")!.companyId, robotNori.id);
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR7-"));
assert.equal(figures.length, 18);
const sales = figures.find((o) => o.id.endsWith("NORI-SALES"))!;
assert.equal(sales.value, 440000);
assert.equal(sales.selection, "historical");
assert.equal(sales.metric, undefined, "hardware sales with unverified preorder/delivery mix must not become ARR");
assert.match(snapshot.companies.find((c) => c.id === "YC32164")!.price!, /额度/);
for (const suffix of ["GUT-PHARMACY", "MANIFOLD-PICKS", "ATC-CLIENT-FLEET", "OPERON-CLIENT-SAVE", "ANYALPHA-OLD-APPS", "LETTER-DEMO"]) {
assert.equal(figures.find((o) => o.id.endsWith(suffix))!.selection, "excluded");
}
assert.match(figures.find((o) => o.id.endsWith("LETTER-DEMO"))!.scope, /fictional/);
assert.equal(snapshot.companies.find((c) => c.id === "YC34899")!.cat, "C06");
const kailash = snapshot.companies.find((c) => c.id === "YC34828")!;
assert.equal(kailash.cat, "C10");
assert.equal(kailash.technicalReview!.level, "description", "an open-weights claim without readable weights or a model card is not a reviewed public artifact");
const graphify = snapshot.companies.find((c) => c.id === "YC32583")!;
assert.equal(graphify.technicalReview!.level, "public-artifact");
const proof = snapshot.evidence.find((e) => e.id === "UNCLEAR7-32583-PROOF")!;
assert.match(proof.summary, /unknown/);
assert.match(proof.summary, /函数/);
assert.match(proof.limitation, /整库正确/);
assert.match(snapshot.companies.find((c) => c.id === "YC32518")!.notes, /Build标Coming soon/);
for (const name of ["Kara", "Molagri", "TareBio", "Atomarine", "Vernius Systems, Inc."]) {
const row = batch.records.find((r) => r.name === name)!;
assert.equal(row.status, "pending-ai");
assert.equal(row.companyId, undefined);
}
assert.ok(figures.every((o) => o.comparisonGroup === null));
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR7-")));
});
test("all original AI-unclear candidates retain a unique follow-up without counting EveryApp twice", () => {
const snapshot = seed as ResearchSnapshot;
const rows = snapshot.candidateBatches!.filter((b) => b.id.startsWith("YC-AI-UNCLEAR-")).flatMap((b) => b.records);
const original = coverageAudit.records.filter((r) => r.status === "ai-unclear");
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, 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");
assert.equal(snapshot.companies.filter((c) => c.id === "AI074").length, 1);
for (const row of rows.filter((r) => r.status === "pending-ai")) {
assert.equal(row.companyId, undefined);
assert.ok(row.evidenceIds!.length > 0, "unresolved AI relevance has explicit evidence and gaps");
}
assert.equal(snapshot.coverage!.newProfiles, 928, "later evidence does not rewrite the original discovery accounting");
});
test("the final uncertain cohort separates delivery-start rentals, reservations and upstream costs", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-AI-UNCLEAR-8")!;
assert.equal(batch.complete, false, "reading the full pool does not resolve all AI, year or financial gaps");
assert.equal(batch.records.filter((r) => r.status === "linked").length, 11);
assert.equal(batch.records.filter((r) => r.status === "pending-ai").length, 13);
const rhem = snapshot.companies.find((c) => c.id === "YC35127")!;
assert.match(rhem.price!, /确认交付后/);
assert.match(rhem.price!, /99 USD/);
assert.equal(rhem.revenueStatus, "unknown");
assert.match(snapshot.companies.find((c) => c.id === "YC37272")!.price!, /预约/);
assert.match(snapshot.companies.find((c) => c.id === "AI074")!.price!, /有效订阅/);
assert.match(snapshot.companies.find((c) => c.id === "YC35396")!.notes, /Coming next/);
assert.match(snapshot.companies.find((c) => c.id === "YC36774")!.notes, /Early access/);
const figures = snapshot.observations.filter((o) => o.id.startsWith("UNCLEAR8-"));
assert.equal(figures.length, 9);
assert.ok(figures.every((o) => ["excluded", "adoption"].includes(o.selection) && !o.metric && o.comparisonGroup === null));
for (const suffix of ["OASIVE-OLD-WELLS", "OASIVE-OLD-GOOGLE", "OBSESSION-CALCULATOR", "EVERYAPP-UPSTREAM"]) {
assert.equal(figures.find((o) => o.id.endsWith(suffix))!.selection, "excluded");
}
for (const name of ["Clean", "STASIS", "RATLOG", "Inflection Space Systems", "twentyfour26"]) {
assert.equal(batch.records.find((r) => r.name === name)!.status, "pending-ai");
}
assert.ok(!platformIncomeSample(snapshot, { windowDays: null }).points.some((p) => p.observation.id.startsWith("UNCLEAR8-")));
});
test("founding conflicts preserve old product history without creating new companies or revenue", () => {
const snapshot = seed as ResearchSnapshot;
const batch = snapshot.candidateBatches!.find((b) => b.id === "YC-DATE-CONFLICT-1")!;
const original = coverageAudit.records.filter((r) => r.status === "date-conflict");
assert.equal(batch.complete, false);
assert.equal(batch.records.length, 6);
assert.deepEqual(batch.records.map((r) => r.url), original.map((r) => r.source));
assert.equal(new Set(batch.records.map((r) => r.url)).size, original.length);
for (const row of batch.records) {
assert.equal(row.status, "pending-year");
assert.equal(row.companyId, undefined);
const evidence = snapshot.evidence.filter((e) => row.evidenceIds!.includes(e.id));
assert.equal(evidence.length, row.evidenceIds!.length);
assert.ok(evidence.every((e) => e.companyId === null));
assert.ok(evidence.some((e) => e.publishedAt?.startsWith("2025-")), "old product launch remains explicit");
assert.ok(!snapshot.companies.some((c) => c.evidenceIds.some((eid) => row.evidenceIds!.includes(eid))));
}
assert.match(batch.records.find((r) => r.name === "Layers")!.note, /未确认当前AI核心/);
assert.ok(!snapshot.observations.some((o) => o.evidence.startsWith("YEARCONFLICT1-")));
const excluded = coverageAudit.records.filter((r) => r.status === "year-outside-or-missing");
assert.equal(excluded.filter((r) => r.foundedYear === null).length, 54);
assert.equal(excluded.filter((r) => r.foundedYear !== null).length, 214);
assert.ok(excluded.filter((r) => r.foundedYear !== null).every((r) => ![2025, 2026].includes(r.foundedYear!)));
});
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")!;
@@ -335,7 +971,7 @@ test("Chinese discovery entries preserve unread articles and use separate primar
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.equal(batch.records.find((row) => row.name === "Utopai")?.status, "outside-cohort");
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");
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+52 -28
View File
@@ -1,6 +1,6 @@
{
"sourceVersion": "2026-10-30-ph-history-1",
"asOf": "2026-10-30",
"sourceVersion": "2026-11-01-sr007-followup-6",
"asOf": "2026-11-01",
"metric": "platform-mrr-usd",
"sampleUnit": "product-or-business",
"note": "独立Python复算参考值,仅验证样本计算;不是行业估计或财务审计。",
@@ -21,7 +21,9 @@
"MR011",
"MR012",
"MR015",
"MR016"
"MR016",
"MR018",
"MR019"
],
"groups": [
{
@@ -35,7 +37,7 @@
"MR016"
],
"n": 4,
"mean": 34815.0,
"mean": 34815,
"median": 36723.0,
"sampleDeviation": 16334.54845820151,
"cv": 0.46918134304758036,
@@ -50,7 +52,7 @@
"MR001"
],
"n": 1,
"mean": 53101.0,
"mean": 53101,
"median": 53101,
"sampleDeviation": null,
"cv": null,
@@ -65,7 +67,7 @@
"MR002"
],
"n": 1,
"mean": 62423.0,
"mean": 62423,
"median": 62423,
"sampleDeviation": null,
"cv": null,
@@ -79,15 +81,17 @@
"products": [
"MR003",
"MR004",
"MR005"
"MR005",
"MR018",
"MR019"
],
"n": 3,
"mean": 41693.0,
"median": 49069,
"sampleDeviation": 28111.375064197768,
"cv": 0.6742468775141575,
"meanWithoutLargest": 29849.0,
"largestRemovalDrop": 0.2840764636749574
"n": 5,
"mean": 51674.8,
"median": 65381,
"sampleDeviation": 24138.82269705795,
"cv": 0.4671294847209461,
"meanWithoutLargest": 47627.5,
"largestRemovalDrop": 0.07832250923080501
},
{
"category": "C09",
@@ -105,7 +109,7 @@
"median": 20483,
"sampleDeviation": 11942.947278624319,
"cv": 0.4711370488466823,
"meanWithoutLargest": 20046.0,
"meanWithoutLargest": 20046,
"largestRemovalDrop": 0.20920581320120557
}
]
@@ -127,9 +131,27 @@
"MR012",
"MR015",
"MR016",
"AI016"
"AI016",
"MR017",
"MR018",
"MR019"
],
"groups": [
{
"category": "C02",
"provider": "Stripe",
"model": "模型网关订阅与用量",
"products": [
"MR017"
],
"n": 1,
"mean": 78764,
"median": 78764,
"sampleDeviation": null,
"cv": null,
"meanWithoutLargest": null,
"largestRemovalDrop": null
},
{
"category": "C06",
"provider": "Stripe",
@@ -146,7 +168,7 @@
"median": 40112,
"sampleDeviation": 174786.78744916618,
"cv": 1.5505509613536426,
"meanWithoutLargest": 34815.0,
"meanWithoutLargest": 34815,
"largestRemovalDrop": 0.6911526751687284
},
{
@@ -157,7 +179,7 @@
"MR001"
],
"n": 1,
"mean": 53101.0,
"mean": 53101,
"median": 53101,
"sampleDeviation": null,
"cv": null,
@@ -172,7 +194,7 @@
"MR002"
],
"n": 1,
"mean": 62423.0,
"mean": 62423,
"median": 62423,
"sampleDeviation": null,
"cv": null,
@@ -186,15 +208,17 @@
"products": [
"MR003",
"MR004",
"MR005"
"MR005",
"MR018",
"MR019"
],
"n": 3,
"mean": 41693.0,
"median": 49069,
"sampleDeviation": 28111.375064197768,
"cv": 0.6742468775141575,
"meanWithoutLargest": 29849.0,
"largestRemovalDrop": 0.2840764636749574
"n": 5,
"mean": 51674.8,
"median": 65381,
"sampleDeviation": 24138.82269705795,
"cv": 0.4671294847209461,
"meanWithoutLargest": 47627.5,
"largestRemovalDrop": 0.07832250923080501
},
{
"category": "C09",
@@ -212,7 +236,7 @@
"median": 20483,
"sampleDeviation": 11942.947278624319,
"cv": 0.4711370488466823,
"meanWithoutLargest": 20046.0,
"meanWithoutLargest": 20046,
"largestRemovalDrop": 0.20920581320120557
}
]
+118 -8
View File
@@ -16,6 +16,20 @@ SOURCE_SLUG = 'ai-company-research/02-coverage-and-gaps'
BASE = '/docs/other-research/ai-company-research/overview'
def yc_coverage_summary():
companies = {row['id']: row for row in DATA['companies']}
imported = [companies[f"YC{row['ycId']}"] for row in AUDIT['records'] if row['status'] == 'imported']
profile_only = sum(bool(row.get('profileOnly')) for row in imported)
groups = {}
for label, prefix in [('unclearAI', 'YC-AI-UNCLEAR-'), ('missingYear', 'YC-MISSING-YEAR-'),
('dateConflict', 'YC-DATE-CONFLICT-')]:
rows = [row for batch in DATA.get('candidateBatches', []) if batch['id'].startswith(prefix)
for row in batch['records']]
groups[label] = {'records': len(rows), 'statuses': dict(sorted(Counter(row['status'] for row in rows).items()))}
return {'initialProfiles': len(AUDIT['records']), 'originalImported': len(imported),
'profileOnly': profile_only, 'deepenedOriginal': len(imported) - profile_only, **groups}
def source_register():
sources = SOURCES['sources']
@@ -26,6 +40,15 @@ def source_register():
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['id'] == 'S01':
assert source.get('coverageSummary') == yc_coverage_summary(), 'YC source summary is stale'
if source['id'] == 'S14' and source.get('rosterAudit'):
roster = source['rosterAudit']
batch = next(batch for batch in DATA['candidateBatches'] if batch['id'] == roster['candidateBatchId'])
assert batch['sourceId'] == 'S14' and not batch['complete']
assert roster['defaultCohort'] == 'SR007' and roster['next'] is None
assert len(batch['records']) == roster['recordCount'] == roster['reportedCount']
assert len({row['url'] for row in batch['records']}) == roster['recordCount']
if source['url']:
assert urlsplit(source['url']).scheme in {'http', 'https'} and urlsplit(source['url']).netloc
for host in source['hosts']:
@@ -49,7 +72,18 @@ def source_register():
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']} |")
boundary = source['boundary']
if source['id'] == 'S01':
summary = source['coverageSummary']
boundary += (f" 当前原候选池{summary['initialProfiles']}项,原新增{summary['originalImported']}项中"
f"{summary['profileOnly']}项目录基础资料、{summary['deepenedOriginal']}项深化个案。")
for key, label in [('unclearAI', '原AI不明'), ('missingYear', '原缺年'), ('dateConflict', '原日期冲突')]:
group = summary[key]
labels = {'linked': '关联公司', 'pending-ai': 'AI主营待核',
'pending-year': '成立/主体待核', 'outside-cohort': '范围外'}
counts = '、'.join(f'{labels.get(status, status)} {count}' for status, count in group['statuses'].items())
boundary += f" {label}{group['records']}项当前:{counts}。"
lines.append(f"| {source['id']} {name} | {statuses[source['status']]} | {len(evidence)} / {len(company_ids)} / {observations} | {boundary} | {source['nextCheck']} |")
lines += ['', 'Dealroom、Product Hunt、机器之心与36氪入口于2026-10-30重新访问。官网首页可读不证明已取得完整筛选结果;安全检测页面不作为文章内容。候选池之外的来源边界尚需建立可回查条目,完成前不宣布来源清单已全部覆盖。', '']
return lines
@@ -85,7 +119,7 @@ def candidate_register():
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': '已关联公司',
statuses = {'pending-year': '成立年待核', 'pending-ai': 'AI主营待核', 'linked': '已关联公司',
'existing-outside-cohort': '已有关联,年份范围外',
'outside-cohort': '范围外或既有业务,不新增公司', 'promotion': '推广活动,非公司新增'}
for batch in batches:
@@ -97,13 +131,13 @@ def candidate_register():
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的研究元数据中。", '',
f"本批{len(rows)}条:{counts['linked']}条关联公司、{counts['existing-outside-cohort']}条关联范围外对照、{counts['outside-cohort']}条范围外不新增公司、{counts['pending-year']}条成立年待核、{counts['pending-ai']}条AI主营待核、{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':
if row['status'] in {'outside-cohort', 'pending-ai'}:
assert row.get('evidenceIds') and not row.get('companyId')
name = f"[{row['name']}]({row['url']})" if row['url'] else row['name']
company = '—'
@@ -121,6 +155,73 @@ def candidate_register():
return lines
def uncertain_ai_followups():
original = {row['source']: row for row in AUDIT['records'] if row['status'] == 'ai-unclear'}
rows = [row for batch in DATA.get('candidateBatches', [])
if batch['id'].startswith('YC-AI-UNCLEAR-') for row in batch['records']]
if not rows:
return []
urls = [row['url'] for row in rows]
assert len(set(urls)) == len(urls), 'Duplicate AI-relevance follow-up URL'
rechecks = {row['ycId']: row for row in DATA.get('directoryRechecks', [])}
counts = Counter(row['status'] for row in rows)
assert set(counts) <= {'linked', 'pending-ai'}
for row in rows:
assert row['url'] in original and row.get('evidenceIds')
baseline = original[row['url']]
if row['status'] == 'linked':
assert rechecks[baseline['ycId']]['companyId'] == row['companyId']
else:
assert not row.get('companyId') and baseline['ycId'] not in rechecks
unread = len(original) - len(rows)
assert unread >= 0
return ['', '## 原AI主营不明名单的后续覆盖', '',
f"原名单{len(original)}项,已有后续记录{len(rows)}项:{counts['linked']}项关联公司、{counts['pending-ai']}项仍待核AI主营,{unread}项尚无本批系列的后续记录。计数按原始YC页面去重;EveryApp此前的独立复核与末批记录不重复相加。", '',
'读完名单只证明每项有可回查的处理记录,不证明待核项均为AI公司,也不证明法律主体、收入或技术效果已独立验证。原始发现审计保持不变,成立年范围外/缺失、成立冲突及其他来源的缺项仍单列。', '']
def founding_year_followups():
original = {row['source']: row for row in AUDIT['records'] if row['status'] == 'date-conflict'}
rows = [row for batch in DATA.get('candidateBatches', [])
if batch['id'].startswith('YC-DATE-CONFLICT-') for row in batch['records']]
urls = [row['url'] for row in rows]
assert len(set(urls)) == len(urls), 'Duplicate founding-conflict follow-up URL'
assert set(urls) <= set(original), 'Unknown founding-conflict follow-up URL'
evidence = {row['id']: row for row in DATA['evidence']}
for row in rows:
assert row['status'] == 'pending-year' and not row.get('companyId')
assert row.get('evidenceIds') and all(eid in evidence for eid in row['evidenceIds'])
assert all(evidence[eid]['companyId'] is None for eid in row['evidenceIds'])
unread = len(original) - len(rows)
return ['', '## 原成立年冲突名单的后续覆盖', '',
f"原名单{len(original)}项,已有后续记录{len(rows)}项,均保留成立年待核,{unread}项尚无本系列后续记录。当前业务初分类见候选表,不计入主公司表及收入统计。", '',
'2025旧品牌发布可证明当时已有产品材料;当前目录的2026字段、批次、条款日期和运营者名称不能单独证明法律主体成立或新旧主体连续。未据此强改成立年。Layers当前AI核心仍另待核。', '']
def missing_year_followups():
original = {row['source'] for row in AUDIT['records']
if row['status'] == 'year-outside-or-missing' and row['foundedYear'] is None}
rows = [row for batch in DATA.get('candidateBatches', [])
if batch['id'].startswith('YC-MISSING-YEAR-') for row in batch['records']]
urls = [row['url'] for row in rows]
assert len(set(urls)) == len(urls) and set(urls) <= original
evidence = {e['id']: e for e in DATA['evidence']}
for row in rows:
assert row['status'] in {'outside-cohort', 'pending-year', 'pending-ai', 'linked'}
assert row.get('evidenceIds')
if row['status'] == 'linked':
matches = [r for r in DATA.get('directoryRechecks', []) if r['companyId'] == row.get('companyId')]
assert len(matches) == 1 and matches[0]['originalStatus'] == 'year-outside-or-missing'
assert all(eid in evidence and evidence[eid]['companyId'] == row['companyId'] for eid in row['evidenceIds'])
else:
assert not row.get('companyId')
assert all(eid in evidence and evidence[eid]['companyId'] is None for eid in row['evidenceIds'])
counts = Counter(row['status'] for row in rows)
return ['', '## 原缺少成立年名单的后续覆盖', '',
f"原快照缺年{len(original)}项,外部补核已有{len(rows)}项:{counts['linked']}项关联公司、{counts['outside-cohort']}项既有主体/业务范围外、{counts['pending-year']}项成立年待核、{counts['pending-ai']}项AI主营待核,{len(original)-len(rows)}项尚无本系列外部补核记录。", '',
'当前YC缺年字段复读与外部主体核查分别统计。登记创建日、公司自述年、产品发布、条款更新和批次不相互替代;已查英国或法国主体不代表完整集团沿革已核。原54项发现计数不回写,也不以Inactive补零收入。', '']
def directory_rechecks():
rows = DATA.get('directoryRechecks', [])
if not rows:
@@ -167,6 +268,11 @@ def render():
'year-outside-or-missing': cov['otherOrMissingYear'],
'ai-unclear': cov['unclearAI'], 'date-conflict': cov['dateConflicts']}
assert len({x['ycId'] for x in AUDIT['records']}) == len(AUDIT['records'])
excluded_years = Counter(x['foundedYear'] for x in AUDIT['records'] if x['status'] == 'year-outside-or-missing')
outside_years = {year: count for year, count in excluded_years.items() if year is not None}
assert all(year not in (2025, 2026) for year in outside_years)
assert sum(outside_years.values()) + excluded_years[None] == cov['otherOrMissingYear']
year_detail = '、'.join(f'{year}年{count}项' for year, count in sorted(outside_years.items()))
lines = ['# AI 公司资料覆盖与缺项', '',
f"截至 {DATA['asOf']},研究表含{len(companies):,}条公司或产品记录。YC候选池有逐条读取审计;其他来源为定向补充。清单尚不能作为全球AI公司普查,也不能将收入未知解释为零。", '',
'验收范围已于2026-10-30确认为:以确认的公开来源清单为边界,逐条核对并标明缺项。下方区分已逐条审计的候选池与定向补充来源;已使用某来源不代表该来源已被完整覆盖。', '',
@@ -178,7 +284,8 @@ def render():
f"| 初筛匹配 | {cov['matchingProfiles']:,} | 成立年及AI相关描述通过初筛,尚非逐家商业验证 |",
f"| 与原表重合 | {cov['existingProfiles']} | URL匹配,未再次新增 |",
f"| 新增目录记录 | {cov['newProfiles']:,} | 原始入表来源,后续深化不改变发现计数 |",
f"| 年份不符或缺失 | {cov['otherOrMissingYear']} | 此轮不作为成立年匹配项 |",
f"| 原目录列为范围外年份 | {sum(outside_years.values())} | {year_detail};原快照自述,不是独立登记核验 |",
f"| 原目录未给成立年 | {excluded_years[None]} | 与已列范围外年份分开,仍待补证 |",
f"| AI业务待确认 | {cov['unclearAI']} | 不强行把背景关键词当主营 |",
f"| 成立年与批次冲突 | {cov['dateConflicts']} | 另保留核查记录 |", '',
f"全表由{depth}条目录基础项和{len(companies)-depth}条个案或平台记录组成。候选池已匹配项之外另有{len(companies)-cov['matchingProfiles']}条记录,包含其他来源及发布/转型对照;其中已有{promoted}条原目录记录深化为个案,仍保留目录来源。两种统计边界不同。候选池数字只是该镜像快照的覆盖,Bud等后续定向补充不回填为原候选池已覆盖。", '',
@@ -191,8 +298,8 @@ def render():
f"| 其他目录基础项 | {depth-reviewed} | 规则初分或资料不足,继续待复核 |",
f"| 有团队数字 | {sum(c['team'] is not None for c in companies)} | 多为自报Team Size或Employees,时点与口径不统一 |",
f"| 专项技术证据记录 | {len(companies)-technical['unreviewed']} | 其中{technical['description']}条自述、{technical['public-artifact']}条公开材料、{technical['independent-test']}条第三方评测 |",
f"| 公司披露收入线索 | {income['self']} | 含历史、下界、币种不明及产品业务声明,不等于可比ARR |",
f"| 仅平台收入线索 | {income['platform']} | 同时有公司披露者计入上一行,不重复计数 |",
f"| 企业披露收入线索 | {income['self']} | 含公司或交易当事方披露、历史、下界及币种不明,不等于可比ARR |",
f"| 仅平台收入线索 | {income['platform']} | 同时有企业披露者计入上一行,不重复计数 |",
f"| 仅第三方估算 | {income['estimate']} | 不与直接披露混合 |",
f"| 暂无可用收入数字 | {income['unknown']} | 包含尚未逐家搜索的目录项,不代表没有收入 |", '',
'## 业务分类与数据入口', '',
@@ -217,13 +324,16 @@ def render():
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]
lines.append(f"| 其他官网、评测及公开资料 | {len(others)} | {len({e['url'] for e in others})} | 按每条来源限制理解,不能统一当作独立证据 |")
lines += uncertain_ai_followups()
lines += founding_year_followups()
lines += missing_year_followups()
lines += directory_rechecks()
lines += source_register()
lines += context_methods()
lines += candidate_register()
lines += ['', '## 尚未满足的研究条件', '',
'行业ARR比较仍缺统一业务截止日、币种、经常性定义和业务边界。现有平台MRR可描述已采集产品,不足以代表全部新公司。技术评测只覆盖所列版本与任务,不宜跨领域汇总为统一强弱分数。', '',
'下一轮优先补收入线索的日期及合同定义、非YC和非英文来源、规则初分的人工复核,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。', '',
'下一轮优先补收入线索的日期及合同定义、成立年冲突、AI主营仍待核项、非YC和非英文来源,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。', '',
]
return '\n'.join(lines)
+29
View File
@@ -116,6 +116,17 @@ def main():
mode.add_argument('--revision', type=int)
reading.add_argument('--limit', type=int, default=50)
reading.add_argument('--offset', type=int, default=0)
sections = sub.add_parser('outline', help='Read chapter titles and internal summaries without bodies')
sections.add_argument('slug')
sections.add_argument('--revision', type=int)
section = sub.add_parser('sections', help='Read selected chapter bodies')
section.add_argument('slug')
section.add_argument('ids', nargs='+')
section.add_argument('--revision', type=int)
editing = sub.add_parser('edit-sections', help='Atomically apply chapter operations from private JSON')
editing.add_argument('slug')
editing.add_argument('file')
editing.add_argument('--request-id')
saving = sub.add_parser('save')
saving.add_argument('file', help='Private JSON file or - for stdin; one document or {documents: [...]}')
saving.add_argument('--request-id', help='Reuse this id only with the same input when retrying')
@@ -135,6 +146,24 @@ def main():
elif args.revision is not None:
path += '?' + urllib.parse.urlencode({'revision': args.revision})
result = request(base, token, 'GET', path)
elif args.command in ('outline', 'sections', 'edit-sections'):
path = '/api/documents/sections/' + urllib.parse.quote(args.slug, safe='/')
if args.command == 'edit-sections':
payload = json.load(sys.stdin) if args.file == '-' else json.loads(Path(args.file).read_text())
request_id = args.request_id or str(uuid.uuid4())
if not request_id or len(request_id) > 100 or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._:-' for c in request_id):
raise ValueError('Invalid request id')
print('Request id: ' + request_id, file=sys.stderr)
result = request(base, token, 'POST', path, payload, request_id)
else:
query = []
if args.revision is not None:
query.append(('revision', args.revision))
if args.command == 'sections':
query.extend(('section', section_id) for section_id in args.ids)
if query:
path += '?' + urllib.parse.urlencode(query)
result = request(base, token, 'GET', path)
else:
payload = json.load(sys.stdin) if args.file == '-' else json.loads(Path(args.file).read_text())
request_id = args.request_id or str(uuid.uuid4())
+22 -3
View File
@@ -89,23 +89,42 @@ def link(cid):
def render():
points, excluded = sample(7)
grouped = groups(points)
described = [(key, stats(values)) for key, values in sorted(grouped.items())]
sizes = [value['n'] for _, value in described]
eligible = [(key, value) for key, value in described if value['n'] >= 3]
interpretation = '各组均不足3个样本,暂不比较分组离散度或收入高低。'
if eligible:
highest_key, highest = max(eligible, key=lambda item: item[1]['mean'])
sensitive_key, sensitive = max(eligible, key=lambda item: item[1]['largestRemovalDrop'] or 0)
cvs = [value['cv'] for _, value in eligible if value['cv'] is not None]
cv_text = f"CV范围为{min(cvs):.3f}—{max(cvs):.3f}。" if cvs else '各组CV均不可计算。'
interpretation = (
f"在至少3个样本的组内,当前均值最高的是{CATEGORIES[highest_key[0]]} · {highest_key[2]}"
f"({highest_key[1]},{money(highest['mean'])}美元每月)。{cv_text}"
f"剔除最大值后均值下降最多的是{CATEGORIES[sensitive_key[0]]} · {sensitive_key[2]}"
f"({sensitive_key[1]},下降{sensitive['largestRemovalDrop'] or 0:.1%})。"
'这些只是当前采集点的描述,不据微小CV差异宣称某领域更稳定。'
)
size_text = f"各组目前只有{min(sizes)}至{max(sizes)}个产品" if sizes else '当前没有可比较的产品'
lines = [
'# AI 公司收入样本分布快照', '',
f"截至 {DATA['asOf']},7日窗口内有 {len(points)} 个产品的美元平台MRR可纳入分组描述。创意软件订阅组的样本均值较高、CV也较大;营销软件组与消费者个人应用组的CV接近。各组只有1至5个产品,尚不足以估计行业总体或造假概率。", '',
f"截至 {DATA['asOf']},7日窗口内有 {len(points)} 个产品的美元平台MRR可纳入分组描述。按业务、支付平台和收费模式分别计算均值、中位数与离散度。{size_text},尚不足以估计行业总体或造假概率。", '',
f"这是已采集资料的固定快照。[动态工作台]({BASE})提供筛选与原始证据。窗口按最后同步日计算,不保证所有点同日,也不是{len(DATA['companies']):,}条公司/产品记录的收入普查。", '',
'## 7日窗口的分组结果', '',
'MRR单位为美元每月。CV为样本标准差除以均值;最大值剔除降幅表示移除最高收入产品后均值下降的比例。单个样本不计算离散度。', '',
'| 业务及收费模式 | 平台 | 样本数 | 均值 | 中位数 | CV | 最大值剔除降幅 |',
'| --- | --- | --- | --- | --- | --- | --- |',
]
for key, values in sorted(groups(points).items()):
for key, values in sorted(grouped.items()):
cat, provider, model = key
v = stats(values)
cv = f"{v['cv']:.3f}" if v['cv'] is not None else '不计算'
drop = f"{v['largestRemovalDrop']:.1%}" if v['largestRemovalDrop'] is not None else '不计算'
lines.append(f"| {CATEGORIES[cat]} · {model} | {provider} | {v['n']} | {money(v['mean'])} | {money(v['median'])} | {cv} | {drop} |")
lines += ['',
'在至少3个样本的组内,内容创意软件订阅的均值较高,但移除最大样本后的下降也最大。营销软件与个人应用的CV仅有微小差别,不据此宣称哪个领域更稳定。两个内容类单样本组虽有更高数值,不能作为典型收入或领域排名。', '',
interpretation, '',
'单个样本不计算离散度,也不能作为典型收入或领域排名;不同收费模式和支付平台的分组保留各自含义。', '',
'均值和中位数接近只描述当前采集点,并不证明样本具有代表性。平台公开者、成立年、规模、获客渠道与收费方式均可能影响结果。', '',
'## 样本与证据入口', '',
'点击产品名称可打开公司明细,再查看平台记录及其限制。连接已过期的近期快照仍按所标日期保留,并不表示连接当前有效。', '',
+25
View File
@@ -4,6 +4,7 @@ import io
import json
from pathlib import Path
import tempfile
import sys
import unittest
from unittest.mock import patch
import urllib.error
@@ -15,6 +16,30 @@ spec.loader.exec_module(client)
class ClientTests(unittest.TestCase):
def test_section_reads_select_bodies_and_revision_without_full_document_fetch(self):
argv = ['client', '--url', 'https://docs.example', '--key-file', '/private/key',
'sections', 'endor/example', 'preamble', 's_one', '--revision', '3']
with patch.object(sys, 'argv', argv), patch.object(client, 'read_token', return_value='test-token'), \
patch.object(client, 'request', return_value={'revision': 3, 'sections': []}) as request, \
patch.object(sys, 'stdout', io.StringIO()):
client.main()
request.assert_called_once_with('https://docs.example', 'test-token', 'GET',
'/api/documents/sections/endor/example?revision=3&section=preamble&section=s_one')
def test_section_edit_keeps_payload_and_retry_id(self):
payload = {'revision': 3, 'operations': [{'op': 'update', 'id': 's_one', 'summary': 'Internal summary'}]}
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'edit.json'
path.write_text(json.dumps(payload))
argv = ['client', '--url', 'https://docs.example', '--key-file', '/private/key',
'edit-sections', 'endor/example', str(path), '--request-id', 'edit-1']
with patch.object(sys, 'argv', argv), patch.object(client, 'read_token', return_value='test-token'), \
patch.object(client, 'request', return_value={'revision': 4}) as request, \
patch.object(sys, 'stdout', io.StringIO()), patch.object(sys, 'stderr', io.StringIO()):
client.main()
request.assert_called_once_with('https://docs.example', 'test-token', 'POST',
'/api/documents/sections/endor/example', payload, 'edit-1')
def test_only_tls_or_loopback_origins_are_accepted(self):
self.assertEqual(client.server_url('https://docs.example/'), 'https://docs.example')
self.assertEqual(client.server_url('http://127.0.0.1:18135'), 'http://127.0.0.1:18135')