import fs from 'node:fs';
|
import path from 'node:path';
|
import crypto from 'node:crypto';
|
|
const ROOT = process.cwd();
|
const CASE_ROOT = path.join(ROOT, 'ana-data', 'cases', '机器人案例', 'ANA-ROBOT-INDUSTRY-001');
|
const AS_OF = '2026-08-05';
|
const BOUNDARY = 'OPEN_RETAINED_GAP|PENDING_VERIFICATION_MAX_NO_UPGRADE|NO_AUTOMATIC_FORMAL_POOL_CHANGE|NO_UPGRADE';
|
|
const PATHS = {
|
v2: path.join(CASE_ROOT, 'outputs', '数据表', 'robot_company_information_v02_20260805.csv'),
|
candidates: path.join(CASE_ROOT, 'evidence', 'detail_strengthening_p0_primary_source_candidate_register_repair001_20260730.csv'),
|
gapRegister: path.join(CASE_ROOT, 'evidence', 'next_robot_075_high_value_gap_register_20260805.csv'),
|
candidateReview: path.join(CASE_ROOT, 'evidence', 'next_robot_075_p0_candidate_semantic_verification_20260805.csv'),
|
sourceBacktrace: path.join(CASE_ROOT, 'evidence', 'next_robot_075_primary_source_backtrace_20260805.csv'),
|
v3: path.join(CASE_ROOT, 'outputs', '数据表', 'robot_company_information_v03_20260805.csv'),
|
summary: path.join(CASE_ROOT, 'outputs', '核心文档', '机器人企业高价值缺口集中回源_第一轮_20260805.md'),
|
validation: path.join(CASE_ROOT, 'manifest', 'next_robot_075_high_value_gap_content_sprint_validation_20260805.csv')
|
};
|
|
function readText(filePath) {
|
return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
}
|
|
function parseCsv(text) {
|
const records = [];
|
let record = [];
|
let value = '';
|
let quoted = false;
|
for (let index = 0; index < text.length; index += 1) {
|
const char = text[index];
|
if (quoted) {
|
if (char === '"') {
|
if (text[index + 1] === '"') {
|
value += '"';
|
index += 1;
|
} else quoted = false;
|
} else value += char;
|
} else if (char === '"') quoted = true;
|
else if (char === ',') {
|
record.push(value);
|
value = '';
|
} else if (char === '\n') {
|
record.push(value.replace(/\r$/, ''));
|
records.push(record);
|
record = [];
|
value = '';
|
} else value += char;
|
}
|
if (value.length || record.length) {
|
record.push(value.replace(/\r$/, ''));
|
records.push(record);
|
}
|
if (!records.length) return [];
|
const columns = records[0];
|
return records.slice(1).filter((row) => row.some(Boolean)).map((row) =>
|
Object.fromEntries(columns.map((column, index) => [column, row[index] || '']))
|
);
|
}
|
|
function readCsv(filePath) {
|
return parseCsv(readText(filePath));
|
}
|
|
function csvCell(value) {
|
return `"${String(value ?? '').replace(/"/g, '""')}"`;
|
}
|
|
function writeCsv(filePath, rows, columns) {
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
const lines = [columns.map(csvCell).join(',')];
|
for (const row of rows) lines.push(columns.map((column) => csvCell(row[column])).join(','));
|
fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
|
}
|
|
function writeText(filePath, text) {
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
fs.writeFileSync(filePath, text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'), 'utf8');
|
}
|
|
function shaFile(filePath) {
|
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
}
|
|
function normalize(value) {
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
}
|
|
function unique(values) {
|
return [...new Set(values.filter(Boolean))];
|
}
|
|
function md(value) {
|
return normalize(value).replace(/\|/g, '/');
|
}
|
|
const gapRules = [
|
['CUSTOMER', /客户|终端客户|具名客户/],
|
['ORDER', /订单|合同|定点|采购|预订|复购|回款/],
|
['MASS_PRODUCTION', /量产|小批量|送样|交付|出货|销量|发货|装机|部署台数/],
|
['CAPACITY', /产能|利用率|产量|良率|排产/],
|
['ASP', /ASP|售价|价格|单机价值|收费|订阅/i],
|
['LIFETIME', /寿命|可靠性|MTBF|循环|质保|故障率|返修|维护/i],
|
['REVENUE_PROFIT', /收入|利润|毛利|经营现金流|财务|营业/]
|
];
|
|
const evidenceNeeded = {
|
CUSTOMER: '客户公告、客户案例、验收材料或双方正式披露',
|
ORDER: '合同、定点通知、采购公告、订单或回款原始披露',
|
MASS_PRODUCTION: '量产公告、交付清单、出货/装机/销量原始数据',
|
CAPACITY: '产线公告、环评/募投、产能产量良率与利用率原始表',
|
ASP: '产品报价、采购单价、合同金额与数量或正式收费页',
|
LIFETIME: '规格书、测试报告、质保条款、现场可靠性或循环寿命数据',
|
REVENUE_PROFIT: '年报、公告、IR或分部收入利润与毛利原表'
|
};
|
|
const companies = readCsv(PATHS.v2);
|
const candidates = readCsv(PATHS.candidates);
|
const v2IdentityBefore = { bytes: fs.statSync(PATHS.v2).size, sha256: shaFile(PATHS.v2) };
|
const companyById = new Map(companies.map((row) => [row.company_id, row]));
|
const companyByName = new Map(companies.map((row) => [row.canonical_name, row]));
|
|
const gapRegister = [];
|
for (const company of companies) {
|
const text = normalize(company.high_value_gap_and_risk);
|
const priority = /^P0/.test(text) ? 'P0' : /^P1/.test(text) ? 'P1' : 'P2';
|
for (const [gapType, pattern] of gapRules) {
|
if (!pattern.test(text)) continue;
|
gapRegister.push({
|
gap_id: `N075-GAP-${String(gapRegister.length + 1).padStart(4, '0')}`,
|
company_id: company.company_id,
|
canonical_name: company.canonical_name,
|
priority,
|
gap_type: gapType,
|
original_gap_text: text,
|
required_primary_evidence: evidenceNeeded[gapType],
|
source_profile_status: company.source_profile_status,
|
formal_output_status: company.formal_output_status,
|
current_status: 'OPEN_RETAINED_GAP',
|
claim_strength_ceiling: 'PENDING_VERIFICATION_MAX_NO_UPGRADE',
|
formal_pool_effect: 'NO_AUTOMATIC_FORMAL_POOL_CHANGE',
|
evidence_strength_effect: 'NO_UPGRADE',
|
sprint_priority: priority === 'P0' ? 'FIRST' : priority === 'P1' ? 'SECOND' : 'BACKLOG'
|
});
|
}
|
}
|
|
const semanticDecisions = {
|
'P0DISCR1-CAND-001': ['中鼎股份-星汇传感', 'VERIFIED_COMPANY_DIRECT', 'CUSTOMER|ORDER|MASS_PRODUCTION', '交易所问询回复可直接支持中鼎机器人产品认证、定点与小批量阶段,但不支持大批量订单。'],
|
'P0DISCR1-CAND-002': ['', 'VERIFIED_INDUSTRY_CONTEXT_NOT_COMPANY_FACT', 'CUSTOMER', '政府采购内容支持认证与测试基础设施存在,不能绑定具体机器人企业客户关系。'],
|
'P0DISCR1-CAND-003': ['', 'VERIFIED_INDUSTRY_CONTEXT_NOT_COMPANY_FACT', 'ORDER', '教学采购文件中的BOM与订单流程只可作为方法背景,不能作为产业链BOM或企业订单。'],
|
'P0DISCR1-CAND-004': ['Universal Robots', 'VERIFIED_COMPANY_PRODUCT_PROCUREMENT', 'ORDER|MASS_PRODUCTION', '官方中标公告可支持UR12e与夹爪采购事实;合同主体与金额边界按公告保留。'],
|
'P0DISCR1-CAND-005': ['埃夫特', 'VERIFIED_COMPANY_DIRECT', 'REVENUE_PROFIT', '交易所半年度报告可直接支持机器人业务收入占比和毛利率,但时点为2024年上半年。'],
|
'P0DISCR1-CAND-006': ['', 'VERIFIED_EXTERNAL_COMPANY_UNMAPPED', 'REVENUE_PROFIT', 'Serve Robotics年报是公司直接经营口径,但该公司当前不在307家主表中,先保留为扩展候选。'],
|
'P0DISCR1-CAND-007': ['', 'VERIFIED_MARKET_PRIMARY', 'REVENUE_PROFIT', 'IFR原始报告入口可支持市场安装与销量口径,不映射为单家公司收入或销量。'],
|
'P0DISCR1-CAND-008': ['', 'VERIFIED_MARKET_FORECAST_PRIMARY', 'REVENUE_PROFIT', 'Omdia官方预测可支持人形出货预测口径,不能替代实际出货或企业业绩。'],
|
'P0DISCR1-CAND-009': ['小米集团', 'VERIFIED_COMPANY_TECHNICAL_PRIMARY', '', '小米机器人官方技术页可支持模型与数据规模的公司自述,不支持客户、订单或收入,因此不计入本轮七类商业缺口回补。'],
|
'P0DISCR1-CAND-010': ['', 'VERIFIED_TECHNICAL_CONTEXT_NOT_COMPANY_FACT', '', '欧盟项目报告可支持工业机器人模型集成研究,不映射为OEM销售或客户采购,因此不计入本轮七类商业缺口回补。'],
|
'P0DISCR1-CAND-011': ['', 'VERIFIED_PRODUCT_PRIMARY_UNMAPPED', '', '华为云产品文档可支持CloudRobo能力;华为不在当前307家主表,也不计入本轮七类商业缺口回补。'],
|
'P0DISCR1-CAND-012': ['Universal Robots', 'DUPLICATE_SOURCE_REUSED_FOR_PRODUCT_GAP', 'ORDER|MASS_PRODUCTION', '与CAND-004为同一采购公告,语义可复用但不计为第二个独立来源。'],
|
'P0DISCR1-CAND-013': ['', 'VERIFIED_PROCUREMENT_PRIMARY_UNNAMED_SUPPLIER', 'ORDER|MASS_PRODUCTION', '开标信息支持数量与交付要求,但不能在未中标绑定时归入具体机器人企业。'],
|
'P0DISCR1-CAND-014': ['', 'VERIFIED_PROCUREMENT_PRIMARY_NO_AWARD', 'ORDER', '框架采购入口支持需求存在,但未形成具体供应商订单或收入。']
|
};
|
|
const candidateReview = candidates.map((candidate) => {
|
const [mappedName, disposition, gapTypes, rationale] = semanticDecisions[candidate.candidate_id] || ['', 'UNREVIEWED', '', ''];
|
const mappedCompany = mappedName ? companyByName.get(mappedName) : null;
|
return {
|
review_id: `N075-SEM-${candidate.candidate_id.slice(-3)}`,
|
candidate_id: candidate.candidate_id,
|
query_id: candidate.query_id,
|
conceptual_gap_key: candidate.conceptual_gap_key,
|
source_class: candidate.source_class,
|
publisher: candidate.publisher,
|
title: candidate.title,
|
canonical_url: candidate.canonical_url,
|
original_relevance_status: candidate.relevance_status,
|
mapped_company_id: mappedCompany?.company_id || '',
|
mapped_company_name: mappedName,
|
gap_types_addressed: gapTypes,
|
semantic_verification_result: disposition,
|
semantic_rationale: rationale,
|
primary_source_status_after: disposition === 'DUPLICATE_SOURCE_REUSED_FOR_PRODUCT_GAP' ? 'PRIMARY_SOURCE_DUPLICATE' : 'PRIMARY_SOURCE_SEMANTICALLY_REVIEWED',
|
gap_effect: 'GAP_REMAINS_OPEN_CONTENT_SCOPE_NARROWED_ONLY',
|
claim_strength_after: 'PENDING_VERIFICATION_MAX_NO_UPGRADE',
|
formal_pool_effect: 'NO_AUTOMATIC_FORMAL_POOL_CHANGE',
|
evidence_strength_effect: 'NO_UPGRADE',
|
reviewed_at: `${AS_OF}T16:30:00+08:00`
|
};
|
});
|
|
function companyId(name) {
|
const row = companyByName.get(name);
|
if (!row) throw new Error(`UNRESOLVED_COMPANY:${name}`);
|
return row.company_id;
|
}
|
|
const sourceBacktrace = [
|
{
|
source_update_id: 'N075-SRC-001', company_id: companyId('中鼎股份-星汇传感'), canonical_name: '中鼎股份-星汇传感', target_field: 'customer_and_commercial_relationship', gap_types_addressed: 'CUSTOMER|ORDER|MASS_PRODUCTION',
|
source_class: 'EXCHANGE_OR_REGULATOR_FILING', publisher: '深圳证券交易所', source_title: '中鼎股份可转债发行审核问询回复', publication_date: '2026-05-19', canonical_url: 'https://disc.static.szse.cn/download/disc/disk03/finalpage/2026-05-19/26188182-330f-4372-ba4a-d34aef7643c3.PDF', source_locator: 'web.open:turn179view0#L2944-L2998',
|
content_update_text: '中鼎披露:谐波减速器已完成认证并进入小批量供货,传感器已通过验证并取得定点,关节总成处客户验证,本体完成首台样机;文件还说明客户A发出定点通知、傅利叶签署战略合作协议。', limitation: '未披露批量订单规模;定点、战略协议、小批量供货和批量供货必须分层。'
|
},
|
{
|
source_update_id: 'N075-SRC-002', company_id: companyId('中鼎股份-星汇传感'), canonical_name: '中鼎股份-星汇传感', target_field: 'capacity_delivery_and_procurement', gap_types_addressed: 'CAPACITY|MASS_PRODUCTION',
|
source_class: 'EXCHANGE_OR_REGULATOR_FILING', publisher: '深圳证券交易所', source_title: '中鼎股份可转债发行审核问询回复', publication_date: '2026-05-19', canonical_url: 'https://disc.static.szse.cn/download/disc/disk03/finalpage/2026-05-19/26188182-330f-4372-ba4a-d34aef7643c3.PDF', source_locator: 'web.open:turn179view0#L3002-L3073',
|
content_update_text: '募投文件规划传感器15万台/年、谐波减速器15万台/年、关节合计28万个/年、控制硬件及线束3万套/年、软件授权与服务400套/年,以及人形与双足本体合计2万台/年。', limitation: '全部为项目达产规划,不等于当前产能、产量或实际订单消化。'
|
},
|
{
|
source_update_id: 'N075-SRC-003', company_id: companyId('埃夫特'), canonical_name: '埃夫特', target_field: 'revenue_and_operational_metric', gap_types_addressed: 'REVENUE_PROFIT',
|
source_class: 'EXCHANGE_OR_REGULATOR_FILING', publisher: '上海证券交易所', source_title: '埃夫特2024年半年度报告', publication_date: '2024-08-30', canonical_url: 'https://big5.sse.com.cn/site/cht/www.sse.com.cn/disclosure/listedinfo/announcement/c/new/2024-08-30/688165_20240830_7UCG.pdf', source_locator: 'N040:P0DISCR1-CAND-005',
|
content_update_text: '埃夫特2024年半年度报告披露机器人业务占营业收入58.81%、系统集成业务占39.77%,机器人业务毛利率18.77%。', limitation: '为2024年上半年口径,不代表2025/2026最新经营状态,也不是人形机器人专项收入。'
|
},
|
{
|
source_update_id: 'N075-SRC-004', company_id: companyId('FANUC'), canonical_name: 'FANUC', target_field: 'revenue_and_operational_metric', gap_types_addressed: 'REVENUE_PROFIT',
|
source_class: 'COMPANY_IR_OR_ANNUAL_REPORT', publisher: 'FANUC Corporation', source_title: 'Financial Results for the Year Ended March 31, 2026', publication_date: '2026-04-24', canonical_url: 'https://www.fanuc.co.jp/en/ir/announce/pdf/2026/financialresult202603_e.pdf', source_locator: 'web.open:turn179view2#L282-L301',
|
content_update_text: 'FANUC截至2026年3月的财年Robot分部销售额为3,786.10亿日元,同比增长14.9%,占集团净销售额44.1%;中国EV及一般工业需求较强,美洲销售同比提高。', limitation: '文件未在该分部口径披露营业利润、销量或ASP。'
|
},
|
{
|
source_update_id: 'N075-SRC-005', company_id: companyId('FANUC'), canonical_name: 'FANUC', target_field: 'customer_and_commercial_relationship', gap_types_addressed: 'ORDER',
|
source_class: 'COMPANY_IR_OR_ANNUAL_REPORT', publisher: 'FANUC Corporation', source_title: 'FY2025 Q3 Financial Results Q&A', publication_date: '2026-01-26', canonical_url: 'https://www.fanuc.co.jp/en/ir/announce/pdf/2026/qasummary202512_e.pdf', source_locator: 'web.open:turn182view0#L9-L18',
|
content_update_text: 'FANUC披露Physical AI开放平台发布后收到超过1,000台CRX系列订单并计入三季度订单,另有数千台项目询盘。', limitation: '询盘不是订单;已披露订单未给出客户、金额、交付与收入确认。'
|
},
|
{
|
source_update_id: 'N075-SRC-006', company_id: companyId('Yaskawa'), canonical_name: 'Yaskawa', target_field: 'revenue_and_operational_metric', gap_types_addressed: 'REVENUE_PROFIT',
|
source_class: 'COMPANY_IR_OR_ANNUAL_REPORT', publisher: 'Yaskawa Electric Corporation', source_title: 'Results Briefing for FY2025', publication_date: '2026-04-10', canonical_url: 'https://www.yaskawa-global.com/wp-content/uploads/2026/04/20260410_haifu_en.pdf', source_locator: 'web.open:turn179view3#L143-L163',
|
content_update_text: 'Yaskawa FY2025 Robotics收入2,470亿日元,同比增长4.0%;营业利润204亿日元,同比下降14.0%,营业利润率8.3%,低于上年的10.0%。', limitation: 'Robotics分部包含工业机器人等多类业务,不是具身/人形机器人专项收入。'
|
},
|
{
|
source_update_id: 'N075-SRC-007', company_id: companyId('KUKA'), canonical_name: 'KUKA', target_field: 'revenue_and_operational_metric', gap_types_addressed: 'ORDER|REVENUE_PROFIT',
|
source_class: 'COMPANY_IR_OR_ANNUAL_REPORT', publisher: 'KUKA Group', source_title: 'KUKA Group Annual Report 2025', publication_date: '2026-03-01', canonical_url: 'https://www.kuka.com/-/media/kuka-corporate/documents/ir/reports-and-presentations/en/annual-report/annual-report-2025.pdf', source_locator: 'Annual Report 2025 p.5,p.17,p.22; web.open:turn185view2#L86-L93; web.open:turn185view3#L728-L756',
|
content_update_text: 'KUKA集团2025年订单41.573亿欧元、收入38.972亿欧元、EBIT率1.5%,期末订单积压32.728亿欧元;收入同比增长4.4%,利润率受重组费用等影响下降。', limitation: '为KUKA集团口径,包含机器人、系统和Swisslog等业务,不能当作机器人单体销量或利润。'
|
},
|
{
|
source_update_id: 'N075-SRC-008', company_id: companyId('Universal Robots'), canonical_name: 'Universal Robots', target_field: 'revenue_and_operational_metric', gap_types_addressed: 'MASS_PRODUCTION|REVENUE_PROFIT',
|
source_class: 'REGULATOR_FILING', publisher: 'U.S. SEC / Teradyne', source_title: 'Teradyne 2025 Form 10-K', publication_date: '2026-02-19', canonical_url: 'https://www.sec.gov/Archives/edgar/data/97210/000119312526059002/ter-20251231.htm', source_locator: 'web.search:turn180search0',
|
content_update_text: 'Teradyne 2025年Robotics分部收入3.083亿美元,同比下降15.5%,主要因协作机器人手臂和AMR销售减少;文件同时披露Universal Robots累计售出超过11万台协作机器人。', limitation: 'Robotics分部还包含MiR,不能据此拆出UR单体收入、利润、年度销量或ASP。'
|
}
|
].map((row) => ({
|
...row,
|
source_fact_status: 'PRIMARY_SOURCE_CONTENT_CAPTURED_PENDING_VERIFICATION_NO_UPGRADE',
|
gap_effect: 'OPEN_RETAINED_GAP_PARTIALLY_NARROWED',
|
claim_strength_after: 'PENDING_VERIFICATION_MAX_NO_UPGRADE',
|
formal_pool_effect: 'NO_AUTOMATIC_FORMAL_POOL_CHANGE',
|
evidence_strength_effect: 'NO_UPGRADE',
|
captured_at: `${AS_OF}T16:35:00+08:00`
|
}));
|
|
const updatesByCompany = new Map();
|
for (const update of sourceBacktrace) {
|
if (!updatesByCompany.has(update.company_id)) updatesByCompany.set(update.company_id, []);
|
updatesByCompany.get(update.company_id).push(update);
|
}
|
const candidateByCompany = new Map();
|
for (const review of candidateReview) {
|
if (!review.mapped_company_id) continue;
|
if (!candidateByCompany.has(review.mapped_company_id)) candidateByCompany.set(review.mapped_company_id, []);
|
candidateByCompany.get(review.mapped_company_id).push(review.candidate_id);
|
}
|
const gapByCompany = new Map();
|
for (const gap of gapRegister) {
|
if (!gapByCompany.has(gap.company_id)) gapByCompany.set(gap.company_id, []);
|
gapByCompany.get(gap.company_id).push(gap);
|
}
|
|
const v3 = companies.map((row) => {
|
const next = { ...row };
|
const updates = updatesByCompany.get(row.company_id) || [];
|
for (const field of ['product_and_technical_capability', 'customer_and_commercial_relationship', 'revenue_and_operational_metric', 'capacity_delivery_and_procurement', 'market_position_and_chain_role', 'high_value_gap_and_risk']) {
|
const additions = updates.filter((update) => update.target_field === field);
|
if (!additions.length) continue;
|
next[field] = [normalize(row[field]), `【N075主源回补】${additions.map((update) => `〔${update.source_update_id}〕${update.content_update_text}(边界:${update.limitation})`).join(';')}`].filter(Boolean).join(';');
|
}
|
const gaps = gapByCompany.get(row.company_id) || [];
|
next.n075_high_value_gap_count = String(gaps.length);
|
next.n075_p0_gap_count = String(gaps.filter((gap) => gap.priority === 'P0').length);
|
next.n075_primary_source_update_ids = updates.map((update) => update.source_update_id).join('|');
|
next.n075_candidate_semantic_ids = candidateByCompany.get(row.company_id)?.join('|') || '';
|
next.n075_content_update_status = updates.length ? 'PRIMARY_SOURCE_CONTENT_ENRICHED_GAP_REMAINS_OPEN_NO_UPGRADE' : 'NO_N075_PRIMARY_SOURCE_UPDATE';
|
next.content_update_batch = updates.length ? unique(String(row.content_update_batch || '').split('|').concat('NEXT-ROBOT-075')).join('|') : row.content_update_batch;
|
next.as_of = AS_OF;
|
return next;
|
});
|
|
const gapColumns = ['gap_id','company_id','canonical_name','priority','gap_type','original_gap_text','required_primary_evidence','source_profile_status','formal_output_status','current_status','claim_strength_ceiling','formal_pool_effect','evidence_strength_effect','sprint_priority'];
|
const candidateColumns = ['review_id','candidate_id','query_id','conceptual_gap_key','source_class','publisher','title','canonical_url','original_relevance_status','mapped_company_id','mapped_company_name','gap_types_addressed','semantic_verification_result','semantic_rationale','primary_source_status_after','gap_effect','claim_strength_after','formal_pool_effect','evidence_strength_effect','reviewed_at'];
|
const sourceColumns = ['source_update_id','company_id','canonical_name','target_field','gap_types_addressed','source_class','publisher','source_title','publication_date','canonical_url','source_locator','content_update_text','limitation','source_fact_status','gap_effect','claim_strength_after','formal_pool_effect','evidence_strength_effect','captured_at'];
|
const v3Columns = unique(Object.keys(companies[0]).concat(['n075_high_value_gap_count','n075_p0_gap_count','n075_primary_source_update_ids','n075_candidate_semantic_ids','n075_content_update_status']));
|
|
writeCsv(PATHS.gapRegister, gapRegister, gapColumns);
|
writeCsv(PATHS.candidateReview, candidateReview, candidateColumns);
|
writeCsv(PATHS.sourceBacktrace, sourceBacktrace, sourceColumns);
|
writeCsv(PATHS.v3, v3, v3Columns);
|
|
const categoryCounts = Object.fromEntries(gapRules.map(([gapType]) => [gapType, gapRegister.filter((row) => row.gap_type === gapType).length]));
|
const p0GapCount = gapRegister.filter((row) => row.priority === 'P0').length;
|
const affectedCompanies = new Set(sourceBacktrace.map((row) => row.company_id));
|
const dispositionCounts = candidateReview.reduce((acc, row) => {
|
acc[row.semantic_verification_result] = (acc[row.semantic_verification_result] || 0) + 1;
|
return acc;
|
}, {});
|
|
const summary = [];
|
summary.push('# 机器人企业高价值缺口集中回源(第一轮)');
|
summary.push('');
|
summary.push(`> 更新日期:${AS_OF}`);
|
summary.push('> 原则:按客户、订单、量产、产能、ASP、寿命、收入与利润合并去重;只将具体企业主源写入企业表,行业背景不冒充公司事实。');
|
summary.push('');
|
summary.push('## 1. 缺口全集');
|
summary.push('');
|
summary.push(`- 307家公司中263家存在七类高价值缺口,共${gapRegister.length}个“公司×缺口类型”组合。`);
|
summary.push(`- P0公司83家,对应P0缺口组合${p0GapCount}个;本轮先集中处理能用公开主源直接回补的对象。`);
|
summary.push('');
|
summary.push('| 缺口类型 | 公司数 | 回源目标 |');
|
summary.push('|---|---:|---|');
|
for (const [gapType] of gapRules) summary.push(`| ${gapType} | ${categoryCounts[gapType]} | ${evidenceNeeded[gapType]} |`);
|
summary.push('');
|
summary.push('## 2. 旧P0候选语义核验');
|
summary.push('');
|
summary.push(`- 已对N040留下的14条主源候选逐条完成语义归类;没有把采购制度、认证规则或研究项目直接写成企业订单/收入。`);
|
summary.push(`- 结果分布:${Object.entries(dispositionCounts).map(([key, value]) => `${key}=${value}`).join(',')}。`);
|
summary.push('');
|
summary.push('| 候选 | 归属企业 | 核验结果 | 核心判断 |');
|
summary.push('|---|---|---|---|');
|
for (const row of candidateReview) summary.push(`| ${row.candidate_id} | ${md(row.mapped_company_name || '不映射具体企业')} | ${row.semantic_verification_result} | ${md(row.semantic_rationale)} |`);
|
summary.push('');
|
summary.push('## 3. 首批企业主源回补');
|
summary.push('');
|
summary.push(`- 新增${sourceBacktrace.length}条主源字段更新,落入${affectedCompanies.size}家公司;所有更新仍保留待核验、无自动入池、无证据升级。`);
|
summary.push('');
|
summary.push('| 企业 | 字段 | 主源事实 | 保留边界 |');
|
summary.push('|---|---|---|---|');
|
for (const row of sourceBacktrace) summary.push(`| ${md(row.canonical_name)} | ${row.target_field} | ${md(row.content_update_text)} | ${md(row.limitation)} |`);
|
summary.push('');
|
summary.push('## 4. 实用结论');
|
summary.push('');
|
summary.push('- 中鼎:认证、定点、小批量供货和募投达产规划已分层写清,不能把规划产能写成当前产量。');
|
summary.push('- FANUC、Yaskawa、KUKA、Teradyne Robotics:最新分部或集团经营数据已进入统一企业表;其中KUKA与Teradyne仍不是单一机器人产品/UR单体口径。');
|
summary.push('- 埃夫特:补入交易所披露的机器人业务结构和毛利率,但保留2024年上半年时点。');
|
summary.push('- 后续优先级:继续处理剩余P0公司,先找客户侧/交易所/年报能一次关闭多个字段的来源;无法找到的直接保留缺口,不用二手转述填空。');
|
summary.push('');
|
summary.push('## 5. 数据入口');
|
summary.push('');
|
summary.push('- 高价值缺口登记:`evidence/next_robot_075_high_value_gap_register_20260805.csv`');
|
summary.push('- P0候选语义核验:`evidence/next_robot_075_p0_candidate_semantic_verification_20260805.csv`');
|
summary.push('- 主源回溯与字段更新:`evidence/next_robot_075_primary_source_backtrace_20260805.csv`');
|
summary.push('- 307家公司统一信息表v03:`outputs/数据表/robot_company_information_v03_20260805.csv`');
|
writeText(PATHS.summary, `${summary.join('\n')}\n`);
|
|
const validation = [];
|
function check(id, description, expected, actual) {
|
validation.push({ check_id: id, check_description: description, expected: String(expected), actual: String(actual), status: String(expected) === String(actual) ? 'PASS' : 'FAIL' });
|
}
|
const immutable = ['company_id','canonical_name','aliases','company_type','region','listed_status','primary_track','detail_track','source_profile_status','formal_output_status','evidence_ceiling','formal_pool_effect'];
|
let immutableMismatch = 0;
|
for (const row of v3) {
|
const before = companyById.get(row.company_id);
|
for (const column of immutable) if ((before?.[column] || '') !== (row[column] || '')) immutableMismatch += 1;
|
}
|
const candidateById = new Map(candidates.map((row) => [row.candidate_id, row]));
|
const candidateIdentityMismatch = candidateReview.filter((row) => {
|
const source = candidateById.get(row.candidate_id);
|
return !source || source.canonical_url !== row.canonical_url || source.query_id !== row.query_id || source.conceptual_gap_key !== row.conceptual_gap_key;
|
}).length;
|
const sourceText = readText(PATHS.sourceBacktrace);
|
const v2IdentityAfter = { bytes: fs.statSync(PATHS.v2).size, sha256: shaFile(PATHS.v2) };
|
|
check('N075-VAL-001', 'company v02 row count', 307, companies.length);
|
check('N075-VAL-002', 'company v02 id uniqueness', 307, new Set(companies.map((row) => row.company_id)).size);
|
check('N075-VAL-003', 'companies with seven-class high-value gaps', 263, new Set(gapRegister.map((row) => row.company_id)).size);
|
check('N075-VAL-004', 'company-gap pair count', 891, gapRegister.length);
|
check('N075-VAL-005', 'P0 company count', 83, new Set(gapRegister.filter((row) => row.priority === 'P0').map((row) => row.company_id)).size);
|
check('N075-VAL-006', 'P0 company-gap pair count', 378, p0GapCount);
|
check('N075-VAL-007', 'customer gap company count', 174, categoryCounts.CUSTOMER);
|
check('N075-VAL-008', 'order gap company count', 98, categoryCounts.ORDER);
|
check('N075-VAL-009', 'mass-production gap company count', 125, categoryCounts.MASS_PRODUCTION);
|
check('N075-VAL-010', 'capacity gap company count', 92, categoryCounts.CAPACITY);
|
check('N075-VAL-011', 'ASP gap company count', 135, categoryCounts.ASP);
|
check('N075-VAL-012', 'lifetime gap company count', 100, categoryCounts.LIFETIME);
|
check('N075-VAL-013', 'revenue-profit gap company count', 167, categoryCounts.REVENUE_PROFIT);
|
check('N075-VAL-014', 'N040 candidate review row count', 14, candidateReview.length);
|
check('N075-VAL-015', 'candidate review id uniqueness', 14, new Set(candidateReview.map((row) => row.candidate_id)).size);
|
check('N075-VAL-016', 'candidate authority identity mismatch', 0, candidateIdentityMismatch);
|
check('N075-VAL-017', 'mapped candidate company ids unresolved', 0, candidateReview.filter((row) => row.mapped_company_id && !companyById.has(row.mapped_company_id)).length);
|
check('N075-VAL-018', 'candidate semantic result missing', 0, candidateReview.filter((row) => !row.semantic_verification_result || row.semantic_verification_result === 'UNREVIEWED').length);
|
check('N075-VAL-019', 'primary source backtrace row count', 8, sourceBacktrace.length);
|
check('N075-VAL-020', 'primary source update id uniqueness', 8, new Set(sourceBacktrace.map((row) => row.source_update_id)).size);
|
check('N075-VAL-021', 'primary source company ids unresolved', 0, sourceBacktrace.filter((row) => !companyById.has(row.company_id)).length);
|
check('N075-VAL-022', 'primary source rows missing URL/locator/content/boundary', 0, sourceBacktrace.filter((row) => !/^https:\/\//.test(row.canonical_url) || !row.source_locator || !row.content_update_text || !row.limitation).length);
|
check('N075-VAL-023', 'company v03 row count', 307, v3.length);
|
check('N075-VAL-024', 'company v03 id uniqueness', 307, new Set(v3.map((row) => row.company_id)).size);
|
check('N075-VAL-025', 'immutable company field mismatch', 0, immutableMismatch);
|
check('N075-VAL-026', 'v03 affected company count', 6, v3.filter((row) => row.n075_content_update_status === 'PRIMARY_SOURCE_CONTENT_ENRICHED_GAP_REMAINS_OPEN_NO_UPGRADE').length);
|
check('N075-VAL-027', 'source update ids missing from v03', 0, sourceBacktrace.filter((update) => !v3.find((row) => row.company_id === update.company_id)?.n075_primary_source_update_ids.includes(update.source_update_id)).length);
|
check('N075-VAL-028', 'formal pool boundary drift', 0, v3.filter((row) => row.formal_pool_effect !== 'NO_AUTOMATIC_FORMAL_POOL_CHANGE').length);
|
check('N075-VAL-029', 'source backtrace forbidden upgrade literal count', 0, (sourceText.match(/FORMAL_POOL_APPROVED|EVIDENCE_UPGRADED|GAP_CLOSED/g) || []).length);
|
check('N075-VAL-030', 'v02 bytes unchanged', v2IdentityBefore.bytes, v2IdentityAfter.bytes);
|
check('N075-VAL-031', 'v02 sha256 unchanged', v2IdentityBefore.sha256, v2IdentityAfter.sha256);
|
|
writeCsv(PATHS.validation, validation, ['check_id','check_description','expected','actual','status']);
|
const failed = validation.filter((row) => row.status === 'FAIL');
|
console.log(JSON.stringify({
|
status: failed.length ? 'FAIL' : 'PASS',
|
companies: companies.length,
|
companiesWithGaps: new Set(gapRegister.map((row) => row.company_id)).size,
|
companyGapPairs: gapRegister.length,
|
p0Companies: new Set(gapRegister.filter((row) => row.priority === 'P0').map((row) => row.company_id)).size,
|
p0CompanyGapPairs: p0GapCount,
|
candidateReviews: candidateReview.length,
|
primarySourceUpdates: sourceBacktrace.length,
|
enrichedCompanies: affectedCompanies.size,
|
validationPass: validation.filter((row) => row.status === 'PASS').length,
|
validationFail: failed.length,
|
outputs: Object.values(PATHS).filter((value) => value !== PATHS.v2 && value !== PATHS.candidates)
|
}, null, 2));
|
if (failed.length) process.exitCode = 1;
|