${escapeHtml(typeof objective === "object" ? JSON.stringify(objective) : objective)}
"use strict"; import { apiGet, apiPost } from "../core/api.js"; let refreshTimer = null; let latestPlans = []; const SHA256_RE = /^(?:sha256:)?[a-f0-9]{64}$/i; const WORKFLOW_STEPS = [ ["proposal", "Proposal"], ["approved", "Approval"], ["provenance", "Source-Provenienz"], ["preregistered", "Präregistrierung"], ["runner_frozen", "Runner-Freeze"], ["authorized", "Autorisierung"], ["executed", "Ausführung"], ["evidence", "DATA → Human Review → EVID"], ]; function escapeHtml(value) { const span = document.createElement("span"); span.textContent = value === null || value === undefined ? "" : String(value); return span.innerHTML; } function planIdDefault() { const stamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14); return `LP-${stamp}`; } function sourceRowMarkup(source = {}, index = 0) { const sourceId = source.source_id || (index === 0 ? "SRC-001" : `SRC-${String(index + 1).padStart(3, "0")}`); const digest = source.digest || ""; const origin = source.origin || "research_artifact"; const partition = source.partition || "train"; return `
`; } function ensurePanel() { let panel = document.getElementById("mhrn-learning-prep"); if (panel) return panel; const workspace = document.getElementById("tab-control"); if (!workspace) return null; panel = document.createElement("section"); panel.id = "mhrn-learning-prep"; panel.className = "mhrn-learning-prep card"; panel.dataset.panelInfo = "Nicht-ausführende Lernvorbereitung. Proposal und Approval bleiben getrennte, append-only Artefakte. Source-Provenienz wird auditiert; Präregistrierung, Runner-Freeze, Autorisierung und Ausführung bleiben nachgelagerte Gates."; panel.innerHTML = `Proposal, Approval und Provenienz sichtbar trennen. Kein Schritt in diesem Panel führt Lernen aus.
Approval speichert nur Freigabe-Metadaten. Es startet weder Runner noch Lernen.
${escapeHtml(shortDigest(source?.digest))}
${valid ? "SHA-256 ✓" : "KEIN SHA-256"}
${escapeHtml(typeof objective === "object" ? JSON.stringify(objective) : objective)}
Keine gespeicherten Lernvorbereitungen.
"; return; } target.innerHTML = groups.map(renderPlanGroup).join(""); target.querySelectorAll("[data-approve-plan]").forEach((button) => button.addEventListener("click", () => { const input = document.getElementById("learning-approve-plan-id"); if (input) input.value = button.dataset.approvePlan || ""; document.getElementById("learning-prep-approve-form")?.scrollIntoView({ behavior: "smooth", block: "center" }); })); target.querySelectorAll("[data-revise-plan]").forEach((button) => button.addEventListener("click", () => prepareRevision(button.dataset.revisePlan || ""))); } function bindSourceRowActions(root = document) { root.querySelectorAll?.("[data-remove-source]").forEach((button) => { if (button.dataset.bound === "1") return; button.dataset.bound = "1"; button.addEventListener("click", () => { const row = button.closest("[data-learning-source-row]"); if (!row || button.disabled) return; row.remove(); renumberSourceRows(); }); }); } function renumberSourceRows() { const rows = document.querySelectorAll("#learning-source-list [data-learning-source-row]"); rows.forEach((row, index) => { const legend = row.querySelector("legend"); if (legend) legend.textContent = `Quelle ${index + 1}`; const remove = row.querySelector("[data-remove-source]"); if (remove) remove.disabled = rows.length === 1; }); } function addSourceRow(source = {}) { const list = document.getElementById("learning-source-list"); if (!list) return; const index = list.querySelectorAll("[data-learning-source-row]").length; list.insertAdjacentHTML("beforeend", sourceRowMarkup(source, index)); bindSourceRowActions(list); renumberSourceRows(); } function resetSourceRows(sources) { const list = document.getElementById("learning-source-list"); if (!list) return; const values = Array.isArray(sources) && sources.length ? sources : [{}]; list.innerHTML = values.map((source, index) => sourceRowMarkup(source, index)).join(""); bindSourceRowActions(list); renumberSourceRows(); } function collectSources() { return Array.from(document.querySelectorAll("#learning-source-list [data-learning-source-row]")).map((row) => { const field = (name) => row.querySelector(`[data-source-field="${name}"]`)?.value?.trim() || ""; return { source_id: field("source_id"), digest: field("digest"), origin: field("origin"), partition: field("partition"), }; }); } function markDigestValidation(sources) { const rows = Array.from(document.querySelectorAll("#learning-source-list [data-learning-source-row]")); let firstInvalid = null; rows.forEach((row, index) => { const input = row.querySelector('[data-source-field="digest"]'); const valid = isSha256Digest(sources[index]?.digest); input?.classList.toggle("learning-digest-invalid", !valid); input?.setAttribute("aria-invalid", valid ? "false" : "true"); if (!valid && !firstInvalid) firstInvalid = input; }); return firstInvalid; } function prepareRevision(planId) { const artifact = latestPlans.find((item) => planIdOf(item) === planId); if (!artifact) return; const proposal = proposalOf(artifact); const objective = proposal?.objective || {}; const set = (id, fieldValue) => { const input = document.getElementById(id); if (input) input.value = fieldValue ?? ""; }; const revisionId = nextRevisionId(planId); set("learning-plan-id", revisionId); set("learning-objective-id", objective.objective_id || "OBJ-001"); set("learning-objective-description", objective.description || ""); set("learning-success-metric", objective.success_metric || ""); set("learning-evaluation-question", objective.evaluation_question || ""); set("learning-baseline-protocol", proposal?.baseline_protocol || "baseline"); set("learning-exposure-protocol", proposal?.exposure_protocol || "exposure"); set("learning-evaluation-protocol", proposal?.evaluation_protocol || "holdout"); set("learning-stopping-rule", proposal?.stopping_rule || "fixed episodes"); set("learning-controls", Array.isArray(proposal?.controls) ? proposal.controls.join(", ") : "learning_off"); resetSourceRows(proposal?.sources || []); const result = document.getElementById("learning-prep-create-result"); if (result) result.textContent = `Revision vorbereitet: ${revisionId}. Ersetze ungültige Source-Digests durch die konkreten Manifest-Hashes; der historische Eintrag bleibt unverändert.`; document.getElementById("learning-prep-create-form")?.scrollIntoView({ behavior: "smooth", block: "start" }); } async function refresh() { const panel = ensurePanel(); if (!panel || panel.hidden) return; try { renderPlans(await apiGet("/api/learning/preparation")); } catch (error) { const target = document.getElementById("learning-prep-detail"); if (target) target.textContent = `Nicht verfügbar: ${error.message}`; const badge = document.getElementById("learning-prep-badge"); if (badge) badge.textContent = "unavailable"; } } async function createProposal(event) { event.preventDefault(); const result = document.getElementById("learning-prep-create-result"); const sources = collectSources(); const firstInvalid = markDigestValidation(sources); if (!sources.length || firstInvalid) { if (result) result.textContent = "Nicht gespeichert: Jeder Source Digest muss ein konkreter SHA-256 sein (64 Hex-Zeichen, optional mit Präfix sha256:). Platzhalter und Beschreibungstexte sind nicht zulässig."; firstInvalid?.focus(); return; } const payload = { action: "create", plan_id: value("learning-plan-id"), objective: { objective_id: value("learning-objective-id"), description: value("learning-objective-description"), success_metric: value("learning-success-metric"), evaluation_question: value("learning-evaluation-question"), }, sources, baseline_protocol: value("learning-baseline-protocol"), exposure_protocol: value("learning-exposure-protocol"), evaluation_protocol: value("learning-evaluation-protocol"), stopping_rule: value("learning-stopping-rule"), controls: value("learning-controls").split(",").map((item) => item.trim()).filter(Boolean), }; if (result) result.textContent = "Proposal wird append-only gespeichert …"; try { const response = await apiPost("/api/learning/preparation", payload); if (result) result.textContent = `Gespeichert: ${response.status || "created"}. Source-Provenienz ist konkret; keine Lern-Ausführung wurde gestartet.`; const approve = document.getElementById("learning-approve-plan-id"); if (approve) approve.value = payload.plan_id; await refresh(); } catch (error) { if (result) result.textContent = `Fehler: ${error.message}`; } } async function approveProposal(event) { event.preventDefault(); const result = document.getElementById("learning-prep-approve-result"); const planId = value("learning-approve-plan-id"); const related = latestPlans.find((item) => planIdOf(item) === planId && statusOf(item) === "proposal"); const audit = related ? provenanceAudit(related) : null; const payload = { action: "approve", plan_id: planId, approved_by: value("learning-approved-by"), }; if (result) { result.textContent = audit && !audit.valid ? "Approval wird append-only gespeichert. Achtung: Die Source-Provenienz dieses historischen Proposals ist unvollständig; Präregistrierung bleibt im Frontend blockiert." : "Approval wird append-only gespeichert …"; } try { const response = await apiPost("/api/learning/preparation", payload); if (result) result.textContent = `Freigabe gespeichert: ${response.status || "approved"}. Der Plan bleibt nicht-ausführend; Präregistrierung, Freeze und Autorisierung sind weiterhin separate Gates.`; await refresh(); } catch (error) { if (result) result.textContent = `Fehler: ${error.message}`; } } export function initLearningPrep() { ensurePanel(); refresh(); if (refreshTimer) clearInterval(refreshTimer); refreshTimer = window.setInterval(refresh, 30000); }