import fs from 'node:fs';
|
import path from 'node:path';
|
import crypto from 'node:crypto';
|
|
const CASE_ID = 'ANA-ROBOT-INDUSTRY-001';
|
const BATCH_ID = 'BATCH-ANA-ROBOT-FORMAL-EVIDENCE-MAP-ALIGNMENT-EXECUTION-001';
|
const RUN_ID = 'RUN-ANA-ROBOT-FORMAL-EVIDENCE-MAP-ALIGNMENT-EXECUTION-001';
|
const BASELINE_REL = 'ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/formal_evidence_map_alignment_baseline_20260728.csv';
|
const TARGET_REL = 'ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/formal_evidence_map_alignment_target_authority_20260728.csv';
|
const AUDIT_REL = 'ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/formal_evidence_map_alignment_audit_authority_repair001_20260728.csv';
|
const LEDGER_REL = 'ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/formal_evidence_map_alignment_change_ledger_20260728.csv';
|
const MAP_REL = 'ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/case_evidence_map.csv';
|
|
const MAP_COLUMNS = [
|
'case_id', 'artifact', 'source_rel', 'evidence_status', 'evidence_entry',
|
'source_binding_ref', 'evidence_fact_plan_ref', 'review_status',
|
];
|
|
const LEDGER_COLUMNS = [
|
'change_id', 'case_id', 'batch_id', 'run_id', 'target_row_id', 'artifact',
|
'row_kind', 'latest_audit_id', 'formal_audit_conclusion_literal',
|
'applicable_status_boundary', 'alignment_action', 'changed_fields',
|
'before_case_id', 'before_artifact', 'before_source_rel', 'before_evidence_status',
|
'before_evidence_entry', 'before_source_binding_ref', 'before_evidence_fact_plan_ref',
|
'before_review_status', 'after_case_id', 'after_artifact', 'after_source_rel',
|
'after_evidence_status', 'after_evidence_entry', 'after_source_binding_ref',
|
'after_evidence_fact_plan_ref', 'after_review_status', 'retained_gap_boundary',
|
'evidence_strength_effect', 'change_reason', 'validation_status',
|
];
|
|
function invariant(condition, message) {
|
if (!condition) throw new Error(message);
|
}
|
|
function parseCsv(text) {
|
const source = text.replace(/^\uFEFF/, '');
|
const rows = [];
|
let row = [];
|
let field = '';
|
let quoted = false;
|
for (let i = 0; i < source.length; i += 1) {
|
const ch = source[i];
|
if (quoted) {
|
if (ch === '"' && source[i + 1] === '"') {
|
field += '"';
|
i += 1;
|
} else if (ch === '"') {
|
quoted = false;
|
} else {
|
field += ch;
|
}
|
} else if (ch === '"' && field.length === 0) {
|
quoted = true;
|
} else if (ch === ',') {
|
row.push(field);
|
field = '';
|
} else if (ch === '\n') {
|
if (field.endsWith('\r')) field = field.slice(0, -1);
|
row.push(field);
|
rows.push(row);
|
row = [];
|
field = '';
|
} else {
|
field += ch;
|
}
|
}
|
if (field.length > 0 || row.length > 0) {
|
if (field.endsWith('\r')) field = field.slice(0, -1);
|
row.push(field);
|
rows.push(row);
|
}
|
invariant(!quoted, 'CSV_UNCLOSED_QUOTE');
|
return rows.filter((r) => !(r.length === 1 && r[0] === ''));
|
}
|
|
function readCsv(filePath) {
|
const rows = parseCsv(fs.readFileSync(filePath, 'utf8'));
|
invariant(rows.length >= 1, `CSV_EMPTY:${filePath}`);
|
const header = rows[0];
|
const data = rows.slice(1).map((values, index) => {
|
invariant(values.length === header.length, `CSV_WIDTH:${filePath}:${index + 2}`);
|
return Object.fromEntries(header.map((name, i) => [name, values[i]]));
|
});
|
return { header, data };
|
}
|
|
function quote(value) {
|
return `"${String(value ?? '').replaceAll('"', '""')}"`;
|
}
|
|
function serialize(columns, rows) {
|
const physical = [columns, ...rows.map((row) => columns.map((name) => row[name] ?? ''))];
|
return `${physical.map((row) => row.map(quote).join(',')).join('\r\n')}\r\n`;
|
}
|
|
function build(root) {
|
const baseline = readCsv(path.join(root, BASELINE_REL)).data;
|
const target = readCsv(path.join(root, TARGET_REL)).data;
|
const audits = readCsv(path.join(root, AUDIT_REL)).data;
|
|
invariant(baseline.length === 41, 'BASELINE_ROWS_NOT_41');
|
invariant(target.length === 45, 'TARGET_ROWS_NOT_45');
|
invariant(audits.length === 7, 'AUDIT_AUTHORITY_ROWS_NOT_7');
|
|
const baselineByArtifact = new Map(baseline.map((row) => [row.artifact, row]));
|
const auditByGroup = new Map(audits.map((row) => [row.authority_group, row]));
|
invariant(baselineByArtifact.size === 41, 'BASELINE_ARTIFACT_DUPLICATE');
|
invariant(auditByGroup.size === 7, 'AUDIT_GROUP_DUPLICATE');
|
|
const ordered = [...target].sort((a, b) => a.target_row_id.localeCompare(b.target_row_id));
|
invariant(ordered.every((row, index) => row.target_row_id === `EMAP-${String(index + 1).padStart(3, '0')}`), 'TARGET_ROW_ORDER_OR_GAP');
|
|
const mapRows = [];
|
const ledgerRows = [];
|
for (let index = 0; index < ordered.length; index += 1) {
|
const targetRow = ordered[index];
|
const before = baselineByArtifact.get(targetRow.artifact);
|
const audit = auditByGroup.get(targetRow.authority_group);
|
invariant(audit, `AUDIT_GROUP_MISSING:${targetRow.authority_group}`);
|
invariant(audit.audit_id === targetRow.latest_audit_id, `AUDIT_ID_MISMATCH:${targetRow.target_row_id}`);
|
invariant(audit.applicable_status_boundary === targetRow.audit_conclusion, `APPLICABLE_STATUS_MISMATCH:${targetRow.target_row_id}`);
|
invariant(targetRow.evidence_strength_effect === 'NO_UPGRADE', `STRENGTH_EFFECT:${targetRow.target_row_id}`);
|
|
const after = {
|
case_id: CASE_ID,
|
artifact: targetRow.artifact,
|
source_rel: targetRow.source_rel,
|
evidence_status: targetRow.target_evidence_status,
|
evidence_entry: targetRow.target_evidence_entry,
|
source_binding_ref: targetRow.target_source_binding_ref,
|
evidence_fact_plan_ref: targetRow.target_evidence_fact_plan_ref,
|
review_status: targetRow.target_review_status,
|
};
|
mapRows.push(after);
|
|
const rowKind = before ? 'EXISTING' : 'NEW_DATABASE';
|
if (before) {
|
invariant(targetRow.allowed_changed_fields === 'evidence_status|evidence_entry|review_status', `EXISTING_CHANGED_FIELDS:${targetRow.target_row_id}`);
|
invariant(before.case_id === after.case_id, `CASE_ID_DRIFT:${targetRow.target_row_id}`);
|
invariant(before.artifact === after.artifact, `ARTIFACT_DRIFT:${targetRow.target_row_id}`);
|
invariant(before.source_rel === after.source_rel, `SOURCE_REL_DRIFT:${targetRow.target_row_id}`);
|
invariant(before.source_binding_ref === after.source_binding_ref, `SOURCE_BINDING_DRIFT:${targetRow.target_row_id}`);
|
invariant(before.evidence_fact_plan_ref === after.evidence_fact_plan_ref, `FACT_PLAN_DRIFT:${targetRow.target_row_id}`);
|
invariant(after.evidence_entry.startsWith(before.evidence_entry), `EVIDENCE_ENTRY_PREFIX:${targetRow.target_row_id}`);
|
} else {
|
invariant(index >= 41 && targetRow.authority_group === 'DATABASE', `NON_DATABASE_NEW_ROW:${targetRow.target_row_id}`);
|
invariant(targetRow.allowed_changed_fields === 'NEW_ROW_ALL_8_SCHEMA_FIELDS_REQUIRED', `NEW_CHANGED_FIELDS:${targetRow.target_row_id}`);
|
}
|
|
ledgerRows.push({
|
change_id: `EMAPCHG-${String(index + 1).padStart(3, '0')}`,
|
case_id: CASE_ID,
|
batch_id: BATCH_ID,
|
run_id: RUN_ID,
|
target_row_id: targetRow.target_row_id,
|
artifact: targetRow.artifact,
|
row_kind: rowKind,
|
latest_audit_id: targetRow.latest_audit_id,
|
formal_audit_conclusion_literal: audit.formal_audit_conclusion_literal,
|
applicable_status_boundary: audit.applicable_status_boundary,
|
alignment_action: targetRow.alignment_action,
|
changed_fields: targetRow.allowed_changed_fields,
|
before_case_id: before?.case_id ?? '',
|
before_artifact: before?.artifact ?? '',
|
before_source_rel: before?.source_rel ?? '',
|
before_evidence_status: before?.evidence_status ?? '',
|
before_evidence_entry: before?.evidence_entry ?? '',
|
before_source_binding_ref: before?.source_binding_ref ?? '',
|
before_evidence_fact_plan_ref: before?.evidence_fact_plan_ref ?? '',
|
before_review_status: before?.review_status ?? '',
|
after_case_id: after.case_id,
|
after_artifact: after.artifact,
|
after_source_rel: after.source_rel,
|
after_evidence_status: after.evidence_status,
|
after_evidence_entry: after.evidence_entry,
|
after_source_binding_ref: after.source_binding_ref,
|
after_evidence_fact_plan_ref: after.evidence_fact_plan_ref,
|
after_review_status: after.review_status,
|
retained_gap_boundary: targetRow.retained_gap_boundary,
|
evidence_strength_effect: targetRow.evidence_strength_effect,
|
change_reason: before
|
? 'ALIGN_TO_LATEST_APPLICABLE_AUDIT_WITHOUT_STRENGTH_UPGRADE'
|
: 'APPEND_DATABASE_MARKET_OBSERVATION_WITHOUT_FORMAL_POOL_CHANGE',
|
validation_status: 'GENERATED_PENDING_INDEPENDENT_EXECUTION_AND_OUTPUT_QUALITY_REVIEW',
|
});
|
}
|
|
invariant(mapRows.length === 45 && ledgerRows.length === 45, 'OUTPUT_ROWS_NOT_45');
|
return {
|
mapText: serialize(MAP_COLUMNS, mapRows),
|
ledgerText: serialize(LEDGER_COLUMNS, ledgerRows),
|
};
|
}
|
|
function selfTest() {
|
const sample = [{ a: 'x,y', b: 'quote"value', c: '中文', d: '' }];
|
const text = serialize(['a', 'b', 'c', 'd'], sample);
|
invariant(!text.startsWith('\uFEFF'), 'SELFTEST_BOM');
|
invariant(text.endsWith('\r\n'), 'SELFTEST_TERMINAL_CRLF');
|
invariant(!/(^|[^\r])\n/.test(text), 'SELFTEST_BARE_LF');
|
invariant(text === '"a","b","c","d"\r\n"x,y","quote""value","中文",""\r\n', 'SELFTEST_SERIALIZATION');
|
const parsed = parseCsv(text);
|
invariant(parsed.length === 2 && parsed[1][0] === 'x,y' && parsed[1][1] === 'quote"value', 'SELFTEST_PARSE');
|
process.stdout.write('SELF_TEST_PASS\n');
|
}
|
|
const args = new Set(process.argv.slice(2));
|
if (args.has('--self-test')) {
|
selfTest();
|
} else if (args.has('--preview')) {
|
invariant(process.argv.length === 3, 'ONLY_PREVIEW_ARGUMENT_ALLOWED');
|
const { mapText, ledgerText } = build(process.cwd());
|
process.stdout.write(`${JSON.stringify({
|
map_rows: 45,
|
map_bytes: Buffer.byteLength(mapText, 'utf8'),
|
map_sha256: crypto.createHash('sha256').update(mapText, 'utf8').digest('hex'),
|
ledger_rows: 45,
|
ledger_bytes: Buffer.byteLength(ledgerText, 'utf8'),
|
ledger_sha256: crypto.createHash('sha256').update(ledgerText, 'utf8').digest('hex'),
|
})}\n`);
|
} else if (args.has('--execute')) {
|
invariant(process.argv.length === 3, 'ONLY_EXECUTE_ARGUMENT_ALLOWED');
|
const root = process.cwd();
|
const { mapText, ledgerText } = build(root);
|
fs.writeFileSync(path.join(root, LEDGER_REL), ledgerText, { encoding: 'utf8', flag: 'wx' });
|
fs.writeFileSync(path.join(root, MAP_REL), mapText, { encoding: 'utf8', flag: 'w' });
|
process.stdout.write('GENERATED_45_LEDGER_AND_MAP_ROWS\n');
|
} else {
|
throw new Error('USAGE: node generator.mjs --self-test|--preview|--execute');
|
}
|