"use strict"; import { openFMFile } from "./file-viewer.js"; function byId(id) { return document.getElementById(id); } function escapeHtml(value) { if (value == null) return ""; const div = document.createElement("div"); div.textContent = String(value); return div.innerHTML; } async function fetchJson(url, options = {}) { const response = await fetch(url, { ...options, headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...(options.headers || {}) }, }); const data = await response.json(); if (!response.ok || data.ok === false) { throw new Error(data.error || `HTTP ${response.status}`); } return data; } const RESEARCH_HELP = Object.freeze({ seeds: "Seeds initialisieren die deterministischen Zufallsprozesse. Derselbe Seed-Satz über Kontroll- und Treatmentbedingungen ermöglicht gepaarte Vergleiche; mehrere unabhängige Seeds prüfen Robustheit.", ticks: "Ticks sind simulierte Zeitschritte. Mehr Ticks bedeuten nicht automatisch stärkere Evidenz; das registrierte Protokoll definiert das notwendige Beobachtungsfenster.", preregistration: "Eine eingefrorene Preregistrierung bindet Forschungsfrage, Hypothese, primäre Outcomes, Kontrollen, Seed-Strategie und Auswertungsgrenzen vor einem bestätigenden Lauf.", humanReview: "Human Review dokumentiert die menschliche Prüfung von Artefakten und Interpretation. REVIEWED/READY bedeutet nicht automatisch, dass eine Hypothese bestätigt ist.", eligibility: "Confirmatory Eligibility erfordert die Einhaltung des eingefrorenen Research Contracts. Unzureichende Seeds oder Vertragsabweichungen stufen den Lauf auf explorativ herunter bzw. blockieren ihn serverseitig.", proposal: "Neue Forschungsfragen werden zunächst als PROPOSED im Experimentartefakt gespeichert. Erst nach menschlicher Prüfung dürfen sie in den kanonischen Registry-Zyklus übernommen werden.", }); function parseSeedExpression(expression) { const values = []; for (const token of String(expression || "").split(",")) { const part = token.trim(); if (!part) continue; const range = part.match(/^(-?\d+)\s*-\s*(-?\d+)$/); if (range) { const start = Number(range[1]); const end = Number(range[2]); const step = start <= end ? 1 : -1; for (let value = start; value !== end + step; value += step) values.push(value); } else if (/^-?\d+$/.test(part)) { values.push(Number(part)); } } return [...new Set(values)]; } function seedSuggestion(minimum) { const count = Number(minimum || 1); if (count <= 1) return "101"; return `101-${100 + count}`; } function resolveRelativeResearchPath(currentPath, href) { if (!href || /^(?:https?:|mailto:|data:|#|\/)/i.test(href)) return href; const raw = href.replace(/\\_/g, "_").split("#")[0].split("?")[0]; const base = String(currentPath || "").split("/").slice(0, -1); const stack = []; for (const part of [...base, ...raw.split("/")]) { if (!part || part === ".") continue; if (part === "..") { if (!stack.length) return null; stack.pop(); continue; } stack.push(part); } return stack.join("/"); } function mermaidForContract(contract) { if (!contract) return ""; const rq = contract.research_question || contract.question || "RQ"; const hyp = contract.hypothesis || "Hypothesis"; const protocol = contract.id || "Protocol"; const lines = [ "```mermaid", "graph TD", ` RQ[\"${rq}\"] --> H[\"${hyp}\"]`, ` H --> P[\"${protocol}\"]`, ]; (contract.controls || []).forEach((item, idx) => lines.push(` P --> C${idx}[\"Control: ${String(item).replace(/\"/g, "'")}\"]`)); (contract.treatments || []).forEach((item, idx) => lines.push(` P --> T${idx}[\"Treatment: ${String(item).replace(/\"/g, "'")}\"]`)); lines.push(" P --> M[\"Measured outcomes\"]"); (contract.primary_outcomes || []).forEach((item, idx) => lines.push(` M --> O${idx}[\"${String(item).replace(/\"/g, "'")}\"]`)); lines.push("```"); return lines.join("\n"); } export class ExperimentWorkflowPanel { constructor({ onCompleted = null } = {}) { this.onCompleted = onCompleted; this.questions = []; this.hypotheses = []; this.protocols = []; this.nextExperimentId = ""; this.activePreset = null; this.activeContract = null; this.lastResult = null; this.currentViewerPath = ""; this.elements = { question: byId("workflow-question"), hypothesis: byId("workflow-hypothesis"), experimentId: byId("workflow-experiment-id"), title: byId("workflow-title"), conditions: byId("workflow-conditions"), conditionProfile: byId("workflow-condition-profile"), seeds: byId("workflow-seeds"), protocol: byId("workflow-protocol"), ticks: byId("workflow-ticks"), notes: byId("workflow-notes"), run: byId("workflow-run"), batchOpen: byId("workflow-batch-open"), batchDialog: byId("workflow-batch-dialog"), batchForm: byId("workflow-batch-form"), batchId: byId("workflow-batch-id"), batchTicks: byId("workflow-batch-ticks"), batchSeeds: byId("workflow-batch-seeds"), batchTitlePrefix: byId("workflow-batch-title-prefix"), batchProtocols: byId("workflow-batch-protocols"), batchStatus: byId("workflow-batch-status"), batchResult: byId("workflow-batch-result"), batchStart: byId("workflow-batch-start"), status: byId("workflow-status"), progressBar: byId("workflow-progress-bar"), progressLabel: byId("workflow-progress-label"), progressValue: byId("workflow-progress-value"), result: byId("workflow-result"), }; this._ensureResearchUX(); this._bindEvents(); this._bindBatchWorkflow(); this._installViewerEnhancements(); } _ensureResearchUX() { const form = this.elements.protocol?.closest(".workflow-form-grid") || this.elements.run?.closest("section"); if (!form) return; const labels = [...form.querySelectorAll("label")]; for (const label of labels) { const text = (label.textContent || "").toLowerCase(); if (text.includes("seed")) label.title = RESEARCH_HELP.seeds; if (text.includes("tick")) label.title = RESEARCH_HELP.ticks; if (text.includes("protokoll")) label.title = RESEARCH_HELP.preregistration; } if (!byId("workflow-research-contract")) { const contract = document.createElement("section"); contract.id = "workflow-research-contract"; contract.className = "research-contract-card"; contract.innerHTML = `
Vorgaben aus Protocol Registry und eingefrorener Preregistrierung vor dem Start.
Berichte und Rohdaten direkt unter dem aktuellen Lauf öffnen.
Status: PENDING
Neue Fragen werden zunächst als PROPOSED mit Herkunft aus diesem Experiment gespeichert.
Keine Forschungsfragen aus dem Registry-Katalog verfügbar.
"; return; } const controls = document.createElement("div"); controls.className = "workflow-batch-selection-tools"; controls.innerHTML = ''; controls.addEventListener("click", (event) => { const action = event.target.closest("[data-batch-select]")?.dataset.batchSelect; if (!action) return; root.querySelectorAll("input[type=checkbox]:not(:disabled)").forEach((input) => { input.checked = action === "all"; }); }); root.replaceChildren(controls, ...this.questions.map((question) => { const protocol = operationalByQuestion.get(question.id); const label = document.createElement("label"); label.className = "workflow-batch-protocol"; if (protocol) { label.innerHTML = `${escapeHtml(question.id)} · ${escapeHtml(question.label || "")}${escapeHtml(protocol.label || protocol.id)} · registrierte VorgabenSeedsTicks`; } else { const hypothesis = this.hypotheses.find((item) => item.question_id === question.id); const hypothesisId = hypothesis?.id || "EXPLORATORY-UNSPECIFIED"; const selection = `exploratory:${question.id}:${hypothesisId}`; label.classList.add("workflow-batch-protocol-exploratory"); label.innerHTML = `${escapeHtml(question.id)} · ${escapeHtml(question.label || "")}EXPLORATORY · Runtime-Ticks${hypothesis ? ` · ${escapeHtml(hypothesis.id)}` : " · ohne registrierte Hypothese"} · automatische DiagnosevorgabenSeedsTicks`; } return label; })); } async _runBatchWorkflow() { const selected = [...(this.elements.batchProtocols?.querySelectorAll("input:checked") || [])] .map((input) => input.value) .filter(Boolean); if (!selected.length) { if (this.elements.batchStatus) this.elements.batchStatus.textContent = "Mindestens ein Protokoll auswählen."; return; } const payload = { batch_id: this.elements.batchId?.value.trim() || undefined, ticks: Number(this.elements.batchTicks?.value || 1000), seeds: this.elements.batchSeeds?.value.trim() || "42-44", title_prefix: this.elements.batchTitlePrefix?.value.trim() || "Experiment workflow", protocols: selected, protocol_options: Object.fromEntries(selected.map((protocol) => { const input = [...(this.elements.batchProtocols?.querySelectorAll("input[type=checkbox]:checked") || [])] .find((candidate) => candidate.value === protocol); const row = input?.closest(".workflow-batch-protocol"); return [protocol, { seeds: row?.querySelector("[data-batch-seeds]")?.value.trim() || this.elements.batchSeeds?.value.trim() || "42-44", ticks: Number(row?.querySelector("[data-batch-ticks]")?.value || this.elements.batchTicks?.value || 1000), }]; })), }; if (this.elements.batchStart) this.elements.batchStart.disabled = true; if (this.elements.batchStart) this.elements.batchStart.textContent = "Läuft …"; if (this.elements.batchStatus) this.elements.batchStatus.textContent = "Workflow läuft …"; document.dispatchEvent(new CustomEvent("brain5d:experiment-progress", { detail: { active: true, progress: 0, label: "Experiment-Workflow läuft", experimentId: payload.batch_id || "Batch" }, })); try { const result = await fetchJson("/api/experiment/workflow/batch", { method: "POST", body: JSON.stringify(payload), }); if (this.elements.batchStatus) { this.elements.batchStatus.textContent = `Abgeschlossen: ${result.completed} erfolgreich, ${result.failed} fehlgeschlagen.`; } const resultText = [ `Workflow: ${result.workflow_id}`, `Bericht: ${result.report}`, `Markdown: ${result.report_markdown}`, `Erfolgreich: ${result.completed}`, `Fehlgeschlagen: ${result.failed}`, ...(result.results || []).map((item) => `${item.protocol}: ${item.status}${item.error ? ` — ${item.error}` : ""}`), ].join("\n"); if (this.elements.batchResult) this.elements.batchResult.textContent = resultText; if (this.elements.result) this.elements.result.textContent = resultText; this._setStatus("Workflow-Bericht erstellt", "completed"); document.dispatchEvent(new CustomEvent("brain5d:experiment-progress", { detail: { active: false, preserveFooter: true, progress: 100, label: "Experiment-Workflow abgeschlossen", experimentId: result.workflow_id }, })); if (typeof this.onCompleted === "function") await this.onCompleted(); const dialog = this.elements.batchDialog; if (dialog) { try { if (dialog.open) dialog.close("completed"); } catch (_) { /* keep result visible */ } dialog.removeAttribute("open"); } } catch (error) { if (this.elements.batchStatus) this.elements.batchStatus.textContent = `Workflow fehlgeschlagen: ${error.message}`; if (this.elements.batchResult) this.elements.batchResult.textContent = `Fehler: ${error.message}`; document.dispatchEvent(new CustomEvent("brain5d:experiment-progress", { detail: { active: false, preserveFooter: true, progress: 0, label: "Experiment-Workflow fehlgeschlagen", experimentId: payload.batch_id || "Batch" }, })); } finally { if (this.elements.batchStart) { this.elements.batchStart.disabled = false; this.elements.batchStart.textContent = "Auswahl starten"; } } } async refresh() { try { const catalog = await fetchJson("/api/experiment/workflow/catalog"); this.questions = catalog.questions || []; this.hypotheses = catalog.hypotheses || []; this.protocols = catalog.protocols || []; this.nextExperimentId = catalog.next_experiment_id || ""; if (this.elements.experimentId && !this.elements.experimentId.value) { this.elements.experimentId.placeholder = this.nextExperimentId || "automatisch"; } this._renderProtocolOptions(); this._renderQuestions(); this._applyProtocol(); this._setStatus("Bereit", "ready"); this._setProgress(0, "Bereit", false); } catch (error) { this._setStatus(`Nicht verfuegbar: ${error.message}`, "error"); } } _renderProtocolOptions() { const select = this.elements.protocol; if (!select) return; const current = select.value; const labels = new Map(this.protocols.map((item) => [item.id, item.label])); for (const item of this.protocols) { if (![...select.options].some((option) => option.value === item.id)) { select.add(new Option(item.label || item.id, item.id)); } else if (labels.get(item.id)) { const option = [...select.options].find((candidate) => candidate.value === item.id); if (option) option.textContent = labels.get(item.id); } } if ([...select.options].some((option) => option.value === current)) select.value = current; } _renderQuestions() { const select = this.elements.question; if (!select) return; const current = select.value; select.replaceChildren(new Option("Forschungsfrage waehlen", "")); for (const question of this.questions) { select.add(new Option(`${question.id} - ${question.label}`, question.id)); } if ([...select.options].some((option) => option.value === current)) select.value = current; this._renderHypotheses(); } _renderHypotheses() { const select = this.elements.hypothesis; if (!select) return; const current = select.value; const questionId = this.elements.question?.value; select.replaceChildren(new Option("Hypothese waehlen", "")); for (const hypothesis of this.hypotheses) { if (hypothesis.question_id === questionId) { select.add(new Option(`${hypothesis.id} - ${hypothesis.label}`, hypothesis.id)); } } if ([...select.options].some((option) => option.value === current)) select.value = current; } _protocolContract(protocolId = this.elements.protocol?.value) { return this.protocols.find((item) => item.id === protocolId && item.preregistration) || null; } _renderContract() { const content = byId("workflow-contract-content"); const badge = byId("workflow-contract-eligibility"); if (!content || !badge) return; const contract = this._protocolContract(); this.activeContract = contract; if (!contract) { const legacy = this.activePreset; content.innerHTML = legacy ? `Legacy / diagnostic protocol. ${escapeHtml(legacy.conditions || "")}
Dieser Lauf besitzt keinen operationalen eingefrorenen Research Contract und ist deshalb nicht automatisch als bestätigende Evidenz zu interpretieren.
` : "Kein registrierter Research Contract verfügbar."; badge.textContent = "EXPLORATORY / DIAGNOSTIC"; badge.className = "gate-badge warning"; return; } const seeds = parseSeedExpression(this.elements.seeds?.value); const minimum = Number(contract.minimum_independent_seeds || 1); const ticks = Number(this.elements.ticks?.value || contract.default_ticks || 0); const seedOk = seeds.length >= minimum; const tickChanged = contract.default_ticks != null && ticks !== Number(contract.default_ticks); const eligible = seedOk && !tickChanged; badge.textContent = eligible ? "CONFIRMATORY ELIGIBLE" : "EXPLORATORY ONLY"; badge.className = `gate-badge ${eligible ? "pass" : "warning"}`; const conditions = (contract.conditions || []).map((item) => `Wissenschaftliche Aussagegrenze: ${escapeHtml(contract.inference_policy || "Keine zusätzliche Inferenzregel angegeben.")}
${!seedOk ? `⚠ Seedzahl zu niedrig: ${seeds.length} statt mindestens ${minimum}.
` : ""} ${tickChanged ? `⚠ Tickzahl weicht vom registrierten Default ab. Der Lauf ist damit nicht unverändert confirmatory.
` : ""}`; } async _run() { const payload = { question_id: this.elements.question?.value || "", hypothesis_id: this.elements.hypothesis?.value || "", experiment_id: this.elements.experimentId?.value.trim() || this.nextExperimentId, title: this.elements.title?.value.trim() || "", conditions: this.elements.conditions?.value.trim() || "", seeds: this.elements.seeds?.value.trim() || "", ticks: Number(this.elements.ticks?.value), notes: this.elements.notes?.value.trim() || "", protocol: this.elements.protocol?.value || "runtime_ticks_v1", }; this._renderContract(); this.elements.run.disabled = true; this._setStatus("Ausfuehrung laeuft", "running"); this._setProgress(8, "Testlauf laeuft", true, payload.experiment_id); this.elements.result.textContent = ""; try { const result = await fetchJson("/api/experiment/workflow/run", { method: "POST", body: JSON.stringify(payload), }); this.lastResult = result; this._setStatus("Bericht eingetragen", "completed"); const lines = [ `Experiment: ${result.experiment_id}`, `Manifest: research/${result.manifest}`, `Bericht: research/${result.report}`, ]; if (result.evidence_id) lines.push(`Evidenz: ${result.evidence_id}`); if (result.data_id) lines.push(`Daten: ${result.data_id}`); if (result.workflow) lines.push(`Workflow: research/${result.workflow}`); if (result.summary) lines.push(`Zusammenfassung: research/${result.summary}`); if (result.statistics) lines.push(`Statistik: research/${result.statistics}`); const runResult = result.result; if (runResult && typeof runResult === "object") { if (runResult.runner) lines.push(`Runner: ${runResult.runner}`); if (typeof runResult.ticks_requested === "number") lines.push(`Ticks angefordert: ${runResult.ticks_requested}`); if (Array.isArray(runResult.seeds_executed)) lines.push(`Seeds ausgeführt: ${runResult.seeds_executed.join(", ")}`); if (runResult.tick_validation?.status) lines.push(`Tick-Vertrag: ${runResult.tick_validation.status}`); if (runResult.start && runResult.end) lines.push(`Tick: ${runResult.start.tick} -> ${runResult.end.tick}`); else if (typeof runResult.run_count === "number") { const duration = typeof runResult.duration_seconds === "number" ? `; Dauer: ${runResult.duration_seconds.toFixed(3)} s` : ""; lines.push(`Runs: ${runResult.run_count}${duration}`); } } if (result.ai_report) { lines.push(`KI-Bericht: ${result.ai_report.status}`); if (result.ai_report.json) lines.push(`KI JSON: research/${result.ai_report.json}`); if (result.ai_report.markdown) lines.push(`KI Markdown: research/${result.ai_report.markdown}`); if (result.ai_report.reason) lines.push(`KI Hinweis: ${result.ai_report.reason}`); if (result.ai_report.message) lines.push(`KI Fehler: ${result.ai_report.message}`); } this.elements.result.textContent = lines.join("\n"); this._setProgress(100, "Testlauf abgeschlossen", false, result.experiment_id || payload.experiment_id); byId("workflow-result-actions").hidden = false; byId("workflow-human-review").hidden = false; byId("workflow-rq-proposal").hidden = false; byId("workflow-human-review-status").textContent = "PENDING"; await this._appendSchemaToReport(); if (this.onCompleted) await this.onCompleted(); } catch (error) { this._setStatus(`Ausfuehrung abgebrochen: ${error.message}`, "error"); this._setProgress(0, "Testlauf fehlgeschlagen", false, payload.experiment_id); } finally { this.elements.run.disabled = false; } } _experimentArtifact(relativePath) { const id = this.lastResult?.experiment_id; return id ? `experiments/${id}/${relativePath}` : null; } async _readResearchFile(path) { if (!path) throw new Error("Artefaktpfad fehlt."); const response = await fetch(`/api/files/content/${encodeURIComponent(path)}?source=research`, { headers: { "Cache-Control": "no-store" } }); if (!response.ok) throw new Error(`File not found: ${path}`); const type = response.headers.get("content-type") || ""; if (!type.includes("application/json")) return { is_binary: true, path }; return response.json(); } async _saveResearchFile(path, content, backup = true) { const response = await fetch(`/api/files/save/${encodeURIComponent(path)}?source=research`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content, backup }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); return data; } async _appendSchemaToReport() { const path = this.lastResult?.report; const contract = this.activeContract; if (!path || !contract) return; try { const file = await this._readResearchFile(path); if (file.is_binary || typeof file.content !== "string") return; if (file.content.includes("## Experimental schema") || file.content.includes("## Experimentelles Schema")) return; const block = `\n\n## Experimental schema\n\nThe following diagram is deterministically derived from the registered Research Contract. It is documentation of the design, not AI-generated evidence.\n\n${mermaidForContract(contract)}\n`; await this._saveResearchFile(path, file.content.replace(/\s*$/, "") + block + "\n", true); } catch (error) { console.warn("Could not append experiment schema:", error); } } async _openArtifact(path) { if (!path) return; this.currentViewerPath = path; await openFMFile(path); this._installExperimentPopupActions(path); } _installExperimentPopupActions(path) { const viewer = byId("fm-viewer"); const header = viewer?.querySelector(".fm-file-header-actions"); if (!viewer || !header || header.querySelector("[data-experiment-popup-actions]")) return; const group = document.createElement("span"); group.dataset.experimentPopupActions = "true"; group.className = "workflow-popup-actions"; group.innerHTML = ` `; header.prepend(group); group.querySelectorAll("[data-popup-artifact]").forEach(button => { button.addEventListener("click", () => { const kind = button.dataset.popupArtifact; const target = kind === "raw" ? this._experimentArtifact("DATA/runs_index.json") : this.lastResult?.[kind]; if (target && target !== path) this._openArtifact(target); }); }); } async _completeHumanReview() { if (!this.lastResult?.workflow) return; const reviewer = (byId("workflow-reviewer")?.value || "").trim(); if (!reviewer) { this._setStatus("Human Review: Reviewer fehlt", "error"); return; } const decision = byId("workflow-review-decision")?.value || "ACCEPT_FOR_REVIEWED_RECORD"; const notes = (byId("workflow-review-notes")?.value || "").trim(); const checklist = {}; document.querySelectorAll("[data-review-check]").forEach((input) => { checklist[input.dataset.reviewCheck] = Boolean(input.checked); }); try { const file = await this._readResearchFile(this.lastResult.workflow); const payload = JSON.parse(file.content || "{}"); payload.human_review = { status: "READY", reviewer, decision, reviewed_at: new Date().toISOString(), notes, checklist, semantics: "Human review completed; this status does not automatically confirm the hypothesis or promote evidence.", }; await this._saveResearchFile(this.lastResult.workflow, JSON.stringify(payload, null, 2) + "\n", true); byId("workflow-human-review-status").textContent = "READY"; this._setStatus("Human Review: READY", "completed"); } catch (error) { this._setStatus(`Human Review fehlgeschlagen: ${error.message}`, "error"); } } async _saveResearchQuestionProposal() { if (!this.lastResult?.workflow) return; const text = (byId("workflow-rq-proposal-text")?.value || "").trim(); if (!text) return; try { const file = await this._readResearchFile(this.lastResult.workflow); const payload = JSON.parse(file.content || "{}"); const proposals = Array.isArray(payload.proposed_research_questions) ? payload.proposed_research_questions : []; proposals.push({ status: "PROPOSED", question: text, origin: { type: "experiment_observation", experiment_id: this.lastResult.experiment_id, report: this.lastResult.report || null, }, proposed_at: new Date().toISOString(), promotion_rule: "Requires human review before canonical registry inclusion and preregistration.", }); payload.proposed_research_questions = proposals; await this._saveResearchFile(this.lastResult.workflow, JSON.stringify(payload, null, 2) + "\n", true); byId("workflow-rq-proposal-status").textContent = "✓ Als PROPOSED im Workflow-Artefakt gespeichert."; byId("workflow-rq-proposal-text").value = ""; } catch (error) { byId("workflow-rq-proposal-status").textContent = `⚠ ${error.message}`; } } _alignProtocolWithQuestion() { const questionId = this.elements.question?.value || ""; if (!questionId || !this.elements.protocol) return; const operational = this.protocols.find(item => item.research_question === questionId && item.preregistration); if (operational) { // A newly selected RQ/protocol must load its frozen defaults. Stale values // from the previously selected experiment must not silently survive. Users // can still edit seeds/ticks explicitly after this contract has been loaded. this.elements.protocol.value = operational.id; this._applyProtocol(); this._renderContract(); return; } if (questionId === "RQ-SNN-001" && this.elements.protocol.value === "science_suite_v1") { // Long-term stability has no dedicated primary protocol yet. Use the complete // suite as an executable diagnostic rather than aborting, while preserving // the selected RQ/H and retaining MISMATCH evidence semantics server-side. this.elements.protocol.value = "science_all_v1"; this.activePreset = { title: "Long-term stability diagnostic suite", conditions: "Diagnostic bundle: PING/Recurrence, Temporal, STDP/Learning, TIME, 5D and Regulation. Not primary long-term stability evidence.", ticks: "1000", seeds: "42,43,44", profiles: { standard: "Seeds 42,43,44; complete diagnostic suite; RQ-SNN-001 remains evidence-blocked until a sustained-activity protocol exists.", }, }; if (this.elements.title) this.elements.title.value = this.activePreset.title; if (this.elements.ticks) this.elements.ticks.value = this.activePreset.ticks; if (this.elements.seeds) this.elements.seeds.value = this.activePreset.seeds; if (this.elements.conditionProfile) this.elements.conditionProfile.value = "standard"; this._applyConditionProfile(this.activePreset); this._renderContract(); } } _applyProtocol() { const protocol = this.elements.protocol?.value; const operational = this._protocolContract(protocol); if (operational) { this.activePreset = { question: operational.research_question, hypothesis: operational.hypothesis, title: operational.label || operational.id, conditions: (operational.conditions || []).map((item) => `${item.id || item}${item.role ? ` (${item.role})` : ""}`).join("; "), ticks: String(operational.default_ticks ?? this.elements.ticks?.value ?? "100"), seeds: seedSuggestion(operational.minimum_independent_seeds), profiles: { standard: (operational.conditions || []).map((item) => `${item.id || item}${item.role ? ` (${item.role})` : ""}`).join("; ") }, }; this._applyPreset(this.activePreset, false); this._renderContract(); return; } const presets = { science_all_v1: { question: "RQ-SUITE-001", hypothesis: "H-SUITE-001-A", title: "Complete science suite diagnostic", conditions: "Diagnostic bundle: PING/Recurrence, Temporal, STDP/Learning, TIME, 5D and Regulation with shared provenance. This suite is not a substitute for each confirmatory preregistration.", ticks: "1000", seeds: "42,43,44", profiles: { standard: "Seeds 42,43,44; all registered science-suite runners; grouped conditions and common provenance.", pilot: "Seed 42; all science-suite runners as a low-cost diagnostic pilot.", }, }, science_suite_v1: { question: "RQ-SNN-002", hypothesis: "H-SNN-002-A", title: "Network impulse response", conditions: "Seeds 42,43,44; identical initial state per seed; impulse current 100.0; recurrence as controlled treatment.", ticks: "8", seeds: "42,43,44", profiles: { standard: "Seeds 42,43,44; identical initial state per seed; impulse current 100.0; recurrence as controlled treatment.", replication: "Seeds 42,43,44; two identical replicates per treatment; compare response signature and state digest.", sensitivity: "Seeds 42,43,44; vary recurrence treatment only; keep all other parameters fixed.", }, }, science_time_v1: { question: "RQ-TIME-001", hypothesis: "H-TIME-001-A", title: "Learning timescale calibration", conditions: "Seeds 42,43,44; tick ladder up to 1,000,000; measure runtime per stage.", ticks: "1000000", seeds: "42,43,44", profiles: { standard: "Seeds 42,43,44; tick ladder up to 1,000,000; measure runtime per stage.", short: "Seeds 42,43,44; tick ladder up to 10,000; fast calibration before long run.", }, }, science_5d_v1: { question: "RQ-5D-001", hypothesis: "H-5D-001-A", title: "Dimensional ablation", conditions: "Seeds 0-29; 1D, 2D, 3D, 5D and random graph; dimension/topology only varies.", ticks: "8", seeds: "0-29", profiles: { standard: "Seeds 0-29; 1D, 2D, 3D, 5D and random graph; dimension/topology only varies.", pilot: "Seeds 0-2; all dimensions/topologies as low-cost pilot.", }, }, stdp_pair_timing_v1: { question: "RQ-STDP-001", hypothesis: "H-STDP-001-A", title: "Pair-Timing STDP", conditions: "Registered protocol: isolated STDPSynapse; initial weight 0.5; delta-t -50 to +50 ms; 10 replicates per delta-t.", ticks: "11", seeds: "42", profiles: { standard: "Registered protocol; inputs defined by protocol.json." }, }, runtime_ticks_v1: { question: "", hypothesis: "", title: "Controlled runtime run", conditions: "Explicitly document seed, configuration, replicates and stopping criteria.", ticks: "100", seeds: "42", profiles: { standard: "Explicitly document seed, configuration, replicates and stopping criteria." }, }, }; const preset = presets[protocol]; if (!preset) return; this.activePreset = preset; this._applyPreset(preset, protocol === "stdp_pair_timing_v1"); this._renderContract(); } _applyPreset(preset, fixedProtocol) { const question = this.elements.question; const hypothesis = this.elements.hypothesis; if (question && preset.question) question.value = preset.question; this._renderHypotheses(); if (hypothesis && preset.hypothesis) hypothesis.value = preset.hypothesis; if (this.elements.title) this.elements.title.value = preset.title; if (this.elements.ticks && preset.ticks) this.elements.ticks.value = preset.ticks; if (this.elements.seeds && preset.seeds) this.elements.seeds.value = preset.seeds; if (this.elements.conditionProfile) this.elements.conditionProfile.value = "standard"; this._applyConditionProfile(preset); [this.elements.question, this.elements.hypothesis, this.elements.title, this.elements.conditions, this.elements.seeds, this.elements.ticks, this.elements.conditionProfile].forEach((element) => { if (element) element.disabled = Boolean(fixedProtocol); }); } _applyConditionProfile(preset = this.activePreset) { const profiles = preset?.profiles || { standard: this.elements.conditions?.value || "" }; const profile = this.elements.conditionProfile?.value || "standard"; const condition = profiles[profile] || profiles.standard || ""; if (this.elements.conditions && condition) this.elements.conditions.value = condition; } _setStatus(message, state) { if (!this.elements.status) return; this.elements.status.textContent = message; this.elements.status.dataset.state = state; } _setProgress(percent, label, active, experimentId = "") { const value = Math.max(0, Math.min(100, percent)); if (this.elements.progressBar) { this.elements.progressBar.style.width = `${value}%`; this.elements.progressBar.parentElement?.classList.toggle("is-running", active); this.elements.progressBar.parentElement?.setAttribute("aria-valuenow", String(value)); } if (this.elements.progressLabel) this.elements.progressLabel.textContent = label; if (this.elements.progressValue) this.elements.progressValue.textContent = active ? "laufend" : `${value}%`; document.dispatchEvent(new CustomEvent("brain5d:experiment-progress", { detail: { active, progress: value, label, experimentId }, })); } _installViewerEnhancements() { document.addEventListener("click", (event) => { const fileNode = event.target.closest?.("[data-path]"); if (fileNode?.dataset?.path && fileNode.closest("#fm-tree, #fm-recent-list")) this.currentViewerPath = fileNode.dataset.path; const anchor = event.target.closest?.("#fm-viewer a[href]"); if (!anchor) return; const href = anchor.getAttribute("href"); if (!href || /^(?:https?:|mailto:|data:|#|\/)/i.test(href)) return; const resolved = resolveRelativeResearchPath(this.currentViewerPath, href); if (!resolved) { event.preventDefault(); anchor.title = "Blocked unsafe relative path"; return; } event.preventDefault(); this.currentViewerPath = resolved; this._openResolvedViewerFile(resolved); }, true); const viewer = byId("fm-viewer"); if (viewer) { const observer = new MutationObserver(() => this._renderMermaidIn(viewer)); observer.observe(viewer, { childList: true, subtree: true }); } } async _openResolvedViewerFile(path) { const matching = [...document.querySelectorAll("#fm-tree [data-path]")].find((node) => node.dataset.path === path); if (matching) { matching.click(); return; } try { const file = await this._readResearchFile(path); if (file.is_binary) { window.open(`/api/files/content/${encodeURIComponent(path)}?source=research`, "_blank", "noopener"); return; } const viewer = byId("fm-viewer"); if (viewer) { viewer.innerHTML = `${escapeHtml(file.content || "")}`;
}
} catch (error) {
const viewer = byId("fm-viewer");
if (viewer) viewer.innerHTML = `