Files

410 lines
31 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Summarize observed research coverage; does not claim an exhaustive AI census."""
import argparse
from collections import Counter
import json
from pathlib import Path
import sys
from urllib.parse import parse_qs, urlsplit
ROOT = Path(__file__).resolve().parents[1]
DATA = json.loads((ROOT / 'content/research/ai-companies.seed.json').read_text())
AUDIT = json.loads((ROOT / 'content/research/ai-companies.coverage.json').read_text())
SOURCES = json.loads((ROOT / 'content/research/ai-companies.sources.json').read_text())
TARGET = ROOT / '.artifacts/reports/02-coverage-and-gaps.md'
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']
ids = {s['id'] for s in sources}
assert len(ids) == len(sources) and ids == {f'S{i:02}' for i in range(1, 17)}
assert SOURCES['scopeConfirmedAt'] <= DATA['asOf']
host_map = {}
for source in sources:
assert source['status'] in {'candidate-audit', 'targeted', 'pending', 'context', 'access-limited', 'per-company'}
assert source['boundary'] and source['nextCheck']
if source['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['id'] == 'S14' and source.get('publicRosterAudits'):
audits = source['publicRosterAudits']
assert {a['cohort'] for a in audits} == {f'SR{i:03d}' for i in range(1, 8)}
assert len(audits) == 7
profile_urls = []
for audit in audits:
batch = next(b for b in DATA['candidateBatches'] if b['id'] == audit['candidateBatchId'])
query = parse_qs(urlsplit(audit['url']).query)
assert urlsplit(audit['url']).netloc == 'speedrun.a16z.com'
assert query == {'limit': ['100'], 'offset': ['0'], 'cohort': [audit['cohort']]}
assert urlsplit(audit['url']).path == '/api/companies/companyparams'
assert audit['httpStatus'] == 200 and audit['accessedAt'] <= DATA['asOf']
assert audit['next'] is None and audit['previous'] is None
assert len(batch['records']) == audit['recordCount'] == audit['reportedCount']
assert audit['uniquePublicIds'] == audit['uniqueSlugs'] == audit['recordCount']
assert len(audit['responseSha256']) == 64 and all(c in '0123456789abcdef' for c in audit['responseSha256'])
assert batch['sourceId'] == 'S14' and not batch['complete']
proof = next(e for e in DATA['evidence'] if e['id'] == audit['evidenceId'])
assert proof['url'] == audit['url'] and proof['companyId'] is None
profile_urls.extend(row['url'] for row in batch['records'])
assert len(profile_urls) == len(set(profile_urls)), 'duplicate profiles across public cohorts'
if source['id'] == 'S14' and source.get('profileAudit'):
audit = source['profileAudit']
entries = audit['records']
candidates = {row['id']: row for b in DATA['candidateBatches'] for row in b['records']}
evidence_by_id = {row['id']: row for row in DATA['evidence']}
assert len(entries) == audit['recordCount'] == audit['readable']
assert audit['failed'] == 0 and len({e['candidateId'] for e in entries}) == len(entries)
years = Counter(str(e['foundedYear']) for e in entries)
assert dict(years) == audit['yearCounts']
for entry in entries:
row = candidates[entry['candidateId']]
proof = evidence_by_id[entry['evidenceId']]
assert row['profileStatus'] == 'readable' and proof['url'] == row['url']
assert entry['evidenceId'] in row['evidenceIds']
assert isinstance(entry['foundedYear'], int) and 1800 <= entry['foundedYear'] <= int(DATA['asOf'][:4])
assert isinstance(entry['teamSize'], int) and entry['teamSize'] >= 0
assert len(entry['htmlSha256']) == 64 and all(c in '0123456789abcdef' for c in entry['htmlSha256'])
if entry['foundedYear'] not in [2025, 2026]:
assert row['status'] == 'outside-cohort' and not row.get('companyId')
assert sum(e['foundedYear'] not in [2025, 2026] for e in entries) == audit['outsideTargetYears']
assert sum(e['foundedYear'] in [2025, 2026] for e in entries) == audit['targetYearSelfReports']
assert sum(candidates[e['candidateId']]['status'] == 'linked' for e in entries) == audit['newLinkedCompanies']
assert sum(candidates[e['candidateId']]['status'] == 'pending-ai' for e in entries) == audit['pendingCurrentAIOrIdentity']
assert sum(candidates[e['candidateId']]['status'] == 'pending-year' for e in entries) == audit['pendingFoundingIdentity']
assert audit['targetYearSelfReports'] == (
audit['newLinkedCompanies'] + audit['pendingCurrentAIOrIdentity'] + audit['pendingFoundingIdentity']
), 'self-reported years must retain unresolved brand/entity dates'
if source['url']:
assert urlsplit(source['url']).scheme in {'http', 'https'} and urlsplit(source['url']).netloc
for host in source['hosts']:
assert host not in host_map, f'Duplicate source host: {host}'
host_map[host] = source['id']
assigned = {key: [] for key in ids}
for evidence in DATA['evidence']:
owner = host_map.get(urlsplit(evidence['url']).netloc)
if owner is None:
owner = 'S12' if any(word in evidence['kind'] for word in ('官方', '公司自述', '公司披露')) else 'S16'
assigned[owner].append(evidence)
assert sum(len(v) for v in assigned.values()) == len(DATA['evidence'])
statuses = {'candidate-audit': '候选池逐条已读', 'targeted': '定向使用,未穷尽', 'pending': '候选台账待建',
'context': '背景研究', 'access-limited': '访问受限', 'per-company': '随公司逐条核对'}
lines = ['', '## 来源清单与验收边界', '', SOURCES['provenance'], '',
'每条证据在下表只计入一个渠道:先按所列域名归属,再将类型明确标为官方/公司自述/公司披露的剩余项计入S12,其余计入S16。公司可跨渠道出现,所以公司数不可跨行相加。零条证据表示尚未进入研究表,不表示该来源没有相关公司。背景报告不承担公司发现全集的角色。', '',
'| 来源 | 当前状态 | 证据 / 公司 / 观测数 | 已核对范围 | 剩余核查 |', '| --- | --- | --- | --- | --- |']
for source in sources:
evidence = assigned[source['id']]
evidence_ids = {e['id'] for e in evidence}
company_ids = {e['companyId'] for e in evidence if e['companyId']}
observations = sum(o['evidence'] in evidence_ids for o in DATA['observations'])
name = f"[{source['name']}]({source['url']})" if source['url'] else source['name']
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
def context_methods():
evidence = {e['id']: e for e in DATA['evidence']}
rows = [s for s in SOURCES['sources'] if s.get('methodology')]
assert {s['id'] for s in rows} == {'S06', 'S07', 'S08', 'S09'}
lines = ['', '## 背景基准适用性与缺项', '',
'以下核对背景资料的方法及适用范围,不是公司名录的完整覆盖。原始方法记录与全局核查随研究版本进入同一数据库;没有把行业汇总值新增为任何公司的收入观测。', '',
'| 来源与证据 | 样本与统计单位 | 时间 | 本次读取范围 | 可用于什么 | 主要限制 |',
'| --- | --- | --- | --- | --- | --- |']
for source in rows:
m = source['methodology']
assert m['checkedAt'] <= DATA['asOf']
assert all(m.get(key) for key in ('sample', 'unit', 'period', 'access', 'use', 'limit', 'evidenceIds'))
assert all(eid in evidence and evidence[eid]['companyId'] is None for eid in m['evidenceIds'])
assert not any(o['evidence'] in m['evidenceIds'] for o in DATA['observations'])
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in m['evidenceIds'])
lines.append(f"| {source['id']} {source['name']}:{proof} | {m['sample']};{m['unit']} | {m['period']} | {m['access']} | {m['use']} | {m['limit']} |")
lines += ['', '本表的应用解释属于研究判断。选样、分母、时间或指标不同会造成表观差异;这些差异不单独证明财务造假,也不能作为给未知公司填补收入的依据。', '']
return lines
def candidate_register():
batches = DATA.get('candidateBatches', [])
companies = {c['id']: c for c in DATA['companies']}
categories = {c['id']: c['name'] for c in DATA['categories']}
known_sources = {s['id'] for s in SOURCES['sources']}
evidence = {e['id']: e for e in DATA['evidence']}
assert len({b['id'] for b in batches}) == len(batches)
all_ids = [r['id'] for b in batches for r in b['records']]
assert len(set(all_ids)) == len(all_ids)
lines = []
statuses = {'pending-year': '成立年待核', 'pending-ai': 'AI主营待核', 'linked': '已关联公司',
'existing-outside-cohort': '已有关联,年份范围外',
'outside-cohort': '范围外或既有业务,不新增公司', 'promotion': '推广活动,非公司新增'}
for batch in batches:
assert batch['sourceId'] in known_sources
assert batch['accessedAt'] <= DATA['asOf']
assert any(e['url'] == batch['url'] for e in DATA['evidence'])
rows = batch['records']
counts = Counter(r['status'] for r in rows)
assert sum(counts.values()) == len(rows)
lines += ['', f"## {batch['sourceId']} 候选核对({batch['id']})", '',
f"[发现页面]({batch['url']}),读取于{batch['accessedAt']}。{batch['scope']}", '',
f"本批{len(rows)}条:{counts['linked']}条关联公司、{counts['existing-outside-cohort']}条关联范围外对照、{counts['outside-cohort']}条范围外不新增公司、{counts['pending-year']}条成立年待核、{counts['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'] 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 = '—'
if row.get('companyId'):
assert row['companyId'] in companies
c = companies[row['companyId']]
company = f"[{c['name']}]({BASE}?company={c['id']}#research-company-detail)"
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in row.get('evidenceIds', [])) or '成立证据待补,产品页读取状态见候选记录'
category = f"[{categories[row['categoryId']]}]({BASE}?category={row['categoryId']})"
lines.append(f"| {name} | {category} | {statuses[row['status']]} | {company} | {proof} | {row['note']} |")
if batch['sourceId'] == 'S04':
lines += ['', '产品页可读只表示有可访问的资料;本批未将所有产品页完成公司、技术或财务深查。既有直接HTTP采集返回403,当前采用可读的公开页面响应,不声称取得底层全量列表;后续公开档案与未读页面继续保留缺项。', '']
else:
lines += ['', '发现条目和可读正文分开记录;登录外壳、读取失败或搜索摘录不充当已阅读全文。关联公司以列出的原始材料补证,仍保留技术、主体与收入未核事项。', '']
return lines
def 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:
return []
original = {row['ycId']: row for row in AUDIT['records']}
companies = {c['id']: c for c in DATA['companies']}
evidence = {e['id']: e for e in DATA['evidence']}
assert len({row['ycId'] for row in rows}) == len(rows)
lines = ['', '## 原候选池的后续复核', '',
'原始1,428条读取快照与初筛计数保持不变;下表记录当时未入表、后续补足证据后进入公司表的项目。初筛未通过不是永久排除。后续入表不回写为当时已匹配,也不重复计入928条原始新增目录项。', '',
'| 原候选 | 原始状态 | 后续公司记录 | 复核证据 | 处理与限制 |', '| --- | --- | --- | --- | --- |']
for row in rows:
baseline = original[row['ycId']]
assert baseline['status'] == row['originalStatus']
assert row['reviewedAt'] <= DATA['asOf'] and row['reason']
assert row['originalStatus'] in {'ai-unclear', 'year-outside-or-missing', 'date-conflict'}
company = companies[row['companyId']]
assert row['evidenceIds'] and all(eid in evidence for eid in row['evidenceIds'])
proof = '、'.join(f"[{eid}]({evidence[eid]['url']})" for eid in row['evidenceIds'])
lines.append(f"| [{baseline['name']} · YC{row['ycId']}]({baseline['source']}) | {row['originalStatus']} | [{company['name']}]({BASE}?company={company['id']}#research-company-detail) | {proof} | {row['reviewedAt']}:{row['reason']} |")
lines += ['', f"已记录{len(rows)}条后续复核入表;其余未入表候选仍按原状态保留缺项,不能把这一批复核视为完成全部模糊项。", '']
return lines
def render():
cov = DATA['coverage']
companies = DATA['companies']
depth = sum(bool(c.get('profileOnly')) for c in companies)
origin = sum(bool(c.get('profileOnly')) or c.get('directoryOrigin') == 'yc-2025-2026' for c in companies)
promoted = origin - depth
reviewed = sum(bool(c.get('profileOnly')) and '人工复核' in c.get('classificationBasis', '') for c in companies)
years = Counter(c['year'] for c in companies)
income = Counter(c['revenueStatus'] for c in companies)
technical = Counter(c.get('technicalReview', {}).get('level', 'unreviewed') for c in companies)
assert cov['profilesRead'] + cov['failedProfiles'] == cov['candidateRecords']
assert cov['matchingProfiles'] == cov['existingProfiles'] + cov['newProfiles']
assert cov['matchingProfiles'] + cov['otherOrMissingYear'] + cov['unclearAI'] + cov['dateConflicts'] == cov['profilesRead']
assert origin == cov['newProfiles']
assert AUDIT['accessedAt'] == cov['accessedAt']
assert AUDIT['counts'] == cov
assert len(AUDIT['records']) == cov['profilesRead']
assert Counter(x['status'] for x in AUDIT['records']) == {
'imported': cov['newProfiles'], 'existing': cov['existingProfiles'],
'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确认为:以确认的公开来源清单为边界,逐条核对并标明缺项。下方区分已逐条审计的候选池与定向补充来源;已使用某来源不代表该来源已被完整覆盖。', '',
f"[分类和数据工作台]({BASE})链接原始证据,[收入样本快照](/docs/other-research/ai-company-research/income-snapshot)展示当前可计算的MRR样本。", '',
'## 候选池与公司表的关系', '',
'| 环节 | 记录数 | 含义 |', '| --- | --- | --- |',
f"| YC批次候选 | {cov['candidateRecords']:,} | 从公开镜像发现,随后读取YC官方页面 |",
f"| 官方页面读取 | {cov['profilesRead']:,} | 此候选池读取失败{cov['failedProfiles']}条 |",
f"| 初筛匹配 | {cov['matchingProfiles']:,} | 成立年及AI相关描述通过初筛,尚非逐家商业验证 |",
f"| 与原表重合 | {cov['existingProfiles']} | URL匹配,未再次新增 |",
f"| 新增目录记录 | {cov['newProfiles']:,} | 原始入表来源,后续深化不改变发现计数 |",
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等后续定向补充不回填为原候选池已覆盖。", '',
'## 成立年与证据深度', '',
'| 维度 | 当前记录 | 解读 |', '| --- | --- | --- |',
f"| 记录为2025年成立 | {years[2025]} | 成立来源和精度见公司明细 |",
f"| 记录为2026年成立 | {years[2026]} | 不将产品发布日期代替成立日 |",
f"| 成立年未知或范围外 | {sum(v for k,v in years.items() if k not in (2025,2026))} | 发布/转型对照单列,不进入两年成立分组 |",
f"| 目录业务分类人工复核 | {reviewed} | 仅复核主营分类,不代表技术或财务审计 |",
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['estimate']} | 不与直接披露混合 |",
f"| 暂无可用收入数字 | {income['unknown']} | 包含尚未逐家搜索的目录项,不代表没有收入 |", '',
'## 业务分类与数据入口', '',
'| 业务类别 | 记录数 |', '| --- | --- |',
]
counts = Counter(c['cat'] for c in companies)
for category in DATA['categories']:
lines.append(f"| [{category['name']}]({BASE}?category={category['id']}) | {counts[category['id']]} |")
lines += ['', '## 已使用来源的分布', '',
f"{len(DATA['evidence']):,}条证据记录对应{len({e['url'] for e in DATA['evidence']}):,}个不同URL。不同URL不自动构成独立验证;同一声明可能由公司、投资方和媒体重复转述。", '',
'| 来源 | 证据记录 | 不同URL | 用途及限制 |', '| --- | --- | --- | --- |',
]
families = [('YC官方目录', 'www.ycombinator.com', '公司自述的成立、团队与业务;英文及加速器样本偏多'),
('TrustMRR', 'trustmrr.com', '支付平台重建指标及方法;公开者存在选择偏差,非财务审计'),
('a16z Speedrun', 'speedrun.a16z.com', '投资机构中的公司介绍及收入声明;存在利益关系'),
('TechCrunch', 'techcrunch.com', '原始采访或报道;转述公司数字不等于查账'),
('Sacra', 'sacra.com', '第三方估算;与直接披露分开')]
included = set()
for name, host, note in families:
entries = [e for e in DATA['evidence'] if urlsplit(e['url']).netloc == host]
included.add(host)
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可描述已采集产品,不足以代表全部新公司。技术评测只覆盖所列版本与任务,不宜跨领域汇总为统一强弱分数。', '',
'下一轮优先补收入线索的日期及合同定义、成立年冲突、AI主营仍待核项、非YC和非英文来源,以及停止服务或被收购案例。这样能减少“只见赢家”和“只见融资公司”的偏差。公开来源池之外的未披露公司仍无法证明已被穷尽。', '',
]
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true')
parser.add_argument('--document-export', type=Path)
args = parser.parse_args()
content = render()
if args.check:
if args.document_export is not None:
bundle = json.loads(args.document_export.read_text())
stored = next((d['content'] for d in bundle['documents'] if d['sourceSlug'] == SOURCE_SLUG), None)
if stored != content:
sys.exit('Research coverage report differs; review and regenerate')
print('Research coverage accounting verified; supplied document export compared when requested')
else:
TARGET.parent.mkdir(parents=True, exist_ok=True)
TARGET.write_text(content)
print(TARGET.relative_to(ROOT))
if __name__ == '__main__':
main()