Skip to content

CompletionInteractionLogger.kt

src/jvmMain/kotlin/afield/runtime/completion/CompletionInteractionLogger.kt · 157 lines

package afield.runtime.completion

import afield.app.server.home
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.attic
import afield.document.graph.introduction
import afield.document.graph.preamble
import afield.document.properties.PropertyBag
import afield.document.workspace.metadata
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.output.logs

import afield.app.protocol.CompletionInteractionEvent
import afield.audio.synth.*
import afield.data.table.get
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.app.diagnostics.logging.lprintln
import afield.runtime.macros.gatherPreambleUpwards
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

Durable, append-only observations of what happens at Field's autocomplete UI boundary.

The server cannot reconstruct this later: the browser merges several candidate sources and owns the final ranking. Conversely the browser does not have the inherited preamble or the other code-bearing properties that shape completion. record joins those two views without placing logging on the latency-sensitive completion reply path.

object CompletionInteractionLogger

Appends one browser-reported completion event, with its box's server-side context, to today's log.

object CompletionInteractionLogger {
    private val lock = Any()
    private val day = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC)
    private val json = Json { encodeDefaults = true }

    private val codeProperties = linkedSetOf(
        "code", "preamble", "introduction", "attic", "metadata",
        "vertex", "fragment", "vertex_fragment", "compute", "task", "mesh",
        "raygen", "miss", "closesthit", "anyhit", "intersection", "callable",
        "vertex_s", "fragment_s", "compute_s", "task_s", "mesh_s",
        "raygen_s", "miss_s", "closesthit_s", "anyhit_s", "intersection_s", "callable_s",
        "stackSL", "rectSL", "msl", "customization", "remote_js"
    )

internal data class Record

    @Serializable
    internal data class Record(
        val timestamp: String,
        val documentName: String,
        val documentPath: String,
        val boxName: String,
        val executionType: String,
        val relevantProperties: Map<String, String>,
        val inheritedPreamble: String,
        val interaction: CompletionInteractionEvent
    )

fun record

    fun record(
        event: CompletionInteractionEvent,
        box: Box,
        documentName: String,
        documentPath: String
    ) {
        runCatching {
            recordTo(defaultRoot(), event, box, documentName, documentPath)
        }.onFailure {
            lprintln(
                "warning,completion-observability",
                "could not append completion interaction ${event.interactionId}/${event.event}: ${it.message}"
            )
        }
    }

internal fun recordTo

    internal fun recordTo(
        root: Path,
        event: CompletionInteractionEvent,
        box: Box,
        documentName: String,
        documentPath: String,
        now: Instant = Instant.now()
    ): Path {
        val properties = relevantProperties(box, event)
        val record = Record(
            timestamp = now.toString(),
            documentName = documentName,
            documentPath = documentPath,
            boxName = box.name,
            executionType = box.getPropertyRaw("executionType")?.toString().orEmpty(),
            relevantProperties = properties,
            inheritedPreamble = runCatching { gatherPreambleUpwards(box) }.getOrDefault(""),
            interaction = event
        )
        val path = root.resolve("${day.format(now)}.jsonl")
        val line = json.encodeToString(record) + "\n"
        synchronized(lock) {
            Files.createDirectories(path.parent)
            Files.newBufferedWriter(
                path,
                StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
            ).use { it.write(line) }
        }
        return path
    }

internal fun relevantProperties

    internal fun relevantProperties(
        box: Box,
        event: CompletionInteractionEvent
    ): Map<String, String> {
        val names = LinkedHashSet(codeProperties).apply { add(event.property) }
        val result = linkedMapOf<String, String>()
        for (name in names) {
            val value = if (name == event.property && event.event == "requested") {
                event.allCode
            } else {
                box.getPropertyRaw(name) as? String
            }
            if (value != null && (value.isNotBlank() || name == event.property)) result[name] = value
        }
        // The editor is ahead of box persistence while the user is typing. Preserve its exact current
        // source on every request even when the backing PropertyBag has not caught up yet.
        if (event.event == "requested") result[event.property] = event.allCode
        return result
    }

    private fun defaultRoot(): Path {
        val configured = System.getProperty("afield.completion.interactionLog.path")
            ?: System.getenv("AFIELD_COMPLETION_INTERACTION_LOG_PATH")
        if (!configured.isNullOrBlank()) {
            val path = Paths.get(configured.trim())
            return if (path.isAbsolute) path else Paths.get(System.getProperty("user.dir")).resolve(path)
        }
        return Paths.get(System.getProperty("user.home"), ".afield", "logs", "completion_interactions")
    }
}