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 =
|
'PENDING_VERIFICATION_MAX_NO_UPGRADE|NO_AUTOMATIC_FORMAL_POOL_CHANGE|CONTENT_ASSIMILATION_ONLY';
|
|
const PATHS = {
|
p0: path.join(CASE_ROOT, 'evidence', 'next_robot_073_g_industry_p0_semantic_verification_20260805.csv'),
|
facts: path.join(CASE_ROOT, 'evidence', 'next_robot_073_g_industry_fact_register_20260805.csv'),
|
sources: path.join(CASE_ROOT, 'evidence', 'next_robot_073_g_industry_source_registry_20260805.csv'),
|
v1: path.join(CASE_ROOT, 'outputs', '数据表', 'robot_company_information_v01_20260805.csv'),
|
p0Updates: path.join(CASE_ROOT, 'evidence', 'next_robot_074_p0_company_content_updates_20260805.csv'),
|
changedUpdates: path.join(CASE_ROOT, 'evidence', 'next_robot_074_changed28_company_content_updates_20260805.csv'),
|
oem: path.join(CASE_ROOT, 'evidence', 'next_robot_074_oem_relationship_supplement_20260805.csv'),
|
v2: path.join(CASE_ROOT, 'outputs', '数据表', 'robot_company_information_v02_20260805.csv'),
|
summary: path.join(CASE_ROOT, 'outputs', '核心文档', 'G盘有用信息企业内容回填_第二批_20260805.md'),
|
validation: path.join(CASE_ROOT, 'manifest', 'next_robot_074_company_content_backfill_validation_20260805.csv')
|
};
|
|
function readText(filePath) {
|
return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
}
|
|
function parseCsv(text) {
|
const rows = [];
|
let row = [];
|
let field = '';
|
let quoted = false;
|
for (let index = 0; index < text.length; index += 1) {
|
const char = text[index];
|
if (quoted) {
|
if (char === '"') {
|
if (text[index + 1] === '"') {
|
field += '"';
|
index += 1;
|
} else {
|
quoted = false;
|
}
|
} else {
|
field += char;
|
}
|
} else if (char === '"') {
|
quoted = true;
|
} else if (char === ',') {
|
row.push(field);
|
field = '';
|
} else if (char === '\n') {
|
row.push(field.replace(/\r$/, ''));
|
rows.push(row);
|
row = [];
|
field = '';
|
} else {
|
field += char;
|
}
|
}
|
if (field.length || row.length) {
|
row.push(field.replace(/\r$/, ''));
|
rows.push(row);
|
}
|
if (!rows.length) return [];
|
const headers = rows[0];
|
return rows
|
.slice(1)
|
.filter((values) => values.some((value) => value !== ''))
|
.map((values) => Object.fromEntries(headers.map((header, index) => [header, values[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 markdownCell(value) {
|
return normalize(value).replace(/\|/g, '/');
|
}
|
|
function cleanFactText(value) {
|
const normalized = normalize(value);
|
if (!normalized.startsWith('|')) {
|
return normalized.replace(/^#{1,6}\s*/, '').replace(/`/g, '');
|
}
|
const cells = normalized
|
.split('|')
|
.map((cell) => normalize(cell))
|
.filter(Boolean)
|
.filter((cell) => !/^[-:]+$/.test(cell));
|
return cells.join(';').replace(/`/g, '');
|
}
|
|
function targetFieldForSemantic(semanticField) {
|
if (/OEM_CUSTOMER|NAMED_CUSTOMERS/.test(semanticField)) return 'customer_and_commercial_relationship';
|
if (/CUSTOMER_STAGE_AND_CAPACITY/.test(semanticField)) return 'capacity_delivery_and_procurement';
|
return 'product_and_technical_capability';
|
}
|
|
function targetFieldForFact(factField) {
|
const map = {
|
PRODUCT_AND_TECHNICAL_CAPABILITY: 'product_and_technical_capability',
|
CUSTOMER_AND_COMMERCIAL_RELATIONSHIP: 'customer_and_commercial_relationship',
|
REVENUE_PRICE_AND_OPERATIONAL_METRIC: 'revenue_and_operational_metric',
|
CAPACITY_DELIVERY_AND_PROCUREMENT: 'capacity_delivery_and_procurement',
|
CHAIN_POSITION_AND_CONTEXT: 'market_position_and_chain_role',
|
RISK_GAP_AND_UNCERTAINTY: 'high_value_gap_and_risk'
|
};
|
return map[factField] || 'high_value_gap_and_risk';
|
}
|
|
function scoreFact(row) {
|
const disposition = {
|
DIRECT_HIGH_VALUE_CANDIDATE: 500,
|
HARD_EVIDENCE_INDEXED_CANDIDATE: 470,
|
CONTEXT_USEFUL_CANDIDATE: 300,
|
EXPLICIT_GAP_RETAINED: 220,
|
INDEXED_COVERAGE_CANDIDATE: 120
|
}[row.semantic_disposition] || 0;
|
const field = {
|
PRODUCT_AND_TECHNICAL_CAPABILITY: 90,
|
CUSTOMER_AND_COMMERCIAL_RELATIONSHIP: 85,
|
CAPACITY_DELIVERY_AND_PROCUREMENT: 80,
|
REVENUE_PRICE_AND_OPERATIONAL_METRIC: 75,
|
RISK_GAP_AND_UNCERTAINTY: 65,
|
CHAIN_POSITION_AND_CONTEXT: 50
|
}[row.fact_field] || 0;
|
const text = normalize(row.fact_text);
|
let quality = 0;
|
if (/一句话结论|当前阶段|核心看点|产品矩阵|客户|收入|销量|产能|量产|交付|参数|自由度|负载|毛利/.test(text)) quality += 30;
|
if (text.length >= 45 && text.length <= 650) quality += 20;
|
if (/^#{1,6}\s/.test(text)) quality -= 1000;
|
if (/^\|\s*P[0-2]\s*\|/.test(text)) quality -= 250;
|
if (/^日期[::]/.test(text)) quality -= 1000;
|
if (/线索\s*\|\s*当前判断\s*\|/.test(text)) quality -= 1000;
|
if (/公开补充-\d+/.test(text)) quality -= 40;
|
return disposition + field + quality;
|
}
|
|
function isSourceReferenceOnly(row) {
|
const text = normalize(row.fact_text);
|
if (/^#{1,6}\s/.test(text) || /^日期[::]/.test(text)) return true;
|
// Source-list rows are already preserved through source_record_id/path/locator.
|
// They are useful for provenance but should not displace substantive company content.
|
if (/(https?:\/\/|`data\/|data\/report\/)/.test(text)) return true;
|
return false;
|
}
|
|
function resolvedFactTarget(row) {
|
const text = normalize(row.fact_text);
|
if (
|
row.semantic_disposition !== 'DIRECT_HIGH_VALUE_CANDIDATE' &&
|
/(情景假设|不得写入事实口径|不能认定|不等同|待.*验证|D 级线索)/.test(text)
|
) {
|
return 'high_value_gap_and_risk';
|
}
|
return targetFieldForFact(row.fact_field);
|
}
|
|
function appendUpdates(base, updates) {
|
const additions = unique(updates.map((row) => `〔${row.update_id}〕${row.content_update_text}`));
|
if (!additions.length) return base;
|
return [normalize(base), `【N074内容增补】${additions.join(';')}`].filter(Boolean).join(';');
|
}
|
|
const p0 = readCsv(PATHS.p0);
|
const facts = readCsv(PATHS.facts);
|
const sources = readCsv(PATHS.sources);
|
const v1 = readCsv(PATHS.v1);
|
const v1ById = new Map(v1.map((row) => [row.company_id, row]));
|
const v1BeforeIdentity = { bytes: fs.statSync(PATHS.v1).size, sha256: shaFile(PATHS.v1) };
|
|
const p0Updates = [];
|
for (const row of p0) {
|
const ids = String(row.company_ids || '').split('|').filter(Boolean);
|
const names = String(row.company_names || '').split('|');
|
for (let index = 0; index < ids.length; index += 1) {
|
const companyId = ids[index];
|
p0Updates.push({
|
update_id: `${row.semantic_record_id}-C${String(index + 1).padStart(2, '0')}`,
|
semantic_record_id: row.semantic_record_id,
|
company_id: companyId,
|
company_name: names[index] || v1ById.get(companyId)?.canonical_name || '',
|
target_field: targetFieldForSemantic(row.semantic_field),
|
semantic_field: row.semantic_field,
|
content_update_text: normalize(row.semantic_summary),
|
source_record_id: row.source_record_id,
|
original_source_id: row.original_source_id,
|
source_title: row.source_title,
|
source_url: row.source_url,
|
source_relative_path: row.source_relative_path,
|
source_locator: row.source_locator,
|
source_document_sha256: row.source_document_sha256,
|
source_excerpt_sha256: row.source_excerpt_sha256,
|
verification_result: row.verification_result,
|
evidence_boundary: row.evidence_boundary,
|
content_update_status: /GAP|MEDIA_LEAD/.test(row.verification_result)
|
? 'CONTENT_LEAD_RETAINED_PENDING_PRIMARY_VERIFICATION'
|
: 'CONTENT_ASSIMILATED_PENDING_VERIFICATION'
|
});
|
}
|
}
|
|
const changedSources = sources.filter(
|
(row) =>
|
row.source_collection === 'CHANGED_RESEARCH_DOCUMENT' &&
|
/公司档案\//.test(row.source_relative_path) &&
|
row.mapped_company_ids
|
);
|
const factsBySource = new Map();
|
for (const fact of facts) {
|
if (!factsBySource.has(fact.source_record_id)) factsBySource.set(fact.source_record_id, []);
|
factsBySource.get(fact.source_record_id).push(fact);
|
}
|
|
const changedUpdates = [];
|
for (const source of changedSources) {
|
const sourceFacts = (factsBySource.get(source.source_record_id) || [])
|
.filter((row) => row.semantic_disposition !== 'EXCLUDED_STRUCTURAL_OR_GENERIC')
|
.filter((row) => !isSourceReferenceOnly(row))
|
.filter((row) => scoreFact(row) > 0)
|
.sort((left, right) => scoreFact(right) - scoreFact(left) || left.fact_id.localeCompare(right.fact_id));
|
const selected = [];
|
const selectedFields = new Set();
|
for (const row of sourceFacts) {
|
if (selectedFields.has(row.fact_field)) continue;
|
selected.push(row);
|
selectedFields.add(row.fact_field);
|
if (selected.length === 5) break;
|
}
|
for (const row of sourceFacts) {
|
if (selected.length === 5) break;
|
if (!selected.includes(row)) selected.push(row);
|
}
|
const companyIds = String(source.mapped_company_ids).split('|').filter(Boolean);
|
const companyNames = String(source.mapped_company_names || '').split('|');
|
for (const fact of selected) {
|
for (let index = 0; index < companyIds.length; index += 1) {
|
const companyId = companyIds[index];
|
changedUpdates.push({
|
update_id: `N074-CHG-${String(changedUpdates.length + 1).padStart(4, '0')}`,
|
fact_id: fact.fact_id,
|
source_record_id: source.source_record_id,
|
company_id: companyId,
|
company_name: companyNames[index] || v1ById.get(companyId)?.canonical_name || '',
|
target_field: resolvedFactTarget(fact),
|
fact_field: fact.fact_field,
|
content_update_text: cleanFactText(fact.fact_text),
|
source_root_alias: fact.source_root_alias,
|
source_relative_path: fact.source_relative_path,
|
source_locator: fact.source_locator,
|
source_document_sha256: fact.source_document_sha256,
|
source_text_sha256: fact.source_text_sha256,
|
semantic_disposition: fact.semantic_disposition,
|
evidence_grade: fact.evidence_grade,
|
evidence_boundary: fact.evidence_boundary,
|
content_update_status:
|
fact.semantic_disposition === 'DIRECT_HIGH_VALUE_CANDIDATE'
|
? 'SELECTED_DIRECT_CONTENT_PENDING_VERIFICATION'
|
: 'SELECTED_CONTEXT_OR_GAP_CONTENT_NO_FACT_UPGRADE'
|
});
|
}
|
}
|
}
|
|
const relationDefinitions = {
|
'N073-P0-012': ['GXO', '', 'CUSTOMER_DEPLOYMENT_AGREEMENT', 'CUSTOMER_CONFIRMED_MULTI_YEAR_RAAS_OPERATION_NO_VALUE_OR_UNIT_COUNT'],
|
'N073-P0-013': ['GXO', '', 'CUSTOMER_OPERATION_MILESTONE', 'SUPPLIER_DISCLOSED_100K_TOTE_TASK_MILESTONE_NO_REVENUE_OR_DEPLOYMENT_COUNT'],
|
'N073-P0-014': ['BMW Group', '', 'CUSTOMER_PRODUCTION_ENVIRONMENT_TEST', 'CUSTOMER_CONFIRMED_TEST_NO_FORMAL_INTRODUCTION_TIMETABLE'],
|
'N073-P0-015': ['BMW Group', '', 'CUSTOMER_PILOT_OPERATION_METRIC', 'CUSTOMER_CONFIRMED_PILOT_KPI_NO_CONTRACT_VALUE'],
|
'N073-P0-016': ['BMW Group', '', 'SUPPLIER_OPERATION_DISCLOSURE_CROSSCHECK', 'SUPPLIER_DISCLOSURE_REQUIRES_CUSTOMER_SIDE_CROSSCHECK'],
|
'N073-P0-017': ['', '', 'FORMAL_ANNUAL_REPORT_SOURCE_ENTRY', 'FORMAL_DISCLOSURE_ENTRY_REQUIRES_ROW_LEVEL_EXTRACTION'],
|
'N073-P0-018': ['智元机器人', 'COMPANY-ed71c3420dbf50a96a81a36c', 'SUPPLIER_RECOGNITION_AND_COMPONENT_PARTICIPATION', 'COMPANY_CONFIRMED_RECOGNITION_NO_VOLUME_SHARE_OR_MODEL'],
|
'N073-P0-019': ['智元机器人|南京蔚蓝科技', 'COMPANY-ed71c3420dbf50a96a81a36c', 'SUPPLIER_RECOGNITION_AND_STRATEGIC_COOPERATION', 'IR_DISCLOSED_RELATIONSHIP_NO_PURCHASE_CONTRACT'],
|
'N073-P0-020': ['', '', 'COMPANY_PRODUCT_PORTFOLIO_CONTEXT', 'PRODUCT_PORTFOLIO_NO_CUSTOMER_CONFIRMATION'],
|
'N073-P0-021': ['智元机器人', 'COMPANY-ed71c3420dbf50a96a81a36c', 'MEDIA_LEAD_SUPPLIER_AND_DELIVERY_CLAIM', 'MEDIA_LEAD_PRIMARY_BACKTRACE_REQUIRED']
|
};
|
|
const oemRows = p0
|
.filter((row) => relationDefinitions[row.semantic_record_id])
|
.map((row, index) => {
|
const definition = relationDefinitions[row.semantic_record_id];
|
let subjectIds = row.company_ids;
|
let subjectNames = row.company_names;
|
if (row.semantic_record_id === 'N073-P0-018') {
|
subjectIds = row.company_ids.split('|')[0];
|
subjectNames = row.company_names.split('|')[0];
|
}
|
return {
|
relationship_update_id: `N074-OEM-${String(index + 1).padStart(3, '0')}`,
|
semantic_record_id: row.semantic_record_id,
|
subject_company_ids: subjectIds,
|
subject_company_names: subjectNames,
|
counterparty_company_ids: definition[1],
|
counterparty_names: definition[0],
|
relationship_type: definition[2],
|
relationship_stage: definition[3],
|
relationship_summary: normalize(row.semantic_summary),
|
source_record_id: row.source_record_id,
|
original_source_id: row.original_source_id,
|
source_title: row.source_title,
|
source_url: row.source_url,
|
source_relative_path: row.source_relative_path,
|
source_locator: row.source_locator,
|
source_document_sha256: row.source_document_sha256,
|
source_excerpt_sha256: row.source_excerpt_sha256,
|
evidence_boundary: row.evidence_boundary,
|
integration_status: 'RELATIONSHIP_CONTEXT_REGISTERED_PENDING_VERIFICATION_NO_UPGRADE'
|
};
|
});
|
|
const updatesByCompany = new Map();
|
function addCompanyUpdate(row) {
|
if (!updatesByCompany.has(row.company_id)) updatesByCompany.set(row.company_id, []);
|
updatesByCompany.get(row.company_id).push(row);
|
}
|
p0Updates.forEach(addCompanyUpdate);
|
changedUpdates.forEach(addCompanyUpdate);
|
|
const oemByCompany = new Map();
|
for (const row of oemRows) {
|
for (const companyId of String(row.subject_company_ids).split('|').filter(Boolean)) {
|
if (!oemByCompany.has(companyId)) oemByCompany.set(companyId, []);
|
oemByCompany.get(companyId).push(row.relationship_update_id);
|
}
|
}
|
|
const contentFields = [
|
'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 v2 = v1.map((row) => {
|
const companyUpdates = updatesByCompany.get(row.company_id) || [];
|
const next = { ...row };
|
for (const field of contentFields) {
|
next[field] = appendUpdates(row[field], companyUpdates.filter((update) => update.target_field === field));
|
}
|
const p0Rows = companyUpdates.filter((update) => update.semantic_record_id);
|
const changedRows = companyUpdates.filter((update) => update.fact_id);
|
const sourceIds = unique(companyUpdates.map((update) => update.source_record_id));
|
next.p0_semantic_record_ids = unique(p0Rows.map((update) => update.semantic_record_id)).join('|');
|
next.changed_profile_selected_fact_ids = unique(changedRows.map((update) => update.fact_id)).join('|');
|
next.oem_relationship_update_ids = unique(oemByCompany.get(row.company_id) || []).join('|');
|
next.n074_content_update_source_count = String(sourceIds.length);
|
next.n074_content_update_status = companyUpdates.length
|
? 'CONTENT_FIELDS_ENRICHED_PENDING_VERIFICATION_NO_UPGRADE'
|
: 'RETAINED_FROM_V01_NO_N074_CHANGE';
|
next.content_update_batch = companyUpdates.length ? 'NEXT-ROBOT-074' : '';
|
next.as_of = AS_OF;
|
return next;
|
});
|
|
const p0Columns = [
|
'update_id', 'semantic_record_id', 'company_id', 'company_name', 'target_field', 'semantic_field',
|
'content_update_text', 'source_record_id', 'original_source_id', 'source_title', 'source_url',
|
'source_relative_path', 'source_locator', 'source_document_sha256', 'source_excerpt_sha256',
|
'verification_result', 'evidence_boundary', 'content_update_status'
|
];
|
const changedColumns = [
|
'update_id', 'fact_id', 'source_record_id', 'company_id', 'company_name', 'target_field', 'fact_field',
|
'content_update_text', 'source_root_alias', 'source_relative_path', 'source_locator',
|
'source_document_sha256', 'source_text_sha256', 'semantic_disposition', 'evidence_grade',
|
'evidence_boundary', 'content_update_status'
|
];
|
const oemColumns = [
|
'relationship_update_id', 'semantic_record_id', 'subject_company_ids', 'subject_company_names',
|
'counterparty_company_ids', 'counterparty_names', 'relationship_type', 'relationship_stage',
|
'relationship_summary', 'source_record_id', 'original_source_id', 'source_title', 'source_url',
|
'source_relative_path', 'source_locator', 'source_document_sha256', 'source_excerpt_sha256',
|
'evidence_boundary', 'integration_status'
|
];
|
const v2Columns = [
|
...Object.keys(v1[0]),
|
'p0_semantic_record_ids',
|
'changed_profile_selected_fact_ids',
|
'oem_relationship_update_ids',
|
'n074_content_update_source_count',
|
'n074_content_update_status',
|
'content_update_batch'
|
];
|
|
writeCsv(PATHS.p0Updates, p0Updates, p0Columns);
|
writeCsv(PATHS.changedUpdates, changedUpdates, changedColumns);
|
writeCsv(PATHS.oem, oemRows, oemColumns);
|
writeCsv(PATHS.v2, v2, v2Columns);
|
|
const affectedCompanies = new Set([...p0Updates, ...changedUpdates].map((row) => row.company_id));
|
const changedCompanyIds = new Set(changedUpdates.map((row) => row.company_id));
|
const directChanged = changedUpdates.filter((row) => row.semantic_disposition === 'DIRECT_HIGH_VALUE_CANDIDATE').length;
|
const contextChanged = changedUpdates.length - directChanged;
|
const summary = [];
|
summary.push('# G盘有用信息企业内容回填(第二批)');
|
summary.push('');
|
summary.push(`> 更新日期:${AS_OF}`);
|
summary.push('> 原则:只吸收有用语义,不复制目录;所有新增内容保持待核验、无自动正式入池、无证据升级。');
|
summary.push('');
|
summary.push('## 1. 本批结果');
|
summary.push('');
|
summary.push(`- 21条P0精选语义已展开为${p0Updates.length}条公司字段更新,覆盖${new Set(p0Updates.map((row) => row.company_id)).size}家公司。`);
|
summary.push(`- 10条OEM/客户/供应关系与正式披露入口已登记为版本化关系增补。`);
|
summary.push(`- 28份变更企业档案已逐公司精选${changedUpdates.length}条高价值内容,其中直接候选${directChanged}条、上下文或缺口边界${contextChanged}条。`);
|
summary.push(`- robot_company_information_v02_20260805.csv保持307家公司全集,实际增强${affectedCompanies.size}家公司;其余公司逐字段继承v01。`);
|
summary.push('');
|
summary.push('## 2. P0精选语义落点');
|
summary.push('');
|
summary.push('| 记录 | 企业 | 目标字段 | 内容 | 边界 |');
|
summary.push('|---|---|---|---|---|');
|
for (const row of p0Updates) {
|
summary.push(`| ${row.semantic_record_id} | ${markdownCell(row.company_name)} | ${row.target_field} | ${markdownCell(row.content_update_text)} | ${row.content_update_status} |`);
|
}
|
summary.push('');
|
summary.push('## 3. OEM与客户关系增补');
|
summary.push('');
|
summary.push('| 记录 | 主体 | 对手方 | 关系类型 | 当前阶段与边界 |');
|
summary.push('|---|---|---|---|---|');
|
for (const row of oemRows) {
|
summary.push(`| ${row.relationship_update_id} | ${markdownCell(row.subject_company_names)} | ${markdownCell(row.counterparty_names || '未指定')} | ${row.relationship_type} | ${row.relationship_stage} |`);
|
}
|
summary.push('');
|
summary.push('## 4. 28份变更企业档案内容增补');
|
summary.push('');
|
summary.push('| 企业 | 精选条数 | 内容维度 | 代表性新增内容 |');
|
summary.push('|---|---:|---|---|');
|
for (const companyId of [...changedCompanyIds].sort((left, right) => (v1ById.get(left)?.canonical_name || '').localeCompare(v1ById.get(right)?.canonical_name || '', 'zh-CN'))) {
|
const rows = changedUpdates.filter((row) => row.company_id === companyId);
|
summary.push(`| ${markdownCell(rows[0]?.company_name || v1ById.get(companyId)?.canonical_name)} | ${rows.length} | ${markdownCell(unique(rows.map((row) => row.fact_field)).join(' / '))} | ${markdownCell(rows.slice(0, 2).map((row) => row.content_update_text).join(';'))} |`);
|
}
|
summary.push('');
|
summary.push('## 5. 使用边界与下一步');
|
summary.push('');
|
summary.push('- v02为版本化统一企业信息表;没有修改canonical company master、A/B、formal evidence map、migration或正式公司池。');
|
summary.push('- 公司官网、年报、客户公告、IR与媒体线索保持各自证据等级;价格、销量、订单、产能、份额、收入和利润没有跨来源升级。');
|
summary.push('- 下一步不再重复整理这批内容,而是从v02和关系增补表直接筛选仍缺主源的客户、订单、量产、ASP、寿命、收入与利润字段,按同类缺口集中回源。');
|
summary.push('');
|
summary.push('## 6. 数据入口');
|
summary.push('');
|
summary.push('- P0公司字段更新:`evidence/next_robot_074_p0_company_content_updates_20260805.csv`');
|
summary.push('- OEM关系增补:`evidence/next_robot_074_oem_relationship_supplement_20260805.csv`');
|
summary.push('- 28份变更档案公司内容更新:`evidence/next_robot_074_changed28_company_content_updates_20260805.csv`');
|
summary.push('- 307家公司统一信息表v02:`outputs/数据表/robot_company_information_v02_20260805.csv`');
|
writeText(PATHS.summary, `${summary.join('\n')}\n`);
|
|
const validationRows = [];
|
function check(id, description, expected, actual) {
|
validationRows.push({
|
check_id: id,
|
check_description: description,
|
expected: String(expected),
|
actual: String(actual),
|
status: String(expected) === String(actual) ? 'PASS' : 'FAIL'
|
});
|
}
|
|
const immutableColumns = [
|
'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 v2) {
|
const before = v1ById.get(row.company_id);
|
for (const column of immutableColumns) {
|
if ((before?.[column] || '') !== (row[column] || '')) immutableMismatch += 1;
|
}
|
}
|
const factById = new Map(facts.map((row) => [row.fact_id, row]));
|
const selectedFactMismatch = changedUpdates.filter((row) => {
|
const source = factById.get(row.fact_id);
|
return !source || source.source_record_id !== row.source_record_id || source.source_locator !== row.source_locator || source.source_text_sha256 !== row.source_text_sha256;
|
}).length;
|
const v1AfterIdentity = { bytes: fs.statSync(PATHS.v1).size, sha256: shaFile(PATHS.v1) };
|
const formalOutputText = [readText(PATHS.p0Updates), readText(PATHS.changedUpdates), readText(PATHS.oem), readText(PATHS.v2), readText(PATHS.summary)].join('\n');
|
|
check('N074-VAL-001', 'P0 semantic authority row count', 21, p0.length);
|
check('N074-VAL-002', 'P0 company-field binding row count', 22, p0Updates.length);
|
check('N074-VAL-003', 'P0 binding company ids unresolved in company v01', 0, p0Updates.filter((row) => !v1ById.has(row.company_id)).length);
|
check('N074-VAL-004', 'P0 update-id uniqueness', p0Updates.length, new Set(p0Updates.map((row) => row.update_id)).size);
|
check('N074-VAL-005', 'OEM relationship/context row count', 10, oemRows.length);
|
check('N074-VAL-006', 'OEM relationship source semantic set mismatch', 0, [...new Set([...Object.keys(relationDefinitions), ...oemRows.map((row) => row.semantic_record_id)])].filter((id) => !relationDefinitions[id] || !oemRows.some((row) => row.semantic_record_id === id)).length);
|
check('N074-VAL-007', 'changed company profile source count', 28, changedSources.length);
|
check('N074-VAL-008', 'changed company profiles represented in selected content', 28, changedCompanyIds.size);
|
check('N074-VAL-009', 'changed selected update-id uniqueness', changedUpdates.length, new Set(changedUpdates.map((row) => row.update_id)).size);
|
check('N074-VAL-010', 'changed selected fact source/hash mismatch', 0, selectedFactMismatch);
|
check('N074-VAL-011', 'excluded structural facts selected', 0, changedUpdates.filter((row) => row.semantic_disposition === 'EXCLUDED_STRUCTURAL_OR_GENERIC').length);
|
check('N074-VAL-012', 'selected content missing locator or document/text hash', 0, changedUpdates.filter((row) => !row.source_locator || !row.source_document_sha256 || !row.source_text_sha256).length);
|
check('N074-VAL-013', 'company information v02 row count', 307, v2.length);
|
check('N074-VAL-014', 'company information v02 company-id uniqueness', 307, new Set(v2.map((row) => row.company_id)).size);
|
check('N074-VAL-015', 'company v01/v02 id-set mismatch', 0, [...new Set([...v1.map((row) => row.company_id), ...v2.map((row) => row.company_id)])].filter((id) => !v1ById.has(id) || !v2.some((row) => row.company_id === id)).length);
|
check('N074-VAL-016', 'immutable company fields mismatch', 0, immutableMismatch);
|
check('N074-VAL-017', 'affected company count', 35, affectedCompanies.size);
|
check('N074-VAL-018', 'v02 enriched company status count', affectedCompanies.size, v2.filter((row) => row.n074_content_update_status === 'CONTENT_FIELDS_ENRICHED_PENDING_VERIFICATION_NO_UPGRADE').length);
|
check('N074-VAL-019', 'formal pool boundary drift', 0, v2.filter((row) => row.formal_pool_effect !== 'NO_AUTOMATIC_FORMAL_POOL_CHANGE').length);
|
check('N074-VAL-020', 'P0 evidence boundary drift', 0, p0Updates.filter((row) => row.evidence_boundary !== BOUNDARY).length);
|
check('N074-VAL-021', 'changed evidence boundary drift', 0, changedUpdates.filter((row) => row.evidence_boundary !== BOUNDARY).length);
|
check('N074-VAL-022', 'formal outputs contain machine absolute G path', 0, (formalOutputText.match(/G:\\\\/gi) || []).length);
|
check('N074-VAL-023', 'v01 bytes unchanged', v1BeforeIdentity.bytes, v1AfterIdentity.bytes);
|
check('N074-VAL-024', 'v01 sha256 unchanged', v1BeforeIdentity.sha256, v1AfterIdentity.sha256);
|
check('N074-VAL-025', 'human summary changed-company row count', 28, (readText(PATHS.summary).match(/^\| [^|]+ \| \d+ \|/gm) || []).length);
|
|
writeCsv(PATHS.validation, validationRows, [
|
'check_id', 'check_description', 'expected', 'actual', 'status'
|
]);
|
|
const failed = validationRows.filter((row) => row.status === 'FAIL');
|
console.log(JSON.stringify({
|
status: failed.length ? 'FAIL' : 'PASS',
|
p0AuthorityRows: p0.length,
|
p0CompanyUpdates: p0Updates.length,
|
p0Companies: new Set(p0Updates.map((row) => row.company_id)).size,
|
oemRelationshipRows: oemRows.length,
|
changedProfileCompanies: changedCompanyIds.size,
|
changedSelectedContentRows: changedUpdates.length,
|
affectedCompanies: affectedCompanies.size,
|
v2Rows: v2.length,
|
validationPass: validationRows.filter((row) => row.status === 'PASS').length,
|
validationFail: failed.length,
|
outputs: [PATHS.p0Updates, PATHS.changedUpdates, PATHS.oem, PATHS.v2, PATHS.summary, PATHS.validation]
|
}, null, 2));
|
if (failed.length) process.exitCode = 1;
|