From 784b84030541fad161c6d0c16bbb563669d3faca Mon Sep 17 00:00:00 2001 From: rafwiewiora Date: Sun, 16 Aug 2026 08:53:43 -0700 Subject: [PATCH 1/2] fix: restore tractability evidence views --- app/index.html | 12 +- app/js/app.js | 279 +++++++++++++++++++++----- app/styles.css | 57 +++--- tests/frontend-live-contract.test.mjs | 137 ++++++++++++- tests/helpers/load-functional-app.mjs | 40 +++- verify_functional_app.mjs | 61 ++++++ 6 files changed, 499 insertions(+), 87 deletions(-) diff --git a/app/index.html b/app/index.html index ae64cc2..d92e165 100644 --- a/app/index.html +++ b/app/index.html @@ -199,13 +199,13 @@

Rheumatoid arthritis

low · tophigh · bottom0–100
representative
- Stage 05

Atomistic simulation

-
- - - + Stage 05

Target tractability

+
+ + +
-
low · tophigh · bottommissing shelf
not wired
+
low · tophigh · bottommetric-specific
basis shown
diff --git a/app/js/app.js b/app/js/app.js index ab56752..f25d5c5 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -92,7 +92,9 @@ support: { label: "Atomistic support", unit: "/100", domain: [0, 100], basis: "No values available: target module is not wired." }, occupancy: { label: "Pose occupancy", unit: "%", domain: [0, 100], basis: "No values available: target module is not wired." }, convergence: { label: "Convergence", unit: "%", domain: [0, 100], basis: "No values available: target module is not wired." }, - tractability_fit: { label: "Branch tractability fit", unit: "/100 representative", domain: [0, 100], basis: "Representative branch-context fit; not an atomistic metric or native module output." } + tractability_fit: { label: "Representative branch fit", unit: "/100 representative", domain: [0, 100], basis: "Representative branch-context fit for demo comparison; not a native tractability output." }, + precedent: { label: "Retrieved precedent", unit: "native categorical", categorical: true, basis: "Native retrieved-precedent verdict and observations; no scalar score is inferred." }, + computed: { label: "Computed pocket evidence", unit: "native categorical", categorical: true, basis: "Native computed tractability observations; reported independently from retrieved precedent and never converted to a score." } } }; @@ -254,6 +256,31 @@ .replace(/\bsimulated\b/gi, "modeled"); } + // Reason codes remain available in the inspector's audit sections. Node cards + // need a concise outcome, not an internal enum that reads like scientific copy. + function publicReasonSummary(node) { + var reason = node && typeof node.reason === "string" ? node.reason.trim() : ""; + if (!reason) return null; + var code = reason.split(" · ")[0]; + var known = { + PINNED_ARTIFACT_REVALIDATED: "Recorded producer artifact revalidated; scientific result unchanged.", + MODULE_CONFIGURED_CACHED: "Cached producer artifact used for this run.", + RUNTIME_UNAVAILABLE: "Live runtime unavailable; labeled fallback shown.", + MODULE_NOT_WIRED: "No producer artifact was created for this stage.", + NOT_AMENABLE: "No tractability result; the stated mechanism is not amenable.", + MOCK_ECONOMICS_FAILURE: "Economics record unavailable; value remains missing.", + MOCK_FORECAST_FAILURE: "Recruitability record unavailable; sibling branches continued." + }; + if (known[code]) return known[code]; + if (/^[A-Z][A-Z0-9_]*$/.test(code)) { + if (node.outputOrigin === "CACHED") return "Cached producer artifact shown; see audit details."; + if (node.outputOrigin === "DEMO_FALLBACK") return "Labeled fallback artifact shown; see audit details."; + if (node.execution === "COMPLETE") return "Producer completed; see audit details."; + return "Terminal producer outcome; see audit details."; + } + return judgeFacingText(reason); + } + function announce(message) { elements.live.textContent = ""; window.requestAnimationFrame(function () { @@ -838,8 +865,14 @@ node.resultBasis = httpMode ? (hasSimulationPayload ? "BACKEND-REPORTED" : "MISSING") : (program.notAmenable ? "NO RESULT" : "NOT WIRED"); node.runtime = httpMode ? "BACKEND SNAPSHOT" : "NOT WIRED"; node.outputOrigin = httpMode ? (hasSimulationPayload ? "UNREPORTED" : "NOT_RUN") : "NOT_RUN"; - node.metrics = program.displayMetricBasis === "REPRESENTATIVE_DEMO_SCENARIO_V1" - ? { tractability_fit: program.metrics.tractability_fit } + node.metrics = httpMode + ? { + tractability_fit: program.displayMetricBasis === "REPRESENTATIVE_DEMO_SCENARIO_V1" + ? program.metrics.tractability_fit + : null, + precedent: null, + computed: null + } : { support: null, occupancy: null, convergence: null }; node.uncertainty = httpMode ? (program.tractabilityUncertainty || "No scalar atomistic metric is imputed; inspect the native tractability interpretation.") @@ -888,8 +921,93 @@ return METRICS[node.stage][metricKey]; } + function simulationMetricView(payload, key) { + if (key !== "precedent" && key !== "computed") return null; + var artifact = payload && typeof payload === "object" + ? (payload.output && typeof payload.output === "object" ? payload.output : payload) + : {}; + var verdict = typeof artifact.verdict === "string" ? artifact.verdict : null; + var verdictBasis = typeof artifact.verdict_basis === "string" ? artifact.verdict_basis : null; + var verdictLabel = verdict ? verdict.replace(/_/g, " ") : null; + + if (key === "precedent") { + var precedent = artifact.target_precedent && typeof artifact.target_precedent === "object" + ? artifact.target_precedent + : {}; + var precedentFacts = []; + if (typeof precedent.best_potency_nm === "number" && Number.isFinite(precedent.best_potency_nm)) { + precedentFacts.push("Best measured potency " + precedent.best_potency_nm + " nM"); + } + if (typeof precedent.approved_small_molecules_count === "number" && Number.isFinite(precedent.approved_small_molecules_count)) { + precedentFacts.push(precedent.approved_small_molecules_count + " approved small molecule" + (precedent.approved_small_molecules_count === 1 ? "" : "s")); + } + if (Array.isArray(precedent.clinical_stage_small_molecules)) { + precedentFacts.push(precedent.clinical_stage_small_molecules.length + " clinical-stage small molecule" + (precedent.clinical_stage_small_molecules.length === 1 ? "" : "s")); + } + var precedentBearing = verdictBasis === "retrieved_precedent"; + var supports = Boolean(verdict && /tractable/i.test(verdict) && !/(?:not|non)[_ -]?tractable|untractable/i.test(verdict)); + var opposes = Boolean(verdict && /(?:not|non)[_ -]?tractable|untractable/i.test(verdict)); + var precedentReported = precedentBearing || precedentFacts.length > 0; + return { + kind: "categorical", + display: precedentReported + ? (supports ? "Supports tractability · retrieved" : (opposes ? "Does not support · retrieved" : "Retrieved precedent · reported")) + : "Retrieved precedent · not reported", + detail: precedentFacts.length + ? precedentFacts.join(" · ") + "." + : (precedentBearing && verdictLabel ? "Native verdict: " + verdictLabel + "." : "The native artifact did not report retrieved-precedent observations."), + placement: precedentReported ? (supports ? "supported" : (opposes ? "not-supported" : "reported")) : "missing", + sourcePaths: ["verdict_basis", "verdict", "target_precedent"], + scalar: null + }; + } + + var computed = artifact.tractability && typeof artifact.tractability === "object" + ? artifact.tractability + : {}; + var rank = computed.site_pocket_rank && typeof computed.site_pocket_rank === "object" + ? computed.site_pocket_rank + : {}; + var volume = computed.pocket_volume_a3 && typeof computed.pocket_volume_a3 === "object" + ? computed.pocket_volume_a3 + : {}; + var computedFacts = []; + if (typeof rank.fpocket === "number" && Number.isFinite(rank.fpocket)) { + computedFacts.push("fPocket rank " + rank.fpocket + (typeof rank.n_pockets === "number" ? " of " + rank.n_pockets : "")); + } + if (typeof rank.prank === "number" && Number.isFinite(rank.prank)) computedFacts.push("PRANK rank " + rank.prank); + if (typeof volume.primary_d1_6_a3 === "number" && Number.isFinite(volume.primary_d1_6_a3)) { + computedFacts.push("Site volume " + volume.primary_d1_6_a3 + " ų"); + } + if (typeof computed.cryptic_pocket_risk === "string" && computed.cryptic_pocket_risk) { + computedFacts.push("Cryptic-pocket risk " + computed.cryptic_pocket_risk.replace(/_/g, " ")); + } + var computedReported = Object.keys(computed).length > 0 || computedFacts.length > 0; + var axisConflict = artifact.axis_conflict !== null && artifact.axis_conflict !== undefined && artifact.axis_conflict !== false; + var computedCarriesVerdict = verdictBasis === "computed_tractability"; + return { + kind: "categorical", + display: computedReported + ? (axisConflict ? "Computed evidence · axis conflict" : (computedCarriesVerdict ? "Computed evidence · informs verdict" : "Computed evidence · context only")) + : "Computed evidence · not reported", + detail: computedFacts.length + ? computedFacts.join(" · ") + ". " + (computedCarriesVerdict ? "This axis informs the native verdict." : "This axis does not carry the native verdict.") + : "The native artifact did not report computed pocket observations.", + placement: computedReported ? (axisConflict ? "conflict" : (computedCarriesVerdict ? "verdict-bearing" : "reported")) : "missing", + sourcePaths: ["tractability", "axis_conflict", "verdict_basis"], + scalar: null + }; + } + + function activeSimulationView(node) { + if (!node || node.stage !== "simulation") return null; + return simulationMetricView(node.metadata && node.metadata.stationPayload, state.metrics.simulation); + } + function metricValue(node) { if (!METRICS[node.stage]) return null; + var categoricalView = activeSimulationView(node); + if (categoricalView) return categoricalView; return Object.prototype.hasOwnProperty.call(node.metrics, state.metrics[node.stage]) ? node.metrics[state.metrics[node.stage]] : null; @@ -900,6 +1018,7 @@ var definition = metricDefinition(node); if (!definition) return "Fixed root"; var value = metricValue(node); + if (value && typeof value === "object" && value.kind === "categorical") return value.display; if (value === null || typeof value !== "number" || Number.isNaN(value)) { return node.kind === "pending" ? "Pending · shelf" : "Missing · shelf"; } @@ -919,6 +1038,19 @@ if (node.kind === "scaffold" || node.kind === "pending") return bandTop + GRAPH_GEOMETRY.shelfOffset; var definition = metricDefinition(node); var value = metricValue(node); + if (value && typeof value === "object" && value.kind === "categorical") { + if (value.placement === "missing") return bandTop + GRAPH_GEOMETRY.shelfOffset; + var categoricalPlacement = { + "not-supported": 0.18, + conflict: 0.42, + reported: 0.62, + "verdict-bearing": 0.82, + supported: 0.88 + }; + var displayPosition = categoricalPlacement[value.placement]; + if (typeof displayPosition !== "number") return bandTop + GRAPH_GEOMETRY.shelfOffset; + return bandTop + GRAPH_GEOMETRY.plotOffset + displayPosition * GRAPH_GEOMETRY.plotTravel; + } if (!definition || value === null || typeof value !== "number" || Number.isNaN(value)) return bandTop + GRAPH_GEOMETRY.shelfOffset; var normalized = (value - definition.domain[0]) / (definition.domain[1] - definition.domain[0]); normalized = Math.max(0, Math.min(1, normalized)); @@ -964,6 +1096,23 @@ return preview; } + function nodeCardDetail(node) { + if (node.kind === "scaffold") { + return node.metadata.retired + ? "Unused capacity · no record created." + : "Requested capacity · awaiting an identity."; + } + if (node.kind === "pending") return "Identity bound · stage output pending."; + var reasonSummary = publicReasonSummary(node); + if (reasonSummary) return reasonSummary; + if (BOOT.mode === "http" && node.metadata.stationPayload) { + return node.stage === "simulation" + ? "Native tractability dossier · choose an evidence view." + : "Native producer artifact · interpretation and provenance attached."; + } + return node.metadata.summary || node.uncertainty || "No additional public summary supplied."; + } + function renderGraph() { if (!state.snapshot) return; var selection = state.selectedId ? lineageOf(state.selectedId) : new Set(); @@ -1008,11 +1157,7 @@ ? '
' + escapeHTML(node.execution) + '' + escapeHTML(node.resultBasis) + '' + escapeHTML(node.outputOrigin || "UNREPORTED") + '' + escapeHTML(node.runtime) + "
" : '
' + escapeHTML(node.execution) + '' + escapeHTML(node.resultBasis) + '' + escapeHTML(node.runtime) + "
"; } - var nodeDetail = node.kind === "scaffold" - ? (node.metadata.retired ? "Unused requested capacity · no scientific record created." : "Requested capacity · identity has not been created.") - : node.kind === "pending" - ? "Candidate identity exists · selected stage output is pending." - : (node.reason || node.metadata.summary || node.uncertainty || "No additional public summary supplied."); + var nodeDetail = nodeCardDetail(node); nodeElement.innerHTML = '' + escapeHTML(node.kind === "scaffold" ? "requested capacity" : node.stage) + '' + escapeHTML(node.label) + '' + escapeHTML(formatMetric(node)) + '' + escapeHTML(nodeDetail) + "" + badges; elements.graphNodes.appendChild(nodeElement); }); @@ -1213,11 +1358,18 @@ }); var qualifiers = Array.isArray(node.metadata.qualifiers) ? node.metadata.qualifiers : []; var origin = node.metadata.outputOrigin || node.outputOrigin || "UNREPORTED"; + var categoricalView = activeSimulationView(node); var liveDefinition = definition - ? definition.label + " · " + definition.unit + " · display domain " + definition.domain[0] + "–" + definition.domain[1] + "." + ? definition.label + " · " + definition.unit + (definition.domain ? " · display domain " + definition.domain[0] + "–" + definition.domain[1] : "") + ". " + definition.basis : "Backend-provided run record."; - if (node.stage === "simulation") liveDefinition = "Native tractability dossier; no scalar atomistic metric is imputed."; - if (node.metadata.displayMetricBasis === "REPRESENTATIVE_DEMO_SCENARIO_V1") { + if (node.stage === "simulation" && categoricalView) { + liveDefinition = definition.label + " · categorical native view; no scalar score is inferred. " + categoricalView.detail; + } else if (node.stage === "simulation") { + liveDefinition = state.metrics.simulation === "tractability_fit" + ? "Representative branch-context fit for demo comparison; not a native tractability output." + : "Native tractability dossier; no scalar atomistic metric is imputed."; + } + if (node.metadata.displayMetricBasis === "REPRESENTATIVE_DEMO_SCENARIO_V1" && state.metrics[node.stage] === "tractability_fit") { liveDefinition = (definition ? definition.label + " · " + definition.unit + ". " : "") + "Representative branch value for demo comparison; not a native module output. The native artifact remains attached unchanged."; } var interpretability = node.metadata.stationPayload && ( @@ -1365,7 +1517,7 @@ // Every metric key the UI tests with === null; wire omissions must become // null (missing shelf), never undefined, or dominates()/renderParetoPlot misbehave. - var WIRE_METRIC_KEYS = ["boldness", "evidence", "plausibility", "rnpv", "positive", "impact", "recruit", "duration", "screens", "risk", "support", "occupancy", "convergence", "tractability_fit"]; + var WIRE_METRIC_KEYS = ["boldness", "evidence", "plausibility", "rnpv", "positive", "impact", "recruit", "duration", "screens", "risk", "support", "occupancy", "convergence", "tractability_fit", "precedent", "computed"]; function finiteNumber(value) { return typeof value === "number" && Number.isFinite(value) ? value : null; @@ -1492,7 +1644,9 @@ support: null, occupancy: null, convergence: null, - tractability_fit: null + tractability_fit: null, + precedent: null, + computed: null }; var program = { id: branch.branch_id || "scientific-branch-" + (index + 1), @@ -1646,6 +1800,21 @@ document.getElementById("module-dialog-summary").textContent = "This scientific run exposes exact per-branch producer artifacts, refs, hashes, origins, terminal reasons, and the server Highlander result."; } + function conciseStageNote(stage, status) { + if (status === "RUNNING") return "Running"; + if (status === "FAILED") return "Terminal · gaps"; + if (status === "COMPLETE_WITH_WARNINGS") return "Complete · warnings"; + if (status === "COMPLETE") { + var origin = stage && typeof stage.output_origin === "string" ? stage.output_origin : ""; + if (origin === "DETERMINISTIC_REPLAY") return "Complete · replay"; + if (origin === "CACHED") return "Complete · cached"; + if (origin === "DEMO_FALLBACK") return "Complete · fallback"; + if (origin === "LIVE") return "Complete · live"; + return "Complete"; + } + return "Queued"; + } + function ingestSnapshot(ws) { var hadPrograms = Boolean(state.runData && state.runData.programs && state.runData.programs.length); var scientific = isScientificSnapshot(ws); @@ -1680,7 +1849,7 @@ status === "COMPLETE" ? "complete" : status === "COMPLETE_WITH_WARNINGS" ? "warning" : status === "FAILED" ? "failed" : "queued"; - state.stageNotes[index] = stage.note || status.toLowerCase(); + state.stageNotes[index] = conciseStageNote(stage, status); if (status === "RUNNING") markStagePending(mappedStageId); if (status === "COMPLETE" || status === "COMPLETE_WITH_WARNINGS" || status === "FAILED") bindStage(mappedStageId); }); @@ -1842,8 +2011,8 @@ return; } if (nonterminal > 0) { - elements.readinessState.textContent = "BLOCKED · " + nonterminal + " nonterminal stage" + (nonterminal === 1 ? "" : "s"); - elements.packetCounts.innerHTML = '0 complete0 partial' + programCount + ' running' + nonterminal + " nonterminal stages"; + elements.readinessState.textContent = "WAITING · " + nonterminal + " stage" + (nonterminal === 1 ? "" : "s"); + elements.packetCounts.innerHTML = '0 complete0 partial' + programCount + ' running' + nonterminal + " waiting"; elements.gapConfirm.classList.remove("visible"); elements.launchHighlander.disabled = true; elements.launchHighlander.textContent = launchName + " · blocked"; @@ -1854,14 +2023,14 @@ : 0; var blockedCount = state.scientificSnapshot ? programCount - completeCount : 0; elements.readinessState.textContent = state.scientificSnapshot - ? "SERVER HIGHLANDER READY · terminal producer packets" - : "READY WITH TERMINAL GAPS · no nonterminal records"; - elements.packetCounts.innerHTML = '' + completeCount + ' complete' + (state.scientificSnapshot ? 0 : programCount) + ' partial' + blockedCount + ' blocked0 nonterminal'; + ? "READY · terminal packets" + : "READY · terminal gaps"; + elements.packetCounts.innerHTML = '' + completeCount + ' complete' + (state.scientificSnapshot ? 0 : programCount) + ' partial' + blockedCount + ' blocked0 waiting'; elements.gapConfirm.classList.add("visible"); elements.launchHighlander.disabled = !elements.gapConfirmInput.checked; elements.launchHighlander.textContent = elements.gapConfirmInput.checked ? launchName + " →" - : (state.scientificSnapshot ? "Acknowledge terminal packets to run" : (BOOT.mode === "http" ? "Acknowledge gaps to continue" : "Acknowledge gaps to launch")); + : (state.scientificSnapshot ? "Acknowledge packets" : (BOOT.mode === "http" ? "Acknowledge gaps" : "Acknowledge gaps to launch")); } function resetDemo() { @@ -2662,6 +2831,7 @@ document.querySelectorAll('[data-metric-stage="' + stage + '"]').forEach(function (candidate) { candidate.setAttribute("aria-pressed", candidate === button ? "true" : "false"); }); + if (stage === "simulation" && BOOT.mode === "http") updateSimulationAxis(value); renderGraph(); var xStable = state.nodes.every(function (node) { return priorX.get(node.id) === node.x; }); announce(METRICS[stage][value].label + " selected. Presentation changed; stored records and x lanes " + (xStable ? "remain unchanged." : "changed unexpectedly.")); @@ -2733,6 +2903,47 @@ }); } + function updateSimulationAxis(metricKey) { + var rail = document.querySelector('.rail-band[data-stage="simulation"]'); + var low = rail.querySelector(".axis-low"); + var high = rail.querySelector(".axis-high"); + var source = document.getElementById("simulation-axis-source"); + if (metricKey === "precedent") { + low.textContent = "not established · top"; + high.textContent = "supported · bottom"; + source.innerHTML = "native verdict
categorical"; + return; + } + if (metricKey === "computed") { + low.textContent = "not reported · top"; + high.textContent = "reported · bottom"; + source.innerHTML = "native pocket evidence
categorical"; + return; + } + low.textContent = "low · top"; + high.textContent = "high · bottom"; + source.innerHTML = "0–100
representative"; + } + + function configureHttpTractabilityControls(defaultMetric) { + var controls = [ + { key: "tractability_fit", label: "Representative branch fit" }, + { key: "precedent", label: "Retrieved precedent" }, + { key: "computed", label: "Computed pocket evidence" } + ]; + state.metrics.simulation = defaultMetric; + document.querySelectorAll('[data-metric-stage="simulation"]').forEach(function (button, index) { + var control = controls[index]; + if (!control) return; + button.dataset.metricValue = control.key; + button.textContent = control.label; + button.hidden = false; + button.title = METRICS.simulation[control.key].basis; + button.setAttribute("aria-pressed", control.key === defaultMetric ? "true" : "false"); + }); + updateSimulationAxis(defaultMetric); + } + function applyBootMode() { if (BOOT.mode !== "http") return; @@ -2741,19 +2952,7 @@ setupModeChip.classList.remove("mock"); elements.runButton.textContent = "Run local exploration →"; document.querySelector('.rail-band[data-stage="simulation"] h2').textContent = "Target tractability"; - state.metrics.simulation = "tractability_fit"; - document.querySelectorAll('[data-metric-stage="simulation"]').forEach(function (button) { - if (button.dataset.metricValue === "support") { - button.dataset.metricValue = "tractability_fit"; - button.textContent = "Branch tractability fit"; - button.hidden = false; - button.setAttribute("aria-pressed", "true"); - } else { - button.hidden = true; - button.setAttribute("aria-pressed", "false"); - } - }); - document.getElementById("simulation-axis-source").innerHTML = "0–100
representative"; + configureHttpTractabilityControls("tractability_fit"); document.getElementById("gap-confirm-copy").textContent = "I acknowledge terminal packet gaps, cached outputs, and labeled fallbacks. Continue to the advisory client-side comparison."; document.getElementById("restart-demo").textContent = "Refresh snapshot now"; elements.freshnessButton.style.display = "none"; // freshness is real in http mode @@ -2779,19 +2978,7 @@ elements.runButton.textContent = "Run scientific branch pipeline →"; elements.maxHypotheses.value = "1"; elements.maxHypotheses.disabled = true; - state.metrics.simulation = "support"; - document.querySelectorAll('[data-metric-stage="simulation"]').forEach(function (button) { - if (button.dataset.metricValue === "tractability_fit") { - button.dataset.metricValue = "support"; - button.textContent = "Native dossier (categorical)"; - button.hidden = false; - button.setAttribute("aria-pressed", "true"); - } else { - button.hidden = true; - button.setAttribute("aria-pressed", "false"); - } - }); - document.getElementById("simulation-axis-source").innerHTML = "categorical
native artifact"; + configureHttpTractabilityControls("precedent"); document.getElementById("highlander-mode-description").textContent = "Server-native producer packet comparison"; document.getElementById("highlander-mode-chip").textContent = "SERVER HIGHLANDER"; document.getElementById("highlander-server-chip").textContent = "AWAITING PACKETS"; diff --git a/app/styles.css b/app/styles.css index 37a1cf4..e5a6e91 100644 --- a/app/styles.css +++ b/app/styles.css @@ -541,7 +541,7 @@ /* Screen 2 */ .graph-screen { - --graph-header: 126px; + --graph-header: 160px; --graph-footer: 84px; background: #ebe6d8; overflow: hidden; @@ -558,10 +558,11 @@ border-bottom: 1px solid var(--line); } - .run-identity h1 { margin: 0 0 5px; font-size: 25px; line-height: 1.1; letter-spacing: -.035em; } - .run-identity p { margin: 0; color: var(--muted); font-size: 10px; } + .run-identity { min-width: 0; } + .run-identity h1 { margin: 0 0 5px; font-size: 25px; line-height: 1.1; letter-spacing: -.035em; overflow-wrap: anywhere; } + .run-identity p { margin: 0; color: var(--muted); font-size: 10px; overflow-wrap: anywhere; } - .progress-strip { display: grid; grid-template-columns: repeat(5, minmax(88px, 1fr)); gap: 5px; } + .progress-strip { min-width: 0; display: grid; grid-template-columns: repeat(5, minmax(88px, 1fr)); gap: 5px; } .progress-step { min-width: 0; @@ -572,8 +573,8 @@ } .progress-step .stage-num { color: #7a827c; font-size: 9px; font-weight: 900; } - .progress-step strong { display: block; overflow: hidden; text-overflow: ellipsis; font-size: 10px; white-space: nowrap; } - .progress-step .stage-state { color: #646c66; font-size: 9px; font-weight: 800; } + .progress-step strong { display: block; font-size: 10px; overflow-wrap: anywhere; } + .progress-step .stage-state { display: block; margin-top: 2px; color: #646c66; font-size: 9px; font-weight: 800; line-height: 1.3; overflow-wrap: anywhere; } .progress-step[data-state="running"] { background: var(--blue-soft); border-color: #7cb0bb; } .progress-step[data-state="complete"] { background: var(--green-soft); border-color: #88caa1; } .progress-step[data-state="warning"] { background: var(--amber-soft); border-color: #d8aa58; } @@ -824,15 +825,17 @@ border-top: 1px solid #3a5046; } - .readiness-title { display: flex; align-items: baseline; gap: 9px; } + .graph-footer > * { min-width: 0; } + .readiness-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 9px; } .readiness-title strong { font-size: 13px; } .readiness-title span { color: #9eb0a7; font-size: 9px; } - .packet-counts { display: flex; gap: 5px; margin-top: 6px; } + .packet-counts { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; } .packet-count { padding: 3px 6px; color: #dce6e1; background: #273b32; border-radius: 5px; font-size: 8px; font-weight: 850; } .gap-confirm { display: none; color: #ffd998; font-size: 9px; } .gap-confirm.visible { display: flex; align-items: start; gap: 7px; } .gap-confirm input { margin-top: 2px; } + .gap-confirm span { overflow-wrap: anywhere; } .launch-button { min-width: 192px; min-height: 48px; } @@ -862,7 +865,7 @@ .inspector-head { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 10px; padding: 13px 14px; @@ -870,14 +873,15 @@ background: var(--dark); } - .inspector-title { min-width: 0; } - .inspector-title h2 { margin: 0 0 2px; overflow: hidden; font-size: 16px; text-overflow: ellipsis; white-space: nowrap; } - .inspector-title p { margin: 0; color: #a3b2ab; font-size: 8px; } - .inspector-head-actions { display: flex; gap: 5px; } + .inspector-title { min-width: 0; flex: 1 1 auto; } + .inspector-title h2 { margin: 0 0 2px; font-size: 16px; line-height: 1.2; overflow-wrap: anywhere; } + .inspector-title h2:focus { outline: 0; padding-left: 8px; box-shadow: inset 3px 0 0 var(--green); } + .inspector-title p { margin: 0; color: #a3b2ab; font-size: 8px; overflow-wrap: anywhere; } + .inspector-head-actions { display: flex; flex: 0 0 auto; gap: 5px; } .icon-button { width: 29px; height: 29px; padding: 0; color: #edf2ef; background: #2e4239; border: 1px solid #53665e; border-radius: 7px; font-weight: 950; } .collapsed-identity { display: none; color: #cbd7d1; } - .inspector-body { overflow: auto; padding: 14px; } + .inspector-body { min-width: 0; overflow-x: hidden; overflow-y: auto; padding: 14px; } .state-warning { margin-bottom: 10px; @@ -890,19 +894,19 @@ font-weight: 750; } - .inspector-status-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; margin-bottom: 12px; } + .inspector-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin-bottom: 12px; } .status-card { min-width: 0; padding: 7px; background: #efede4; border-radius: 7px; } .status-card span { display: block; color: #777e78; font-size: 7px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; } - .status-card strong { display: block; overflow: hidden; font-size: 9px; text-overflow: ellipsis; } + .status-card strong { display: block; font-size: 9px; overflow-wrap: anywhere; } .primary-result { margin-bottom: 12px; padding: 12px; background: var(--green-soft); border: 1px solid #9bcfb0; border-radius: 9px; } .primary-result span { display: block; color: var(--green-dark); font-size: 8px; font-weight: 950; text-transform: uppercase; } - .primary-result strong { display: block; margin-top: 3px; font-size: 20px; } - .primary-result p { margin: 4px 0 0; font-size: 8px; } + .primary-result strong { display: block; margin-top: 3px; font-size: 20px; line-height: 1.15; overflow-wrap: anywhere; } + .primary-result p { margin: 4px 0 0; font-size: 8px; overflow-wrap: anywhere; } .inspector-section { margin: 10px 0; border-top: 1px solid #ddd8ca; } - .inspector-section summary { padding: 10px 0 6px; cursor: pointer; font-size: 10px; font-weight: 900; } - .inspector-section p, .inspector-section li { color: #4f5953; font-size: 9px; } + .inspector-section summary { padding: 10px 0 6px; cursor: pointer; font-size: 10px; font-weight: 900; overflow-wrap: anywhere; } + .inspector-section p, .inspector-section li { color: #4f5953; font-size: 9px; overflow-wrap: anywhere; } .inspector-section ul { margin: 4px 0 8px; padding-left: 17px; } .native-artifact { max-height: 340px; @@ -917,9 +921,10 @@ white-space: pre-wrap; word-break: break-word; } - .interpretability-view { margin: 10px 0; padding: 10px; background: #eef4e9; border: 1px solid #b7c7ad; border-radius: 8px; } - .interpretability-view > section h4 { margin: 0 0 4px; font-size: 13px; } - .interpretability-view > section p { margin: 4px 0; } + .interpretability-view { min-width: 0; margin: 10px 0; padding: 10px; background: #eef4e9; border: 1px solid #b7c7ad; border-radius: 8px; } + .interpretability-view > section { min-width: 0; } + .interpretability-view > section h4 { margin: 0 0 4px; font-size: 13px; overflow-wrap: anywhere; } + .interpretability-view > section p { margin: 4px 0; overflow-wrap: anywhere; } .evidence-artifact { padding: 9px; background: #f0eee5; border-radius: 8px; } .evidence-bars { display: grid; gap: 5px; margin: 8px 0; } @@ -928,9 +933,11 @@ .evidence-bar span { display: block; height: 100%; background: var(--green-dark); } @media (min-width: 1800px) { + .graph-screen.inspector-open { --inspector-reserved: calc(var(--inspector) + 34px); } + .graph-screen.inspector-open:has(.inspector.collapsed) { --inspector-reserved: calc(var(--inspector-collapsed) + 34px); } .graph-screen.inspector-open .run-header, - .graph-screen.inspector-open .graph-scroller, - .graph-screen.inspector-open .graph-footer { padding-right: calc(var(--inspector) + 34px); } + .graph-screen.inspector-open .graph-scroller { margin-right: var(--inspector-reserved); } + .graph-screen.inspector-open .graph-footer { right: var(--inspector-reserved); } .inspector { top: 10px; right: 10px; bottom: 10px; border-radius: 10px; } } diff --git a/tests/frontend-live-contract.test.mjs b/tests/frontend-live-contract.test.mjs index 3a1a9c0..15ad6cb 100644 --- a/tests/frontend-live-contract.test.mjs +++ b/tests/frontend-live-contract.test.mjs @@ -41,19 +41,100 @@ function ingestScientificSnapshot(snapshot = scientificSnapshot) { test("stage metrics are exposed as visible button groups instead of selects", () => { assert.doesNotMatch(appHtml, /]+data-metric-stage=/); - for (const [stage, expectedCount] of [ - ["biomarker", 3], - ["hypothesis", 3], - ["roi", 3], - ["recruitability", 4], - ["simulation", 3], - ]) { - const matches = appHtml.match(new RegExp(`data-metric-stage="${stage}"`, "g")) || []; - assert.equal(matches.length, expectedCount, `${stage} must expose every metric as a button`); + for (const [stage, expectedValues] of Object.entries({ + biomarker: ["exploration", "evidence", "pursuit"], + hypothesis: ["boldness", "evidence", "plausibility"], + roi: ["rnpv", "positive", "impact"], + recruitability: ["recruit", "duration", "screens", "risk"], + simulation: ["tractability_fit", "precedent", "computed"], + })) { + const values = Array.from( + appHtml.matchAll( + new RegExp( + `data-metric-stage="${stage}"[^>]+data-metric-value="([^"]+)"`, + "g", + ), + ), + (match) => match[1], + ); + assert.deepEqual( + values, + expectedValues, + `${stage} must expose every metric as a stable button`, + ); } assert.equal((appHtml.match(/aria-pressed="true"/g) || []).length, 5); }); +test("both HTTP modes keep all three tractability views available", () => { + for (const search of ["?backend=http", "?backend=http&mode=scientific"]) { + const harness = loadFunctionalApp({ search }); + harness.hooks.applyBootMode(); + + const controls = harness.metricButtons.filter( + (button) => button.dataset.metricStage === "simulation", + ); + assert.deepEqual( + controls.map((button) => button.dataset.metricValue), + ["tractability_fit", "precedent", "computed"], + search, + ); + assert.deepEqual( + controls.map((button) => button.hidden), + [false, false, false], + `${search} must not collapse Stage 05 back to one control`, + ); + assert.equal( + controls.filter( + (button) => button.attributes.get("aria-pressed") === "true", + ).length, + 1, + `${search} must select exactly one Stage 05 view`, + ); + } +}); + +test("precedent and computed tractability views stay categorical", () => { + const { hooks } = loadFunctionalApp(); + assert.equal(typeof hooks.simulationMetricView, "function"); + const payload = { + verdict: "small_molecule_tractable", + verdict_basis: "retrieved_precedent", + axis_conflict: null, + target_precedent: { + best_potency_nm: 0.022, + clinical_stage_small_molecules: [{ name: "ZIMLOVISERTIB", phase: 2 }], + }, + tractability: { + pocket_volume_a3: { primary_d1_6_a3: 682.5 }, + site_pocket_rank: { + fpocket: 2, + prank: 1, + n_pockets: 11, + structure_pdb_id: "6EGE", + }, + }, + }; + + for (const key of ["precedent", "computed"]) { + const view = hooks.simulationMetricView(payload, key); + assert.equal(view.kind, "categorical", key); + assert.equal(view.scalar, null, `${key} must not invent a scientific scalar`); + assert.equal(typeof view.placement, "string", `${key} uses a named display lane`); + assert.ok(view.display, `${key} supplies legible node copy`); + assert.ok(view.detail, `${key} supplies an interpretation`); + assert.ok(Array.isArray(view.sourcePaths) && view.sourcePaths.length > 0); + } + assert.match( + hooks.simulationMetricView(payload, "precedent").sourcePaths.join(" "), + /verdict_basis|target_precedent/, + ); + assert.match( + hooks.simulationMetricView(payload, "computed").sourcePaths.join(" "), + /tractability|axis_conflict/, + ); +}); + test("HTTP ingestion keeps result status separate from execution, origin, and basis", () => { const { hooks } = ingestTerminalSnapshot(); @@ -111,6 +192,44 @@ test("a cached tractability payload is not relabeled as an unwired simulation", assert.equal(simulation.metadata.stationPayload.verdict, "small_molecule_tractable"); }); +test("machine reason codes stay in audit detail instead of primary node copy", () => { + const snapshot = structuredClone(terminalSnapshot); + const simulationStage = snapshot.stages.find( + (stage) => stage.stage_id === "simulation", + ); + simulationStage.reason_code = "PINNED_ARTIFACT_REVALIDATED"; + + const harness = loadFunctionalApp(); + prepareRun(harness); + harness.hooks.ingestSnapshot(snapshot); + + const simulation = harness.hooks.findNode("simulation-slot-0"); + assert.equal(simulation.reason, "PINNED_ARTIFACT_REVALIDATED"); + assert.equal(typeof harness.hooks.publicReasonSummary, "function"); + assert.doesNotMatch( + harness.hooks.publicReasonSummary(simulation), + /PINNED_ARTIFACT_REVALIDATED/, + ); + + const nodeCards = harness.hooks.elements.graphNodes.children.filter( + (element) => element.dataset.nodeId === "simulation-slot-0", + ); + const nodeCard = nodeCards.at(-1); + assert.ok(nodeCard, "the tractability node card must be rendered"); + assert.doesNotMatch( + nodeCard.innerHTML, + /PINNED_ARTIFACT_REVALIDATED/, + "the compact card needs plain-language status, not a raw machine token", + ); + + harness.hooks.renderInspector(simulation); + assert.match( + harness.hooks.elements.inspectorBody.innerHTML, + /PINNED_ARTIFACT_REVALIDATED/, + "the exact backend reason code remains inspectable in run qualifications", + ); +}); + test("the first real lineage is recentered out from under the sticky rail", () => { const harness = loadFunctionalApp(); const { hooks } = harness; diff --git a/tests/helpers/load-functional-app.mjs b/tests/helpers/load-functional-app.mjs index 05efe9b..17ff468 100644 --- a/tests/helpers/load-functional-app.mjs +++ b/tests/helpers/load-functional-app.mjs @@ -74,6 +74,30 @@ export class FakeElement { export function loadFunctionalApp({ search = "?backend=http", httpBackend = null } = {}) { const elements = new Map(); + const metricButtons = [ + ["biomarker", "exploration", true], + ["biomarker", "evidence", false], + ["biomarker", "pursuit", false], + ["hypothesis", "boldness", true], + ["hypothesis", "evidence", false], + ["hypothesis", "plausibility", false], + ["roi", "rnpv", true], + ["roi", "positive", false], + ["roi", "impact", false], + ["recruitability", "recruit", true], + ["recruitability", "duration", false], + ["recruitability", "screens", false], + ["recruitability", "risk", false], + ["simulation", "tractability_fit", true], + ["simulation", "precedent", false], + ["simulation", "computed", false], + ].map(([stage, value, pressed]) => { + const button = new FakeElement(); + button.dataset.metricStage = stage; + button.dataset.metricValue = value; + button.setAttribute("aria-pressed", String(pressed)); + return button; + }); const getElement = (id) => { if (!elements.has(id)) elements.set(id, new FakeElement()); return elements.get(id); @@ -83,7 +107,16 @@ export function loadFunctionalApp({ search = "?backend=http", httpBackend = null createElementNS: () => new FakeElement(), getElementById: getElement, querySelector: () => new FakeElement(), - querySelectorAll: () => [], + querySelectorAll: (selector) => { + if (selector === ".metric-button") return metricButtons; + const stageMatch = selector.match(/^\[data-metric-stage=["']([^"']+)["']\]$/); + if (stageMatch) { + return metricButtons.filter( + (button) => button.dataset.metricStage === stageMatch[1], + ); + } + return []; + }, }; const window = { clearTimeout() {}, @@ -143,6 +176,10 @@ export function loadFunctionalApp({ search = "?backend=http", httpBackend = null renderInspector, renderServerHighlanderResult, serverStatusLabel, + simulationMetricView: + typeof simulationMetricView === "function" ? simulationMetricView : null, + publicReasonSummary: + typeof publicReasonSummary === "function" ? publicReasonSummary : null, translateScientificWire, translateWire, validateSetup, @@ -162,6 +199,7 @@ export function loadFunctionalApp({ search = "?backend=http", httpBackend = null document, elements, hooks: context.__LABRADOR_TEST_HOOKS__, + metricButtons, }; } diff --git a/verify_functional_app.mjs b/verify_functional_app.mjs index 5dfd3ad..c111224 100644 --- a/verify_functional_app.mjs +++ b/verify_functional_app.mjs @@ -150,6 +150,8 @@ for (const required of [ "normalizeStageTruth", "stationPayloadFor", "interpretabilityView", + "simulationMetricView", + "publicReasonSummary", "HIGHLANDER CLIENT-SIDE · SERVER CONSUMER NOT WIRED", "labrador.run-setup.v3", "Run server Highlander", @@ -185,6 +187,28 @@ assert.match(styles, /\.pareto-depth-grid\s*\{/); assert.match(styles, /\.pareto-frontier-line\s*\{/); assert.match(contract, /fills the\s+remaining comparison-panel height/i); +const stageFiveControls = Array.from( + html.matchAll( + /data-metric-stage="simulation"[^>]+data-metric-value="([^"]+)"/g, + ), + (match) => match[1], +); +assert.deepEqual( + stageFiveControls, + ["tractability_fit", "precedent", "computed"], + "Stage 05 must retain representative, retrieved-precedent, and computed views", +); +assert.match(app, /kind:\s*"categorical"/); +assert.match(app, /scalar:\s*null/); +assert.match(app, /sourcePaths:\s*\["verdict_basis",\s*"verdict",\s*"target_precedent"\]/); +assert.match(app, /sourcePaths:\s*\["tractability",\s*"axis_conflict",\s*"verdict_basis"\]/); +assert.match(app, /var reasonSummary = publicReasonSummary\(node\)/); +assert.doesNotMatch( + app, + /:\s*\(node\.reason \|\| node\.metadata\.summary/, + "raw backend reason enums must not be used as primary node-card copy", +); + const comparisonPanelRule = styles.match(/\[data-region="program-comparison"\]\s*\{([^}]*)\}/s); assert.ok(comparisonPanelRule, "the comparison panel must expose its stable layout hook"); assert.match(comparisonPanelRule[1], /display:\s*flex/); @@ -202,10 +226,47 @@ assert.match(paretoPlotRule[1], /min-height:\s*0/); assert.match(paretoPlotRule[1], /height:\s*100%/); assert.match(paretoPlotRule[1], /flex:\s*1\s+1\s+auto/); +const graphScreenRule = styles.match(/\.graph-screen\s*\{([^}]*)\}/s); +assert.ok(graphScreenRule, "the graph screen must declare its header geometry"); +assert.match(graphScreenRule[1], /--graph-header:\s*160px/); +const progressNameRule = styles.match(/\.progress-step strong\s*\{([^}]*)\}/s); +assert.ok(progressNameRule, "progress stage names must expose a stable text rule"); +assert.match(progressNameRule[1], /overflow-wrap:\s*anywhere/); +assert.doesNotMatch(progressNameRule[1], /white-space:\s*nowrap|text-overflow:\s*ellipsis/); +const progressStateRule = styles.match(/\.progress-step \.stage-state\s*\{([^}]*)\}/s); +assert.ok(progressStateRule, "progress stage state must expose a stable text rule"); +assert.match(progressStateRule[1], /display:\s*block/); +assert.match(progressStateRule[1], /overflow-wrap:\s*anywhere/); +assert.doesNotMatch(progressStateRule[1], /white-space:\s*nowrap|text-overflow:\s*ellipsis/); + +const inspectorTitleRule = styles.match(/\.inspector-title h2\s*\{([^}]*)\}/s); +assert.ok(inspectorTitleRule, "long inspector titles must have an explicit wrap rule"); +assert.match(inspectorTitleRule[1], /overflow-wrap:\s*anywhere/); +assert.doesNotMatch(inspectorTitleRule[1], /white-space:\s*nowrap|text-overflow:\s*ellipsis/); +assert.match(styles, /\.inspector-title h2:focus\s*\{[^}]*box-shadow:\s*inset 3px 0 0 var\(--green\)/s); +const inspectorBodyRule = styles.match(/\.inspector-body\s*\{([^}]*)\}/s); +assert.ok(inspectorBodyRule, "the inspector body must own its overflow behavior"); +assert.match(inspectorBodyRule[1], /overflow-x:\s*hidden/); +assert.match(inspectorBodyRule[1], /overflow-y:\s*auto/); +const inspectorGridRule = styles.match(/\.inspector-status-grid\s*\{([^}]*)\}/s); +assert.ok(inspectorGridRule, "inspector status cards must have a shrinkable grid"); +assert.match(inspectorGridRule[1], /repeat\(2,\s*minmax\(0,\s*1fr\)\)/); + +assert.match(styles, /@media \(min-width:\s*1800px\)[\s\S]*--inspector-reserved:/); +assert.match(styles, /\.graph-screen\.inspector-open \.run-header,[\s\S]*\.graph-screen\.inspector-open \.graph-scroller\s*\{\s*margin-right:\s*var\(--inspector-reserved\)/); +assert.match(styles, /\.graph-screen\.inspector-open \.graph-footer\s*\{\s*right:\s*var\(--inspector-reserved\)/); +assert.doesNotMatch( + styles, + /\.graph-screen\.inspector-open \.run-header,[\s\S]{0,180}padding-right:/, + "the docked inspector must reserve layout width rather than overlay content with padding", +); + console.log("Functional app integration verification passed."); console.log(" Stage truth: module execution remains separate from fallback origin."); console.log(" Payloads: biomarker singular and program stage maps are consumed."); console.log(" Interpretability: readable projection retains native JSON verbatim."); +console.log(" Stage 05: representative, precedent, and computed views remain separate and non-scalar where required."); +console.log(" Layout: progress, inspector, and wide-screen dock rules resist text and panel clipping."); console.log(" Backend base: integrated serving defaults to same origin."); console.log(" Highlander: returned plans map to the ROI × recruitability × simulation 3D view; the chart fills its panel."); console.log(" Scientific v1: native branches and server Highlander results stay separate from representative display values."); From 2538fb5a9afc7e6a05927ff75b3c23052d4d80f9 Mon Sep 17 00:00:00 2001 From: rafwiewiora Date: Sun, 16 Aug 2026 09:07:58 -0700 Subject: [PATCH 2/2] fix: distinguish cached replay stages --- app/js/app.js | 14 ++++++++++++-- app/styles.css | 1 + tests/frontend-live-contract.test.mjs | 2 ++ verify_functional_app.mjs | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/js/app.js b/app/js/app.js index f25d5c5..ed40d3c 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -714,7 +714,7 @@ function stageTerminal(index) { var stageState = state.stageStates[index]; - return stageState === "complete" || stageState === "warning" || stageState === "failed"; + return stageState === "complete" || stageState === "replay" || stageState === "warning" || stageState === "failed"; } function retireUnusedCapacity() { @@ -1803,6 +1803,12 @@ function conciseStageNote(stage, status) { if (status === "RUNNING") return "Running"; if (status === "FAILED") return "Terminal · gaps"; + if ( + status === "COMPLETE_WITH_WARNINGS" && + stage && + stage.output_origin === "CACHED" && + stage.reason_code === "PINNED_ARTIFACT_REVALIDATED" + ) return "Complete · cached replay"; if (status === "COMPLETE_WITH_WARNINGS") return "Complete · warnings"; if (status === "COMPLETE") { var origin = stage && typeof stage.output_origin === "string" ? stage.output_origin : ""; @@ -1844,10 +1850,14 @@ if (index === -1) return; var truth = normalizeStageTruth(stage, { execution: "QUEUED" }); var status = truth.presentationStatus; + var cachedReplay = + status === "COMPLETE_WITH_WARNINGS" && + stage.output_origin === "CACHED" && + stage.reason_code === "PINNED_ARTIFACT_REVALIDATED"; state.stageStates[index] = status === "RUNNING" ? "running" : status === "COMPLETE" ? "complete" : - status === "COMPLETE_WITH_WARNINGS" ? "warning" : + status === "COMPLETE_WITH_WARNINGS" ? (cachedReplay ? "replay" : "warning") : status === "FAILED" ? "failed" : "queued"; state.stageNotes[index] = conciseStageNote(stage, status); if (status === "RUNNING") markStagePending(mappedStageId); diff --git a/app/styles.css b/app/styles.css index e5a6e91..fb47634 100644 --- a/app/styles.css +++ b/app/styles.css @@ -577,6 +577,7 @@ .progress-step .stage-state { display: block; margin-top: 2px; color: #646c66; font-size: 9px; font-weight: 800; line-height: 1.3; overflow-wrap: anywhere; } .progress-step[data-state="running"] { background: var(--blue-soft); border-color: #7cb0bb; } .progress-step[data-state="complete"] { background: var(--green-soft); border-color: #88caa1; } + .progress-step[data-state="replay"] { background: var(--green-soft); border-color: #88caa1; box-shadow: inset 4px 0 0 var(--amber); } .progress-step[data-state="warning"] { background: var(--amber-soft); border-color: #d8aa58; } .progress-step[data-state="failed"] { background: var(--red-soft); border-color: #cf8d84; } diff --git a/tests/frontend-live-contract.test.mjs b/tests/frontend-live-contract.test.mjs index 15ad6cb..e316be2 100644 --- a/tests/frontend-live-contract.test.mjs +++ b/tests/frontend-live-contract.test.mjs @@ -204,6 +204,8 @@ test("machine reason codes stay in audit detail instead of primary node copy", ( harness.hooks.ingestSnapshot(snapshot); const simulation = harness.hooks.findNode("simulation-slot-0"); + assert.equal(harness.hooks.state.stageStates[4], "replay"); + assert.equal(harness.hooks.state.stageNotes[4], "Complete · cached replay"); assert.equal(simulation.reason, "PINNED_ARTIFACT_REVALIDATED"); assert.equal(typeof harness.hooks.publicReasonSummary, "function"); assert.doesNotMatch( diff --git a/verify_functional_app.mjs b/verify_functional_app.mjs index c111224..3076794 100644 --- a/verify_functional_app.mjs +++ b/verify_functional_app.mjs @@ -238,6 +238,11 @@ assert.ok(progressStateRule, "progress stage state must expose a stable text rul assert.match(progressStateRule[1], /display:\s*block/); assert.match(progressStateRule[1], /overflow-wrap:\s*anywhere/); assert.doesNotMatch(progressStateRule[1], /white-space:\s*nowrap|text-overflow:\s*ellipsis/); +assert.match( + styles, + /\.progress-step\[data-state="replay"\]\s*\{[^}]*background:\s*var\(--green-soft\)[^}]*box-shadow:\s*inset 4px 0 0 var\(--amber\)/s, + "cached replay stages must stay green while retaining a distinct provenance edge", +); const inspectorTitleRule = styles.match(/\.inspector-title h2\s*\{([^}]*)\}/s); assert.ok(inspectorTitleRule, "long inspector titles must have an explicit wrap rule");