import { useState, useEffect, useRef } from "react"; // ───────────────────────────────────────────────────────────── // turnEngine — ported verbatim from the spec. The gate, untouched. // ───────────────────────────────────────────────────────────── const PHI = 0.618; function runTurnEngine(input) { const { deltaTheta, volatility, groundCoherence, skyCoherence, lagTrend, phase } = input; const wobbleHigh = deltaTheta > PHI; const volatilityHigh = volatility > PHI; const groundReady = groundCoherence >= PHI; const skyReady = skyCoherence >= PHI; const lagImproving = lagTrend <= 0; if (wobbleHigh || volatilityHigh) return { action: "BREATHE", nextPhase: "TURN", releaseAllowed: false, reason: "Wobble or volatility breached threshold. Pause injected." }; if (!groundReady || !skyReady) return { action: "CLARIFY", nextPhase: "LINE", releaseAllowed: false, reason: "Ground/Sky alignment incomplete." }; if (!lagImproving) return { action: "WAIT", nextPhase: "TURN", releaseAllowed: false, reason: "Lag curve worsening. Hold until response timing stabilizes." }; if (phase === "TURN" || phase === "LINE") return { action: "RELEASE", nextPhase: "RELEASE", releaseAllowed: true, reason: "Coherence threshold met. Release permitted." }; if (phase === "RELEASE") return { action: "RECOVER", nextPhase: "OBSERVE", releaseAllowed: false, reason: "Release complete. Observe before next action." }; return { action: "HOLD", nextPhase: phase, releaseAllowed: false, reason: "System stable. No intervention required." }; } // ───────────────────────────────────────────────────────────── // constants / theme // ───────────────────────────────────────────────────────────── const PHASES = ["STATIC", "LOAD", "LINE", "TURN", "RELEASE", "OBSERVE", "RECOVERY"]; const C = { bg: "#0a0c0a", panel: "rgba(20,24,18,0.72)", edge: "rgba(216,162,74,0.16)", ink: "#e9e4d6", mute: "#7d8276", faint: "#565b50", phi: "#d8a24a", stable: "#6fae6f", adaptive: "#c9a94a", turn: "#e08a3c", collapse: "#d65a4e", ground: "#bd7c45", sky: "#7fa8c9", }; const ACTION_COLOR = { HOLD: C.stable, BREATHE: C.turn, CLARIFY: C.sky, WAIT: C.adaptive, RELEASE: C.phi, RECOVER: "#8fbf9f", }; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const c01 = (v) => clamp(v, 0, 1); function wobbleState(dt) { if (dt <= 0.3) return { label: "STABLE", color: C.stable }; if (dt <= 0.618) return { label: "ADAPTIVE WOBBLE", color: C.adaptive }; if (dt <= 0.85) return { label: "TURN REQUIRED", color: C.turn }; return { label: "COLLAPSE RISK", color: C.collapse }; } // gauge geometry const GX = 110, GY = 120, GR = 88; const angleOf = (v) => 180 - 180 * c01(v); function polar(cx, cy, r, deg) { const a = (deg * Math.PI) / 180; return { x: cx + r * Math.cos(a), y: cy - r * Math.sin(a) }; } function arcPath(startDeg, endDeg, r = GR) { const s = polar(GX, GY, r, startDeg); const e = polar(GX, GY, r, endDeg); const large = startDeg - endDeg > 180 ? 1 : 0; return `M ${s.x.toFixed(2)} ${s.y.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${e.x.toFixed(2)} ${e.y.toFixed(2)}`; } // ───────────────────────────────────────────────────────────── // simulation harness — drives inputs; the engine governs releases. // ───────────────────────────────────────────────────────────── function initSim() { return { dt: 0.12, vol: 0.1, ground: 0.78, sky: 0.74, lag: -0.05, phase: "STATIC", observeT: 0, recoverT: 0, tick: 0, out: runTurnEngine({ deltaTheta: 0.12, volatility: 0.1, groundCoherence: 0.78, skyCoherence: 0.74, lagTrend: -0.05, phase: "STATIC" }), log: [], lagHist: Array(46).fill(0), actHist: [], }; } function step(prev) { const n = () => Math.random() - 0.5; let { dt, vol, ground, sky, lag, phase, observeT, recoverT, tick, log, lagHist, actHist } = prev; tick += 1; const out = runTurnEngine({ deltaTheta: dt, volatility: vol, groundCoherence: ground, skyCoherence: sky, lagTrend: lag, phase }); switch (out.action) { case "BREATHE": dt *= 0.86; vol *= 0.86; ground = c01(ground + 0.016); sky = c01(sky + 0.016); lag -= 0.06; break; case "WAIT": lag -= 0.09; dt *= 0.96; ground = c01(ground + 0.01); sky = c01(sky + 0.01); break; case "CLARIFY": ground = c01(ground + 0.032); sky = c01(sky + 0.032); dt *= 0.98; lag -= 0.01; break; case "RELEASE": dt = c01(dt + 0.05 + 0.03 * Math.random()); lag += 0.02; ground = c01(ground - 0.02); sky = c01(sky - 0.02); break; case "RECOVER": dt *= 0.9; vol *= 0.9; lag -= 0.04; ground = c01(ground + 0.02); sky = c01(sky + 0.02); break; default: dt += 0.015 + 0.02 * Math.random(); lag += 0.012; // HOLD: the world keeps moving, load accrues } // ambient drift dt = c01(dt + 0.02 * n()); vol = c01(vol + 0.02 * n()); ground = c01(ground + 0.015 * n()); sky = c01(sky + 0.015 * n()); lag = clamp(lag + 0.03 * n(), -1, 1); // phase progression — engine sets the gated phases; harness walks the quiet cycle let phase2 = out.nextPhase; if (out.action === "HOLD") { if (phase === "OBSERVE") { observeT += 1; phase2 = observeT > 3 ? (observeT = 0, "RECOVERY") : "OBSERVE"; } else if (phase === "RECOVERY") { recoverT += 1; phase2 = recoverT > 2 ? (recoverT = 0, "STATIC") : "RECOVERY"; } else if (phase === "STATIC") phase2 = "LOAD"; else if (phase === "LOAD") phase2 = "LINE"; else phase2 = phase; } // log on action change const lastAct = log[0]?.action; const newLog = out.action !== lastAct ? [{ t: tick, action: out.action, reason: out.reason }, ...log].slice(0, 7) : log; const newHist = [...lagHist.slice(1), lag]; const newActHist = [...actHist.slice(-13), out.action]; return { dt, vol, ground, sky, lag, phase: phase2, observeT, recoverT, tick, out, log: newLog, lagHist: newHist, actHist: newActHist }; } function chatterCount(actHist) { let c = 0; for (let i = 1; i < actHist.length; i++) if (actHist[i] !== actHist[i - 1]) c++; return c; } // ───────────────────────────────────────────────────────────── // small UI pieces // ───────────────────────────────────────────────────────────── function Panel({ title, children, style }) { return ( <div className="te-panel" style={style}> {title && <div className="te-ptitle">{title}</div>} {children} </div> ); } function Meter({ label, value, color }) { const ready = value >= PHI; return ( <div style={{ marginBottom: 14 }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}> <span style={{ fontSize: 11, letterSpacing: "0.14em", color: C.mute }}>{label}</span> <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 13, color }}> {value.toFixed(3)} <span style={{ color: ready ? C.stable : C.faint, marginLeft: 8, fontSize: 9, letterSpacing: "0.12em" }}> {ready ? "READY" : "LOW"} </span> </span> </div> <div className="te-bartrack"> <div style={{ position: "absolute", left: `${PHI * 100}%`, top: -2, bottom: -2, width: 1, background: C.phi, opacity: 0.7 }} /> <div style={{ height: "100%", width: `${value * 100}%`, background: color, borderRadius: 3, transition: "width 0.35s ease, background 0.35s" }} /> </div> </div> ); } // ───────────────────────────────────────────────────────────── // main // ───────────────────────────────────────────────────────────── export default function TurnEngineDashboard() { const [sim, setSim] = useState(initSim); const [running, setRunning] = useState(true); const [speed, setSpeed] = useState(620); const simRef = useRef(sim); simRef.current = sim; useEffect(() => { if (!running) return; const id = setInterval(() => setSim((p) => step(p)), speed); return () => clearInterval(id); }, [running, speed]); const perturb = (fn) => setSim((p) => ({ ...p, ...fn(p) })); const injectWobble = () => perturb((p) => ({ dt: c01(p.dt + 0.5), vol: c01(p.vol + 0.32) })); const applyLoad = () => perturb((p) => ({ ground: c01(p.ground - 0.32), sky: c01(p.sky - 0.28), lag: clamp(p.lag + 0.35, -1, 1) })); const settle = () => perturb((p) => ({ dt: p.dt * 0.4, vol: p.vol * 0.4, ground: c01(p.ground + 0.2), sky: c01(p.sky + 0.2), lag: clamp(p.lag - 0.3, -1, 1) })); const ws = wobbleState(sim.dt); const out = sim.out; const actColor = ACTION_COLOR[out.action] || C.ink; const needleAngle = angleOf(sim.dt); const lagImproving = sim.lag <= 0; const chatter = chatterCount(sim.actHist); // lag sparkline const W = 320, H = 56; const pts = sim.lagHist.map((v, i) => { const x = (i / (sim.lagHist.length - 1)) * W; const y = H / 2 - v * (H / 2) * 0.92; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(" "); return ( <div className="te-root"> <style>{` @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=IBM+Plex+Mono:wght@400;500;600&display=swap'); .te-root{ min-height:100vh; box-sizing:border-box; padding:26px 22px 40px; background: radial-gradient(1100px 600px at 78% -8%, rgba(216,162,74,0.07), transparent 60%), radial-gradient(900px 700px at 8% 110%, rgba(127,168,201,0.05), transparent 55%), ${C.bg}; color:${C.ink}; font-family:'IBM Plex Mono',monospace; } .te-root *{box-sizing:border-box;} .te-wrap{max-width:1040px; margin:0 auto;} .te-head{display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:16px; margin-bottom:22px;} .te-title{font-family:'Fraunces',serif; font-weight:600; font-size:34px; letter-spacing:0.01em; line-height:0.95; margin:0;} .te-motto{font-family:'Fraunces',serif; font-style:italic; font-size:14.5px; color:${C.mute}; margin-top:8px; max-width:380px; line-height:1.4;} .te-status{display:flex; align-items:center; gap:9px; font-size:11px; letter-spacing:0.16em; color:${C.mute};} .te-dot{width:9px; height:9px; border-radius:50%; background:${C.stable}; box-shadow:0 0 10px ${C.stable};} .te-dot.off{background:${C.faint}; box-shadow:none;} .te-pulse{animation:tepulse 1.6s ease-in-out infinite;} @keyframes tepulse{0%,100%{opacity:1;}50%{opacity:0.35;}} .te-grid{display:grid; grid-template-columns:1.05fr 1fr; gap:16px;} .te-panel{background:${C.panel}; border:1px solid ${C.edge}; border-radius:14px; padding:18px; backdrop-filter:blur(3px); box-shadow:0 18px 40px -28px rgba(0,0,0,0.9);} .te-ptitle{font-size:10.5px; letter-spacing:0.22em; color:${C.faint}; margin-bottom:14px; text-transform:uppercase;} .te-bartrack{position:relative; height:9px; border-radius:3px; background:rgba(255,255,255,0.05);} .te-phase-row{display:flex; align-items:center; gap:0;} .te-cell{flex:1; text-align:center; padding:11px 4px; border-radius:8px; font-size:10.5px; letter-spacing:0.09em; color:${C.faint}; border:1px solid transparent; transition:all 0.35s; position:relative;} .te-cell.on{color:${C.bg}; font-weight:600;} .te-conn{width:10px; height:1px; background:${C.edge}; flex:0 0 auto;} .te-controls{display:flex; flex-wrap:wrap; gap:9px; align-items:center;} .te-btn{font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.08em; color:${C.ink}; background:rgba(255,255,255,0.04); border:1px solid ${C.edge}; border-radius:8px; padding:9px 14px; cursor:pointer; transition:all 0.18s;} .te-btn:hover{background:rgba(216,162,74,0.12); border-color:${C.phi};} .te-btn.warn:hover{background:rgba(214,90,78,0.14); border-color:${C.collapse};} .te-btn.cool:hover{background:rgba(127,168,201,0.14); border-color:${C.sky};} .te-readouts{display:flex; flex-wrap:wrap; gap:14px 22px; font-size:11px; color:${C.mute}; margin-top:14px;} .te-readouts b{color:${C.ink}; font-weight:500;} .te-log{font-size:11px; line-height:1.7; color:${C.mute}; max-height:118px; overflow:hidden;} .te-foot{font-family:'Fraunces',serif; font-style:italic; color:${C.faint}; font-size:13px; text-align:center; margin-top:26px; line-height:1.5;} @media(max-width:760px){ .te-grid{grid-template-columns:1fr;} .te-title{font-size:27px;} .te-cell{font-size:8.5px; padding:9px 2px;} } `}</style> <div className="te-wrap"> <header className="te-head"> <div> <h1 className="te-title">TURN ENGINE</h1> <p className="te-motto">Do not force the swing at the top. Wait for gravity to agree.</p> </div> <div style={{ textAlign: "right" }}> <div className="te-status"> <span className={`te-dot ${running ? "te-pulse" : "off"}`} /> {running ? "LIVE" : "HELD"} · t={sim.tick} </div> <div style={{ fontSize: 11, color: C.faint, marginTop: 8, letterSpacing: "0.1em" }}> τ-gate φ = 0.618 </div> </div> </header> <div className="te-grid"> {/* Delta-Theta gauge */} <Panel title="Δθ — Wobble Gauge"> <svg viewBox="0 0 220 150" style={{ width: "100%", display: "block" }}> <path d={arcPath(180, 126)} stroke={C.stable} strokeWidth="11" fill="none" opacity="0.85" /> <path d={arcPath(126, 68.76)} stroke={C.adaptive} strokeWidth="11" fill="none" opacity="0.85" /> <path d={arcPath(68.76, 27)} stroke={C.turn} strokeWidth="11" fill="none" opacity="0.85" /> <path d={arcPath(27, 0)} stroke={C.collapse} strokeWidth="11" fill="none" opacity="0.85" /> {/* trigger tick at 0.618 */} {(() => { const a = polar(GX, GY, GR + 9, 68.76); const b = polar(GX, GY, GR - 11, 68.76); return <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={C.ink} strokeWidth="1.5" />; })()} <text x={polar(GX, GY, GR + 18, 68.76).x} y={polar(GX, GY, GR + 18, 68.76).y} fill={C.phi} fontSize="9" textAnchor="middle" fontFamily="'IBM Plex Mono'">φ</text> {/* needle */} <g style={{ transform: `rotate(${(90 - needleAngle).toFixed(2)}deg)`, transformOrigin: `${GX}px ${GY}px`, transition: "transform 0.4s cubic-bezier(.2,.7,.2,1)" }}> <line x1={GX} y1={GY} x2={GX} y2={GY - GR + 6} stroke={ws.color} strokeWidth="2.5" strokeLinecap="round" /> </g> <circle cx={GX} cy={GY} r="6" fill={C.bg} stroke={ws.color} strokeWidth="2" /> <text x={GX} y={GY - 30} textAnchor="middle" fill={ws.color} fontFamily="'Fraunces',serif" fontWeight="600" fontSize="30"> {sim.dt.toFixed(2)} </text> </svg> <div style={{ textAlign: "center", fontSize: 11, letterSpacing: "0.18em", color: ws.color, marginTop: -4 }}> {ws.label} </div> <div className="te-readouts"> <span>vol <b style={{ color: sim.vol > PHI ? C.collapse : C.ink }}>{sim.vol.toFixed(3)}</b></span> <span>chatter/13 <b style={{ color: chatter >= 6 ? C.turn : C.ink }}>{chatter}</b></span> </div> </Panel> {/* Action gate */} <Panel title="Action Gate"> <div style={{ display: "flex", alignItems: "baseline", gap: 14, flexWrap: "wrap" }}> <span style={{ fontFamily: "'Fraunces',serif", fontWeight: 600, fontSize: 46, color: actColor, lineHeight: 1, transition: "color 0.3s" }}> {out.action} </span> <span style={{ fontSize: 10, letterSpacing: "0.14em", padding: "5px 11px", borderRadius: 20, border: `1px solid ${out.releaseAllowed ? C.phi : C.edge}`, color: out.releaseAllowed ? C.phi : C.mute, background: out.releaseAllowed ? "rgba(216,162,74,0.12)" : "transparent", }}> {out.releaseAllowed ? "RELEASE ALLOWED" : "HELD"} </span> </div> <p style={{ fontSize: 12.5, color: C.mute, lineHeight: 1.55, marginTop: 14, minHeight: 38 }}> {out.reason} </p> <div style={{ fontSize: 10.5, color: C.faint, letterSpacing: "0.1em", marginTop: 4 }}> When uncertain, preserve optionality. </div> </Panel> {/* Ground / Sky */} <Panel title="Ground / Sky Alignment"> <Meter label="GROUND · constraint · cost · body" value={sim.ground} color={C.ground} /> <Meter label="SKY · meaning · model · narrative" value={sim.sky} color={C.sky} /> <div style={{ fontSize: 10.5, color: C.faint, marginTop: 6, lineHeight: 1.5 }}> Release requires both ≥ φ, lag decreasing, Δθ settling. </div> </Panel> {/* Lag curve */} <Panel title="Lag Curve"> <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", display: "block" }}> <line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke={C.edge} strokeWidth="1" strokeDasharray="3 4" /> <polyline points={pts} fill="none" stroke={lagImproving ? C.stable : C.collapse} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" /> </svg> <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 11, color: C.mute }}> <span>trend <b style={{ color: lagImproving ? C.stable : C.collapse }}>{lagImproving ? "↓ improving" : "↑ worsening"}</b></span> <span style={{ fontFamily: "'IBM Plex Mono'", color: lagImproving ? C.stable : C.collapse }}>{sim.lag.toFixed(3)}</span> </div> </Panel> </div> {/* Phase strip */} <Panel title="Phase Strip · no RELEASE without TURN" style={{ marginTop: 16 }}> <div className="te-phase-row"> {PHASES.map((p, i) => { const on = sim.phase === p; return ( <div key={p} style={{ display: "contents" }}> <div className="te-cell" style={on ? { background: C.phi, boxShadow: `0 0 18px -2px ${C.phi}`, borderColor: C.phi } : {}}> {on ? <span className={i < 99 ? "" : ""}>{p}</span> : p} </div> {i < PHASES.length - 1 && <div className="te-conn" />} </div> ); })} </div> </Panel> {/* Controls + log */} <div className="te-grid" style={{ marginTop: 16 }}> <Panel title="Controls"> <div className="te-controls"> <button className="te-btn" onClick={() => setRunning((r) => !r)}>{running ? "❚❚ HOLD" : "▶ RUN"}</button> <button className="te-btn" onClick={() => { setSim(initSim()); }}>↻ RESET</button> <button className="te-btn warn" onClick={injectWobble}>INJECT WOBBLE</button> <button className="te-btn cool" onClick={applyLoad}>APPLY LOAD</button> <button className="te-btn" onClick={settle}>SETTLE</button> </div> <div className="te-controls" style={{ marginTop: 14 }}> <span style={{ fontSize: 10.5, color: C.faint, letterSpacing: "0.12em" }}>TEMPO</span> {[["slow", 1100], ["normal", 620], ["fast", 280]].map(([lbl, ms]) => ( <button key={lbl} className="te-btn" onClick={() => setSpeed(ms)} style={speed === ms ? { borderColor: C.phi, color: C.phi } : {}}>{lbl}</button> ))} </div> </Panel> <Panel title="Decision Trace"> <div className="te-log"> {sim.log.length === 0 && <div style={{ color: C.faint }}>awaiting transition…</div>} {sim.log.map((e, i) => ( <div key={`${e.t}-${i}`} style={{ opacity: i === 0 ? 1 : 0.55 - i * 0.05 }}> <span style={{ color: C.faint }}>t={String(e.t).padStart(3, "0")}</span>{" "} <span style={{ color: ACTION_COLOR[e.action], fontWeight: 500 }}>{e.action}</span>{" "} <span>— {e.reason.replace(/\.$/, "")}</span> </div> ))} </div> </Panel> </div> <div className="te-foot"> It is a porch instrument. Watch the swing, not just the gauge. </div> </div> </div> ); } Live. runTurnEngine is ported verbatim from the spec — same φ = 0.618 gate, same five branches, untouched. Everything else is a harness wrapped around it so you can watch the gate work: • The sim loop drives Δθ, volatility, ground/sky, and lag with drift; the engine decides every tick. Pauses decay wobble and recover coherence — the thesis (stability is timing, not filters) made visible. • Inject Wobble spikes Δθ past threshold → instant BREATHE. Apply Load drops alignment → CLARIFY/WAIT. Settle lets it breathe back to RELEASE. • Phase strip enforces no RELEASE without TURN; Decision Trace scrolls the engine’s own reason strings. One honest thing it surfaces: the hard 0.618 cutoff chatters when Δθ hovers right at the line — I left a raw chatter count (flips per 13 ticks) in the gauge panel so you can see it rather than smooth it away. That’s the same boundary behavior governor.py flagged toward hysteresis; the dashboard shows it instead of hiding it. Nothing in the gate was changed to make the demo behave — if it chatters, that’s the spec chattering. Want me to drop in a Schmitt-style dual threshold as a toggle so you can A/B the bare gate against hysteresis on the same live signal? Have fun 🤩 do it the way you think
Let's do it. I'll add a Schmitt-trigger hysteresis toggle so you can A/B the bare gate against the dampened version on the same live signal.
Done. You now have a GATE toggle in the controls:
The gauge visually shows the deadband (purple arc) when Schmitt is active, and a latch readout appears so you can see the triggered/clear state persist even as the needle wanders through the band. Crank it to fast, inject wobble near threshold, and watch the chatter count diverge between modes — that's the A/B.