ExecutionVerdict.kt
src/jvmMain/kotlin/afield/runtime/execution/ExecutionVerdict.kt · 155 lines
package afield.runtime.execution
import afield.app.server.flag
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.lifecycle.map
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.notation.with
import afield.app.diagnostics.BoxOutputLog
import afield.app.protocol.Execute
import afield.app.protocol.ExecutionReport
import afield.app.protocol.Message
import afield.app.protocol.payload
import afield.audio.synth.*
import afield.document.graph.Box
import afield.document.graph.box
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.tools.docs.format
import afield.ui.output.outputDocument
import afield.ui.output.templates.filter
import kotlinx.serialization.json.*
import java.security.MessageDigest
Whether a box run succeeded, decided from what the run emitted rather than from what its engine reported.
An ExecutionTrace is built where an execution request arrives and finished where the work
completes, which for every engine but Kotlin's is a later dispatch onto the Runloop. While it is
open it watches the output the run routes through its reply, so a box that reports success while
printing a compiler error ends with a failed receipt. The verdict collects up to twenty of those
diagnostics and the receipt carries them back as an ExecutionReport.
ExecutionReports keeps the last attempt and the last successful setup per property on the box
itself, so an agent asking later whether a box ran gets an answer that outlived the run, and
sourceRevision hashes the source text so that answer can be checked against the code the box
holds now. A request with no id is not awaited: the trace still forwards output, and finishes
without publishing a receipt.
internal fun sourceRevision(source: String): String
Content identity, independent of save time or sidecar location.
internal fun sourceRevision(source: String): String = MessageDigest.getInstance("SHA-256")
.digest(source.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
internal class ExecutionVerdict(val uid: String, val property: String, source: String)
Verdict for one awaited setup/fragment execution. A running stripe is not a verdict, and completing this scope does not assert that a GPU frame has rendered.
val revision = sourceRevision(source)
private val failures = ArrayList<String>()
@Synchronized fun observe(message: Message)
@Synchronized fun observe(message: Message) {
val payload = message.payload as? JsonObject ?: return
fun value(key: String) = (payload[key] as? JsonPrimitive)?.contentOrNull.orEmpty()
if (value("uid") != uid) return
if (message.name == "execution-verdict") {
if (value("success") == "false" && failures.size < 20)
failures.add("$uid/$property [$revision]: " + value("error").ifBlank { "Execution failed before setup completed" })
return
}
if (message.name !in setOf("output", "peer-output") || value("kind") !in setOf("error", "err")) return
val text = value("html").trim()
// An empty diagnostic clears an earlier editor marker; it is not a failure.
if (text.isEmpty() || failures.size >= 20) return
val at = value("property").ifBlank { property }
failures.add("$uid/$at:${value("line")} [$revision]: ${text.take(4096)}")
}
@Synchronized fun check()
@Synchronized fun check() {
check(failures.isEmpty()) { failures.joinToString("\n") }
}
}
internal object ExecutionReports
Last attempted and last successful setup, stored with the box rather than in a global UID map.
internal object ExecutionReports {
private const val KEY = "_executionReports"
@Suppress("UNCHECKED_CAST")
private fun reports(box: Box): java.util.concurrent.ConcurrentHashMap<String, ExecutionReport> = synchronized(box) {
box.properties.getOrPut(KEY) { java.util.concurrent.ConcurrentHashMap<String, ExecutionReport>() }
as java.util.concurrent.ConcurrentHashMap<String, ExecutionReport>
}
fun latest(box: Box, property: String, successful: Boolean = false): ExecutionReport?
fun latest(box: Box, property: String, successful: Boolean = false): ExecutionReport? =
reports(box)[if (successful) "success:$property" else "attempt:$property"]
fun publish(box: Box, report: ExecutionReport)
fun publish(box: Box, report: ExecutionReport) {
reports(box)["attempt:${report.property}"] = report
if (report.success && report.stage == "setup-complete" && report.mode != "fragment") reports(box)["success:${report.property}"] = report
}
}
internal class ExecutionTrace
internal class ExecutionTrace(
private val box: Box, private val property: String, source: String,
private val requestId: String, private val mode: String,
private val downstream: ((Message) -> Unit)?
) {
private val verdict = ExecutionVerdict(box.id, property, source)
private var completed = false
A caller that sent no request id is not awaiting a receipt, so there is nobody to correlate one with. The trace still exists and still forwards, which is what lets an engine construct one unconditionally and thread it through a single code path instead of carrying a nullable reply and a branch at every output site.
private val awaited = requestId.isNotBlank()
val reply: (Message) -> Unit =
val reply: (Message) -> Unit =
if (awaited) { message -> verdict.observe(message); downstream?.invoke(message) }
else { message -> downstream?.invoke(message) }
fun finish(success: Boolean, reason: String = "")
fun finish(success: Boolean, reason: String = "") {
if (!awaited || completed) return
completed = true
val failure = runCatching { verdict.check() }.exceptionOrNull()?.message.orEmpty()
val ok = success && failure.isEmpty()
val result = ExecutionReport(
box.id, property, verdict.revision, requestId, mode,
if (ok) "setup-complete" else "failed", ok,
listOf(reason, failure).filter { it.isNotBlank() }.joinToString("\n")
.ifBlank { if (ok) "" else "Execution did not complete, or its launch was cancelled" }.take(32768)
)
ExecutionReports.publish(box, result)
BoxOutputLog.observeVerdict(result, outputDocument(box))
downstream?.invoke(Message("execution-verdict").payload(result))
}
companion object
companion object {
fun forEngine(box: Box, execute: Execute, mode: String, reply: ((Message) -> Unit)?) =
The receipt for one engine execution, made where the request arrives.
Every engine but Kotlin's dispatches its work onto the runloop and returns before any of
it has run, so a receipt can only be finished from inside that dispatch — in the same
finally that lowers the running stripe. Building it here and finishing it there is what
keeps "the box ran" and "the receipt says the box ran" the same event rather than two
that happen to agree.
The engine's own success flag is necessary but not sufficient: output routed through reply is watched too, so a box that reports completion while printing an error still yields a failed receipt. That is the rule this file is for — a receipt is earned by observed behaviour, never invented from an engine's optimism.
fun forEngine(box: Box, execute: Execute, mode: String, reply: ((Message) -> Unit)?) =
ExecutionTrace(box, execute.property, execute.code, execute.requestId, mode, reply)
}
}