def test_direction_is_taken_from_the_route_row_never_from_the_kind_name(): """THE REGRESSION GUARD. If the detector's wiring ever flips again, the panel must report what the route row SAYS, not what the kind name implies — a chain that reads 'oversold' while the engine routed a SHORT is a real event the operator needs to see, not one the panel should silently 'correct' by parsing the word.""" > out = _run_js(""" const contradictory = [{event_et:'14:51:00', direction:'short', account:'cuixia'}]; return { label:_chSignal('14:51:00','tna_tide_oversold', [], contradictory).label, noDir:_chSignal('09:46:00','tna_tide_oversold', [], [{event_et:'09:46:00'}]).label, phrases:[_chDirPhrase('long'), _chDirPhrase('up'), _chDirPhrase('short'), _chDirPhrase('down'), _chDirPhrase(''), _chDirPhrase('sideways')] }; """) tests/test_tide_wave_chain_fill_identity.py:219: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/test_tide_wave_chain_fill_identity.py:74: in _run_js res = subprocess.run(["node", "-e", script], cwd=str(REPO_ROOT), _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ input = None, capture_output = True, timeout = None, check = True popenargs = (['node', '-e', "const _CH_STATE_WORD = /(^|_)(oversold|overbought|stretched|extreme)(_|$)/;\nconst _CH_NAMES_DIR = /...rPhrase('down'), _chDirPhrase(''), _chDirPhrase('sideways')] };\n \n})();\nconsole.log(JSON.stringify(__out));\n"],) kwargs = {'cwd': '/Users/cao/robinhood-chain-fill-identity', 'stderr': -1, 'stdout': -1, 'text': True} process = stdout = '' stderr = "[eval]:774\n}\n^\nExpression expected\n\nSyntaxError: Unexpected token '}'\n at makeContextifyScript (node:interna...alTypeScript (node:internal/process/execution:260:22)\n at node:internal/main/eval_string:71:3\n\nNode.js v25.6.0\n" retcode = 1 def run(*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs): """Run command with arguments and return a CompletedProcess instance. The returned instance will have attributes args, returncode, stdout and stderr. By default, stdout and stderr are not captured, and those attributes will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them, or pass capture_output=True to capture both. If check is True and the exit code was non-zero, it raises a CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute, and output & stderr attributes if those streams were captured. If timeout (seconds) is given and the process takes too long, a TimeoutExpired exception will be raised. There is an optional argument "input", allowing you to pass bytes or a string to the subprocess's stdin. If you use this argument you may not also use the Popen constructor's "stdin" argument, as it will be used internally. By default, all communication is in bytes, and therefore any "input" should be bytes, and the stdout and stderr will be bytes. If in text mode, any "input" should be a string, and stdout and stderr will be strings decoded according to locale encoding, or by "encoding" if set. Text mode is triggered by setting any of text, encoding, errors or universal_newlines. The other arguments are the same as for the Popen constructor. """ if input is not None: if kwargs.get('stdin') is not None: raise ValueError('stdin and input arguments may not both be used.') kwargs['stdin'] = PIPE if capture_output: if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None: raise ValueError('stdout and stderr arguments may not be used ' 'with capture_output.') kwargs['stdout'] = PIPE kwargs['stderr'] = PIPE with Popen(*popenargs, **kwargs) as process: try: stdout, stderr = process.communicate(input, timeout=timeout) except TimeoutExpired as exc: process.kill() if _mswindows: # Windows accumulates the output in a single blocking # read() call run on child threads, with the timeout # being done in a join() on those threads. communicate() # _after_ kill() is required to collect that and add it # to the exception. exc.stdout, exc.stderr = process.communicate() else: # POSIX _communicate already populated the output so # far into the TimeoutExpired exception. process.wait() raise except: # Including KeyboardInterrupt, communicate handled that. process.kill() # We don't call process.wait() as .__exit__ does that for us. raise retcode = process.poll() if check and retcode: > raise CalledProcessError(retcode, process.args, output=stdout, stderr=stderr) E subprocess.CalledProcessError: Command '['node', '-e', 'const _CH_STATE_WORD = /(^|_)(oversold|overbought|stretched|extreme)(_|$)/;\nconst _CH_NAMES_DIR = /(^|_)(up|down|long|short|bull|bear|buy|sell)(_|$)/;\nfunction _chShort(s, n){ s = String(s==null?\'\':s); return s.length>n ? s.slice(0,n-1)+\'…\' : s; }\nfunction _chFillsFor(rows, events){\n const mine = new Set((rows||[]).map(r => r && r.coid).filter(Boolean).map(String));\n if(!mine.size) return [];\n return (events||[]).filter(e => e && e.type===\'trade\' && e.coid && mine.has(String(e.coid)));\n }\nfunction _chExec(dRows, fills){\n if(fills && fills.length){\n const f = fills[0];\n const qty = fills.reduce((a,x)=>a+(+x.qty||0),0);\n return { state:\'fired\', label:`FIRED ${f.symbol||\'\'}`.trim(),\n facts:`${qty||f.qty||\'\'} @ $${f.price!=null?f.price:\'—\'}${fills.length>1?` · ${fills.length} fills`:\'\'}`, dead:false };\n }\n const fired = (dRows||[]).find(r=>r.row_kind===\'execution\' && r.fill_price!=null);\n if(fired) return { state:\'fired\', label:\'FIRED\', facts:`${fired.shares||\'\'} @ $${fired.fill_price}`, dead:false };\n const vetoed = (dRows||[]).find(r=>r.row_kind===\'veto\' || r.row_kind===\'governor_demotion\');\n if(vetoed){\n const why = String(vetoed.engine_reason||(vetoed.demoted_by||[]).join(\'+\')||\'vetoed\');\n return /^llm:no_trade/.test(why)\n ? { state:\'declined\', label:\'no order\', facts:\'the reasoner declined\', dead:true }\n : { state:\'vetoed\', label:\'no order\', facts:_chShort(why,40), dead:true };\n }\n const skip = (dRows||[]).find(r=>r.row_kind===\'allowlist_skip\');\n if(skip) return { state:\'skipped\', label:\'no order\', facts:\'never consulted\', dead:true };\n const close = (dRows||[]).find(r=>r.row_kind===\'macro_silent_close\');\n if(close) return { state:\'manual\', label:\'MACRO CLOSE\', facts:`${close.exit_symbol||\'\'} ${close.exit_shares||\'\'}`.trim(), dead:false };\n return { state:\'skipped\', label:\'no order\', facts:\'chain ended before execution\', dead:true };\n }\nfunction _chDirPhrase(dir){\n const d = String(dir||\'\').toLowerCase();\n if(/^(long|up|buy|bull)$/.test(d)) return \'BUY / go LONG\';\n if(/^(short|down|sell|bear)$/.test(d)) return \'SELL / go SHORT\';\n return \'\'; // unknown direction — say nothing rather than guess\n }\nfunction _chSignalLabel(arrow, k, dir){\n const nice = String(k||\'\').replace(/_+/g,\' \').trim();\n const phrase = _chDirPhrase(dir);\n if(!phrase || _CH_NAMES_DIR.test(String(k||\'\'))) return `${arrow} ${nice}`;\n const m = String(k||\'\').match(_CH_STATE_WORD);\n const state = m ? m[2] : \'\';\n const base = (state ? String(k).replace(_CH_STATE_WORD, \'$1$3\') : String(k))\n .replace(/_+/g,\' \').trim() || nice;\n return `${arrow} ${base} — ${phrase}${state ? ` (was ${state})` : \'\'}`;\n }\nfunction _chSignal(et, kind, evs, dRows){\n // Never let an `engine_routed_*` row supply the signal text: its `detail` is a dumped python dict\n // — exactly the boilerplate wall the operator rejected.\n const evsClean = (evs||[]).filter(e => !/^engine_routed_/.test(String(e.type||\'\')));\n const lead = evsClean.find(e => /^(level_|rollover_)/.test(String(e.type||\'\'))) || evsClean[0] || {};\n const k = String(kind || lead.marker_kind || lead.type || \'event\').replace(/^level_/,\'\');\n const dir = (dRows||[]).map(r=>r.direction).find(Boolean) || \'\';\n // Whole tokens only: a loose /up/ matched "s-UP-port_break" and drew a bullish arrow on a breakdown.\n const hay = `${dir} ${k}`.toLowerCase();\n const isUp = /(^|[^a-z])(up|long|bull|reclaim|bounce)([^a-z]|$)/.test(hay) || /_reclaim|rollover_up|cross_up/.test(hay);\n const isDn = /(^|[^a-z])(down|short|bear|below)([^a-z]|$)/.test(hay) || /_break|breakdown|rollover_down|liquidation/.test(hay);\n const arrow = isUp && !isDn ? \'▲\' : (isDn && !isUp ? \'▼\' : (isUp ? \'▲\' : \'•\'));\n const cls = arrow===\'▲\' ? \'up\' : (arrow===\'▼\' ? \'down\' : \'\');\n let facts = lead.detail || (dRows||[]).map(r=>r.skip_reason_code).find(Boolean) || \'\';\n if(/^decision=|^\\{|reason=.*status=/.test(String(facts))) facts = \'\';\n return { label:_chSignalLabel(arrow, k, dir), cls, facts:String(facts), pattern:lead.label||\'\' };\n }\n\n // ---- stage 3: the two brains + the conclusion ---------------------------------------------------\n // THE RM CONTRACT (feature/rm-veto e9f5c7c25f): the decision row carries an additive `risk` key:\n // {tier:\'rm\'|\'mechanical-fallback\', forbidden[], rationale, slug, day_type, doc, doc_sha,\n // elapsed_s, why_fallback, readings[{name, available, sentence, detail{}}]}\n // `risk` is NULL when the tier did not run (switch off / test origin). That is a THIRD state and\n // the panel must show it differently from "ran and allowed" — only the null distinguishes them.\n function _chRisk(d){ return (d && typeof d.risk === \'object\') ? d.risk : null; }\n\n function _chDecision(dRows){\n const rows = dRows||[];\n const d = rows.find(r=>r.decision) || rows.find(r=>r.skip_reason_code) || rows[0] || {};\n const rv = _chRisk(d);\n const skip = rows.find(r=>r.skip_reason_code);\n const engine = String(d.engine_reason || rows.map(r=>r.engine_reason).find(Boolean) || \'\');\n const demoted = (d.demoted_by||[]);\n\n let trader;\n if(d.decision){\n const conf = (typeof d.consult_confidence===\'number\' && typeof d.confidence===\'number\')\n ? `${(d.consult_confidence*100).toFixed(0)}→${(d.confidence*100).toFixed(0)}%`\n : (typeof d.confidence===\'number\' ? `${(d.confidence*100).toFixed(0)}%` : \'\');\n const scale = (typeof d.scale===\'number\') ? ` ×${d.scale}` : \'\';\n trader = { text:`${d.decision} ${conf}${scale}`.trim(), cls:d.decision };\n } else trader = { text: skip ? \'not consulted\' : \'—\', cls:\'none\' };\n\n const isLlmNoTrade = /^llm:no_trade/.test(engine);\n let risk;\n if(rv){\n const blocked = (rv.forbidden||[]).length > 0;\n const tier = rv.tier === \'rm\' ? \'🧑\u200d⚖️ RM judge (LLM)\'\n : `mechanical fallback${rv.why_fallback?` — ${rv.why_fallback}`:\'\'}`;\n risk = { text: blocked ? `⛔ ${_chShort(rv.slug || rv.rationale, 30)}` : \'allow\',\n cls: blocked ? \'block\' : \'allow\', tier, ran:true, rv };\n } else if(demoted.length){\n risk = { text:`⛔ ${demoted.join(\'+\')}`, cls:\'block\', tier:\'mechanical guard\', ran:true };\n } else if(engine && !isLlmNoTrade && !/^allowlist/.test(engine) && d.fill_price==null && d.decision){\n risk = { text:`⛔ ${_chShort(engine,28)}`, cls:\'block\', tier:\'mechanical guard\', ran:true };\n } else if(d.decision){\n // No RM ruling recorded and no guard fired. Say "did not run" — NOT "allow". They are\n // different states and conflating them would claim a review that never happened.\n risk = { text:\'not run\', cls:\'none\', tier:\'risk tier did not run\', ran:false };\n } else risk = { text:\'—\', cls:\'none\', tier:\'risk tier did not run\', ran:false };\n\n let outcome;\n if(skip && !d.decision) outcome = { text:\'NOT CONSULTED — policy\', cls:\'none\' };\n else if(risk.cls===\'block\') outcome = { text:`VETOED — ${_chShort((rv&&(rv.slug||rv.rationale))||demoted.join(\'+\')||engine,26)}`, cls:\'vetoed\' };\n else if(isLlmNoTrade || /NO_TRADE/i.test(String(d.decision||\'\')))\n outcome = { text:\'DECLINED — no trade\', cls:\'declined\' };\n else if(d.decision) outcome = { text:\'APPROVED → execute\', cls:\'approved\' };\n else outcome = { text:\'—\', cls:\'none\' };\n\n return { trader, risk, outcome, row:d, rv,\n latency:(typeof d.consult_elapsed_s===\'number\') ? `${d.consult_elapsed_s.toFixed(0)}s` : \'\' };\n }\n\n function _chDecNone(){\n return { trader:{text:\'—\',cls:\'none\'}, risk:{text:\'—\',cls:\'none\',tier:\'risk tier did not run\',ran:false},\n outcome:{text:\'—\',cls:\'none\'}, row:{}, rv:null, latency:\'\' };\n }\n\n // ---- stage 4: what reached the BROKER, or where the chain died ---------------------------------\n function _chExec(dRows, fills){\n if(fills && fills.length){\n const f = fills[0];\n const qty = fills.reduce((a,x)=>a+(+x.qty||0),0);\n return { state:\'fired\', label:`FIRED ${f.symbol||\'\'}`.trim(),\n facts:`${qty||f.qty||\'\'} @ $${f.price!=null?f.price:\'—\'}${fills.length>1?` · ${fills.length} fills`:\'\'}`, dead:false };\n }\n const fired = (dRows||[]).find(r=>r.row_kind===\'execution\' && r.fill_price!=null);\n if(fired) return { state:\'fired\', label:\'FIRED\', facts:`${fired.shares||\'\'} @ $${fired.fill_price}`, dead:false };\n const vetoed = (dRows||[]).find(r=>r.row_kind===\'veto\' || r.row_kind===\'governor_demotion\');\n if(vetoed){\n const why = String(vetoed.engine_reason||(vetoed.demoted_by||[]).join(\'+\')||\'vetoed\');\n return /^llm:no_trade/.test(why)\n ? { state:\'declined\', label:\'no order\', facts:\'the reasoner declined\', dead:true }\n : { state:\'vetoed\', label:\'no order\', facts:_chShort(why,40), dead:true };\n }\n const skip = (dRows||[]).find(r=>r.row_kind===\'allowlist_skip\');\n if(skip) return { state:\'skipped\', label:\'no order\', facts:\'never consulted\', dead:true };\n const close = (dRows||[]).find(r=>r.row_kind===\'macro_silent_close\');\n if(close) return { state:\'manual\', label:\'MACRO CLOSE\', facts:`${close.exit_symbol||\'\'} ${close.exit_shares||\'\'}`.trim(), dead:false };\n return { state:\'skipped\', label:\'no order\', facts:\'chain ended before execution\', dead:true };\n }\n\n // ---- LIVE ATTENTION: is this chain still in flight? ---------------------------------------------\n // Only for TODAY and only for a YOUNG event: a chain that never terminated hours ago is not "in\n // progress", it is a chain that died silently, and calling that live is the lie this panel ends.\n function _chLive(c){\n if(!isToday(curDay)) return null;\n const p = new Date().toLocaleTimeString(\'en-US\',{timeZone:\'America/New_York\',hour12:false}).split(\':\').map(Number);\n const ageMin = ((p[0]*3600+p[1]*60+(p[2]||0)) - c.tsec) / 60;\n if(!(ageMin >= 0 && ageMin <= 15)) return null;\n const row = (c.dec && c.dec.row) || {};\n if(c.exe && (c.exe.state===\'fired\' || c.exe.state===\'manual\')) return null;\n if(row.skip_reason_code) return null;\n if(!row.decision) return \'decision\'; // consult running\n if(!row.route_status || row.route_phase===\'ordering\') return \'execution\'; // ordering in flight\n return null;\n }\n\n // ---- which fills belong to THIS chain — BY IDENTITY, never by time proximity -------------------\n // The old rule was "any trade within 180s after the onset", and proximity is not ownership. On\n // 2026-07-29 the operator\'s two manual TNA fills (09:45:13 buy, 09:51:43 sell) landed near five\n // unrelated onsets and were rendered as \'FIRED TNA\' on every one of them — including chains whose\n // own outcome was VETOED — rail_guard, DECLINED — no trade and allowlist_excluded. A chain cannot\n // be vetoed and fired at the same time; the column was reporting trades the chain never made.\n //\n // A chain\'s rows already carry the ONLY correct answer: the decisions payload resolves each route\'s\n // flip coid against AlpacaOrders server-side, so ``row.coid`` is the order this chain actually\n // placed. Matching that against the fill\'s own coid is the identity join, and three properties fall\n // straight out of it with no special-casing:\n // - a manual / protection fill (mktstate-bull, mktstate-exit, a genome stop) matches no engine\n // chain, stays unclaimed, and is picked up by the manual-chain loop below as its own\n // \'✋ direct order\' row — which is where the original spec always said it belonged;\n // - an engine chain shows a fill ONLY when its own route reached an executed terminal, because\n // that is the only way a coid gets onto its rows;\n // - a vetoed / declined / skipped chain has no coid at all, so its execution cell can never\n // show a fill. Pinned by tests/test_tide_wave_chain_fill_identity.py.\n // No coid-prefix list here on purpose: classifying manual orders by parsing their names would be a\n // second source of truth that drifts the day someone mints a new prefix.\n function _chFillsFor(rows, events){\n const mine = new Set((rows||[]).map(r => r && r.coid).filter(Boolean).map(String));\n if(!mine.size) return [];\n return (events||[]).filter(e => e && e.type===\'trade\' && e.coid && mine.has(String(e.coid)));\n }\n\n // ---- assemble every chain for the day ----------------------------------------------------------\n function _chBuild(events, cutoff){\n const dRows = _chain.decisions || [];\n const finite = isFinite(cutoff);\n const byEt = new Map(), evByEt = new Map();\n for(const r of dRows){ const et=r.event_et||\'\'; if(!byEt.has(et)) byEt.set(et,[]); byEt.get(et).push(r); }\n for(const e of (events||[])){ const et=String(e.et||\'\'); if(!evByEt.has(et)) evByEt.set(et,[]); evByEt.get(et).push(e); }\n const used = new Set(), chains = [];\n const _secs = et => { const p=String(et||\'\').split(\':\').map(Number); return (p[0]||0)*3600+(p[1]||0)*60+(p[2]||0); };\n\n for(const [et, rows] of byEt){\n const evs = evByEt.get(et) || [];\n evs.forEach(e => used.add(e));\n const fills = _chFillsFor(rows, events);\n fills.forEach(f => used.add(f));\n const kind = rows.map(r=>r.kind).find(Boolean) || \'\';\n chains.push({ et, tsec:_secs(et), sig:_chSignal(et, kind, evs, rows), dec:_chDecision(rows),\n exe:_chExec(rows, fills), accts:[...new Set(rows.map(r=>r.account).filter(Boolean))],\n lag:rows.map(r=>r.claim_lag_min).find(v=>typeof v===\'number\'), rows, fills });\n }\n for(const e of (events||[])){\n if(used.has(e)) continue;\n const et = String(e.et||\'\'), type = String(e.type||\'\');\n if(/^engine_routed_/.test(type)) continue;\n let exe, sig, dec = _chDecNone();\n if(type===\'trade\'){\n exe = { state:\'manual\', label:`FILL ${e.symbol||\'\'}`.trim(),\n facts:`${e.qty||\'\'} @ $${e.price!=null?e.price:\'—\'} · ${_chShort(e.strategy||e.detail||\'\',26)}`, dead:false };\n sig = { label:\'✋ direct order\', cls:\'\', facts:_chShort(e.detail||\'\',48), pattern:e.label||\'\' };\n } else if(/manual_/.test(type)){\n exe = { state:\'manual\', label:(e.label||\'MANUAL\'), facts:_chShort(e.detail||\'\',44), dead:false };\n sig = { label:\'✋ operator action\', cls:\'\', facts:\'\', pattern:e.label||\'\' };\n } else if(/stop_out|auto_exit|genome/.test(type)){\n exe = { state:\'fired\', label:(e.label||type).replace(/_/g,\' \'), facts:_chShort(e.detail||\'\',44), dead:false };\n sig = { label:\'🛡 protection\', cls:\'\', facts:_chShort(e.detail||\'\',48), pattern:e.label||\'\' };\n } else {\n const r = e.reasoning || null;\n sig = _chSignal(et, type, [e], []);\n if(r && r.decision){\n const declined = /no_trade/i.test(String(r.decision));\n const c2 = typeof r.confidence===\'number\' ? `${(r.confidence*100).toFixed(0)}%` : \'\';\n exe = declined ? { state:\'declined\', label:\'no order\', facts:\'the reasoner declined\', dead:true }\n : { state:\'skipped\', label:\'no order\', facts:\'decided, never routed\', dead:true };\n dec = { trader:{ text:`${r.decision} ${c2}`.trim(), cls:r.decision },\n risk:{ text:\'not run\', cls:\'none\', tier:\'risk tier did not run\', ran:false },\n outcome: declined ? {text:\'DECLINED — no trade\',cls:\'declined\'} : {text:\'APPROVED → execute\',cls:\'approved\'},\n row:{ rationale:r.rationale, kind:type, event_et:et }, rv:null,\n latency: typeof r.elapsed_s===\'number\' ? `${r.elapsed_s.toFixed(0)}s` : \'\' };\n } else {\n exe = { state:\'skipped\', label:\'no order\', facts:\'no decision recorded\', dead:true };\n }\n }\n chains.push({ et, tsec:_secs(et), sig, dec, exe, accts:e.account?[e.account]:[],\n lag:null, rows:[], fills:[] });\n }\n let out = chains;\n for(const c of out){\n c.live = _chLive(c);\n c.noise = (!c.live && c.exe.state===\'skipped\');\n }\n if(finite){\n const hh = new Date(Math.min(cutoff, replayHi())).toLocaleTimeString(\'en-US\',{timeZone:\'America/New_York\',hour12:false});\n const lim = _secs(hh);\n out = out.filter(x => x.tsec <= lim);\n }\n out.sort((a,b)=> _chain.desc ? (b.tsec-a.tsec) : (a.tsec-b.tsec));\n out.sort((a,b)=> (a.live?0:1) - (b.live?0:1)); // in-flight pinned to the top, order preserved\n return out;\n }\n\n // ---- INLINE DETAIL — grows the chart downward; no modal, ever ----------------------------------\n function _chDetail(c, i){\n const row = c.dec.row || {}, rv = c.dec.rv;\n const pills = [];\n if(c.accts.length) pills.push(c.accts.join(\', \'));\n if(c.dec.latency) pills.push(`consult ${c.dec.latency}`);\n if(typeof c.lag===\'number\') pills.push(`claim lag ${c.lag.toFixed(1)}m`);\n if((row.kinds||[]).length>1) pills.push(`stages: ${row.kinds.join(\' + \')}`);\n if(rv && typeof rv.elapsed_s===\'number\') pills.push(`risk ${rv.elapsed_s.toFixed(1)}s`);\n if(rv && rv.day_type) pills.push(`day: ${rv.day_type}`);\n let h = `
`;\n if(pills.length) h += `
`+pills.map(p=>`${_chEsc(p)}`).join(\'\')+`
`;\n h += `
🧠 Trader — why we wanted this trade
`+\n `
${_chEsc(row.rationale || \'(no rationale recorded — the reasoner was not consulted for this event)\')}
`;\n // RISK — three distinct states: the RM judge ruled / a mechanical guard ruled / the tier never ran\n let riskBody;\n if(rv){\n const bits = [rv.rationale || \'(no sentence recorded)\'];\n if((rv.forbidden||[]).length) bits.push(`\\nsides forbidden: ${rv.forbidden.join(\', \')}`);\n if(rv.doc) bits.push(`profile: ${rv.doc}${rv.doc_sha?` @ ${rv.doc_sha}`:\'\'}`);\n if(rv.why_fallback) bits.push(`fell back to mechanical: ${rv.why_fallback}`);\n const reads = (rv.readings||[]).map(x=> x.available===false\n ? ` ${x.name}: measurement unavailable` : (x.sentence || ` ${x.name}`)).join(\'\\n\');\n if(reads) bits.push(`\\nreadings it weighed:\\n${reads}`);\n riskBody = bits.join(\'\\n\');\n } else {\n const bits = [];\n if(row.engine_reason) bits.push(`guard / reason: ${row.engine_reason}`);\n if((row.demoted_by||[]).length) bits.push(`demoted by: ${row.demoted_by.join(\', \')}`);\n for(const g of (row.governors||[])) bits.push(`${g.governor}: ${g.decision_in||\'?\'} → ${g.decision_out||\'?\'}${g.reason?(\' — \'+g.reason):\'\'}`);\n if(row.rail_guard && row.rail_guard.reason) bits.push(`rail_guard: ${row.rail_guard.reason}`);\n if(row.skip_reason_text) bits.push(row.skip_reason_text);\n if(row.route_status) bits.push(`route status: ${row.route_status}`);\n bits.push(bits.length ? \'\\n(mechanical guard chain — the RM judge did not rule on this one)\'\n : \'The risk tier did not run for this decision (switch off, or no consult happened).\');\n riskBody = bits.join(\'\\n\');\n }\n h += `
🛡 Risk — ${_chEsc(c.dec.risk.tier)}
${_chEsc(riskBody)}
`;\n if(c.rows.length){\n h += `
Per-account fate
`+\n _chEsc(c.rows.map(r=>`${r.account||\'—\'}: ${r.route_status||\'—\'}`+\n `${r.engine_reason?(\' · \'+r.engine_reason):\'\'}${r.fill_price!=null?(\' · filled $\'+r.fill_price):\'\'}`).join(\'\\n\'))+`
`;\n }\n if(c.fills.length){\n h += `
Broker fills
`+\n _chEsc(c.fills.map(x=>`${x.et} ${x.side||\'\'} ${x.symbol||\'\'} ×${x.qty||\'\'} @ $${x.price} · ${x.strategy||\'\'}`).join(\'\\n\'))+`
`;\n }\n h += _chOverride(c, i);\n return h + `
`;\n }\n\n // ---- INLINE OVERRIDE — the operator\'s own judgment, on the chain, two taps --------------------\n // Same server plumbing as every other operator action (Phase 2 tide_veto_override →\n // run_manual_control → flip_account, MANUAL_ORIGIN, TideManualActions, compare-and-set idempotency,\n // ATR drift refusal, server-enforced window). This is a SECOND ENTRY POINT, never a second path.\n // SIZING: overriding to the brain\'s OWN vetoed/declined side uses that decision\'s computed scale;\n // forcing the OPPOSITE side is a fresh operator judgment and uses the manual-roll pct machinery.\n function _chOverride(c, i){\n const out = c.dec.outcome.cls;\n if(out!==\'vetoed\' && out!==\'declined\') return \'\'; // nothing to override\n if(c.exe.state===\'fired\' || c.exe.state===\'manual\') return \'\';\n const dl = (c.rows||[]).map(r=>r.override_deadline).find(Boolean);\n const already = (c.rows||[]).map(r=>r.operator_override).find(Boolean);\n if(already) return `
Operator override
`+\n _chEsc(`already decided at ${already.at||\'?\'} by ${already.by||\'?\'} — ${already.status||\'\'}`)+`
`;\n // THREE distinct states, never conflated: the feature has not shipped / the window has closed /\n // the window is open. Saying "closed" when the server never offered a window at all would be a\n // guess dressed as a fact — the same class of lie this panel exists to end.\n if(!dl) return `
Operator override
`+\n `not available — this build carries no override window for the chain.\\n`+\n `(the server stamps override_deadline once MARKET_TURN_VETO_OVERRIDE_ENABLED ships; until then `+\n `a veto is final and nothing here can place an order)
`;\n const open = Date.parse(dl) > Date.now();\n if(!open) return `
Operator override
`+\n `the disapprove window has closed — the veto stands
`;\n const own = String(c.dec.trader.cls||\'\').toUpperCase(); // the side the brain wanted\n const mk = side => {\n const isOwn = side===own;\n const armedKey = _chKey(c)+\'|\'+side;\n const armed = _chain.armed === armedKey;\n const label = armed ? `CONFIRM ${side}?` : `OVERRIDE → ${side}`;\n const why = isOwn ? `execute the ${side} the brain wanted, at its own size (×${(c.dec.row||{}).scale||1})`\n : `force the OPPOSITE side — a fresh operator judgment, sized by the manual-roll pct`;\n return ``;\n };\n const secs = Math.max(0, Math.round((Date.parse(dl)-Date.now())/1000));\n return `
Operator override — ${_chEsc(own||\'—\')} was ${out}
`+\n `
${mk(\'LONG\')}${mk(\'SHORT\')}`+\n `window closes in ${secs}s · tap once to arm, again to confirm · `+\n `own side uses the decision\'s size, opposite side uses the manual-roll pct
`;\n }\n\n // ---- one chain -> DOM --------------------------------------------------------------------------\n function _chHtml(c, i){\n const acctTxt = c.accts.length>1 ? `${c.accts.length} acct` : (c.accts[0]||\'\');\n const lagTxt = (typeof c.lag===\'number\' && c.lag>=1) ? ` · ⏱+${c.lag.toFixed(1)}m` : \'\';\n const D = c.dec, open = _chain.expanded.has(_chKey(c));\n const liveCls = c.live ? \' is-live\' : \'\';\n const lv = s => (c.live===s ? \' live\' : \'\');\n return (\n `
`+\n `
`+\n `
time`+\n `${c.live?\'\':\'\'}${_chEsc(c.et)}`+\n `${_chEsc(acctTxt)}
`+\n `
`+\n `
event / signal`+\n `${_chEsc(c.sig.label)}`+\n `${_chEsc(c.sig.facts)}
`+\n `
`+\n `
decision`+\n `
🧠 TRADER`+\n `${_chEsc(D.trader.text)}`+\n (D.latency?`${_chEsc(D.latency)}`:\'\')+`
`+\n `
🛡 RISK`+\n `${_chEsc(D.risk.text)}
`+\n `
${_chEsc(D.outcome.text)}
`+\n `
${c.exe.dead?\'⊘\':\'→\'}
`+\n `
execution`+\n `${_chEsc(c.exe.label)}`+\n `${_chEsc(c.exe.facts)}${_chEsc(lagTxt)}
`+\n `
`+\n `${open?\'▴\':\'i\'}
`+\n `
`+\n (open ? _chDetail(c, i) : \'\')+\n `
`\n );\n }\n\n function renderChains(){\n const box = el(\'tw-events\'); if(!box) return;\n const all = _chBuild(window._twChainEvents || [], window._twChainCutoff);\n const shown = _chain.noise ? all : all.filter(c=>!c.noise);\n const hidden = all.length - shown.length;\n window._twChains = all;\n if(!all.length){\n box.innerHTML = \'
No signals recorded for this day.
\';\n } else {\n const idx = new Map(all.map((c,i)=>[c,i]));\n let h = \'
\' + shown.map(c=>_chHtml(c, idx.get(c))).join(\'\');\n if(hidden>0 && !_chain.noise)\n h += `
+ ${hidden} routine skip${hidden===1?\'\':\'s\'} / marker${hidden===1?\'\':\'s\'} hidden — show all
`;\n box.innerHTML = h + \'
\';\n const sn = el(\'tw-chain-shownoise\');\n if(sn) sn.onclick = () => { _chain.noise = true; renderChains(); };\n }\n const cnt = el(\'tw-evcount\');\n const nLive = all.filter(c=>c.live).length;\n if(cnt) cnt.textContent = `(${shown.length}${hidden>0?` +${hidden} quiet`:\'\'}${nLive?` · ${nLive} live`:\'\'})`;\n // DEEP LINK — `?open=HH:MM:SS` expands that signal\'s chain on load, so a decision can be linked\n // to directly ("look at 15:21:14") instead of described. Runs once, and only after the decisions\n // payload has arrived, so the link never opens a chain built from the event side alone.\n if(!window._twOpenDone && (_chain.decisions||[]).length){\n const want = new URLSearchParams(location.search).get(\'open\');\n if(want){\n const hit = all.find(c => c.et === want) || all.find(c => String(c.et).startsWith(want));\n if(hit){ window._twOpenDone = true; _chain.expanded.add(_chKey(hit)); renderChains(); }\n }\n }\n }\n\n // ---- the override POST (Phase 2 endpoint; per-account, each independently idempotent) ----------\n async function _chDoOverride(c, side){\n const targets = (c.rows||[]).filter(r => r.override_deadline && Date.parse(r.override_deadline) > Date.now());\n const list = targets.length ? targets : (c.rows||[]);\n const msgs = []; let ok = 0;\n for(const t of list){\n const p = new URLSearchParams();\n p.set(\'date\', curDay); p.set(\'event_et\', c.et); p.set(\'account\', t.account||\'\');\n p.set(\'side\', side);\n if (DEBUG_Q) p.set(\'debug\', DEBUG_Q);\n try{\n const r = await fetch(\'/api/tide-wave/veto-override?\'+p.toString(), {method:\'POST\'});\n const d = await r.json();\n if(d.ok) ok++;\n msgs.push(`${t.account||\'?\'}: ${d.message || d.error || (d.ok?\'ok\':\'refused\')}`);\n }catch(err){ msgs.push(`${t.account||\'?\'}: ${err}`); }\n }\n _chain.armed = null;\n alert(`override → ${side}\\n${ok}/${list.length} accepted\\n\\n`+msgs.join(\'\\n\'));\n loadDecisionsPanel().catch(()=>{});\n }\n\n async function loadDecisionsPanel(){\n try{\n const r = await fetch(decisionsApi(curDay));\n const d = await r.json();\n _chain.decisions = (d && d.events) || [];\n }catch(err){ _chain.decisions = []; }\n renderChains();\n }\n function stopPoll(){ if(pollTimer){ clearInterval(pollTimer); pollTimer=null; } }\n\n // ---- Camera: export the current TIDE-wave chart (overlays included) as a PNG ----\n // Chart.js paints on a TRANSPARENT canvas, so a naive canvas.toDataURL() yields a see-through\n // (renders black) PNG. Draw the chart onto an offscreen canvas pre-filled with the panel\n // background first → solid, theme-matched PNG. Uses the canvas\' device-pixel dimensions so the\n // export keeps full chart resolution and every active overlay (🌙 premarket band, prev-day S/R,\n // POC, ROC, state zones, trade markers, replay cursor) that the overlay plugin drew on-canvas.\n function captureChartPNG(){\n if(!chart){ return; }\n const src = chart.canvas;\n const off = document.createElement(\'canvas\');\n off.width = src.width; off.height = src.height;\n const octx = off.getContext(\'2d\');\n octx.fillStyle = \'#0e131c\'; // --panel (theme bg) → non-transparent PNG\n octx.fillRect(0, 0, off.width, off.height);\n octx.drawImage(src, 0, 0);\n const a = document.createElement(\'a\');\n a.href = off.toDataURL(\'image/png\');\n a.download = `tide-wave-${curDay || \'day\'}.png`;\n document.body.appendChild(a); a.click(); a.remove();\n }\n\n // wiring\n el(\'tw-date\').addEventListener(\'change\', e=>{ unpinEtfSim(); el(\'tw-etfsim-badge\').innerHTML=\'\'; load(e.target.value, true); });\n el(\'tw-prev\').addEventListener(\'click\', e=>{ const d=e.target.dataset.day; if(d){ unpinEtfSim(); el(\'tw-etfsim-badge\').innerHTML=\'\'; load(d, true); } });\n el(\'tw-next\').addEventListener(\'click\', e=>{ const d=e.target.dataset.day; if(d){ unpinEtfSim(); el(\'tw-etfsim-badge\').innerHTML=\'\'; load(d, true); } });\n el(\'tw-today\').addEventListener(\'click\', ()=>{ unpinEtfSim(); el(\'tw-etfsim-badge\').innerHTML=\'\'; load(\'\', true); });\n el(\'tw-rth\').addEventListener(\'click\', ()=>{ rthFocus = !rthFocus; applyCutoff(R.active?R.cutoff:Infinity); });\n el(\'tw-camera\').addEventListener(\'click\', captureChartPNG);\n el(\'tw-play\').addEventListener(\'click\', ()=> R.playing ? pause() : play());\n el(\'tw-reset\').addEventListener(\'click\', reset);\n el(\'tw-liveBtn\').addEventListener(\'click\', goLive);\n el(\'tw-etfsim-run\').addEventListener(\'click\', loadEtfSim);\n el(\'tw-etfsim-live\').addEventListener(\'click\', exitEtfSim);\n // Persisted-run picker (bug 371d487cd2): select a persisted SignalSimStudy for the day, or clear\n // the choice. The change handler either clears state (empty) or asks loadPersistedSimRun to\n // fetch + render the chosen run via the existing pinEtfSimResult envelope. The refresh\n // button re-fetches the day\'s run list, preserving the operator\'s current selection when it\n // survives. History is updated via history.replaceState so the choice is shareable via URL.\n if (el(\'tw-sim-run-picker\')) {\n el(\'tw-sim-run-picker\').addEventListener(\'change\', e=>{\n const v = e.target.value;\n if(!v){ persistedSimRunId=null; persistedSimPayload=null; updateSimRunUrl(curDay, null); return; }\n loadPersistedSimRun(v);\n });\n el(\'tw-sim-run-refresh\').addEventListener(\'click\', ()=> loadPersistedSimRuns(persistedSimRunId));\n }\n // engine toggle: ⚙️ Mechanic vs 🧠 LLM\n el(\'tw-engine-toggle\').addEventListener(\'click\', e=>{\n const b = e.target.closest(\'.tw-eng\'); if(!b) return;\n etfEngine = b.getAttribute(\'data-eng\');\n el(\'tw-eng-mech\').classList.toggle(\'active\', etfEngine===\'mechanic\');\n el(\'tw-eng-llm\').classList.toggle(\'active\', etfEngine===\'llm\');\n el(\'tw-etfsim-run\').textContent = etfEngine===\'llm\' ? \'🧠 LLM sim ▶\' : \'🎢 ETF sim ▶\';\n });\n // reveal-mode toggle: ▶ Pre-play (batch) vs 🔬 Step (trigger-by-trigger monitor). Orthogonal to engine.\n el(\'tw-sim-mode\').addEventListener(\'change\', e=>{ etfSimMode = e.target.value; });\n // 🧠 LLM auto-drive (SIM/display autonomy ONLY — never wired to live order execution). Confirm BOTH ways.\n function _reflectAutoDrive(){\n const b = el(\'tw-autodrive\'); if(!b) return;\n b.classList.toggle(\'on\', etfAutoDrive); b.classList.toggle(\'off\', !etfAutoDrive);\n b.textContent = etfAutoDrive ? \'🧠 auto-drive ON\' : \'🧠 auto-drive OFF\';\n }\n el(\'tw-autodrive\').addEventListener(\'click\', ()=>{\n if(etfAutoDrive){\n if(!confirm(\'Turn off LLM auto-drive? You will approve each decision manually.\')) return;\n etfAutoDrive = false;\n } else {\n if(!confirm(\'Re-enable LLM auto-drive? The LLM will act on its own for the rest of the day.\')) return;\n etfAutoDrive = true;\n }\n localStorage.setItem(\'tw_autodrive\', etfAutoDrive ? \'1\' : \'0\');\n _reflectAutoDrive();\n // if a step-monitor is live, reflect the new autonomy on its controls immediately\n if(_etfStep && !_etfStep.done){\n if(!etfAutoDrive) etfStepPause();\n const pb = el(\'tw-etfstep-play\'); if(pb){ pb.disabled = !etfAutoDrive; }\n }\n });\n _reflectAutoDrive();\n // step-monitor controls (Mode 2)\n el(\'tw-etfstep-play\').addEventListener(\'click\', ()=>{ if(_etfStep && _etfStep.playing) etfStepPause(); else etfStepPlay(); });\n el(\'tw-etfstep-next\').addEventListener(\'click\', ()=>etfStepNext());\n el(\'tw-etfstep-finish\').addEventListener(\'click\', finishEtfStep);\n el(\'tw-etfstep-dwell\').addEventListener(\'change\', e=>{ if(_etfStep) _etfStep.dwellMs = parseInt(e.target.value,10)||1500; });\n // operator manual entry (hermetic SIM overlay — never halts the LLM walk, never touches the broker)\n el(\'tw-etfstep-manlong\').addEventListener(\'click\', ()=>injectManualEntry(\'up\'));\n el(\'tw-etfstep-manshort\').addEventListener(\'click\', ()=>injectManualEntry(\'down\'));\n // per-account long/short config: click ON/OFF/def → POST to Redis (live source of truth)\n el(\'tw-cfg-body\').addEventListener(\'click\', e=>{\n const b = e.target.closest(\'.tw-flagbtn\'); if(!b) return;\n const acct = b.getAttribute(\'data-acct\'), side = b.getAttribute(\'data-side\'), v = b.getAttribute(\'data-v\');\n if(v===\'off\'){ if(!confirm(`Disable ${side.toUpperCase()} for ${acct}? The LIVE sleeve will stop opening ${side} legs for this account (flatten-to-cash instead) on the next rollover.`)) return; }\n setEtfFlag(acct, side, v);\n });\n loadEtfFlags();\n // per-account equity-% for the SIZED sleeve buttons — fetched once at boot (read-only, no broker) so\n // the confirm sheet + the button tooltips always quote the CURRENT settings, never a literal.\n loadManualSizing();\n\n // ── BROKER EXECUTION PAUSE (bug 7d6f6039f442fa13eae436f3f4998a9f5cbc417e) ──\n // One-click halt: flips PAUSE_BROKER_EXECUTION_REMAINING_DAY (Redis-backed, hot switch). When ON:\n // every broker-write path short-circuits to a logged SKIP; the model keeps thinking + deciding so\n // the dashboard can show what would-have-done. Auto-clears at 16:00 ET so it never bleeds into\n // tomorrow. The button is the operator\'s only path — no PM daemon writes back.\n let _pauseState = {paused: false, skip_count_today: 0};\n function renderPauseBtn(){\n const label = el(\'tw-pause-label\'); const btn = el(\'tw-pause-btn\');\n const stats = el(\'tw-pause-stats\');\n if(!label || !btn) return;\n if(_pauseState.paused){\n label.innerHTML = \'⏸️ BROKER EXECUTION · PAUSED — observe only\';\n btn.textContent = \'▶️ RESUME BROKER\'; btn.style.background=\'#3a1010\'; btn.style.borderColor=\'#ff6f6f\'; btn.style.color=\'#ff6f6f\';\n } else {\n label.innerHTML = \'🟢 BROKER EXECUTION · RUNNING\';\n btn.textContent = \'⏸️ PAUSE BROKER\'; btn.style.background=\'#1f2a44\'; btn.style.borderColor=\'#3ddc84\'; btn.style.color=\'#3ddc84\';\n }\n stats.textContent = `skips today: ${_pauseState.skip_count_today || 0} · auto-clears at 16:00 ET`;\n }\n async function loadPauseState(){\n try{\n const p = new URLSearchParams(); if (DEBUG_Q) p.set(\'debug\', DEBUG_Q);\n const r = await fetch(\'/api/tide-wave/pause-broker?\'+p.toString());\n const d = await r.json();\n if(d.ok){ _pauseState = {paused: !!d.paused, skip_count_today: d.skip_count_today || 0}; renderPauseBtn(); }\n }catch(_){ /* keep the cached state — the button is idempotent */ }\n }\n async function flipPause(){\n const next = _pauseState.paused ? \'resume\' : \'pause\';\n if(next === \'pause\' && !confirm(\'PAUSE broker execution for the rest of the trading day? Every buy / sell / cover / flip / cancel will be short-circuited to a logged SKIP. The model keeps thinking so you can observe decisions without capital risk. Auto-clears at 16:00 ET.\')) return;\n try{\n const p = new URLSearchParams(); p.set(\'action\', next);\n if (DEBUG_Q) p.set(\'debug\', DEBUG_Q);\n const r = await fetch(\'/api/tide-wave/pause-broker?\'+p.toString(), {method:\'POST\'});\n const d = await r.json();\n if(d.ok){ _pauseState = {paused: !!d.paused, skip_count_today: d.skip_count_today || 0}; renderPauseBtn(); }\n else { alert(\'pause-broker error: \'+(d.error||\'?\')); }\n }catch(err){ alert(\'pause-broker request failed: \'+err); }\n }\n const _pauseBtn = el(\'tw-pause-btn\'); if(_pauseBtn) _pauseBtn.addEventListener(\'click\', flipPause);\n loadPauseState();\n // poll every 30s so the SKIP count + any cross-process flip is reflected live\n setInterval(loadPauseState, 30000);\n // 🧠 reasoned-onset ⓘ (incl. NO_TRADE) → open the LLM decision + rationale + facts/premarket it read\n el(\'tw-etfsim-decisions\').addEventListener(\'click\', e=>{\n const btn = e.target.closest(\'.tw-dec-info\'); if(!btn) return;\n const d = (window._twLlmDecs||[])[parseInt(btn.getAttribute(\'data-i\'),10)]; if(!d) return;\n const lab = d.trigger_kind===\'level\'\n ? \'EVAL-ONLY · \'+String(d.kind||\'level\').replace(/_/g,\' \') // level event: surfaced, not traded\n : (d.taken?\'TAKEN\':\'declined\')+\' · \'+(d.onset_dir===\'up\'?\'roll-up\':\'roll-down\');\n openReasonModal({decision:d.decision, confidence:d.confidence, scale:d.scale, rationale:d.rationale,\n struct_context:d.struct_context, story:d.story}, {label:lab, et:(d.et||\'\')});\n });\n // per-trade ⓘ info button → open the LLM reasoning + CAUSAL struct-context it saw\n el(\'tw-etf-trades\').addEventListener(\'click\', e=>{\n const btn = e.target.closest(\'.tw-etf-info\'); if(!btn) return;\n e.stopPropagation(); // don\'t also open the row\'s price chart\n const i = parseInt(btn.getAttribute(\'data-i\'),10);\n const r = (window._twEtfRows && window._twEtfRows[i]) || null;\n if(!r) return;\n openReasonModal({decision:r.llm_decision, confidence:r.llm_confidence, scale:r.llm_scale,\n rationale:r.llm_rationale, struct_context:r.struct_context, story:r.story},\n {label:(r.account||\'\'), et:(r.entry_et||\'\')});\n });\n el(\'tw-reason-run\').addEventListener(\'click\', loadTideReason);\n // PER-ACCOUNT PICKER for every sleeve trade/marker button (operator 2026-07-24: "allow manual roll\n // down or up on a specific account... current is for all, i would like from UI to do it per account").\n // \'all\' (default) or one nick; the backend ?account= param (status.py api_tide_wave_manual) validates\n // the nick and scopes run_manual_control\'s fan-out. Cancel (Esc / backdrop / Cancel) aborts without a\n // POST. Locks stay fleet-wide on purpose (they are the operator\'s account-wide veto; a per-account\n // lock is a different feature, not this one).\n //\n // The scope now comes from a TAP SHEET, not window.prompt (operator 2026-07-30): the native prompt\n // required TYPING a nick — unusable one-handed on a phone — and showed no size. The sheet lists each\n // sleeve account with the % of equity the action commits (backend-sourced) BEFORE the tap. The\n // subsequent manualControl() confirm is UNCHANGED, so every action keeps at least as many\n // deliberate steps as it had, and the blast-radius warning is still the last thing before the POST.\n const pickAccount = async (action, label, danger)=>{\n const acct = await askManualScope(action, label, danger);\n if(!acct) return; // cancelled\n const s = manualSizeFor(action, acct); // one-account pick → name the size in the confirm\n const sizeNote = s ? (\' · \'+_pctTxt(s.x1_pct)+\' of equity\') : \'\';\n manualControl(action, label+\' · account=\'+acct+sizeNote, 1, false, acct);\n };\n el(\'tw-manual-exit\').addEventListener(\'click\', ()=>pickAccount(\'exit\', \'⛔ EXIT → cash\', true));\n el(\'tw-manual-up\').addEventListener(\'click\', ()=>pickAccount(\'roll_up\', \'△ MANUAL ROLL-UP → bull\'));\n el(\'tw-manual-down\').addEventListener(\'click\', ()=>pickAccount(\'roll_down\', \'▽ MANUAL ROLL-DOWN → bear\', true));\n el(\'tw-manual-boost\').addEventListener(\'click\', ()=>pickAccount(\'boost\', \'➕ EXECUTION BOOST\'));\n el(\'tw-manual-trim\').addEventListener(\'click\', ()=>pickAccount(\'trim\', \'➖ EXECUTION TRIM\', true));\n el(\'tw-manual-lock\').addEventListener(\'click\', ()=>manualControl(\'lock_all\', \'🔒 INTRADAY LOCK → sleeve cannot sell intraday (EOD flatten still runs)\'));\n el(\'tw-manual-unlock\').addEventListener(\'click\', ()=>manualControl(\'unlock_all\', \'🔓 INTRADAY UNLOCK → sleeve manages again\'));\n el(\'tw-manual-permlock\').addEventListener(\'click\', ()=>manualControl(\'perm_lock_all\', \'🔐 PERM LOCK → NOTHING sells this leg (not even EOD flatten / next-morning sale) until you PERM UNLOCK. Carries overnight.\'));\n el(\'tw-manual-permunlock\').addEventListener(\'click\', ()=>manualControl(\'perm_unlock_all\', \'🗝 PERM UNLOCK → hard lock force-cleared; full sleeve management incl. EOD flatten resumes\'));\n el(\'tw-manual-holdovn\').addEventListener(\'click\', ()=>pickAccount(\'hold_overnight\', \'🌙 HOLD OVERNIGHT → EOD flatten skips\'));\n el(\'tw-manual-unholdovn\').addEventListener(\'click\', ()=>pickAccount(\'unhold_overnight\', \'🌤 UNHOLD → EOD flatten resumes\'));\n // click-to-chart modal close (button, backdrop click, Esc)\n const closeEtfModal = ()=>{ el(\'tw-etf-modal\').style.display=\'none\';\n if(etfModalChart){ etfModalChart.destroy(); etfModalChart=null; } };\n el(\'tw-etf-modal-close\').addEventListener(\'click\', closeEtfModal);\n el(\'tw-etf-modal\').addEventListener(\'click\', e=>{ if(e.target===el(\'tw-etf-modal\')) closeEtfModal(); });\n document.addEventListener(\'keydown\', e=>{ if(e.key===\'Escape\'){ closeEtfModal(); closeReasonModal(); } });\n // full-reasoning modal: ℹ button on a rollover row (event delegation — rows are rebuilt each cutoff)\n el(\'tw-events\').addEventListener(\'click\', e=>{\n // OVERRIDE — two taps, inline: the first ARMS the button, the second FIRES. Handled BEFORE the\n // expand toggle so a click on a button never collapses the panel out from under the operator.\n const ovr = e.target.closest(\'button[data-ovr]\');\n if(ovr){\n e.stopPropagation();\n const c2 = (window._twChains||[])[parseInt(ovr.getAttribute(\'data-ci\'),10)];\n if(!c2) return;\n const side = ovr.getAttribute(\'data-ovr\'), key = _chKey(c2)+\'|\'+side;\n if(_chain.armed === key) _chDoOverride(c2, side); // second tap = confirm\n else { _chain.armed = key; renderChains(); } // first tap = arm\n return;\n }\n // Clicking the chain (or its ⓘ) EXPANDS IT IN PLACE — no popup. A finger on a phone should not\n // have to find a small target, so the whole row is the hit area.\n const info = e.target.closest(\'.tw-info-i\') || e.target.closest(\'.tw-chain\');\n if(info){\n const ci = parseInt(info.getAttribute(\'data-ci\'), 10);\n const c = (window._twChains||[])[ci];\n if(c){\n // EXPAND IN PLACE — no modal. Second click collapses.\n const k = _chKey(c);\n if(_chain.expanded.has(k)) _chain.expanded.delete(k); else _chain.expanded.add(k);\n _chain.armed = null; // collapsing/expanding disarms any primed override\n renderChains();\n return;\n }\n }\n const btn = e.target.closest(\'.tw-rz-info\'); if(!btn) return;\n const rid = parseInt(btn.getAttribute(\'data-rid\'), 10);\n const ev = fullData && fullData._events && fullData._events[rid];\n if(ev && ev.reasoning) openReasonModal(ev.reasoning, ev);\n });\n // chain toolbar: sort order + noise toggle\n el(\'tw-chain-sort\').addEventListener(\'click\', ()=>{\n _chain.desc = !_chain.desc;\n el(\'tw-chain-sort\').textContent = _chSortLabel();\n renderChains();\n });\n el(\'tw-chain-noise\').addEventListener(\'click\', ()=>{\n _chain.noise = !_chain.noise;\n el(\'tw-chain-noise\').textContent = _chain.noise ? \'hide noise\' : \'show noise\';\n renderChains();\n });\n el(\'tw-reason-modal-close\').addEventListener(\'click\', closeReasonModal);\n el(\'tw-reason-modal\').addEventListener(\'click\', e=>{ if(e.target===el(\'tw-reason-modal\')) closeReasonModal(); });\n el(\'tw-speed\').addEventListener(\'change\', e=>{ R.speed = parseFloat(e.target.value)||30; });\n el(\'tw-scrub\').addEventListener(\'input\', e=>{\n startReplayIfNeeded();\n R.cutoff = replayLo() + (parseInt(e.target.value,10)/1000)*(replayHi()-replayLo());\n applyCutoff(R.cutoff);\n });\n document.querySelectorAll(\'.tw-f\').forEach(cb=>cb.addEventListener(\'change\', ()=>{\n hidden.clear(); document.querySelectorAll(\'.tw-f\').forEach(x=>{ if(!x.checked) hidden.add(x.value); });\n applyCutoff(R.active ? R.cutoff : Infinity);\n }));\n // reference-overlay toggles: prev-day support/ceiling, POC, ROC (redraw at the current cutoff)\n (function(){\n const wire = (id, set) => { const cb = el(id); if(cb) cb.addEventListener(\'change\', ()=>{\n set(cb.checked); applyCutoff(R.active ? R.cutoff : Infinity); }); };\n wire(\'tw-ov-levels\', v=>{ ovShowSR = v; });\n wire(\'tw-ov-poc\', v=>{ ovShowPoc = v; });\n wire(\'tw-ov-roc\', v=>{ ovShowRoc = v; });\n wire(\'tw-ov-tna\', v=>{ ovShowTna = v; });\n wire(\'tw-ov-energy\', v=>{ ovShowEnergy = v; }); // ⚡ energy blocks shaded on the oscillator panel\n // premarket toggle also re-frames the x-axis (04:00 left edge on / 09:30 off), so re-apply the window\n const pcb = el(\'tw-ov-premkt\');\n if(pcb) pcb.addEventListener(\'change\', ()=>{ ovShowPremkt = pcb.checked; applyRthWindow();\n applyCutoff(R.active ? R.cutoff : Infinity); });\n })();\n\n // additive: sound mute toggle (default OFF) + lazily unlock the AudioContext on any control gesture\n (function(){\n const sb = el(\'tw-sound\');\n if(sb){\n const paint = ()=>{ const m = twSound.isMuted();\n sb.textContent = m ? \'🔇\' : \'🔊\';\n sb.title = m ? \'Audio cues off — click to enable sound\' : \'Audio cues on — click to mute\';\n sb.style.color = m ? \'\' : \'var(--accent)\'; };\n paint();\n sb.addEventListener(\'click\', ()=>{ twSound.ensure(); twSound.toggle(); paint(); });\n }\n [\'tw-play\',\'tw-reset\',\'tw-liveBtn\'].forEach(id=>{ const n = el(id); if(n) n.addEventListener(\'click\', ()=>twSound.ensure()); });\n const sc = el(\'tw-scrub\'); if(sc) sc.addEventListener(\'pointerdown\', ()=>twSound.ensure());\n })();\n\n // First data load. A #flywheel write-up link carries `?sim=llm` (or `?sim=etf`) so the operator lands\n // on the REASONER\'s simulated entries/exits — NOT the live-account trades the page shows by default\n // (bug acbfeaf9527f). After the live/historic payload lands, switch the engine and auto-run the sim ONCE.\n // Display-only + hermetic: the sim endpoints write nothing live, `?sim=llm` sizes off CACHED decisions\n // (instant, no LLM call), and the page\'s ✕/Live buttons revert to the live view. Fail-open — a missing\n // param, an empty cache, or a sim error leaves the normal live/historic dashboard fully intact.\n const _boot = load(curDay);\n const _simQ = (new URLSearchParams(location.search).get(\'sim\') || \'\').trim().toLowerCase();\n if (_simQ === \'llm\' || _simQ === \'etf\' || _simQ === \'mechanic\') {\n const _eng = _simQ === \'llm\' ? \'llm\' : \'mechanic\';\n Promise.resolve(_boot).then(() => {\n try {\n etfEngine = _eng; // reflect the ⚙️ Mechanic / 🧠 LLM engine toggle\n const em = el(\'tw-eng-mech\'), elm = el(\'tw-eng-llm\');\n if (em) em.classList.toggle(\'active\', _eng === \'mechanic\');\n if (elm) elm.classList.toggle(\'active\', _eng === \'llm\');\n const rb = el(\'tw-etfsim-run\'); if (rb) rb.textContent = _eng === \'llm\' ? \'🧠 LLM sim ▶\' : \'🎢 ETF sim ▶\';\n loadEtfSim(); // no event arg → cached (instant) reasoner overlay\n } catch (e) { /* fail-open: keep the live/historic view intact */ }\n });\n }\n\n // Persisted-run deep link (bug 371d487cd2): `?date=YYYY-MM-DD&sim_run=` cold-loads the\n // named SignalSimStudy for the day, mirroring the `?sim=...` deep-link pattern above. After\n // the day payload lands, the list-loader auto-fetches the named run when it\'s in the day\'s\n // list, or surfaces the "unknown persisted run X" named state and clears the URL. Fail-open:\n // a missing picker (flag OFF), a missing run, or any 404 leaves the live/historic view intact.\n const _deepRun = (new URLSearchParams(location.search).get(\'sim_run\') || \'\').trim();\n if (_deepRun && el(\'tw-sim-run-picker\')) {\n Promise.resolve(_boot).then(() => {\n try { loadPersistedSimRuns(_deepRun); } catch(e) { /* fail-open */ }\n });\n }\n}\n\nconst __out = (function(){\n\n const contradictory = [{event_et:\'14:51:00\', direction:\'short\', account:\'cuixia\'}];\n return { label:_chSignal(\'14:51:00\',\'tna_tide_oversold\', [], contradictory).label,\n noDir:_chSignal(\'09:46:00\',\'tna_tide_oversold\', [], [{event_et:\'09:46:00\'}]).label,\n phrases:[_chDirPhrase(\'long\'), _chDirPhrase(\'up\'), _chDirPhrase(\'short\'),\n _chDirPhrase(\'down\'), _chDirPhrase(\'\'), _chDirPhrase(\'sideways\')] };\n \n})();\nconsole.log(JSON.stringify(__out));\n']' returned non-zero exit status 1. ../.pyenv/versions/3.12.12/lib/python3.12/subprocess.py:571: CalledProcessError