"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 `
Quelle ${index + 1}
`; } 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 = `
LEARNING PREPARATION · GUARDED

Lern-Vorbereitung

Proposal, Approval und Provenienz sichtbar trennen. Kein Schritt in diesem Panel führt Lernen aus.

lade …
Governance-Grenze Approval ≠ Ausführung. Präregistrierung → Runner-Freeze → Autorisierung → Ausführung bleiben separate Schritte. Keine direkte Gewichts-, Spike-, Current- oder Reward-Manipulation.

Plan-Historie & Audit

lade …

Neues Proposal / Revision

Source-Provenienz Nur konkrete SHA-256-Digests. Beschreibungstexte oder Platzhalter werden nicht gespeichert.
${sourceRowMarkup({}, 0)}

Human Approval

Approval speichert nur Freigabe-Metadaten. Es startet weder Runner noch Lernen.

`; const anchor = workspace.querySelector(":scope > header"); if (anchor) anchor.insertAdjacentElement("afterend", panel); else workspace.prepend(panel); panel.querySelector("#learning-prep-create-form")?.addEventListener("submit", createProposal); panel.querySelector("#learning-prep-approve-form")?.addEventListener("submit", approveProposal); panel.querySelector("#learning-add-source")?.addEventListener("click", () => addSourceRow()); bindSourceRowActions(panel); return panel; } function value(id) { return document.getElementById(id)?.value?.trim() || ""; } function proposalOf(artifact) { return artifact?.proposal && typeof artifact.proposal === "object" ? artifact.proposal : artifact; } function planIdOf(artifact) { const proposal = proposalOf(artifact); return proposal?.plan_id || artifact?.plan_id || artifact?.id || "—"; } function statusOf(artifact) { if (artifact?.status) return String(artifact.status); if (artifact?.approved_by || artifact?.proposal) return "approved"; return "proposal"; } function digestOf(artifact) { return artifact?.digest || artifact?.content_digest || "—"; } function sourcesOf(artifact) { const sources = proposalOf(artifact)?.sources; return Array.isArray(sources) ? sources : []; } function isSha256Digest(digest) { return SHA256_RE.test(String(digest || "").trim()); } function provenanceAudit(artifact) { const sources = sourcesOf(artifact); const invalid = sources.filter((source) => !isSha256Digest(source?.digest)); return { total: sources.length, invalid, valid: sources.length > 0 && invalid.length === 0, }; } function shortDigest(digest) { const text = String(digest || "—"); if (text.length <= 28) return text; return `${text.slice(0, 16)}…${text.slice(-10)}`; } function artifactSortValue(artifact) { return statusOf(artifact) === "approved" ? 1 : 0; } function groupArtifacts(plans) { const groups = new Map(); plans.forEach((artifact) => { const id = planIdOf(artifact); if (!groups.has(id)) groups.set(id, []); groups.get(id).push(artifact); }); return Array.from(groups.entries()).map(([id, artifacts]) => ({ id, artifacts: artifacts.sort((a, b) => artifactSortValue(a) - artifactSortValue(b)), })); } function renderSourceAudit(artifact) { const audit = provenanceAudit(artifact); if (!audit.total) { return `
Source-Provenienz fehltKein Quellen-Digest registriert. Nachgelagerte Gates bleiben gesperrt.
`; } const rows = sourcesOf(artifact).map((source) => { const valid = isSha256Digest(source?.digest); return `
  • ${escapeHtml(source?.source_id || "Quelle")}${escapeHtml(source?.origin || "—")} · ${escapeHtml(source?.partition || "—")} ${escapeHtml(shortDigest(source?.digest))} ${valid ? "SHA-256 ✓" : "KEIN SHA-256"}
  • `; }).join(""); return `
    ${audit.valid ? "Provenienzkette vollständig" : "Provenienzkette unterbrochen"} ${audit.valid ? `${audit.total} konkrete SHA-256-Digest(s) registriert.` : `${audit.invalid.length} von ${audit.total} Source-Digest(s) sind Platzhalter oder ungültig.`}
    `; } function renderWorkflow(group) { const approved = group.artifacts.some((artifact) => statusOf(artifact) === "approved"); const representative = group.artifacts.find((artifact) => statusOf(artifact) === "approved") || group.artifacts[0]; const provenanceValid = provenanceAudit(representative).valid; const states = { proposal: group.artifacts.length > 0 ? "done" : "locked", approved: approved ? "done" : "current", provenance: provenanceValid ? "done" : "blocked", preregistered: "locked", runner_frozen: "locked", authorized: "locked", executed: "locked", evidence: "locked", }; return `
      ${WORKFLOW_STEPS.map(([key, label]) => { const state = states[key]; const stateLabel = state === "done" ? "erfüllt" : state === "blocked" ? "blockiert" : state === "current" ? "nächster Gate" : "gesperrt"; return `
    1. ${escapeHtml(label)}${stateLabel}
    2. `; }).join("")}
    `; } function renderArtifact(artifact) { const proposal = proposalOf(artifact); const status = statusOf(artifact); const objective = proposal?.objective?.description || proposal?.objective || "—"; const artifactDigest = digestOf(artifact); const proposalDigest = proposal?.digest || (status === "proposal" ? artifactDigest : "—"); const authority = artifact?.runtime_authority || proposal?.authority || "proposal_only"; return `
    ${status === "approved" ? "Approved Plan" : "Proposal"}${status === "approved" ? `freigegeben durch ${escapeHtml(artifact?.approved_by || "—")}` : "append-only Ausgangsversion"}
    ${escapeHtml(status)}

    ${escapeHtml(typeof objective === "object" ? JSON.stringify(objective) : objective)}

    Artifact Digest
    ${escapeHtml(shortDigest(artifactDigest))}
    Proposal Digest
    ${escapeHtml(shortDigest(proposalDigest))}
    Runtime authority
    ${escapeHtml(authority)}
    Executed
    ${artifact?.executed === true ? "true" : "false"}
    `; } function nextRevisionId(baseId) { const ids = new Set(latestPlans.map(planIdOf)); const root = String(baseId).replace(/-R\d+$/i, ""); let revision = 2; while (ids.has(`${root}-R${revision}`)) revision += 1; return `${root}-R${revision}`; } function renderPlanGroup(group) { const approved = group.artifacts.some((artifact) => statusOf(artifact) === "approved"); const representative = group.artifacts.find((artifact) => statusOf(artifact) === "approved") || group.artifacts[0]; const audit = provenanceAudit(representative); return `
    PLAN

    ${escapeHtml(group.id)}

    ${approved ? "approved" : "proposal"} ${audit.valid ? "provenance ok" : "provenance offen"}
    ${group.artifacts.map(renderArtifact).join("")}
    ${renderSourceAudit(representative)}
    Governance-Workflow
    ${renderWorkflow(group)}
    ${approved ? "" : ``} ${audit.valid ? "" : ``}
    `; } function renderPlans(payload) { const target = document.getElementById("learning-prep-detail"); if (!target) return; const plans = Array.isArray(payload?.plans) ? payload.plans : []; latestPlans = plans; const groups = groupArtifacts(plans); const badge = document.getElementById("learning-prep-badge"); if (badge) badge.textContent = `${plans.length} Artefakt(e) · ${groups.length} Plan/Pläne`; if (!plans.length) { target.innerHTML = "

    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); }