// Experiment manifest: versions, configuration, dates, parameter ranges, seeds and SHA-256 checksums of every delivered file. const fs = require('fs'), path = require('path'), crypto = require('crypto'), os = require('os'), cp = require('child_process'); const ROOT = __dirname; const HFX = require(path.join(ROOT, 'app', 'src', 'hfx_core.js')); const base = require(path.join(ROOT, 'study', 'plans', 'study_base.js')); const SKIP = new Set(['.claude', 'node_modules', '.DS_Store', 'pages', 'manifest.json', '__pycache__']); const files = []; function walk(dir) { for (const name of fs.readdirSync(dir).sort()) { if (SKIP.has(name)) continue; const p = path.join(dir, name); const st = fs.statSync(p); if (st.isDirectory()) walk(p); else files.push({ path: path.relative(ROOT, p), bytes: st.size, mtime: st.mtime.toISOString() }); } } walk(ROOT); for (const f of files) f.sha256 = crypto.createHash('sha256').update(fs.readFileSync(path.join(ROOT, f.path))).digest('hex'); const log = fs.readFileSync(path.join(ROOT, 'study', 'experiment_log.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l)); const ranges = {}; const seeds = new Set(); const stages = {}; let first = null, last = null; for (const r of log) { seeds.add(r.seed); stages[r.stage] = (stages[r.stage] || 0) + 1; if (!first || r.started < first) first = r.started; if (!last || r.finished > last) last = r.finished; for (const [k, v] of Object.entries(r.params)) { if (typeof v === 'number') { if (!ranges[k]) ranges[k] = { min: v, max: v, values: new Set() }; ranges[k].min = Math.min(ranges[k].min, v); ranges[k].max = Math.max(ranges[k].max, v); ranges[k].values.add(v); } else if (typeof v === 'string' || typeof v === 'boolean') { if (!ranges[k]) ranges[k] = { values: new Set() }; ranges[k].values.add(String(v)); } } } for (const k of Object.keys(ranges)) { const vals = Array.from(ranges[k].values); ranges[k].values = vals.length <= 12 ? vals.sort((a, b) => (a > b) - (a < b)) : vals.length + ' distinct values'; } const val = JSON.parse(fs.readFileSync(path.join(ROOT, 'validation', 'validation_results.json'), 'utf8')); const pred = JSON.parse(fs.readFileSync(path.join(ROOT, 'study', 'predictions.json'), 'utf8')); const hold = fs.readFileSync(path.join(ROOT, 'analysis', 'processed', 'holdout_comparison.csv'), 'utf8').trim().split('\n').slice(1); const coreSrc = fs.readFileSync(path.join(ROOT, 'app', 'src', 'hfx_core.js'), 'utf8'); const appHtml = fs.readFileSync(path.join(ROOT, 'app', 'HierarchicalFractureLab.html'), 'utf8'); const ver = (cmd) => { try { return cp.execSync(cmd, { encoding: 'utf8' }).trim().split('\n')[0]; } catch (e) { return null; } }; const manifest = { title: 'Hierarchical Fracture Lab — experiment manifest', generated: new Date().toISOString(), application: { file: 'app/HierarchicalFractureLab.html', appVersion: '1.0.0', coreVersion: HFX.VERSION, coreBuild: HFX.BUILD, coreSha256: crypto.createHash('sha256').update(coreSrc).digest('hex'), appSha256: crypto.createHash('sha256').update(appHtml).digest('hex'), appBytes: appHtml.length, threejs: 'r128 (inlined)', orbitControls: 'three@0.128.0 examples/js/controls/OrbitControls.js (inlined)', coreEmbeddedInApp: appHtml.includes(coreSrc.slice(0, 2000)) }, modelConfiguration: { family: 'hierarchical cellular venation network (recursive clipped Voronoi, 2x2 root grid, no frame, 256 leaf cells)', studyBase: base, solver: '2-D Euler–Bernoulli beam network, envelope LDL^T with RCM ordering, exact event tracking, brittle member removal, ground-spring stabilisation 1e-12, detached fragments frozen', units: 'normalised: length W=1, modulus E_s=1, depth b_z=1; force in E_s W b_z, energy in E_s W^2 b_z', defaultParams: HFX.defaultParams() }, study: { runs: log.length, designs: new Set(log.map(r => r.designId)).size, stages: stages, firstRunStarted: first, lastRunFinished: last, seedsUsed: Array.from(seeds).sort((a, b) => a - b), allConverged: log.every(r => r.converged), errors: log.filter(r => r.status !== 'ok').length, parameterRanges: ranges, preliminaryArchivedRuns: fs.readFileSync(path.join(ROOT, 'study', 'prelim', 'experiment_log_prelim_frame.jsonl'), 'utf8').trim().split('\n').length }, validation: { tests: val.tests.length, allPass: val.allPass, essentialPass: val.essentialPass, build: val.build, date: val.date }, holdout: { designs: Object.keys(pred.predictions).length, predictionsRecorded: pred.recorded, predictionsSha256: crypto.createHash('sha256').update(JSON.stringify(pred)).digest('hex'), numericWithinRange: hold.reduce((a, l) => a + parseInt(l.split(',').slice(-1)[0].split('/')[0], 10), 0), numericTotal: hold.reduce((a, l) => a + parseInt(l.split(',').slice(-1)[0].split('/')[1], 10), 0) }, environment: { platform: os.platform() + ' ' + os.release(), cpus: os.cpus().length, node: process.version, python: ver('python3 --version'), ffmpeg: (ver('ffmpeg -version') || '').slice(0, 40), pdflatex: (ver('pdflatex --version') || '').slice(0, 60) }, fileCount: files.length, totalBytes: files.reduce((a, f) => a + f.bytes, 0), files: files }; fs.writeFileSync(path.join(ROOT, 'manifest.json'), JSON.stringify(manifest, null, 1)); console.log('manifest:', files.length, 'files,', (manifest.totalBytes / 1e6).toFixed(1), 'MB; runs', log.length, 'designs', manifest.study.designs, 'seeds', manifest.study.seedsUsed.join(','), '; holdout', manifest.holdout.numericWithinRange + '/' + manifest.holdout.numericTotal, '; core embedded in app:', manifest.application.coreEmbeddedInApp);