| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const { execSync } = require('child_process'); |
| const fs = require('fs'); |
| const path = require('path'); |
|
|
| |
| |
| |
| const DEFAULT_CYCLES = 10; |
| const DEFAULT_TIMEOUT_SEC = 120; |
| const OUTPUT_DIR = path.resolve(__dirname, '..', 'test'); |
| const REPORTS_DIR = path.resolve(__dirname, '..', 'demo'); |
|
|
| |
| if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); |
| if (!fs.existsSync(REPORTS_DIR)) fs.mkdirSync(REPORTS_DIR, { recursive: true }); |
|
|
| |
| |
| |
| function checkDependencies() { |
| const missing = []; |
|
|
| |
| const requiredFiles = [ |
| 'config/assets.json', |
| 'prover/validator.js', |
| 'prover/scorer.js', |
| 'prover/fetcher.js', |
| 'prover/submitter.js', |
| 'prover/broadcaster.js', |
| ]; |
|
|
| for (const f of requiredFiles) { |
| if (!fs.existsSync(path.resolve(__dirname, '..', f))) { |
| missing.push(`Missing file: ${f}`); |
| } |
| } |
|
|
| |
| |
| const envVars = ['POLYGON_AMOY_RPC_URL', 'ORACLE_PRIVATE_KEY', 'CIRCUIT_BREAKER_ADDRESS']; |
| const missingEnv = envVars.filter(v => !process.env[v]); |
| if (missingEnv.length > 0) { |
| console.warn(`[Pipeline] Warning: Missing environment variables: ${missingEnv.join(', ')}. Dry-run will still work, but live broadcast will fail if attempted.`); |
| } |
|
|
| |
| if (!process.env.NVIDIA_API_KEY) { |
| console.warn('[Pipeline] Note: NVIDIA_API_KEY not set. AI audit analysis will be skipped.'); |
| } |
|
|
| if (missing.length > 0) { |
| console.error('[Pipeline] Fatal: Required files missing:'); |
| missing.forEach(m => console.error(` - ${m}`)); |
| process.exit(1); |
| } |
|
|
| console.log('[Pipeline] All required files present.'); |
| } |
|
|
| |
| |
| |
| function runCmd(cmd, timeoutSec) { |
| try { |
| return execSync(cmd, { encoding: 'utf8', stdio: 'pipe', timeout: timeoutSec * 1000 }); |
| } catch (err) { |
| if (err.killed) return `ERROR: Command timed out after ${timeoutSec}s`; |
| return `ERROR: ${err.message}`; |
| } |
| } |
|
|
| |
| |
| |
| async function main() { |
| checkDependencies(); |
|
|
| const args = process.argv.slice(2); |
| const cycles = args.includes('--cycles') ? parseInt(args[args.indexOf('--cycles') + 1], 10) : DEFAULT_CYCLES; |
| const timeoutSec = args.includes('--timeout') ? parseInt(args[args.indexOf('--timeout') + 1], 10) : DEFAULT_TIMEOUT_SEC; |
|
|
| |
| const globalTimeoutMs = cycles * (timeoutSec + 10) * 1000; |
| const globalTimeout = setTimeout(() => { |
| console.error(`[Pipeline] Global timeout (${globalTimeoutMs/1000}s) reached. Aborting.`); |
| process.exit(1); |
| }, globalTimeoutMs); |
| |
| globalTimeout.unref(); |
|
|
| console.log(`[Pipeline] Starting full-length test — ${cycles} cycles, per-command timeout ${timeoutSec}s`); |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
| const allResults = []; |
|
|
| for (let i = 1; i <= cycles; i++) { |
| console.log(`\n[Pipeline] Cycle ${i}/${cycles}`); |
|
|
| |
| console.log(' → Running fetcher...'); |
| const fetchOutput = runCmd('node prover/fetcher.js', timeoutSec); |
| console.log(' Fetcher done.'); |
|
|
| |
| const statePath = path.resolve(__dirname, '..', '.local', 'state', 'prover-state.json'); |
| let proverState = { results: [] }; |
| try { |
| proverState = JSON.parse(fs.readFileSync(statePath, 'utf8')); |
| } catch (err) { |
| console.warn(` Could not read prover state: ${err.message}`); |
| continue; |
| } |
|
|
| |
| const cycleAssets = []; |
| for (const asset of proverState.results) { |
| const entry = { |
| assetId: asset.assetId, |
| ipfsCid: asset.ipfsCid, |
| status: asset.status, |
| health: asset.health, |
| triggerScore: asset.triggerScore || null, |
| documentValid: asset.documentValid, |
| schemaFailures: asset.schemaFailures || [], |
| }; |
|
|
| console.log(` Asset ${asset.assetId.slice(0, 10)}... status=${asset.status} health=${asset.health} score=${asset.triggerScore?.toFixed(3) || 'N/A'}`); |
| cycleAssets.push(entry); |
| } |
|
|
| |
| console.log(' → Running submitter (dry-run)...'); |
| const submitOutput = runCmd('node prover/submitter.js --dry-run', timeoutSec); |
| console.log(' Submitter done.'); |
|
|
| |
| console.log(' → Running broadcaster (dry-run)...'); |
| const broadcastOutput = runCmd('node prover/broadcaster.js --dry-run', timeoutSec); |
| console.log(' Broadcaster done.'); |
|
|
| |
| allResults.push({ |
| cycle: i, |
| timestamp: new Date().toISOString(), |
| assets: cycleAssets, |
| submitOutput: submitOutput.slice(-500), |
| broadcastOutput: broadcastOutput.slice(-500), |
| }); |
|
|
| |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| } |
|
|
| |
| clearTimeout(globalTimeout); |
|
|
| |
| |
| |
| const rawFile = path.join(OUTPUT_DIR, `pipeline-run-${timestamp}.json`); |
| fs.writeFileSync(rawFile, JSON.stringify(allResults, null, 2)); |
| console.log(`\n[Pipeline] Raw data saved to ${rawFile}`); |
|
|
| |
| |
| |
| const summary = buildSummary(allResults, cycles); |
| const reportMd = generateReport(summary, cycles, timestamp); |
| const reportFile = path.join(REPORTS_DIR, `pipeline-run-${timestamp}.md`); |
| fs.writeFileSync(reportFile, reportMd); |
| console.log(`[Pipeline] Summary report saved to ${reportFile}`); |
|
|
| console.log('\n[Pipeline] Full-length test complete.'); |
| } |
|
|
| |
| |
| |
| function buildSummary(allResults, totalCycles) { |
| const assetStats = {}; |
| let totalTrips = 0; |
| let totalUpdates = 0; |
|
|
| for (const cycle of allResults) { |
| for (const asset of cycle.assets) { |
| if (!assetStats[asset.assetId]) { |
| assetStats[asset.assetId] = { |
| cyclesSeen: 0, |
| mismatches: 0, |
| unreachable: 0, |
| tripsTriggered: 0, |
| updates: 0, |
| triggerScores: [], |
| documentValidCount: 0, |
| documentInvalidCount: 0, |
| }; |
| } |
| const stat = assetStats[asset.assetId]; |
| stat.cyclesSeen++; |
| if (asset.status === 'mismatch') stat.mismatches++; |
| if (asset.health === 'unreachable') stat.unreachable++; |
| if (asset.triggerScore !== null && asset.triggerScore !== undefined) { |
| stat.triggerScores.push(asset.triggerScore); |
| } |
| if (asset.documentValid) stat.documentValidCount++; |
| else stat.documentInvalidCount++; |
| } |
| if (cycle.submitOutput.includes('tripCircuit')) totalTrips++; |
| if (cycle.submitOutput.includes('updateProof')) totalUpdates++; |
| } |
|
|
| return { assetStats, totalCycles, totalTrips, totalUpdates, allResults }; |
| } |
|
|
| |
| |
| |
| function generateReport(summary, totalCycles, timestamp) { |
| let md = `# ProofBridge Liner — Full-Length Integration Test Report\n\n`; |
| md += `**Date:** ${timestamp}\n`; |
| md += `**Cycles:** ${totalCycles}\n`; |
| md += `**Total Trip Actions Planned:** ${summary.totalTrips}\n`; |
| md += `**Total Update Actions Planned:** ${summary.totalUpdates}\n\n`; |
|
|
| md += `## Per-Asset Statistics\n\n`; |
| md += `| Asset ID | Cycles | Mismatches | Unreachable | Avg Score | Max Score | Doc Valid | Doc Invalid |\n`; |
| md += `|----------|--------|------------|-------------|-----------|-----------|-----------|------------|\n`; |
|
|
| for (const [assetId, stat] of Object.entries(summary.assetStats)) { |
| const avgScore = stat.triggerScores.length > 0 |
| ? (stat.triggerScores.reduce((a, b) => a + b, 0) / stat.triggerScores.length).toFixed(3) |
| : 'N/A'; |
| const maxScore = stat.triggerScores.length > 0 |
| ? Math.max(...stat.triggerScores).toFixed(3) |
| : 'N/A'; |
| md += `| ${assetId.slice(0, 12)}... | ${stat.cyclesSeen} | ${stat.mismatches} | ${stat.unreachable} | ${avgScore} | ${maxScore} | ${stat.documentValidCount} | ${stat.documentInvalidCount} |\n`; |
| } |
|
|
| md += `\n## Trigger Score Distribution\n`; |
| for (const [assetId, stat] of Object.entries(summary.assetStats)) { |
| if (stat.triggerScores.length > 0) { |
| md += `- **${assetId.slice(0, 12)}...**: min=${Math.min(...stat.triggerScores).toFixed(3)}, max=${Math.max(...stat.triggerScores).toFixed(3)}, mean=${(stat.triggerScores.reduce((a, b) => a + b, 0) / stat.triggerScores.length).toFixed(3)}\n`; |
| } |
| } |
|
|
| md += `\n## Observations\n`; |
| if (summary.totalTrips > 0) { |
| md += `- **Circuit trips were planned** in ${summary.totalTrips} out of ${totalCycles} cycles. This indicates ghost-risk or adversarial conditions.\n`; |
| } else { |
| md += `- No circuit trips were planned — system remained open throughout.\n`; |
| } |
| if (summary.allResults.some(c => c.assets.some(a => a.documentValid === false))) { |
| md += `- **Deterministic validator rejected** at least one document as structurally invalid. The scorer clamped the score.\n`; |
| } |
| md += `\n## Notes\n`; |
| md += `- Broadcaster was run in dry-run mode (no on-chain transactions).\n`; |
| md += `- AI audit analysis skipped unless NVIDIA_API_KEY was set and document text was extractable.\n`; |
|
|
| return md; |
| } |
|
|
| |
| |
| |
| main().catch(err => { |
| console.error(`[Pipeline] Fatal error: ${err.message}`); |
| process.exit(1); |
| }); |