// Run with Node.js from the downloaded media directory. Read-only; no network. // Required: lab55-data-method.json and its four listed source CSV files. const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'); const root=__dirname,method=JSON.parse(fs.readFileSync(path.join(root,'lab55-data-method.json'),'utf8')); const sha=b=>crypto.createHash('sha256').update(b).digest('hex'); function load(source){const b=fs.readFileSync(path.join(root,source.file));if(sha(b)!==source.sha256)throw Error('Source SHA mismatch: '+source.file);const lines=b.toString('utf8').trim().split(/\r?\n/),keys=lines.shift().replace(/^\uFEFF/,'').split(','),scale=10**source.digits;return lines.map((line,index)=>{const values=line.split(','),r={index};keys.forEach((k,i)=>r[k]=k==='time_mt5'?values[i]:Number(values[i]));for(const k of ['open','high','low','close']){const v=r[k]*scale,n=Math.round(v);if(Math.abs(v-n)>=1e-6)throw Error('Price not on declared grid');r[k]=n;}if(!(r.low<=Math.min(r.open,r.close)&&Math.max(r.open,r.close)<=r.high))throw Error('Invalid OHLC');return r;});} function geometry(r){return{R:r.high-r.low,B:Math.abs(r.close-r.open),U:r.high-Math.max(r.open,r.close),D:Math.min(r.open,r.close)-r.low};} function prior(rows,start,up=false){const v=rows.slice(start-4,start);return v.length===4&&v.slice(1).every((r,i)=>up?r.close>v[i].close:r.close=3*R&&10*U<=R; if(kind==='not-a-hammer')return prior(rows,i)&&3*R<10*B&&20*B<=7*R&&5*D>=3*R&&10*U<=R; if(kind==='shooting-star')return prior(rows,i,true)&&R<10*B&&10*B<=3*R&&5*U>=3*R&&10*D<=R; const b=rows[i-1],g=geometry(b); if(kind==='harami')return prior(rows,i-1)&&b.close=3*g.R&&a.close>a.open&&b.closeb.low); if(kind==='fx-morning-star'){const f=rows[i-2],fg=geometry(f);return prior(rows,i-2)&&f.close0&&5*fg.B>=3*fg.R&&g.R>0&&10*g.B<=3*g.R&&a.close>a.open&&5*B>=3*R&&2*a.close>=f.open+f.close&&Math.max(b.open,b.close)>=f.close;} throw Error('Unknown rule: '+kind); } const cache=new Map(method.source_exports.map(s=>[s.original_file,{source:s,rows:load(s)}])),out=[]; for(const rule of method.selection.specs){const d=cache.get(rule.file);if(!d)throw Error('Missing listed source');const matches=[];for(let i=10;i=method.selection.range.first&&t<=method.selection.range.last&&match(rule.id,d.rows,i))matches.push(i);}const i=matches[0],expected=method.cases.find(c=>c.id==='lab55-'+rule.id);if(i===undefined||i!==expected.pattern_indices[1]||i-rule.pattern_bars+1!==expected.pattern_indices[0])throw Error('First-match mismatch: '+rule.id);if(!match(rule.id,d.rows.slice(0,i+1),i))throw Error('Prefix invariance failed');out.push({id:rule.id,source:rule.file,index:i,pattern_indices:expected.pattern_indices,pattern_open_times:d.rows.slice(i-rule.pattern_bars+1,i+1).map(r=>r.time_mt5),geometry:d.rows.slice(i-rule.pattern_bars+1,i+1).map(geometry),matches_in_fixed_range:matches.length,future_unused_in_selection:true});} console.log(JSON.stringify({pass:true,verified_sources:cache.size,reproduced_cases:out.length,original_mt5_labels:true,no_network_or_trading_calls:true,cases:out},null,2));