#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'local-seed-material-index-latest'); function main() { const args = parseArgs(process.argv.slice(2)); const workspaceRoot = path.resolve(args.workspaceRoot || path.join(ROOT, '..')); const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs')); const outputDir = path.resolve(args.output || DEFAULT_OUTPUT); const summary = buildLocalSeedMaterialIndex({ root: ROOT, workspaceRoot, outputsDir }); fs.mkdirSync(outputDir, { recursive: true }); const summaryPath = path.join(outputDir, 'local-seed-material-index-summary.json'); const reportPath = path.join(outputDir, 'local-seed-material-index.md'); const csvPath = path.join(outputDir, 'local-seed-material-index.csv'); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8'); fs.writeFileSync(csvPath, withBom(renderCsv(summary)), 'utf8'); console.log(JSON.stringify({ outputDir, summary: summaryPath, report: reportPath, csv: csvPath, materialCount: summary.materialCount, existingMaterialCount: summary.existingMaterialCount, canCloseProofGap: summary.canCloseProofGap }, null, 2)); if (args.strict && summary.directCustomerProof !== false) process.exitCode = 1; } function buildLocalSeedMaterialIndex({ root, workspaceRoot, outputsDir }) { const specs = [ { id: 'dha-brief-workbook', title: 'DHA Brief 原始工作簿', file: path.join(workspaceRoot, 'dha_brief.xlsx'), role: 'brief_seed', usableFor: ['补真实 Brief 模板的需求字段', '识别 DHA/母婴/小红书账号类型线索', '作为候选池召回种子'], missingForProof: [ '至少 5 个真实历史 Brief', '人工最终提报名单', '客户最终选中/拒绝记录', '拒绝原因或商务复核标签', '历史人工补号量基线' ] }, { id: 'dna-brief-workbook-legacy-name', title: 'DNA Brief 旧命名工作簿', file: path.join(workspaceRoot, 'dna_brief.xlsx'), role: 'brief_seed_legacy_name', usableFor: ['兼容旧交接命名,确认是否另有 Brief 种子材料'], missingForProof: ['若文件不存在,不影响当前 DHA 种子材料索引'] }, { id: 'dha-poc-recommendation-workbook', title: 'AI 提号 DHA 小红书推荐名单 PoC', file: path.join(workspaceRoot, 'output', 'dha-tihao-poc', 'AI提号-DHA小红书推荐名单-PoC.xlsx'), role: 'candidate_seed', usableFor: ['候选池种子', '商务复核展示样例', '参考账号分析口径校准'], missingForProof: [ '客户选择/拒绝结果', '负样本归因', '本轮人工补号量', '可审计的生成策略和 live 采集证明' ] }, { id: 'dha-poc-social-reference-workbook', title: 'AI 提号 DHA 社媒参考分析版', file: path.join(workspaceRoot, 'output', 'dha-tihao-poc', 'AI提号-DHA小红书推荐名单-社媒参考分析版.xlsx'), role: 'reference_analysis_seed', usableFor: ['参考账号基准分析', '候选池补充', '视频/主页证据字段设计参考'], missingForProof: [ '真实候选视频资源', '真实视频 A/B live proof', '商务复核结果', '客户最终选择' ] } ]; const materials = specs.map(spec => inspectMaterial({ root, workspaceRoot, spec })); const pipelineArtifacts = inspectPipelineArtifacts({ root, outputsDir }); const liveAutomationCandidates = inspectLiveAutomationCandidates({ root, outputsDir }); const existingMaterialCount = materials.filter(item => item.exists).length; const workbookCount = materials.filter(item => item.exists && item.extension === '.xlsx').length; const promisingLiveAggregateCount = liveAutomationCandidates.filter(item => item.liveAutomationPass).length; return { generatedAt: new Date().toISOString(), root, workspaceRoot, outputsDir, proofLevel: 'not_business_proof', directCustomerProof: false, canCloseProofGap: false, complete: false, materialCount: materials.length, existingMaterialCount, workbookCount, promisingLiveAggregateCount, materials, pipelineArtifacts, liveAutomationCandidates, guardrails: [ '本索引只把本地已有 Brief/PoC/历史输出做成可发现的种子材料台账,不关闭 proof-gap。', 'DHA Brief 和 PoC 推荐表可用于补真实模板、召回候选和校准口径,但不能证明客户效果。', '历史 pipeline 失败产物、模板数据、currentManualSupplementCount=0 或候选名单本身不能证明人工补号量下降。', 'live overnight aggregate 即使是真实自动化证据,也必须被 proof-gap:closure 在当前审计上下文中接受后,才可关闭 live/provider 缺口。', '不得在本索引或后续文档中写入 sessionToken、Authorization、模型 token 或 npm token。' ], nextActions: [ { owner: '商务', title: '把 DHA Brief 种子转写进统一历史数据模板', action: '只把真实客户 Brief 字段、参考账号/视频线索和账号类型要求转写进 outputs/data-intake-pack-latest/history-data-template.csv;不足 5 个 Brief 时继续保持未完成。', acceptance: 'intake:readiness historyReady=true,且 history:audit readyForCustomerEffectProof=true。' }, { owner: '商务/投放', title: '从 PoC 候选里补真实候选视频资源', action: '为候选账号补至少一个真实候选视频 URL、封面、ASR/字幕、帧图或正文证据,并写入 video-resource-template.csv。', acceptance: 'video:resource-readiness readyForVideoAbPreflight=true,realCandidateRows>=1。' }, { owner: '技术/AI', title: '只把本索引用作补证入口,不作为完成证明', action: '补齐真实材料后重跑 intake:readiness、video:resource-readiness、proof-gap:closure、optimization:completion 和 round:refresh。', acceptance: 'proof-gap-closure.openCount=0 且 optimization-completion.readyForClaim=true 后,才进入长期目标完成判断。' } ] }; } function inspectMaterial({ root, workspaceRoot, spec }) { const exists = fs.existsSync(spec.file); const extension = path.extname(spec.file).toLowerCase(); const base = { id: spec.id, title: spec.title, role: spec.role, path: relPath(root, spec.file), workspacePath: relPath(workspaceRoot, spec.file), exists, extension, proofLevel: 'not_business_proof', directCustomerProof: false, canCloseProofGap: false, usableFor: spec.usableFor, missingForProof: spec.missingForProof }; if (!exists) { return { ...base, status: 'missing', summary: '本地未发现该文件;只记录旧交接线索。' }; } const stat = fs.statSync(spec.file); const workbook = extension === '.xlsx' ? readWorkbookSummary(spec.file) : null; const allText = workbook ? workbook.searchText : ''; const signals = detectSignals(allText); return { ...base, status: 'available_seed', size: stat.size, updatedAt: stat.mtime.toISOString(), workbook: workbook ? { sheetCount: workbook.sheets.length, sheets: workbook.sheets.map(sheet => ({ name: sheet.name, rowCount: sheet.rowCount, columnCount: sheet.columnCount, headers: sheet.headers, sampleRows: sheet.sampleRows })), secretLikeHitCount: workbook.secretLikeHitCount, parseWarnings: workbook.parseWarnings } : null, signals, summary: renderMaterialSummary({ spec, signals, workbook }) }; } function inspectPipelineArtifacts({ root, outputsDir }) { const pipelineDir = path.join(outputsDir, 'optimization-pipeline-latest'); const pipeline = readJson(path.join(pipelineDir, 'optimization-pipeline-summary.json')); const history = readJson(path.join(pipelineDir, 'history-audit', 'historical-dataset-audit.json')); const customer = readJson(path.join(pipelineDir, 'customer-effect-audit', 'customer-effect-summary.json')); return { proofLevel: 'not_business_proof', directCustomerProof: false, canCloseProofGap: false, pipeline: pipeline ? { path: relPath(root, path.join(pipelineDir, 'optimization-pipeline-summary.json')), readyForClaim: Boolean(pipeline.readyForClaim), passCount: Number(pipeline.counts?.pass || 0), failCount: Number(pipeline.counts?.fail || 0), currentManualSupplementCount: pipeline.inputs?.currentManualSupplementCount ?? null, warning: '旧 pipeline 只能说明编排链路曾运行;包含模板/示例输入或 currentManualSupplementCount=0 时不能证明人工补号量下降。' } : null, historyAudit: history ? { path: relPath(root, path.join(pipelineDir, 'history-audit', 'historical-dataset-audit.json')), briefCount: Number(history.briefCount || 0), readyForCustomerEffectProof: Boolean(history.acceptance?.readyForCustomerEffectProof), readyForLongRun: Boolean(history.acceptance?.readyForLongRun), categoryCoverageMet: Boolean(history.acceptance?.categoryCoverageMet), warning: '历史审计未达到 readyForCustomerEffectProof=true 前,不能支撑客户效果证明。' } : null, customerEffect: customer ? { path: relPath(root, path.join(pipelineDir, 'customer-effect-audit', 'customer-effect-summary.json')), overallPass: Boolean(customer.acceptance?.overallPass), customerSelectedRateMeasured: Boolean(customer.acceptance?.customerSelectedRateMeasured), referenceCustomerSelectedRateMeasured: Boolean(customer.acceptance?.referenceCustomerSelectedRateMeasured), historyReadyForCustomerEffectProof: Boolean(customer.acceptance?.historyReadyForCustomerEffectProof), missingEvidence: customer.missingEvidence || [], warning: '客户效果审计 overallPass=false 或选择率未测量时,不能证明提号效果达标。' } : null }; } function inspectLiveAutomationCandidates({ root, outputsDir }) { if (!fs.existsSync(outputsDir)) return []; return fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory() && entry.name.startsWith('overnight-quality-')) .map(entry => { const dir = path.join(outputsDir, entry.name); const aggregatePath = path.join(dir, 'aggregate-summary.json'); const aggregate = readJson(aggregatePath); if (!aggregate) return null; const failedGateCount = Number(aggregate.acceptance?.failedGateCount ?? aggregate.failedGateCount ?? 0); const liveAutomationPass = Boolean(aggregate.manifest?.liveEnabled) && aggregate.acceptance?.overallPass === true && Number(aggregate.failureCount || 0) === 0 && failedGateCount === 0; return { id: entry.name, path: relPath(root, aggregatePath), updatedAt: fs.statSync(dir).mtime.toISOString(), liveEnabled: Boolean(aggregate.manifest?.liveEnabled), overallPass: Boolean(aggregate.acceptance?.overallPass), failureCount: Number(aggregate.failureCount || 0), failedGateCount, runCount: Number(aggregate.runCount || 0), fixtureCount: Number(aggregate.fixtureCount || 0), variantCount: Number(aggregate.variantCount || 0), liveAutomationPass, proofLevel: liveAutomationPass ? 'real_evidence_for_live_automation' : 'not_business_proof', directCustomerProof: false, canCloseCustomerEffectProof: false, canCloseProofGapNow: false, missingForClosure: liveAutomationPass ? ['需要 proof-gap:closure 在当前审计上下文中明确接受该 aggregate', '仍不能替代客户选择和人工补号量证明'] : ['liveEnabled=true', 'overallPass=true', 'failureCount=0', 'failedGateCount=0'] }; }) .filter(Boolean) .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) .slice(0, 8); } function readWorkbookSummary(file) { const parseWarnings = []; try { const zip = readZipEntries(fs.readFileSync(file)); const sharedStrings = parseSharedStrings(readZipText(zip, 'xl/sharedStrings.xml') || ''); const workbookXml = readZipText(zip, 'xl/workbook.xml') || ''; const relsXml = readZipText(zip, 'xl/_rels/workbook.xml.rels') || ''; const sheets = parseWorkbookSheets(workbookXml, relsXml); if (sheets.length === 0) { Object.keys(zip) .filter(name => /^xl\/worksheets\/sheet\d+\.xml$/.test(name)) .sort() .forEach((name, index) => sheets.push({ name: `Sheet${index + 1}`, target: name })); } const sheetSummaries = sheets.map(sheet => parseSheet({ name: sheet.name, xml: readZipText(zip, sheet.target) || '', sharedStrings })); const searchText = sheetSummaries .flatMap(sheet => [sheet.name, ...sheet.allValues]) .join('\n'); return { sheets: sheetSummaries, searchText, secretLikeHitCount: countSecretLike(searchText), parseWarnings }; } catch (error) { return { sheets: [], searchText: '', secretLikeHitCount: 0, parseWarnings: [`xlsx 解析失败:${error.message}`] }; } } function readZipEntries(buffer) { const eocdOffset = findEndOfCentralDirectory(buffer); if (eocdOffset < 0) throw new Error('未找到 ZIP central directory'); const totalEntries = buffer.readUInt16LE(eocdOffset + 10); const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16); const entries = {}; let offset = centralDirectoryOffset; for (let index = 0; index < totalEntries; index += 1) { if (buffer.readUInt32LE(offset) !== 0x02014b50) throw new Error('ZIP central directory 损坏'); const method = buffer.readUInt16LE(offset + 10); const compressedSize = buffer.readUInt32LE(offset + 20); const fileNameLength = buffer.readUInt16LE(offset + 28); const extraLength = buffer.readUInt16LE(offset + 30); const commentLength = buffer.readUInt16LE(offset + 32); const localHeaderOffset = buffer.readUInt32LE(offset + 42); const name = buffer.slice(offset + 46, offset + 46 + fileNameLength).toString('utf8'); const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26); const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28); const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength; const compressed = buffer.slice(dataStart, dataStart + compressedSize); let data; if (method === 0) data = compressed; else if (method === 8) data = zlib.inflateRawSync(compressed); else data = Buffer.alloc(0); entries[name] = data; offset += 46 + fileNameLength + extraLength + commentLength; } return entries; } function findEndOfCentralDirectory(buffer) { const min = Math.max(0, buffer.length - 66000); for (let offset = buffer.length - 22; offset >= min; offset -= 1) { if (buffer.readUInt32LE(offset) === 0x06054b50) return offset; } return -1; } function readZipText(entries, name) { const item = entries[name]; return item ? item.toString('utf8') : ''; } function parseSharedStrings(xml) { if (!xml) return []; return matchAll(xml, /<(?:\w+:)?si\b[\s\S]*?<\/(?:\w+:)?si>/g).map(item => { const textParts = []; const regex = /<(?:\w+:)?t\b[^>]*>([\s\S]*?)<\/(?:\w+:)?t>/g; let match; while ((match = regex.exec(item)) !== null) textParts.push(decodeXml(match[1])); return textParts.join(''); }); } function parseWorkbookSheets(workbookXml, relsXml) { const relMap = {}; for (const rel of matchAll(relsXml, /]*?)\/>/g)) { const attrs = parseAttrs(rel); if (attrs.Id && attrs.Target) { relMap[attrs.Id] = normalizeWorkbookTarget(attrs.Target); } } return matchAll(workbookXml, /<(?:\w+:)?sheet\b([^>]*?)\/>/g).map((tag, index) => { const attrs = parseAttrs(tag); const rid = attrs['r:id'] || attrs.id || ''; return { name: attrs.name || `Sheet${index + 1}`, target: relMap[rid] || `xl/worksheets/sheet${index + 1}.xml` }; }); } function normalizeWorkbookTarget(target) { const value = String(target || '').replace(/\\/g, '/'); if (value.startsWith('/')) return path.posix.normalize(value.slice(1)); return path.posix.normalize(path.posix.join('xl', value)); } function parseSheet({ name, xml, sharedStrings }) { const rows = []; let maxColumn = 0; for (const rowXml of matchAll(xml, /<(?:\w+:)?row\b[^>]*>([\s\S]*?)<\/(?:\w+:)?row>/g)) { const values = []; for (const cellXml of matchAll(rowXml, /<(?:\w+:)?c\b([^>]*?)>([\s\S]*?)<\/(?:\w+:)?c>/g)) { const attrs = parseAttrs(cellXml); const body = cellXml; const colIndex = columnIndex((attrs.r || '').replace(/\d+/g, '')); const value = parseCellValue(body, attrs, sharedStrings); if (value !== '') { values[colIndex >= 0 ? colIndex : values.length] = sanitizeCell(value); maxColumn = Math.max(maxColumn, (colIndex >= 0 ? colIndex : values.length - 1) + 1); } } if (values.some(value => String(value || '').trim())) rows.push(values); } const headers = (rows[0] || []).map(value => sanitizeCell(value)).filter(Boolean).slice(0, 20); const sampleRows = rows.slice(1, 6).map(row => row.map(value => sanitizeCell(value)).slice(0, 12)); const allValues = rows.flat().map(value => String(value || '')).filter(Boolean); return { name, rowCount: rows.length, columnCount: maxColumn, headers, sampleRows, allValues }; } function parseCellValue(body, attrs, sharedStrings) { if (attrs.t === 'inlineStr') { const inline = /<(?:\w+:)?is\b[\s\S]*?<(?:\w+:)?t\b[^>]*>([\s\S]*?)<\/(?:\w+:)?t>[\s\S]*?<\/(?:\w+:)?is>/.exec(body); return inline ? decodeXml(inline[1]) : ''; } const value = /<(?:\w+:)?v\b[^>]*>([\s\S]*?)<\/(?:\w+:)?v>/.exec(body); if (!value) return ''; const raw = decodeXml(value[1]); if (attrs.t === 's') return sharedStrings[Number(raw)] || ''; if (attrs.t === 'b') return raw === '1' ? 'TRUE' : 'FALSE'; return raw; } function detectSignals(text) { const urls = text.match(/https?:\/\/[^\s'"<>]+/g) || []; return { mentionsDha: /DHA/i.test(text), mentionsXiaohongshu: /小红书|xiaohongshu/i.test(text), mentionsReference: /参考|reference/i.test(text), mentionsCustomerDecision: /客户.*(选中|拒绝|通过)|最终选择|customer/i.test(text), mentionsManualSupplement: /人工补号|补号量|manual supplement/i.test(text), urlCount: urls.length, sampleUrls: unique(urls.map(sanitizePublicUrl)).slice(0, 5) }; } function renderMaterialSummary({ spec, signals, workbook }) { const parts = []; if (signals.mentionsDha) parts.push('包含 DHA 线索'); if (signals.mentionsXiaohongshu) parts.push('包含小红书线索'); if (signals.mentionsReference) parts.push('包含参考账号/参考内容线索'); if (signals.urlCount > 0) parts.push(`发现 URL ${signals.urlCount} 个`); if (workbook?.sheets?.length) parts.push(`工作表 ${workbook.sheets.length} 个`); return parts.length ? parts.join(';') : `${spec.title} 可作为本地种子材料,但不能直接证明业务效果。`; } function renderReport(summary) { return [ '# 本地种子材料索引', '', `- 生成时间:${summary.generatedAt}`, `- proofLevel:${summary.proofLevel}`, `- directCustomerProof:${summary.directCustomerProof}`, `- canCloseProofGap:${summary.canCloseProofGap}`, `- 本地材料:${summary.existingMaterialCount}/${summary.materialCount}`, `- 可疑 live 自动化候选:${summary.promisingLiveAggregateCount}`, '', '## 边界', '', ...summary.guardrails.map(item => `- ${item}`), '', '## 本地材料', '', '| ID | 状态 | 路径 | 可用于 | 缺失证明 | 摘要 |', '| --- | --- | --- | --- | --- | --- |', ...summary.materials.map(item => tableRow([ item.id, item.status, item.path, item.usableFor.join(';'), item.missingForProof.join(';'), item.summary || '' ])), '', '## 工作簿概览', '', ...summary.materials.filter(item => item.workbook).flatMap(item => [ `### ${item.title}`, '', `- 文件:${item.path}`, `- secretLikeHitCount:${item.workbook.secretLikeHitCount}`, '', '| Sheet | 行数 | 列数 | 表头 |', '| --- | ---: | ---: | --- |', ...item.workbook.sheets.map(sheet => tableRow([ sheet.name, sheet.rowCount, sheet.columnCount, sheet.headers.join(' / ') ])), '' ]), '## 旧 Pipeline 产物边界', '', `- pipeline.readyForClaim:${summary.pipelineArtifacts.pipeline?.readyForClaim ?? 'missing'}`, `- pipeline.failCount:${summary.pipelineArtifacts.pipeline?.failCount ?? 'missing'}`, `- history.readyForCustomerEffectProof:${summary.pipelineArtifacts.historyAudit?.readyForCustomerEffectProof ?? 'missing'}`, `- customerEffect.overallPass:${summary.pipelineArtifacts.customerEffect?.overallPass ?? 'missing'}`, '', '## Live 自动化候选', '', '| 输出 | liveEnabled | overallPass | failureCount | failedGateCount | proofLevel | 当前可关闭缺口 |', '| --- | ---: | ---: | ---: | ---: | --- | --- |', ...summary.liveAutomationCandidates.map(item => tableRow([ item.path, item.liveEnabled, item.overallPass, item.failureCount, item.failedGateCount, item.proofLevel, item.canCloseProofGapNow ])), '', '## 下一步', '', '| 负责人 | 动作 | 验收 |', '| --- | --- | --- |', ...summary.nextActions.map(item => tableRow([item.owner, `${item.title}:${item.action}`, item.acceptance])), '' ].join('\n'); } function renderCsv(summary) { const rows = [ ['类型', 'ID', '状态', '路径', 'proofLevel', 'directCustomerProof', 'canCloseProofGap', '可用于', '缺失证明', '摘要'], ...summary.materials.map(item => [ 'local-material', item.id, item.status, item.path, item.proofLevel, item.directCustomerProof, item.canCloseProofGap, item.usableFor.join(';'), item.missingForProof.join(';'), item.summary || '' ]), ...summary.liveAutomationCandidates.map(item => [ 'live-automation-candidate', item.id, item.liveAutomationPass ? 'pass' : 'not_passed', item.path, item.proofLevel, item.directCustomerProof, item.canCloseProofGapNow, 'live/provider 自动化证据候选', item.missingForClosure.join(';'), `runCount=${item.runCount}` ]) ]; return rows.map(row => row.map(csvCell).join(',')).join('\n'); } function parseArgs(argv) { const result = {}; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (!arg.startsWith('--')) continue; const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) result[key] = true; else { result[key] = next; index += 1; } } return result; } function parseAttrs(tag) { const attrs = {}; for (const match of matchAll(tag, /([:\w-]+)="([^"]*)"/g)) { const [, key, value] = /([:\w-]+)="([^"]*)"/.exec(match) || []; if (key) attrs[key] = decodeXml(value || ''); } return attrs; } function matchAll(text, regex) { return Array.from(String(text || '').matchAll(regex)).map(match => match[0]); } function columnIndex(column) { if (!column) return -1; let value = 0; for (const char of column.toUpperCase()) value = value * 26 + (char.charCodeAt(0) - 64); return value - 1; } function decodeXml(text) { return String(text || '') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'"); } function sanitizeCell(value) { const text = String(value || '').replace(/\s+/g, ' ').trim(); const withoutTracking = sanitizeUrlsInText(text); if (isSecretLike(withoutTracking)) return '[REDACTED_SECRET_LIKE_VALUE]'; return withoutTracking.slice(0, 240); } function sanitizeUrlsInText(text) { return String(text || '').replace(/https?:\/\/[^\s'"<>]+/g, match => sanitizePublicUrl(match)); } function sanitizePublicUrl(value) { const text = String(value || '').trim(); if (!text) return ''; if (isSecretLike(text) && !/^https?:\/\//i.test(text)) return '[REDACTED_SECRET_LIKE_VALUE]'; try { const parsed = new URL(text); parsed.search = ''; parsed.hash = ''; return parsed.toString(); } catch (_) { return text .replace(/([?&])(?:xsec_token|access_token|refresh_token|session_token|sessionToken|api_key|apikey|token)=[^&#\s]+/gi, '$1') .replace(/[?&]+$/, ''); } } function isSecretLike(text) { return /Authorization\s*[:=]\s*Bearer/i.test(text) || /sessionToken\s*[:=]/i.test(text) || /(?:xsec_token|access_token|refresh_token|session_token|api_key|apikey|token)=[^&#\s]+/i.test(text) || /sk-[A-Za-z0-9_-]{10,}/.test(text) || /npm_[A-Za-z0-9]{10,}/.test(text); } function countSecretLike(text) { return String(text || '').split(/\s+/).filter(isSecretLike).length; } function unique(values) { return Array.from(new Set(values.filter(Boolean))); } function readJson(file) { try { if (!fs.existsSync(file)) return null; return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } } function relPath(root, file) { const relative = path.relative(root, file).replace(/\\/g, '/'); return relative || '.'; } function withBom(text) { return `\uFEFF${text}`; } function tableRow(values) { return `| ${values.map(value => escapeCell(value)).join(' | ')} |`; } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, '
'); } function csvCell(value) { return `"${String(value ?? '').replace(/"/g, '""')}"`; } if (require.main === module) main(); module.exports = { buildLocalSeedMaterialIndex, readWorkbookSummary, sanitizeCell, sanitizePublicUrl, sanitizeUrlsInText, isSecretLike };