File size: 25,399 Bytes
c061513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/* Oracle7 architecture explorer (Workbench copy: preserveDrawingBuffer for films, addModule for your specialists) — themeable. Requires THREE (r128) + OrbitControls.

   Usage: const ex = O7Explorer(hostEl, { palette: {...}, onSelect(id) {} });

   Data: the release head w540 (chain of 317 folds) and the specialist ledger on Box C, 2026-10-14.

   trace(steps) lights the exact path one answer took: steps = [[stageId, moduleName|null], ...]. */
(function () {
  const STAGES = [
    { id: "L5", name: "Self-model", y: 8.0, r: 2.4, blurb: "The model reading and explaining its own record.", mods: [
      ["Self record & atlas", "w33 · w110 · w117"], ["Self-explanation", "w412: explains itself from its records"],
      ["Self-improvement voice", "30 epoch folds, w10 → w342"], ["Charter & milestones", "open capability milestones"]] },
    { id: "L4", name: "Speak & verify", y: 6.4, r: 3.0, blurb: "Turning a bound answer into cited text, and refusing what fails a check.", mods: [
      ["Receipts", "w536: a relation answer names the record it read, both ends with ids"],
      ["Final-answer checks", "w532–w534: sign kept, private questions, false date premise, thousands, units, word sense"],
      ["Same-name check", "w538: stays with the thing the conversation is about"],
      ["Supporting sentence", "w538: a quoted sentence that lost its numbers is dropped"],
      ["Answer checks", "w539 · w540: a name is not arithmetic; a count names what it counts; a question's direction; a zero read back; a definition is not a private answer"],
      ["Precision guard", "declines what it cannot verify, and says why"], ["Speaker", "evidence-bound"]] },
    { id: "L3", name: "Conversation state", y: 4.8, r: 3.5, blurb: "What the conversation is about, and what was already said.", mods: [
      ["Conversation engine", "subject, topic, last answer and last result, per session"],
      ["Follow-up resolver", "\"When was he born?\" is read against the last answer"],
      ["Two-part turns", "w533 · w537: each part goes through the whole pipeline"],
      ["Hand-offs", "w533: a follow-up goes back to the specialist that answered"],
      ["Conversation acts", "greetings, thanks, clarification"], ["Dialogue organ", "w480: what did I ask, you didn't answer me"]] },
    { id: "L2", name: "Route", y: 3.2, r: 3.9, blurb: "Choosing what answers: a specialist, an organ, or the stored text.", mods: [
      ["Exact router", "w524: typed question shapes to one relation"],
      ["General reader", "w539 · w540: one Wikidata fact away, on reading declines; held-out 81 of 82"],
      ["Family precedence", "w536: relation specialists answer relation questions first (34 more right, 0 lost)"],
      ["Specialist call", "picks the admitted specialist for the domain"], ["Organ dispatch", "w480: math, relation, dialogue, code"],
      ["Factoid & reading", "w420 · w432 · w434 · w442"], ["Deliberation", "where / when / who over stored text"],
      ["Capability routes", "summarize, compare, timeline, explain…"], ["Compose", "w426: inference over two sentences"],
      ["Path memory", "research: learned route credit; off until it wins a sealed test"],
      ["Front parser", "about half of open trivia phrasings are not parsed (census 10-13); declined, never guessed", "hazard"]] },
    { id: "L1", name: "Parse", y: 1.6, r: 3.5, blurb: "From words to a typed request.", mods: [
      ["Construction grammar", "K8 grammar, runtime N"], ["Typed request / λ-IR", "M13E4 runtime, frozen"], ["Obligations IR", "program_ir_v1"],
      ["Splitting", "runtimes AJ · AK · and-parts w533"], ["Frame", "w34"]] },
    { id: "L0", name: "Intake", y: 0.0, r: 3.0, blurb: "Where a request arrives and leaves.", mods: [
      ["Front door", "chat(text, session): sessions, provenance"], ["Harness v21", "response contract"], ["Salience arbitrator", "harness v20"],
      ["Live demo", "a public talk page · nothing typed is stored"]] },
    { id: "S", name: "Specialists", y: -1.8, r: 4.3, blurb: "267 small verified programs. Each was admitted by a held-out exam it saw once; 23 failed and stayed out. On their exams: 15,478 right, 14 wrong.", spec: true, mods: [
      ["Relation readers", "161: moons, directors, novels, painters, composers…"], ["Physical science", "densities, units, energy, thermo, optics, decay…"],
      ["Earth & life", "geology, ecology, botany, medicine, genetics…"], ["Computing", "python, sql, git, linux, networking…"],
      ["Space", "orbits, spacecraft, space comms, planetary"], ["Everyday", "cooking, gardening, first aid, finance…"],
      ["Humanities", "literature, law, music, world history…"], ["Exam ledger", "every admission and failure, published"]] },
    { id: "E", name: "Engines", y: -3.6, r: 3.7, blurb: "Computing what should never be memorised.", engine: true, mods: [
      ["Calculator", "w29 · w458: calculator or refuse"], ["Algebra", "w55"], ["Deduction", "w54 · closure runtime AD"], ["Dates", "runtimes Y · AB"],
      ["TeX reader", "w428"], ["Math organ", "exact moves, independently checked"], ["Code organ", "w482 · w486: run on your examples, graded"],
      ["Word problems", "w488: single-step schemas"], ["Weight from mass", "w538: W = m·g from a stated mass"], ["Operator host", "w59 · w61 · w65"]] },
    { id: "V", name: "Knowledge vault", y: -5.4, r: 5.2, blurb: "Everything it knows, each sentence addressable and cited.", store: true, mods: [
      ["Deep store", "6,339,707 pages · 20,529,863 sentences"], ["Relation edges", "edges.sqlite, 2.2 GB"], ["ConceptNet", "3,422,938 edges"],
      ["Wiktionary", "1,486,439 entries"], ["WordNet", "kinds, hypernyms"], ["Textbook store", "20 books · 143,355 typed items"], ["Relation store", "Wikidata · 10.35M entities"]] },
  ];
  const LOOP = [["Failure inbox", "structured failure records", 270], ["Fold worker", "repairs drafted from the inbox", 215], ["Candidate", "one in flight at a time", 150],
    ["GATE", "4,114 held-out checks; nothing may get worse", 90, "gate"], ["HEAD_MANIFEST", "every consumer resolves the tip here", 35],
    ["Sealed tests", "written before the build, run once (12–19)", 5], ["Served head", "w540", 342], ["Benchmark organ", "rotating unseen windows", 318], ["Stimuli organ", "random conversation pressure", 294]];
  const FAMILY = [["oracle7-deepspace", "98.9% on fresh spacecraft faults"], ["oracle7-vision", "MNIST 98.58% · cites its examples"], ["oracle7-trafficcop", "30/32 held-out routes"],
    ["oracle7-chameleon", "builds sites · 0 false completions"], ["oracle7-artist", "composes from cited parts"]];

  function O7Explorer(host, opt) {
    opt = opt || {};
    const P = Object.assign({ ink: 0x15100d, line: 0x342820, accent: 0x2f7d73, store: 0x2a3b52, hazard: 0xc49132, fresh: 0xc0602e, bad: 0x9a3b1d, spec: 0x946d1e, lit: 0xc0602e, muted: 0x6a5a47, discOpacity: 0.05, lineOpacity: 0.6 }, opt.palette || {});
    const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
    const canvas = document.createElement("canvas"); canvas.className = "o7-canvas"; host.appendChild(canvas);
    const labelsEl = document.createElement("div"); labelsEl.className = "o7-labels"; host.appendChild(labelsEl);
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, preserveDrawingBuffer: true });
    renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
    const scene = new THREE.Scene(), camera = new THREE.PerspectiveCamera(38, 1, 0.1, 500);
    const controls = new THREE.OrbitControls(camera, canvas);
    controls.enableDamping = true; controls.dampingFactor = 0.08; controls.minDistance = 8; controls.maxDistance = 80; controls.enableZoom = opt.zoom !== false;
    controls.autoRotate = !reduce; controls.autoRotateSpeed = 0.35;
    canvas.addEventListener("pointerdown", () => { controls.autoRotate = false; });

    function line(points, color, opacity, dashed) {
      const g = new THREE.BufferGeometry().setFromPoints(points);
      const m = dashed ? new THREE.LineDashedMaterial({ color, transparent: true, opacity, dashSize: 0.25, gapSize: 0.18 }) : new THREE.LineBasicMaterial({ color, transparent: true, opacity });
      const l = new THREE.Line(g, m); if (dashed) l.computeLineDistances(); return l;
    }
    const circle = (r, seg) => { const p = []; for (let i = 0; i <= seg; i++) { const a = i / seg * Math.PI * 2; p.push(new THREE.Vector3(Math.cos(a) * r, 0, Math.sin(a) * r)); } return p; };
    const labels = [];
    function label(text, getPos, cls, show, extra) { const el = document.createElement("div"); el.className = "o7l " + (cls || ""); el.textContent = text; labelsEl.appendChild(el); const L = Object.assign({ el, getPos, show: show || (() => true) }, extra || {}); labels.push(L); return L; }

    const st = { mode: opt.mode || "improve", selected: opt.select === undefined ? "L2" : opt.select, showAll: false, spread: 0, spreadT: 0, cube: false };
    const stageObjs = [], idx = s => STAGES.length - 1 - STAGES.indexOf(s), stageY = s => s.y + st.spread * (idx(s) - 3) * 0.9;
    STAGES.forEach(s => {
      const g = new THREE.Group(); scene.add(g);
      const col = s.store ? P.store : P.line;
      const disc = new THREE.Mesh(new THREE.CircleGeometry(s.r, 64).rotateX(-Math.PI / 2), new THREE.MeshBasicMaterial({ color: s.store ? P.store : P.accent, transparent: true, opacity: P.discOpacity, side: THREE.DoubleSide, depthWrite: false }));
      disc.userData.stage = s.id; g.add(disc);
      g.add(line(circle(s.r, 96), col, P.lineOpacity)); g.add(line(circle(s.r * 0.35, 48), col, 0.2));
      const mods = [];
      s.mods.forEach((m, i) => {
        const a = i / s.mods.length * Math.PI * 2 + s.y * 0.3, hz = m[2] === "hazard", x = Math.cos(a) * s.r, z = Math.sin(a) * s.r;
        const geo = s.engine ? new THREE.BoxGeometry(0.28, 0.28, 0.28) : s.spec ? new THREE.SphereGeometry(0.17, 12, 8) : new THREE.OctahedronGeometry(hz ? 0.26 : 0.17);
        const base = hz ? P.hazard : s.store ? P.store : s.spec ? P.spec : P.accent;
        const node = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: base }));
        node.position.set(x, 0, z); node.userData.stage = s.id; g.add(node);
        g.add(line([new THREE.Vector3(0, 0, 0), new THREE.Vector3(x, 0, z)], col, hz ? 0.55 : 0.14));
        let Lr = null;
        Lr = label(m[0], () => new THREE.Vector3(x, g.position.y + 0.1, z), hz ? "hazard" : s.store ? "store" : s.spec ? "spec" : "", () => st.showAll || st.selected === s.id || (hz && !st.quiet) || (Lr && Lr.lit));
        mods.push({ x, z, hz, name: m[0], node, base, L: Lr });
      });
      const LL = label(s.name, () => new THREE.Vector3(s.r + 0.4, g.position.y, 0), "layer", null, { stage: s.id });
      LL.el.style.pointerEvents = "auto"; LL.el.style.cursor = "pointer";
      LL.el.addEventListener("click", () => { api.select(s.id); opt.onSelect && opt.onSelect(s.id); });
      stageObjs.push({ s, g, disc, mods });
    });
    const V = stageObjs.find(o => o.s.store);
    { const n = 6000, pos = new Float32Array(n * 3);
      for (let i = 0; i < n; i++) { const r = Math.sqrt(Math.random()) * 5.1, a = Math.random() * Math.PI * 2; pos[i * 3] = Math.cos(a) * r; pos[i * 3 + 1] = (Math.random() - 0.5) * 0.5 * (1 - r / 6); pos[i * 3 + 2] = Math.sin(a) * r; }
      const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
      V.cloud = new THREE.Points(geo, new THREE.PointsMaterial({ color: P.store, size: 0.035, transparent: true, opacity: 0.6 })); V.g.add(V.cloud); }
    label("20,529,863 cited sentences", () => new THREE.Vector3(0, V.g.position.y - 0.7, 5.4), "store");
    const spine = line([new THREE.Vector3(0, -3.8, 0), new THREE.Vector3(0, 8.6, 0)], P.accent, 0.7); scene.add(spine);

    const LC = { y: 2.1, rx: 8.2, ry: 7.4 };
    const loopPt = (deg, out) => { const a = deg * Math.PI / 180; return (out || new THREE.Vector3()).set(Math.cos(a) * LC.rx, LC.y + Math.sin(a) * LC.ry, 0); };
    const loopPts = []; for (let i = 0; i <= 160; i++) loopPts.push(loopPt(i / 160 * 360));
    const loopLine = line(loopPts, P.accent, 0.5, true); scene.add(loopLine);
    const gateMesh = new THREE.Mesh(new THREE.TorusGeometry(0.75, 0.05, 8, 48), new THREE.MeshBasicMaterial({ color: P.accent })); gateMesh.position.copy(loopPt(90)); scene.add(gateMesh);
    LOOP.forEach(L => { const p = loopPt(L[2]);
      if (L[3] !== "gate") { const b = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.34, 0.34), new THREE.MeshBasicMaterial({ color: P.accent })); b.position.copy(p); scene.add(b); }
      label(L[0], () => p.clone().add(new THREE.Vector3(0, L[3] === "gate" ? 1.0 : 0.25, 0)), L[3] === "gate" ? "gate" : "loop", () => st.mode === "improve" || st.showAll || L[3] === "gate"); });
    label("Self-improvement loop", () => loopPt(160).add(new THREE.Vector3(-0.4, 0, 0)), "big loop", () => st.mode === "improve");
    const lp = new THREE.Mesh(new THREE.SphereGeometry(0.18, 16, 12), new THREE.MeshBasicMaterial({ color: P.accent })); scene.add(lp);
    const rejects = [];

    const famObjs = FAMILY.map((f, i) => {
      const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.42, 1), new THREE.MeshBasicMaterial({ color: P.accent, wireframe: true }));
      m.add(new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 8), new THREE.MeshBasicMaterial({ color: P.ink }))); scene.add(m);
      const tether = line([new THREE.Vector3(), new THREE.Vector3(0, 1, 0)], P.accent, 0.25, true); scene.add(tether);
      label(f[0], () => m.position.clone().add(new THREE.Vector3(0, 0.55, 0)), "fam", () => st.mode === "family" || st.showAll);
      label(f[1], () => m.position.clone().add(new THREE.Vector3(0, -0.25, 0)), "famsub", () => st.mode === "family");
      return { m, tether, a0: i / FAMILY.length * Math.PI * 2 };
    });
    const famOrbit = line(circle(11.5, 128), P.muted, 0.25, true); famOrbit.position.y = 3.2; scene.add(famOrbit);

    const SEPH = { keter: [0, 6.6], chokmah: [1.7, 5.6], binah: [-1.7, 5.6], chesed: [1.7, 4.4], geburah: [-1.7, 4.4], tiferet: [0, 3.3], netzach: [1.7, 2.2], hod: [-1.7, 2.2], yesod: [0, 1.2], malkuth: [0, 0] };
    const PATHS = [["keter","chokmah"],["keter","binah"],["keter","tiferet"],["chokmah","binah"],["chokmah","tiferet"],["chokmah","chesed"],["binah","tiferet"],["binah","geburah"],["chesed","geburah"],["chesed","tiferet"],["chesed","netzach"],["geburah","tiferet"],["geburah","hod"],["tiferet","netzach"],["tiferet","yesod"],["tiferet","hod"],["netzach","hod"],["netzach","yesod"],["netzach","malkuth"],["hod","yesod"],["hod","malkuth"],["yesod","malkuth"]];
    const MID = new Set(["keter", "tiferet", "yesod", "malkuth"]);
    const cube = new THREE.Group(); cube.visible = false; scene.add(cube);
    [0, 60, 120].forEach((deg, ti) => {
      const a = deg * Math.PI / 180, tp = s => { const [x0, y] = SEPH[s]; const x = MID.has(s) ? 0 : x0 * 0.6; return new THREE.Vector3(x * Math.cos(a), y * (8 / 6.6), -x * Math.sin(a)); };
      const pts = []; PATHS.forEach(([p, q]) => { if (!(MID.has(p) && MID.has(q)) || ti === 0) pts.push(tp(p), tp(q)); });
      cube.add(new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({ color: P.fresh, transparent: true, opacity: 0.85 })));
    });
    label("Reasoning cube (proposed)", () => new THREE.Vector3(0, 10.1, 0), "new big", () => st.cube);

    const rq = new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 12), new THREE.MeshBasicMaterial({ color: P.ink })); scene.add(rq);
    let rqT = 0, rqStop = false;
    const Y = id => stageObjs.find(o => o.s.id === id).g.position.y;
    const modPt = (sid, name) => { const o = stageObjs.find(o => o.s.id === sid); const d = o.mods.find(m => m.name === name) || o.mods[0]; return new THREE.Vector3(d.x, o.g.position.y, d.z); };
    function rqPath() {
      const up = id => new THREE.Vector3(0, Y(id), 0);
      if (st.cube) return [up("L0"), up("L1"), up("L2"), new THREE.Vector3(0, Y("E"), 0), new THREE.Vector3(2.5, Y("V"), 1.5), up("L3"), up("L4"), up("L0")];
      // a typical answer: parse, read the conversation, route to a specialist, read its record, check, speak. Some are declined at the guard.
      return rqStop ? [up("L0"), up("L1"), up("L3"), modPt("L2", "Specialist call"), modPt("S", "Relation readers"), modPt("V", "Relation store"), modPt("L4", "Precision guard"), modPt("L4", "Precision guard"), up("L0")]
                    : [up("L0"), up("L1"), up("L3"), modPt("L2", "Specialist call"), modPt("S", "Relation readers"), modPt("V", "Relation store"), modPt("L4", "Receipts"), up("L4"), up("L0")];
    }

    const traceLine = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: P.lit, transparent: true, opacity: 0.95 }));
    traceLine.visible = false; scene.add(traceLine);
    function clearTrace() {
      stageObjs.forEach(o => o.mods.forEach(m => { if (m.L.lit) { m.L.lit = false; m.L.el.classList.remove("lit"); m.node.material.color.setHex(m.base); m.node.scale.setScalar(1); } }));
      st.trace = null;
    }
    const api = {
      data: { STAGES, LOOP, FAMILY },
      trace(steps, declined) {
        clearTrace();
        const refs = [];
        (steps || []).forEach(([sid, name]) => { const o = stageObjs.find(x => x.s.id === sid); if (!o) return;
          const m = name ? o.mods.find(x => x.name === name) || null : null;
          const last = refs[refs.length - 1]; if (last && last.o === o && last.m === m) return; refs.push({ o, m }); });
        if (!refs.length) return [];
        st.trace = refs; st.traceT = 0; st.traceDeclined = !!declined; st.mode = "trace"; controls.autoRotate = false;
        return refs.filter(r => r.m).map(r => [r.o.s.name, r.m.name]);
      },
      clearTrace() { clearTrace(); if (st.mode === "trace") st.mode = opt.mode || "improve"; },
      setQuiet(on) { st.quiet = !!on; },
      setMode(m) { st.mode = m; },
      get mode() { return st.mode; },
      select(id) { st.selected = id; stageObjs.forEach(o => { o.disc.material.opacity = o.s.id === id ? P.discOpacity * 3.5 : P.discOpacity; }); labels.forEach(L => { if (L.stage) L.el.classList.toggle("sel", L.stage === id); }); },
      setSpread(on) { st.spreadT = on ? 1 : 0; if (reduce) st.spread = st.spreadT; },
      setCube(on) { st.cube = !!on; cube.visible = st.cube; },
      setShowAll(on) { st.showAll = !!on; },
      canvas,
      // a specialist you admitted joins the Specialists ring, outside the release's own groups
      addModule(sid, name) {
        const o = stageObjs.find(x => x.s.id === sid); if (!o || o.mods.some(m => m.name === name)) return;
        const k = o.mods.filter(m => m.mine).length, a = -Math.PI / 2 + k * 0.42, R = o.s.r + 1.3, x = Math.cos(a) * R, z = Math.sin(a) * R;
        const node = new THREE.Mesh(new THREE.OctahedronGeometry(0.24), new THREE.MeshBasicMaterial({ color: P.fresh }));
        node.position.set(x, 0, z); node.userData.stage = sid; o.g.add(node);
        o.g.add(line([new THREE.Vector3(Math.cos(a) * o.s.r, 0, Math.sin(a) * o.s.r), new THREE.Vector3(x, 0, z)], P.fresh, 0.7, true));
        const m = { x, z, hz: false, name, node, base: P.fresh, mine: true }; let Lr = null;
        Lr = label(name, () => new THREE.Vector3(x, o.g.position.y + 0.1, z), "mine", () => true); m.L = Lr; o.mods.push(m);
      },
      // what is on screen, for drawing a film frame: every visible label with its pixel position and class
      labelsOnScreen() { return labels.filter(L => L.el.style.display !== "none").map(L => ({ text: L.el.textContent, x: parseFloat(L.el.style.left) || 0, y: parseFloat(L.el.style.top) || 0, cls: L.el.className })); },
      setView(v) { const VIEWS = { iso: [20, 11, 24], side: [0.01, 2.6, 34], top: [0.01, 44, 0.02] }; camera.position.set(...VIEWS[v]); controls.target.set(0, 1.6, 0); controls.update(); },
    };
    const ray = new THREE.Raycaster(), mouse = new THREE.Vector2(); let downAt = null;
    canvas.addEventListener("pointerdown", e => { downAt = [e.clientX, e.clientY]; });
    canvas.addEventListener("pointerup", e => {
      if (!downAt || Math.hypot(e.clientX - downAt[0], e.clientY - downAt[1]) > 5) return;
      const r = canvas.getBoundingClientRect(); mouse.set((e.clientX - r.left) / r.width * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
      ray.setFromCamera(mouse, camera); const hit = ray.intersectObjects(stageObjs.map(o => o.g), true).find(h => h.object.userData.stage);
      if (hit) { api.select(hit.object.userData.stage); opt.onSelect && opt.onSelect(hit.object.userData.stage); }
    });
    function resize() { const w = host.clientWidth, h = host.clientHeight; if (!w || !h) return; renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); }
    new ResizeObserver(resize).observe(host);

    const v = new THREE.Vector3(); let last = performance.now(), loopDeg = 270, cycle = 0;
    function frame(now) {
      const dt = Math.max(0, Math.min(0.05, (now - last) / 1000)), s = now / 1000; last = now;
      st.spread += (st.spreadT - st.spread) * Math.min(1, dt * 4);
      stageObjs.forEach(o => { o.g.position.y = stageY(o.s); });
      if (!reduce) V.cloud.rotation.y += dt * 0.05;
      spine.geometry.setFromPoints([new THREE.Vector3(0, Y("V"), 0), new THREE.Vector3(0, Y("L5") + 0.6, 0)]);
      if (!reduce) loopDeg -= dt * 40; if (loopDeg < -90) loopDeg += 360;
      const d = ((loopDeg % 360) + 360) % 360; loopPt(d, lp.position);
      lp.visible = st.mode === "improve"; loopLine.material.opacity = st.mode === "improve" ? 0.7 : 0.25;
      gateMesh.rotation.y = s * 0.8; gateMesh.scale.setScalar(1 + 0.08 * Math.sin(s * 3));
      if (st.mode === "improve" && Math.abs(d - 90) < dt * 40 + 0.01) {
        cycle++;
        if (cycle % 3 === 0) { lp.material.color.setHex(P.bad); const rj = new THREE.Mesh(new THREE.SphereGeometry(0.16, 10, 8), new THREE.MeshBasicMaterial({ color: P.bad, transparent: true })); rj.position.copy(lp.position); rj.userData.v = new THREE.Vector3(1.8, 2.4, 1.2); scene.add(rj); rejects.push(rj); opt.onGate && opt.onGate(false); }
        else { lp.material.color.setHex(P.accent); opt.onGate && opt.onGate(true); }
      }
      if (d < 60 || d > 280) lp.material.color.setHex(P.accent);
      for (let i = rejects.length - 1; i >= 0; i--) { const r = rejects[i]; r.position.addScaledVector(r.userData.v, dt); r.userData.v.y -= dt * 3; r.material.opacity -= dt * 0.6; if (r.material.opacity <= 0) { scene.remove(r); rejects.splice(i, 1); } }
      famObjs.forEach((f, i) => { const a = f.a0 + (reduce ? 0 : s * 0.08); f.m.position.set(Math.cos(a) * 11.5, 3.2 + Math.sin(s * 0.7 + i) * 0.3, Math.sin(a) * 11.5); f.m.rotation.y = s * 0.5;
        f.tether.geometry.setFromPoints([f.m.position, new THREE.Vector3(0, Y("L2"), 0)]); f.tether.computeLineDistances(); f.tether.material.opacity = st.mode === "family" ? 0.6 : 0.12; });
      famOrbit.material.opacity = st.mode === "family" ? 0.45 : 0.15;
      rq.visible = st.mode === "answer" || st.mode === "trace";
      traceLine.visible = st.mode === "trace";
      if (st.mode === "trace" && st.trace) {
        const pts = st.trace.map(r => r.m ? new THREE.Vector3(r.m.x, r.o.g.position.y, r.m.z) : new THREE.Vector3(0, r.o.g.position.y, 0));
        const n = pts.length - 1;
        if (n <= 0) { rq.position.copy(pts[0]); } else {
          st.traceT = reduce ? n : Math.min(n, st.traceT + dt * 1.5);
          const i = Math.min(n - 1, Math.floor(st.traceT)), k = st.traceT - i;
          rq.position.copy(pts[i]).lerp(pts[i + 1], Math.min(1, k));
          const drawn = pts.slice(0, i + 1).concat([rq.position.clone()]);
          traceLine.geometry.setFromPoints(drawn);
          st.trace.forEach((r, j) => { if (r.m && j <= st.traceT + 0.01 && !r.m.L.lit) { r.m.L.lit = true; r.m.L.el.classList.add("lit"); r.m.node.material.color.setHex(P.lit); r.m.node.scale.setScalar(1.9); } });
        }
        rq.material.color.setHex(st.traceDeclined ? P.hazard : P.lit);
      }
      if (st.mode === "answer") {
        const path = rqPath(); if (!reduce) rqT += dt * 0.9;
        if (rqT >= path.length - 1) { rqT = 0; rqStop = Math.random() < 0.3; }
        const i = Math.max(0, Math.min(path.length - 2, Math.floor(rqT))), k = Math.min(1, Math.max(0, rqT - i)); rq.position.copy(path[i]).lerp(path[Math.min(path.length - 1, i + 1)], k);
        rq.material.color.setHex(!st.cube && rqStop && i >= 6 ? P.hazard : st.cube ? P.fresh : P.ink);
      }
      controls.update(); renderer.render(scene, camera);
      const w = host.clientWidth, h = host.clientHeight;
      labels.forEach(L => { if (!L.show()) { L.el.style.display = "none"; return; } v.copy(L.getPos()).project(camera);
        if (v.z > 1) { L.el.style.display = "none"; return; } L.el.style.display = ""; L.el.style.left = ((v.x + 1) / 2 * w) + "px"; L.el.style.top = ((1 - v.y) / 2 * h) + "px"; });
      requestAnimationFrame(frame);
    }
    resize(); api.setView(opt.view || "iso"); if (st.selected) api.select(st.selected);
    requestAnimationFrame(frame);
    return api;
  }
  O7Explorer.data = { STAGES, LOOP, FAMILY };
  window.O7Explorer = O7Explorer;
})();