Skip to content

Workspace

The box's own surroundings: properties, persistence, and the handful of names every box already has in scope.

237 source file(s), 3209 public symbol(s), 1290 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

ToolPool.kt

The tool pool: a local library, shared across projects, of tools written in box Kotlin, with their tests and the evidence those tests produced.

BoxToolTest attaches an executable example to a Box.aiTool definition, and ToolDiagnostics keeps bounded per-call evidence (ToolCallEvidence) and suite results (ToolTestEvidence) keyed by source hash, so an edited tool does not inherit an old pass. ToolPool stores immutable ToolPoolEntry snapshots under ~/.afield/tool-pool; loading an entry returns its source and never runs it.

src/jvmMain/kotlin/afield/ai/tools/ToolPool.kt — read the source

BoxToolTest

class BoxToolTest(val name: String, val arguments: String, internal val verify: (Any) -> Unit)

An executable example attached to an ordinary Box.aiTool definition.

ToolTestOutcome

data class ToolTestOutcome(val name: String, val passed: Boolean, val message: String, val elapsedMs: Long)

A test verdict for one exact registered source revision.

ToolCallEvidence

data class ToolCallEvidence(val id: String = UUID.randomUUID().toString(), val at: String = Instant.now().toString(), val tool: String, val sourceHash: String, val caller: String, val arguments: String, val success: Boolean, val result: String, val elapsedMs: Long)

Bounded, local diagnostics for a call. Images stay in the existing image/transcript path.

Declaration Summary
val tool: String, val sourceHash: String, val caller: String, val arguments: String
val success: Boolean, val result: String, val elapsedMs: Long)

ToolTestEvidence

data class ToolTestEvidence(val sourceHash: String, val at: String = Instant.now().toString(), val tests: List<ToolTestOutcome>)

A saved suite result; source identity prevents an edited tool inheriting an old green result.

ToolDiagnostics

class ToolDiagnostics internal constructor(private val tool: BoxTool)

Diagnostics attached to a registered tool, rather than a second execution engine. Every entry point uses the same recorder. Records live with the document and survive reopening. Capture is bounded and best effort: a full disk must not turn a successful edit into a reported failure.

Declaration Summary
var storageError: String?
fun calls(): List<ToolCallEvidence> Recent calls for this tool source, including diagnostics retained before a restart.
fun tests(): ToolTestEvidence? Null means untested, never a pass. An empty suite is also untested.

PooledTool

data class PooledTool(val name: String, val description: String, val schema: String, val tests: List<String>)

Description of one tool supplied by a pool entry. The source remains ordinary box Kotlin.

ToolPoolEntry

data class ToolPoolEntry(val key: String, val revision: String, val description: String, val sourceHash: String, val source: String, val tools: List<PooledTool>, val checks: Map<String, ToolTestEvidence>, val originDocument: String, val originBox: String, val created: String = Instant.now().toString())

An immutable, explicit snapshot of a tool-provider box, its attached checks and provenance.

Declaration Summary
val source: String, val tools: List<PooledTool>, val checks: Map<String, ToolTestEvidence>
val originDocument: String, val originBox: String, val created: String

ToolPool

class ToolPool(val directory: File = File(System.getProperty("user.home"), ".afield/tool-pool"))

A local, cross-project library of box-authored tools. This stores source; it is not a tool registry. Curating is explicit. Loading an entry never runs it or imports the originating project's state. Run the returned source in a normal provider box to register its tools, then test in that project. Keep project-specific dependencies explicit in the provider source and entry description.

Declaration Summary
fun list(query: String = ""): List<Map<String, Any>> Discover immutable versions without reading executable source into the caller's context.
fun read(key: String, revision: String): ToolPoolEntry Read an exact curated version; modified source is refused.
fun publish(box: Box, key: String, description: String): ToolPoolEntry Explicitly curate this provider's registered source, retaining its exact source-specific checks.

BoxOutputLog.kt

A bounded server-side record of each box's printed text, errors, display(...) channels and execution verdicts, readable without a websocket.

Readers take BoxOutputLog.mark before acting and read BoxOutputLog.since afterwards to see what one run produced. The bounds and the feed points are described on BoxOutputLog.

src/jvmMain/kotlin/afield/app/diagnostics/BoxOutputLog.kt — read the source

BoxOutputLog

object BoxOutputLog

A bounded, server-side record of what each box has said: printed text, error diagnostics, display(...) channels, and execution verdicts.

Declaration Summary
data class Entry(val seq: Long, val uid: String, val kind: String, val text: String, val channel: String = "", val property: String = "", val line: Int = 0, val document: String = "", val atMillis: Long = System.currentTimeMillis(), val success: Boolean? = null) One thing a box said. kind is out, err, display, or verdict.
data class Page(val entries: List<Entry>, val marker: Long, val dropped: Boolean)
const val maxEntriesPerBox
const val maxTextChars
const val maxBoxes
const val maxTotalChars About 32 MB of UTF-16 across all boxes.
fun mark(): Long The current sequence number; everything recorded afterwards has a larger Entry.seq.
fun record(uid: String, kind: String, text: String, channel: String = "", property: String = "", line: Int = 0, success: Boolean? = null, document: String = "")
fun since(box: Box, marker: Long, limit: Int = maxEntriesPerBox)
fun since(uid: String, marker: Long, limit: Int = maxEntriesPerBox, document: String = ""): Page
fun clear(uid: String, document: String = "") Forget a box's history (tests, and a deleted box).
fun heldChars(): Long Characters currently held, for tests and diagnostics.

CodeUseTelemetryService.kt

The local history of code Field compiles and runs, stored in SQLite and summarised for the code-use panels and agent tools.

CodeUseTelemetryService takes records from the compile, evaluation and launch hooks without blocking them, persists the source by content hash, and resolves which library symbols each snippet used. The CodeUse...Row and CodeUseSummary types are the shapes its queries return. Storage, the analyzers and the failure policy are described on CodeUseTelemetryService.

src/jvmMain/kotlin/afield/app/diagnostics/CodeUseTelemetryService.kt — read the source

CodeUseSummary

data class CodeUseSummary(val enabled: Boolean, val databasePath: String, val queueSize: Int, val droppedEvents: Long, val quietMode: Boolean = false, val writerBusyRetries: Long = 0, val writerError: String = "", val analyzerQueueSize: Int = 0, val droppedAnalysisJobs: Long = 0, val analyzerCompletedJobs: Long = 0, val analyzerJobsPerMinute: Double = 0.0, val catchupBacklog: Long = 0, val catchupQueuedJobs: Long = 0, val analyzerBackend: String = "source-index", val analyzerMessage: String = "", val semanticBackend: String = "source-index-fallback", val semanticMessage: String = "", val semanticSidecarUrl: String = "", val snippets: Long = 0, val compileEvents: Long = 0, val evalEvents: Long = 0, val launchEvents: Long = 0, val sourceBlobs: Long = 0, val symbolUseEdges: Long = 0, val k2ResolvedUseEdges: Long = 0, val sourceIndexUseEdges: Long = 0, val textualUseEdges: Long = 0, val unknownUseEdges: Long = 0, val unresolvedUseEdges: Long = 0, val message: String = "")

The local history of code Field compiles and runs, stored in SQLite and summarised for the code-use panels and agent tools.

Declaration Summary
val enabled: Boolean
val databasePath: String
val queueSize: Int
val droppedEvents: Long
val quietMode: Boolean
val writerBusyRetries: Long
val writerError: String
val analyzerQueueSize: Int
val droppedAnalysisJobs: Long
val analyzerCompletedJobs: Long
val analyzerJobsPerMinute: Double
val catchupBacklog: Long
val catchupQueuedJobs: Long
val analyzerBackend: String
val analyzerMessage: String
val semanticBackend: String
val semanticMessage: String
val semanticSidecarUrl: String
val snippets: Long
val compileEvents: Long
val evalEvents: Long
val launchEvents: Long
val sourceBlobs: Long
val symbolUseEdges: Long
val k2ResolvedUseEdges: Long
val sourceIndexUseEdges: Long
val textualUseEdges: Long
val unknownUseEdges: Long
val unresolvedUseEdges: Long
val message: String

CodeUseSnippetRow

data class CodeUseSnippetRow(val id: String, val createdAtEpochMs: Long, val document: String, val boxUid: String, val boxName: String, val property: String, val mode: String, val strand: String, val sourceName: String, val sourceHash: String, val sourcePreview: String, val lastCompileSuccess: Boolean? = null, val lastEvalSuccess: Boolean? = null, val lastReturnType: String = "", val lastRuntimeClass: String = "", val lastCompileDiagnostics: String = "", val lastEvalDiagnostics: String = "", val lastException: String = "")

Declaration Summary
val id: String
val createdAtEpochMs: Long
val document: String
val boxUid: String
val boxName: String
val property: String
val mode: String
val strand: String
val sourceName: String
val sourceHash: String
val sourcePreview: String
val lastCompileSuccess: Boolean?
val lastEvalSuccess: Boolean?
val lastReturnType: String
val lastRuntimeClass: String
val lastCompileDiagnostics: String
val lastEvalDiagnostics: String
val lastException: String

CodeUseLaunchRow

data class CodeUseLaunchRow(val id: Long, val createdAtEpochMs: Long, val snippetId: String, val document: String, val boxUid: String, val boxName: String, val property: String, val mode: String, val strand: String, val lifecycle: String, val watermark: Int, val returnType: String = "", val exceptionJson: String = "")

Declaration Summary
val id: Long
val createdAtEpochMs: Long
val snippetId: String
val document: String
val boxUid: String
val boxName: String
val property: String
val mode: String
val strand: String
val lifecycle: String
val watermark: Int
val returnType: String
val exceptionJson: String

CodeUseFailureRow

data class CodeUseFailureRow(val id: String, val eventId: Long, val createdAtEpochMs: Long, val kind: String, val snippetId: String, val document: String, val boxUid: String, val boxName: String, val property: String, val mode: String, val strand: String, val sourcePreview: String, val diagnosticsJson: String = "", val exceptionJson: String = "", val lifecycle: String = "", val returnType: String = "")

Declaration Summary
val id: String
val eventId: Long
val createdAtEpochMs: Long
val kind: String
val snippetId: String
val document: String
val boxUid: String
val boxName: String
val property: String
val mode: String
val strand: String
val sourcePreview: String
val diagnosticsJson: String
val exceptionJson: String
val lifecycle: String
val returnType: String

CodeUseUnresolvedRow

data class CodeUseUnresolvedRow(val id: Long, val createdAtEpochMs: Long, val snippetId: String, val text: String, val line: Int, val column: Int, val reason: String)

Declaration Summary
val id: Long
val createdAtEpochMs: Long
val snippetId: String
val text: String
val line: Int
val column: Int
val reason: String

CodeUseSymbolRow

data class CodeUseSymbolRow(val id: Long, val fqName: String, val callableId: String, val kind: String, val packageName: String, val sourceFile: String, val line: Int, val useCount: Long, val uniqueSnippetCount: Long)

Declaration Summary
val id: Long
val fqName: String
val callableId: String
val kind: String
val packageName: String
val sourceFile: String
val line: Int
val useCount: Long
val uniqueSnippetCount: Long

CodeUseSourceTarget

data class CodeUseSourceTarget(val symbolName: String, val kind: String, val sourceFile: String, val line: Int, val confidence: String)

Declaration Summary
val symbolName: String
val kind: String
val sourceFile: String
val line: Int
val confidence: String

CodeUseSnippetUseRow

data class CodeUseSnippetUseRow(val snippetId: String, val createdAtEpochMs: Long, val document: String, val boxUid: String, val boxName: String, val property: String, val mode: String, val strand: String, val sourcePreview: String, val lastCompileSuccess: Boolean? = null, val lastEvalSuccess: Boolean? = null, val lastReturnType: String = "", val lastRuntimeClass: String = "", val recentLaunches: String = "", val confidence: String = "textual", val evidence: String = "")

Declaration Summary
val snippetId: String
val createdAtEpochMs: Long
val document: String
val boxUid: String
val boxName: String
val property: String
val mode: String
val strand: String
val sourcePreview: String
val lastCompileSuccess: Boolean?
val lastEvalSuccess: Boolean?
val lastReturnType: String
val lastRuntimeClass: String
val recentLaunches: String
val confidence: String
val evidence: String

CodeUseSourceReferenceRow

data class CodeUseSourceReferenceRow(val symbolName: String, val kind: String, val sourceFile: String, val line: Int, val score: Int, val confidence: String, val evidence: String = "")

Declaration Summary
val symbolName: String
val kind: String
val sourceFile: String
val line: Int
val score: Int
val confidence: String
val evidence: String

CodeUseHotSourceSymbolRow

data class CodeUseHotSourceSymbolRow(val symbolName: String, val kind: String, val sourceFile: String, val line: Int, val useCount: Long, val uniqueSnippetCount: Long, val firstSeenEpochMs: Long, val lastSeenEpochMs: Long, val confidence: String)

Declaration Summary
val symbolName: String
val kind: String
val sourceFile: String
val line: Int
val useCount: Long
val uniqueSnippetCount: Long
val firstSeenEpochMs: Long
val lastSeenEpochMs: Long
val confidence: String

CodeUseEarlySourceSymbolRow

data class CodeUseEarlySourceSymbolRow(val document: String, val symbolName: String, val kind: String, val sourceFile: String, val line: Int, val earlyUseCount: Long, val laterUseCount: Long, val firstSeenEpochMs: Long, val lastSeenEpochMs: Long, val confidence: String)

Declaration Summary
val document: String
val symbolName: String
val kind: String
val sourceFile: String
val line: Int
val earlyUseCount: Long
val laterUseCount: Long
val firstSeenEpochMs: Long
val lastSeenEpochMs: Long
val confidence: String

CodeUseRelatedSourceRow

data class CodeUseRelatedSourceRow(val symbolName: String, val kind: String, val sourceFile: String, val line: Int, val score: Int, val confidence: String, val evidence: String, val sourceStartLine: Int, val sourceEndLine: Int, val sourceExcerpt: String, val useCount: Long, val uniqueSnippetCount: Long, val firstSeenEpochMs: Long, val lastSeenEpochMs: Long, val peerCallers: List<CodeUseSnippetUseRow> = emptyList())

Declaration Summary
val symbolName: String
val kind: String
val sourceFile: String
val line: Int
val score: Int
val confidence: String
val evidence: String
val sourceStartLine: Int
val sourceEndLine: Int
val sourceExcerpt: String
val useCount: Long
val uniqueSnippetCount: Long
val firstSeenEpochMs: Long
val lastSeenEpochMs: Long
val peerCallers: List<CodeUseSnippetUseRow>

CodeUseSemanticAnalyzeRequest

data class CodeUseSemanticAnalyzeRequest(val code: String, val sourceName: String = "AgentSnippet.kt", val limit: Int = 5, val callersPerSymbol: Int = 5, val sourceRadiusLines: Int = 24, val databasePath: String = "", val workspaceRoot: String = "", val classpathHash: String = "", val sourceRoots: List<String> = emptyList(), val classpathRoots: List<String> = emptyList(), val imports: List<String> = emptyList(), val implicitReceivers: List<String> = emptyList(), val timeoutMs: Long = 0L)

Declaration Summary
val code: String
val sourceName: String
val limit: Int
val callersPerSymbol: Int
val sourceRadiusLines: Int
val databasePath: String
val workspaceRoot: String
val classpathHash: String
val sourceRoots: List<String>
val classpathRoots: List<String>
val imports: List<String>
val implicitReceivers: List<String>
val timeoutMs: Long

CodeUseSemanticUseRow

data class CodeUseSemanticUseRow(val symbolName: String, val callableId: String = "", val kind: String = "", val packageName: String = "", val sourceFile: String = "", val line: Int = 0, val useKind: String = "", val receiverType: String = "", val snippetLine: Int = 0, val snippetColumn: Int = 0, val confidence: String = "k2-resolved", val evidence: String = "")

Declaration Summary
val symbolName: String
val callableId: String
val kind: String
val packageName: String
val sourceFile: String
val line: Int
val useKind: String
val receiverType: String
val snippetLine: Int
val snippetColumn: Int
val confidence: String
val evidence: String

CodeUseSemanticUnresolvedRow

data class CodeUseSemanticUnresolvedRow(val text: String, val line: Int = 0, val column: Int = 0, val reason: String = "")

Declaration Summary
val text: String
val line: Int
val column: Int
val reason: String

CodeUseSemanticAnalyzeResult

data class CodeUseSemanticAnalyzeResult(val success: Boolean, val backend: String, val message: String, val sourceName: String = "", val sidecarUrl: String = "", val resolvedUses: List<CodeUseSemanticUseRow> = emptyList(), val unresolvedUses: List<CodeUseSemanticUnresolvedRow> = emptyList(), val references: List<CodeUseSourceReferenceRow> = emptyList(), val relatedSources: List<CodeUseRelatedSourceRow> = emptyList())

Declaration Summary
val success: Boolean
val backend: String
val message: String
val sourceName: String
val sidecarUrl: String
val resolvedUses: List<CodeUseSemanticUseRow>
val unresolvedUses: List<CodeUseSemanticUnresolvedRow>
val references: List<CodeUseSourceReferenceRow>
val relatedSources: List<CodeUseRelatedSourceRow>

CodeUseTelemetryService

object CodeUseTelemetryService

Asynchronous, best-effort history of code compiled and run by Field.

Declaration Summary
fun newSnippetKey(): String
fun captureBoxContext(box: Box?, property: String = "", mode: String = "", strand: String = ""): CodeUseContext
fun recordCompileResult(snippetId: String, source: String, sourceName: String, context: CodeUseContext, success: Boolean, diagnostics: List<String>, elapsedMs: Double, classpathHash: String = "")
fun recordEvaluationResult(snippetId: String, context: CodeUseContext, success: Boolean, runtimeClass: String = "", returnType: String = "", exception: Throwable? = null, diagnostics: List<String> = emptyList(), elapsedMs: Double)
fun recordLaunchLifecycle(snippetId: String, source: String, context: CodeUseContext, lifecycle: String, watermark: Int = -1, returnType: String = "", exception: Throwable? = null)
fun summary(): CodeUseSummary
fun summarySnapshot(): CodeUseSummary Non-blocking counterpart to summary for hot polling paths such as the welcome screen. Returns the most recent computed summary immediately (even if a little stale), or a cheap "still gathering" placeholder if none has been computed yet, and kicks off a background recompute when the cache is stale. It never runs the full-table SQLite scans on the caller's thread, so it can't stall Netty request or workspace-search dispatcher threads — the cold first computeSummary() (which can take many seconds on a large database) no longer blocks the welcome response or, by dispatcher starvation, a document the user clicks right after.
fun recentSnippets(limit: Int = 100): List<CodeUseSnippetRow>
fun recentLaunches(limit: Int = 100): List<CodeUseLaunchRow>
fun failures(limit: Int = 100): List<CodeUseFailureRow>
fun unresolved(limit: Int = 100): List<CodeUseUnresolvedRow>
fun symbols(limit: Int = 100): List<CodeUseSymbolRow>
fun callersForSourceLine(sourcePath: String, line: Int, limit: Int = 20): Pair<CodeUseSourceTarget?, List<CodeUseSnippetUseRow>>
fun referencesForCode(code: String, limit: Int = 5): List<CodeUseSourceReferenceRow>
fun relatedSourceForCode(code: String, symbolLimit: Int = 5, callersPerSymbol: Int = 5, sourceRadiusLines: Int = 24): List<CodeUseRelatedSourceRow>
fun semanticAnalyzeCode(request: CodeUseSemanticAnalyzeRequest): CodeUseSemanticAnalyzeResult
fun semanticAnalyzeCode(code: String, sourceName: String = "AgentSnippet.kt", limit: Int = 5, callersPerSymbol: Int = 5, sourceRadiusLines: Int = 24): CodeUseSemanticAnalyzeResult
fun hotSourceSymbols(window: String = "all", limit: Int = 20): List<CodeUseHotSourceSymbolRow>
fun earlyDocumentSymbols(document: String = "", earlyWindowMs: Long = 10 * 60 * 1000L, limit: Int = 20): List<CodeUseEarlySourceSymbolRow>
fun configureQuietMode(enabled: Boolean)
fun sha256(text: String): String
data class CodeUseContext(val document: String = "", val documentPath: String = "", val boxUid: String = "", val boxName: String = "", val property: String = "", val mode: String = "", val strand: String = "", val imports: List<String> = emptyList(), val implicitReceivers: List<String> = emptyList(), val parentIds: List<String> = emptyList(), val childIds: List<String> = emptyList(), val preambleHash: String = "")

CoroutineProfiling.kt

Per-box coroutine profiling behind the Profiling panel: wall time, CPU time and allocation charged to the box and strand that launched each continuation.

ProfileOrigin is the coroutine context element that names the launching box, and CoroutineProfiles holds the one capture a process records at a time. installCoroutineProfilingHandlers installs the panel's request handlers.

src/jvmMain/kotlin/afield/app/diagnostics/CoroutineProfiling.kt — read the source

Used in: Profiling

Top-level functions

Declaration Summary
fun installCoroutineProfilingHandlers()

ProfileOrigin

class ProfileOrigin(val box: Box, val strand: String = "code") : AbstractCoroutineContextElement(Key)

Immutable source reference captured at launch, inherited by child coroutines. No hashing when off.

Declaration Summary
companion object Key : CoroutineContext.Key<ProfileOrigin>
val document: String
val property: String The property this strand was launched from: code unless an alternate property was launched.
val source: String The source that was launched, captured with the launch, not the property's text now.

CoroutineProfiles

object CoroutineProfiles

Box-owned continuation instrumentation, shared by Runloop and SimulationClock. A continuation's CPU/heap counters exclude its suspended lifetime. No timers or allocations on the disabled dispatch path. Export copies happen on request, not per resume. A process permits one capture at a time; document isolation also applies to stop/snapshot.

Declaration Summary
fun resume(context: CoroutineContext, task: Runnable)
fun request(document: String, request: CoroutineProfileRequest): CoroutineProfileSnapshot

FrameProfile.kt

Explicit frame boundaries and named stages for the Profiling panel, for code that has its own frame loop.

Call FrameProfile.beginFrame once per frame while the panel is recording, and wrap stages in FrameProfile.measure or profileScope. Nothing is read or allocated while recording is off.

src/jvmMain/kotlin/afield/app/diagnostics/FrameProfile.kt — read the source

Used in: Profiling

Top-level functions

Declaration Summary
suspend fun frameProfile(name: String = "Frame", targetFps: Double = 60.0, allocationKiB: Double = 512.0): FrameProfile Create frame instrumentation inheriting the source and document of this ordinary box coroutine.
fun <T> profileScope(name: String, block: () -> T): T A synchronous named stage inside any profiled box or simulation coroutine. block cannot suspend (it is an ordinary lambda), and a stage that ends on another thread is marked in the capture.

FrameProfile

class FrameProfile(private val context: CoroutineContext, val name: String = "Frame", val targetFps: Double = 60.0, val allocationKiB: Double = 512.0) : AutoCloseable

Explicit frame boundaries for the existing Profiling panel. This object does not start a capture. Press Record in the panel, then call beginFrame once per application frame, on a stable thread. Intervals include time between boxes and waits; CPU/heap counters cover that thread only. Use measure for named inclusive stages. No counter reads or sample allocation while recording is off.

Declaration Summary
val name: String
val targetFps: Double
val allocationKiB: Double
fun beginFrame() The first boundary in each capture establishes a baseline and is not a completed frame.
fun <T> measure(name: String, block: () -> T): T A synchronous stage. Nested scopes retain inclusive and self totals. block is an ordinary lambda, so a suspension inside it does not compile; a stage that ends on another thread is marked "moved threads" in the capture, with no CPU or allocation figures.
override fun close() Discard an incomplete last interval. A stop halfway through a frame is not a full-frame sample.

JvmProfileCapture.kt

The stack-sampling half of the profiling panel: a JFR recording that runs alongside a coroutine capture, read back as frames, samples and the source each frame came from.

CoroutineProfiles starts one only when a capture asks for stacks. It records execution samples every 10 ms, throttled allocation samples and GC pauses, under a size ceiling and a recording length the capture cannot outrun. Nothing is parsed while the capture is live: stopping the recording hands the dump to a single daemon thread, and the panel is told how far that has got.

Two things keep the result bounded and safe to hold. Registering a box's snippet copies its source text and a few identifiers, never the box, its classloader or its coroutines, and every list has a cap past which samples are counted as dropped rather than kept. A frame that is not box code is traced back to a file by reading the class's own SourceFile attribute and then requiring the declared package to agree, so two files of the same name in different packages are never guessed between.

src/jvmMain/kotlin/afield/app/diagnostics/JvmProfileCapture.kt — read the source

LatencySpans.kt

Where a Field server's own time goes during the moments someone is waiting on it.

Each phase worth attributing is wrapped in LatencySpans.time (creating and bootstrapping a box's Kotlin engine, compiling a snippet, compiling a shader, building or updating a pipeline, running a transcript operation) and lands in a ring buffer with its name, the box it belongs to, the thread it ran on, and a start time in epoch microseconds. GET /api/perf/spans?since=<seq> serves everything recorded after a sequence number.

tools/feel_bench.py is what reads it: it measures a step from the client side, then asks the server which of its own phases that wall time was spent in, and the shared epoch clock is what lets the two be laid against each other. Recording is always on, because a span costs one nanoTime pair and one array write against work that costs milliseconds.

src/jvmMain/kotlin/afield/app/diagnostics/LatencySpans.kt — read the source

LatencySpans

object LatencySpans

Wall-clock spans for the moments a user waits on: building and bootstrapping a box's Kotlin engine, compiling and evaluating a snippet, a whole execute-all/launch-all up to its verdict, compiling a shader, creating a pipeline.

Declaration Summary
data class Span(val seq: Long, val name: String, val startEpochMicros: Long, val durationMicros: Long, val box: String? = null, val detail: String? = null, val thread: String)
data class Snapshot(val spans: List<Span>, val nextSeq: Long, val dropped: Long, val jvmStartEpochMillis: Long, val readyMillis: Long?, val nowEpochMicros: Long)
var readyMillis: Long? JVM start to afield.app.diagnostics.logging.PinnedConsole.finishStartup, once it has run.
fun epochMicros(nanos: Long): Long
fun record(name: String, startNanos: Long, endNanos: Long = System.nanoTime(), box: String? = null, detail: String? = null)
inline fun <T> time(name: String, box: String? = null, detail: String? = null, block: () -> T): T
fun since(after: Long): Snapshot

JdiProbeHelper.kt

The JDI probe helper: a separate JVM that attaches to Field over JDWP and samples a variable at a source line.

Its main attaches with attachSocket and hands requests read from stdin to JdiProbeService, which installs breakpoints that suspend only the hitting thread and writes responses to stdout.

src/jvmMain/kotlin/afield/app/diagnostics/debug/JdiProbeHelper.kt — read the source

Top-level functions

Declaration Summary
fun attachSocket(host: String, port: String): VirtualMachine
fun main(args: Array<String>)

ActiveProbe

data class ActiveProbe(val request: ProbeVariableRequest, val eventRequests: MutableSet<EventRequest> = LinkedHashSet(), val createdAtMs: Long, var hits: Int = 0, var done: Boolean = false, var lastDetail: String? = null, var lastScope: ScopeSnapshotPayload? = null)

The JDI probe helper: a separate JVM that attaches to Field over JDWP and samples a variable at a source line.

Declaration Summary
val request: ProbeVariableRequest
val eventRequests: MutableSet<EventRequest>
val createdAtMs: Long
var hits: Int
var done: Boolean
var lastDetail: String?
var lastScope: ScopeSnapshotPayload?

JdiProbeService

class JdiProbeService(private val vm: VirtualMachine, private val out: PrintWriter)

Installs JDI class-prepare and breakpoint requests for runtime value probes.

Declaration Summary
fun startEventLoop()
fun stop()
fun submit(req: ProbeVariableRequest)

LiveProbeClient.kt

Field's side of the JDI probe helper: LiveProbeClient starts the helper process, sends it probe requests and routes each response to the listener registered for its request id.

src/jvmMain/kotlin/afield/app/diagnostics/debug/LiveProbeClient.kt — read the source

LiveProbeClient

class LiveProbeClient(private val helperMainClass: String, private val debugHost: String = "127.0.0.1", private val debugPort: Int = 5005, private val javaBin: String = "java", private val classPath: String = System.getProperty("java.class.path")) : AutoCloseable

Field's side of the JDI probe helper: LiveProbeClient starts the helper process, sends it probe requests and routes each response to the listener registered for its request id.

Declaration Summary
fun probe(request: ProbeVariableRequest, onEvent: (ProbeResponse) -> Unit): String
fun isAlive(): Boolean
override fun close()

RuntimeDebugProbeService.kt

The runtime debug probe: samples a symbol at a line of a running box or a src/ file and shows the values in the box's "debug probe" output channel.

RuntimeDebugProbeService.startProbe targets box code and RuntimeDebugProbeService.startSourceProbe targets a source file. Both need Field to have been launched with a JDWP port; RuntimeDebugProbeService.availabilityError explains when it was not.

src/jvmMain/kotlin/afield/app/diagnostics/debug/RuntimeDebugProbeService.kt — read the source

RuntimeDebugProbeService

object RuntimeDebugProbeService

Declaration Summary
fun availabilityError(): String?
fun startProbe(box: Box, request: DebugProbeRequest, execution: KotlinExecution, reply: (Message) -> Unit)
fun startSourceProbe(request: DebugSourceProbeRequest, reply: (Message) -> Unit)

RuntimeProbeProtocol.kt

The JSON line protocol between Field and the JDI probe helper process: probe requests and the ack, sample, error and completion responses.

ProbeJson is the codec both sides use; responses are discriminated by their type field.

src/jvmMain/kotlin/afield/app/diagnostics/debug/RuntimeProbeProtocol.kt — read the source

Top-level values

Declaration Summary
val ProbeJson The JSON line protocol between Field and the JDI probe helper process: probe requests and the ack, sample, error and completion responses.

ProbeRequest

sealed interface ProbeRequest

Declaration Summary
val requestId: String

ProbeVariableRequest

data class ProbeVariableRequest(override val requestId: String, val className: String? = null, val sourceName: String? = null, val sourceNamePrefix: String? = null, val line: Int, val variableName: String, val threadName: String? = null, val maxHits: Int = 1, val timeoutMs: Long = 1_500L) : ProbeRequest

Declaration Summary
override val requestId: String
val className: String?
val sourceName: String?
val sourceNamePrefix: String?
val line: Int
val variableName: String
val threadName: String?
val maxHits: Int
val timeoutMs: Long

ProbeResponse

sealed interface ProbeResponse

Declaration Summary
val requestId: String

AckResponse

data class AckResponse(override val requestId: String) : ProbeResponse

Declaration Summary
override val requestId: String

ErrorResponse

data class ErrorResponse(override val requestId: String, val error: String, val detail: String? = null) : ProbeResponse

Declaration Summary
override val requestId: String
val error: String
val detail: String?

CompleteResponse

data class CompleteResponse(override val requestId: String, val hits: Int, val error: String? = null, val detail: String? = null, val scope: ScopeSnapshotPayload? = null) : ProbeResponse

Declaration Summary
override val requestId: String
val hits: Int
val error: String?
val detail: String?
val scope: ScopeSnapshotPayload?

ValuePayload

data class ValuePayload(val kind: String, val type: String? = null, val text: String)

Declaration Summary
val kind: String
val type: String?
val text: String

ScopeSymbolPayload

data class ScopeSymbolPayload(val name: String, val resolution: String, val type: String? = null, val preview: String = "", val frameIndex: Int = -1, val declaringType: String? = null, val method: String? = null, val line: Int = -1)

Declaration Summary
val name: String
val resolution: String
val type: String?
val preview: String
val frameIndex: Int
val declaringType: String?
val method: String?
val line: Int

ScopeSnapshotPayload

data class ScopeSnapshotPayload(val symbols: List<ScopeSymbolPayload> = emptyList(), val omittedCount: Int = 0)

Declaration Summary
val symbols: List<ScopeSymbolPayload>
val omittedCount: Int

SampleResponse

data class SampleResponse(override val requestId: String, val hitIndex: Int, val threadName: String, val declaringType: String, val method: String, val line: Int, val resolution: String, val value: ValuePayload) : ProbeResponse

Declaration Summary
override val requestId: String
val hitIndex: Int
val threadName: String
val declaringType: String
val method: String
val line: Int
val resolution: String
val value: ValuePayload

PinnedConsole.kt

A LogSink that pins chosen channels to a small, non-scrolling region at the top of the terminal while every other log line scrolls normally below it.

The heavy lifting is a single ANSI scroll-region (DECSTBM): the top N rows are fenced off so ordinary output can only scroll in the region beneath them, which means a plain println "just works" — there is no per-frame render loop over application state. The header is repainted only when a pinned value actually changes (coalesced by a single daemon tick), and the terminal size is re-checked about once a second.

Selection is by channel tag: a log event whose comma-separated channel contains a pinned tag (e.g. pinning "audio" matches "info,audio") updates that tag's row instead of scrolling. Configure via the AFIELD_PIN_CHANNELS env var (comma/space separated) and/or at runtime with pin/unpin/clear.

Degrades cleanly: if stdout is not an interactive TTY, ANSI/cursor control is unavailable, or the terminal is a legacy Windows console (Mordant reports AnsiLevel.NONE), pinning switches off and every event — pinned or not — simply scrolls, with would-be-pinned lines tagged pin so they stay visible. Set AFIELD_PIN_DISABLE=1 to force this fallback.

Known limitation: raw System.out.println/stderr writes elsewhere in the app bypass this sink's lock and can, very occasionally, interleave with a header repaint. They still scroll below the header; the visual glitch is transient. Routing all app output through the dispatcher avoids it.

src/jvmMain/kotlin/afield/app/diagnostics/logging/PinnedConsole.kt — read the source

PinnedChannel

data class PinnedChannel(val tag: String, val heatmap: Boolean = true)

A channel to pin, at "channel picking" time (see PinnedConsole.install/PinnedConsole.pin). heatmap colors the channel's rolling numeric values (foo=1.2ms) blue-through-red by whether they fell or rose since the channel's last line; it's on by default and opt-out per channel.

PinnedConsole

object PinnedConsole

Declaration Summary
fun installWithStartup() Install the pinned console for boot: pins the "startup" channel as the first (top) row, plus the DEFAULT_PINS (fps + uploads) and anything in AFIELD_PIN_CHANNELS, and starts the boot stopwatch. Call once at the very top of main(). Combined with the per-phase annotations in runAll, the startup row ticks through the boot stages and then collapses, leaving the fps/upload rows pinned during normal running. Honors AFIELD_PIN_DISABLE (falls back to plain stdout).
fun finishStartup() Mark startup complete: write a "✓ ready" line to the pinned startup row, then collapse that row a few seconds later so the header tidies itself once Field is running. Idempotent.
fun installFromEnv() Install by reading AFIELD_PIN_CHANNELS (comma/space separated tag names). No-op if unset/empty.
fun installTags(tags: List<String>) Install the pinned console, pinning tags with heatmap-coloring on by default (see PinnedChannel). Convenience wrapper around install for callers that don't need per-channel control.
fun install(channels: List<PinnedChannel>) Install the pinned console, picking channels (and per-channel heatmap on/off) via channels. Replaces StdoutLog in the dispatcher (preserving any other sinks, e.g. the web LogStream) and registers a shutdown hook that restores the terminal. Safe to call once; a second call just re-points the runtime API.
fun pin(tag: String, heatmap: Boolean = true) Start pinning tag at runtime (adds a row if not already pinned). Rolling-stats values in the row (foo=1.2ms) are heatmap-colored by whether they rose or fell since last time unless heatmap is set to false at this "channel picking" stage.
fun unpin(tag: String) Stop pinning tag at runtime (removes its row).
fun clear(tag: String) Blank a pinned channel's current value while keeping its row.
fun beat(channel: String) Register one beat of a heartbeat/spinner row named channel. Cheap enough to call every frame: the row shows an animated spinner + beats/sec while beats keep arriving, and freezes to "idle" when they stop. No-op if the console isn't installed or isn't rendering (non-TTY). Use this instead of a per-frame lprintln/print.
fun error(key: String, title: String, lines: List<String>) Pin a compilation/execution error as a sticky, multi-line panel at the very top of the header, keyed by key (use a stable id such as the box id so re-runs replace the same panel). Unlike the channel rows, an error panel never expires on the silence timer — a broken build stays put until clearError is called (typically on the next clean compile) or a fresh error for the same key replaces it. title is a short one-line summary; lines are the detail (each clipped to width, the panel body capped in height with a "+N more" tail). No-op when pinning is off (non-TTY); the error still scrolls via whatever error-channel line the caller already logged.
fun clearError(key: String) Remove the sticky error panel key (e.g. once the box compiles cleanly). No-op if none exists.

SaveReloadLog.kt

A timestamped append-only log of document save and reload events, written to ~/.afield/logs/save_reload.log.

Each line is an ISO instant, the event name in brackets and a message. logTiming adds the elapsed milliseconds since a System.nanoTime() start and skips events faster than its threshold. The file rolls over to save_reload.log.1 once it reaches 50 MB. Every line also goes to LogDispatcher on info,persist,savereload, so the log widget and other sinks see it; the console mutes info,persist by default.

src/jvmMain/kotlin/afield/app/diagnostics/logging/SaveReloadLog.kt — read the source

SaveReloadLog

object SaveReloadLog

A timestamped append-only log of document save and reload events, written to ~/.afield/logs/save_reload.log.

Declaration Summary
const val CHANNEL
fun elapsedMillis(startedAtNs: Long): Long
fun logTiming(event: String, startedAtNs: Long, message: String, thresholdMs: Long = 0L): Long
fun log(event: String, message: String)

SessionErrorLog.kt

A per-session copy of everything written to System.err, kept under build/logs/ in the working directory, with stack traces also pinned on the console.

SessionErrorLog.install tees stderr: every byte still reaches the real stream, every line is appended to a session-<timestamp>-pid<pid>.log file (the newest 20 are kept, each capped at 32 MB), and a stack trace or slf4j ERROR line is raised as a sticky panel that a repaint of the log viewport does not clear.

src/jvmMain/kotlin/afield/app/diagnostics/logging/SessionErrorLog.kt — read the source

SessionErrorLog

object SessionErrorLog

Everything written to System.err this run: appended to a session file, and — when it looks like a failure — pinned on screen where a repaint cannot take it away.

Declaration Summary
var path: Path? This run's session file, once install has run. Null when the file could not be opened.
fun install() Tee System.err into the session file and the pinned console. Idempotent; call once, after the PinnedConsole sink exists so that a stack trace thrown on the way up can already be pinned.

StatsHeatmap.kt

Heatmap colouring for the numeric values in the pinned console's rolling stat lines.

StatsHeatmap splits a line such as fps=61.2 upload=3.4MB into fragments and colours each number by how it moved since the last value of the same key on that channel.

src/jvmMain/kotlin/afield/app/diagnostics/logging/StatsHeatmap.kt — read the source

StatsHeatmap

object StatsHeatmap

Colors the numeric tokens of a rolling-stats line ("fps=61.2 upload=3.4MB net:102%") on a blue -> green -> yellow -> orange -> red heatmap keyed by whether each value rose or fell since the last line the same pinned channel emitted. A value identical to last time renders grey. History is kept per channel+key so unrelated metrics sharing a channel don't interfere.

Declaration Summary
data class Fragment(val text: String, val style: TextStyle?)
fun fragments(channel: String, line: String): List<Fragment> Split line into plain/heatmap-colored fragments, updating channel's per-key value history.
fun reset(channel: String) Drop channel's value history (unpin/expire) so a later reuse doesn't diff against stale values.

logger.kt

The lprintln / lprint / lblock logging calls, which send a message to the server log on a named channel such as "info,startup".

Each call goes to LogDispatcher with the current thread's indent. lblock logs a begin line, runs its block with the indent deepened, and logs a matching end line even if the block throws, so nested blocks read as a tree. The single-argument forms are deprecated and log on info,legacy. formatMillis formats a nanosecond duration with its unit for stat lines.

src/jvmMain/kotlin/afield/app/diagnostics/logging/logger.kt — read the source

Used in: Logging: channels, sinks and the console

Top-level functions

Declaration Summary
fun lprintln(message: Any?) The lprintln / lprint / lblock logging calls, which send a message to the server log on a named channel such as "info,startup".
fun lprint(message: Any?)
fun lprintln(channel: String, message: Any?)
fun lprint(channel: String, message: Any?)
fun formatMillis(nanos: Long): String Format a nanosecond duration as a compact, human-readable value with its unit for pinned-console stat lines: more significant digits for sub-millisecond values, plain integers once past 100ms, and a rollover to decimal seconds past 1s (so a slow compile=1.63s reads cleanly rather than compile=1629ms). The trailing unit (ms/s) is part of the returned string and satisfies the afield.app.diagnostics.logging heatmap token grammar (key=value unit), so callers emit "compile=${formatMillis(ns)}" with no unit of their own.
inline fun lblock(channel: String, message: Any?, block: () -> Unit)
inline fun lblock(message: Any?, block: () -> Unit)

Top-level values

Declaration Summary
var indent by threadLocal
var isNL

stdlib_diagnostics.kt

The frame-presentation log: per-frame graphics timing, written to a file while it runs.

frameLogStart begins recording and returns the path, choosing one if you do not give it one, and the log keeps growing until frameLogStop closes it. frameLogMark drops a named marker tied to a frame boundary, so an event can be lined up against the frame that was slow, and frameLogLine appends free text with no frame attached. The waitAfterPresent argument changes what is measured rather than how it is reported: each frame waits for the present to complete, which gives true per-frame numbers at some cost to throughput.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/app/diagnostics/stdlib_diagnostics.kt — read the source

Top-level functions

Declaration Summary
fun frameLogStart(path: String? = null, waitAfterPresent: Boolean = Options.framePresentationLogWaitAfterPresent, append: Boolean = false): String Starts recording the frame-presentation log — per-frame timing for the graphics pipeline — and returns the path it is being written to.
fun frameLogStop() Stops frame-presentation logging and closes the file. Harmless if nothing was being logged.
fun frameLogMark(message: String) Drops a named marker into the frame log at the current frame — "this is where I clicked the thing".
fun frameLogLine(message: String) Appends a free-text line to the frame log, with no frame association. See frameLogMark for events.

util.kt

src/jvmMain/kotlin/afield/app/diagnostics/util.kt — read the source

Top-level functions

Declaration Summary
fun Number.tDP(): String
inline fun <T> logTime(name: String, max_window: Int = afield.app.diagnostics.max_window, block: () -> T): T
suspend fun <T> logTime_(name: String, max_window: Int = afield.app.diagnostics.max_window, block: suspend () -> T): T
fun LinkedList<Long>.windowedAppendAndPrint(name: String, max_window: Int, data: Long)
infix fun <T> T.named(name: String)
fun <T> T.namedHere(name: String = "${this!!.javaClass.name}", prefix : String = "${Box.context.get()}")
fun Any?.reflectionDump(includeStatic: Boolean = false, includeSynthetic: Boolean = false, showInterfaces: Boolean = true) Dump an object's class hierarchy (superclasses + interfaces) and print: - declared fields + their values - declared methods + their signatures
fun showMethodNames(a : Any)

Top-level values

Declaration Summary
var timingInfo
var disableTiming
var max_window

Named

data class Named<T>(val name: String, val value: T)

MethodParameterSpy

class MethodParameterSpy

Declaration Summary
fun <E : RuntimeException?> genericThrow()

Server_notifications.kt

src/jvmMain/kotlin/afield/app/notifications/Server_notifications.kt — read the source

Top-level functions

Declaration Summary
fun notify(text: String, category: String = "")

Server_protocol.kt

src/jvmMain/kotlin/afield/app/protocol/Server_protocol.kt — read the source

Top-level functions

Declaration Summary
fun _send(message: Message, document: String = ".*")
fun _send(session: WebSocketSession?, message: Message)

ButtonControlHandler.kt

The server side of button(...): the button-update websocket command, which carries one press or release from the browser and runs the box's callback with it.

A button is the one control in the family whose value is not worth keeping. The handler updates the in-memory control registry so other clients see the press, broadcasts that, and calls the callback; nothing reaches the box's saved value cache, so re-running the box does not replay the press. Everything else about a button is in the controls DSL in outputs.kt and in sliders.js.

src/jvmMain/kotlin/afield/app/server/ButtonControlHandler.kt — read the source

CustomMarking.kt

Custom marking menus: Box.customMarking maps a direction and a label to a callback, and the frontend shows the entries as a radial menu on the box.

MarkingDirections names the directions and their angles. When the menu is shown the server registers each callback under an id and sends the frontend serialisable entries; choosing an entry runs its callback.

src/jvmMain/kotlin/afield/app/server/CustomMarking.kt — read the source

Top-level values

Declaration Summary
var Box.customMarking: MutableMap<Pair<String, String>, () -> Unit

MarkingDirections

object MarkingDirections

Direction definitions for custom marking menus. Angles match buttons.js polarToRect which uses sin(angle) for x, cos(angle) for y: 0=South, 90=East, 180=North, 270=West (clockwise from south).

Declaration Summary
data class DirectionInfo(val angle: Double, val radius: Double)
val directions

InterruptSignal.kt

Restores SIGINT handling so kill -INT and Ctrl-C shut Field down cleanly even when the JVM inherited SIGINT as ignored.

src/jvmMain/kotlin/afield/app/server/InterruptSignal.kt — read the source

InterruptSignal

object InterruptSignal

Makes kill -INT (and Ctrl-C) stop Field even when Field was started with SIGINT ignored.

Declaration Summary
enum class Outcome What install found and did; reported once on the startup channel.
fun install(): Outcome

KitRoutes.kt

The HTTP surface behind Welcome's kit library, one route per action that panel offers.

GET /api/kits is the list: every registered package with its manifest and its state, whether the registry and the revisions this host launched with have diverged, and the packages bundled in the checkout's templates/. install and remove edit the registry; restart-status and restart drive afield.document.workspace.KitRestarts; create copies a starting point into the workspace and announces the new document; open-host starts a kit that brings its own host and answers with a URL into it; guide renders the package's Markdown guide as a standalone page, served under a sandboxing Content-Security-Policy because that text belongs to the package rather than to Field.

The three routes that touch an existing document are the careful ones. instances and inspect report a three-way comparison and write nothing; migrate refuses unless the document is closed, flushing and unloading one that is loaded with nothing attached to it first; and activate starts the kit's runtime for a loaded document and launches its boxes in order, or answers with the URL that would open it when it is not loaded. A document is addressed by name and has to resolve to a direct child of the workspace, so a path cannot reach outside it.

Nothing here runs code from a package. Installing is validation and copying; it is the next launch that puts a kit's classes on the classpath.

src/jvmMain/kotlin/afield/app/server/KitRoutes.kt — read the source

Top-level functions

Declaration Summary
fun Route.installKitRoutes(restart: () -> Unit) Local kit lifecycle APIs used by Welcome; registration never executes the package's code.

KitPackageRequest

data class KitPackageRequest(val path: String = "", val id: String = "", val startingPoint: String = "", val name: String = "", val document: String = "")

One request body for every kit route; each reads the fields its own action needs.

KitLibraryResponse

data class KitLibraryResponse(val kits: List<KitPackageStatus>, val restartRequired: Boolean, val bundled: List<String>)

The whole kit library as Welcome draws it, including packages bundled in templates/.

NetStats.kt

Per-second counters of WebSocket traffic between the backend and the browser, shown on the pinned "net" console row.

src/jvmMain/kotlin/afield/app/server/NetStats.kt — read the source

NetStats

object NetStats

Time-windowed WebSocket traffic stats for the front-end /ws channel, surfaced on the pinned "net" console row: ↑ = backend → front end, ↓ = front end → backend. Counters are bumped at the send choke point (sendWithOutputBackPressure) and the inbound frame loop; a 1s reporter emits per-second packet + KB rates — and only when there's traffic, so the pinned row dims then expires when the front end goes idle (see PinnedConsole's lifecycle).

Declaration Summary
fun sent(bytes: Int) backend → front end: one WebSocket text frame of bytes.
fun received(bytes: Int) front end → backend: one WebSocket text frame of bytes.
fun start() Start the 1s reporter (idempotent).

OscEditorBackdoor.kt

Lets an OSC controller drive the editor by forwarding float, launch and stop messages to the front end as osc-editor commands.

OscEditorBackdoor reads its port, addresses, debounce and delta mode from FieldPreferences, listens with an OSCPortIn, and forwards each matching message to the front end as an osc-editor message. OscEditorBackdoor.learnNextAddress listens once on a port and returns the first address it hears, for filling in the preferences.

src/jvmMain/kotlin/afield/app/server/OscEditorBackdoor.kt — read the source

OscEditorBackdoor

object OscEditorBackdoor

Declaration Summary
fun configure(preferences: FieldPreferences)
fun stop()
suspend fun learnNextAddress(port: Int, timeoutMs: Long = 10_000, ignoreAddresses: Set<String> = emptySet()): String?

PatchyWebStorage.kt

The HTTP side of a Patchy editor that edits a .psd on disk: one route pair, addressed by an opaque output-binding id rather than by a path.

patchyWeb registers a binding when a box opens a document file and drops it again when the run stops, so the browser never names a path and a stale id resolves to nothing. Registration refuses anything that is not a .psd inside that document's own files directory, both requests are refused unless they are same-site, and a write to a read-only document is refused as well.

Writes are compare-and-swap on the file's content hash. A GET returns the hash with the bytes, a PUT has to send the hash it is replacing, and a PUT whose hash does not match the file on disk is answered with a conflict rather than overwriting whatever arrived in the meantime. The new bytes have to open with a PSD signature and fit the size limit, and they land through a temporary file moved into place rather than by writing over the original.

src/jvmMain/kotlin/afield/app/server/PatchyWebStorage.kt — read the source

Server.kt

The Field server: the Ktor HTTPS and WebSocket application that holds open documents, runs box code and answers the browser's RPC messages.

Server starts the HTTPS listener (and the loopback HTTP mirror), mounts the /ws socket and the static and iframe routes such as /browse, /inspect and /logs, and installs the message handlers. Each connected browser has a SessionInformation for the document it shows; sessions maps sockets to those, and documents maps document names to them. OutputBackPressure limits how much box output is in flight to a client at once. Command-line options such as --workspace and --document are read here as home and documentArgument.

src/jvmMain/kotlin/afield/app/server/Server.kt — read the source

Top-level functions

Declaration Summary
fun updateBoxVisibility(box: Box, sessionInfo: SessionInformation)
fun parseLayersList(layersString: String): List<String>
fun SessionInformation.scheduleAutorunBoxes(delayMs: Long = 500L)
fun SessionInformation.autorunBoxes(targetBoxes: Collection<Box>, delayMs: Long = 500L)
fun languageNameForProperty(box: Box, property: String): String

Top-level values

Declaration Summary
val documentReadyHooks Run once for each document when it has loaded, beside its autorun boxes (scheduleAutorunBoxes). AI4 uses it to arm the build waits saved with its box chats again after a restart.
var sessions
var documents
var min_frame_delay
val sessionOutputPressure Per-WebSocketSession back pressure state for output messages.
val home
val app
val documentArgument --document <name>: the document a fresh window should open, instead of the welcome page and instead of whatever was open last. Blank when not given; normalised to a document name by startupDocumentNameFrom.
val preferSystemBrowser: Boolean
val visibleBackdoorPlaywright: Boolean
var lastFn: String?
var recentFns by PersistentString
var fieldPreferencesJson by PersistentString
val thisSession
val thisSessionInfo
var globalPlaywright: Playwright?
var globalPlaywrightBrowser: Browser?
var globalPlaywrightBrowserContext: BrowserContext?
var globalPlaywriteContext: Page?
var playwrightUsesElectron: Boolean
var globalElectronCapture: afield.tools.docs.ElectronCaptureBrowser?
var globalServerPort: Int
var globalLoopbackHttpPort: Int?

SessionInformation

class SessionInformation(val filename: String, val documentName: String = filename, var boxes: ConcurrentHashMap<String, Box> = ConcurrentHashMap<String, Box>(), var root: Box, var sessions: MutableSet<WebSocketSession> = ConcurrentHashMap.newKeySet<WebSocketSession>(), var topologyGeneration: Int = 0, var autorunTriggered: Boolean = false, val ephemeral: Boolean = false, val readOnly: Boolean = ReadOnlyDocuments.global)

Declaration Summary
val filename: String
val documentName: String
var boxes: ConcurrentHashMap<String, Box>
var root: Box
var sessions: MutableSet<WebSocketSession>
var topologyGeneration: Int
var autorunTriggered: Boolean
val ephemeral: Boolean
val readOnly: Boolean This session may read its document but must never write back to it.
var viewport: Rect? The latest viewport a browser reported for this document; see Box.viewport.
var viewports: List<Rect> The latest per-pane visible world rects a browser reported; see Box.viewports.
var keepRunningWhenDetached: Boolean Keep this document's windows and running boxes when its last client leaves; see AgentOpenDocument.keepRunning. Set by an automation client that asked for it.
val writesToDisk: Boolean False when nothing in this session may reach the document's directory on disk.
var loading: Boolean
var documentExisted: Boolean Whether this document was already on disk when it was first opened.
var attachedToLiveDocument: Boolean Whether the open that produced this session joined a document already held in memory.
var agentManaged: Boolean
val pendingSidecarSaves
var savedFingerprint: Long? afield.document.persistence.PersistedFingerprint of this document when it last matched disk: set when the loader has read it and at every save; null for a document never read or saved. A save is due when the current fingerprint differs, even with nothing in pendingSidecarSaves.
var savedFingerprintEntries: Map<Pair<String, String>, Long>? The per-property hashes behind savedFingerprint, for the unmarked-edit tripwire.
var visibleLayers: MutableSet<String>
var activeLayer: String?
val fileWatchManager: FileWatchManager
fun markSidecarPending(uid: String, property: String)
fun hasPendingSidecarSave(uid: String, property: String): Boolean
fun pendingSidecarSnapshot(): Map<Pair<String, String>, Long>
fun clearSavedSidecars(snapshot: Map<Pair<String, String>, Long>)
fun recordPropertySyncOrigin(uid: String, property: String, value: String, clientId: String?, clientSequence: Long?)
fun nextSetStringSync(uid: String, property: String, value: String): Set_string
fun cleanup() Tear this document's runtime down: stop every running box, dispose any Field 4 renderer/window its boxes own, and dispose each box's Kotlin engine.

ReadOnlyDocuments

object ReadOnlyDocuments

Open documents without writing back to them.

Declaration Summary
val global: Boolean by lazy Every session in this process is read-only.
fun blocksWritesTo(documentPath: String): Boolean True when documentPath must not be written.
fun blocksWritesFrom(box: Box): Boolean True when nothing box does may reach the filesystem.
fun blocksWritesFromCurrentSession(): Boolean True when the ambient session must not write, for producers that take no box either.
fun refuseWrite(producer: String, target: String) Record that a write was refused, so a refusal is never a silent no-op.

OutputBackPressureSnapshot

data class OutputBackPressureSnapshot(val pending: Int, val skipped: Int, val buffered: Int, val maxPending: Int, val bufferSize: Int)

Tracks per-session back pressure for output messages. When the frontend is slow to acknowledge outputs, the pending count rises. Once it reaches maxPending, new outputs are held in a rolling buffer of size bufferSize. Messages that overflow the buffer are counted as truly skipped. When pressure eases, the buffered messages are flushed after the skip notification — they are never counted as skipped.

Declaration Summary
val pending: Int
val skipped: Int
val buffered: Int
val maxPending: Int
val bufferSize: Int

OutputBackPressure

class OutputBackPressure(val maxPending: Int = 8, val bufferSize: Int = 4)

Declaration Summary
data class Drained(val frame: String, val slotted: Boolean) A frame drainFrames released, and whether it took a pending slot.
fun tryAcquire(): Int Try to acquire a send slot. Returns the number of previously truly skipped messages (≥ 0) if a slot is available, or -1 if the caller should buffer the message instead (back-pressured).
fun bufferMessage(message: String, coalesceKey: String? = null) Buffer a message for later delivery (call when tryAcquire returns -1).
fun enqueueBehind(message: String): Boolean Queue a frame that is not back-pressured (an output-control clear or open, say) behind the buffered outputs, so it cannot overtake them. Returns false, queueing nothing, when the buffer is empty: the caller sends it directly. The frame takes no slot, is never evicted and never coalesces.
fun takeSkipped(): Int The skipped count since it was last taken, reset to zero.
fun hasBufferedMessages(): Boolean
fun snapshot(): OutputBackPressureSnapshot
fun isDrained(): Boolean
fun drainBuffer(): List<String> Drain the rolling buffer, acquiring a pending slot for each recoverable message. Returns messages that were successfully acquired; messages that still cannot acquire a slot remain buffered for the next acknowledgement cycle.
fun drainFrames(): List<Drained> drainBuffer, saying which frames took a slot; unslotted frames leave as soon as they reach the head.
fun release() Release a pending slot — call when the frontend acknowledges receipt.
fun releaseAndDrain(): Pair<Int, List<Drained>>

Server

class Server

StopWatch.kt

Tells a box's clients when a stop has not taken effect, so the box's status marker can say "asked to stop, not responding".

A stop is carried out on the runloop. When the runloop is blocked (a box sleeping or in a busy loop, or a propUp pull of one, whose nested pump does not advance Runloop.frame), nothing stops until the block ends, and the client's own running state can already say "stopped" because the queued launch was cancelled. So the test is server-side: the stop is pending while the runloop has not finished a tick since the stop was asked for, or while the box is still inside a run. It is watched from a daemon thread, because a blocked runloop cannot report on itself.

src/jvmMain/kotlin/afield/app/server/StopWatch.kt — read the source

UserThemeManager.kt

Serves the user's CSS theme from ~/.afield/theme.css to the browser and pushes it again whenever the file changes.

UserThemeManager.start creates the file with seed tokens if it is missing, then polls its modification time and size and broadcasts the new CSS to every client on a change. UserThemeManager.sendTo sends the current theme to one session.

src/jvmMain/kotlin/afield/app/server/UserThemeManager.kt — read the source

UserThemeManager

object UserThemeManager

Owns ~/.afield/theme.css and its delivery to connected clients.

Declaration Summary
fun start()
fun sendTo(session: WebSocketSession?)

agentRouteAccess.kt

src/jvmMain/kotlin/afield/app/server/agentRouteAccess.kt — read the source

agent_backdoor.kt

src/jvmMain/kotlin/afield/app/server/agent_backdoor.kt — read the source

Top-level functions

Declaration Summary
fun installAgentBackdoorHandlers()

arguments.kt

Command-line option parsing for the server: option reads a typed --name value pair and flag tests for a bare --name.

src/jvmMain/kotlin/afield/app/server/arguments.kt — read the source

Top-level functions

Declaration Summary
inline fun <reified T> option(name: String, def: () -> T): T The value following --name on the command line, converted to T by convertTo, or def() when the flag is absent. Throws when the flag is the last argument.
fun flag(name: String): Boolean True when --name was given on the command line, with or without a value.
fun <T> convertTo(target: Class<T>, value: String): T

asciiWelcome.kt

src/jvmMain/kotlin/afield/app/server/asciiWelcome.kt — read the source

Top-level values

Declaration Summary
var asciiWelcome

assimpBrowsing.kt

src/jvmMain/kotlin/afield/app/server/assimpBrowsing.kt — read the source

Top-level functions

Declaration Summary
fun registerAssimpBrowsingAdapters() One-time registration of Browsable adapters for AssImp + GLTF helpers.

browsing.kt

The object inspector behind /browse: a live, clickable tree of any value, reached from a box with Inspector.linkFor.

A value takes part by implementing Browsable (a heading, a directory of named children, optional HTML) or HasBrowsable. Other values fall back to BrowsableAdapters and to the HTML renderers registered with LeafRenderers. installInspectorHandler installs the socket handler the browser's inspector page talks to.

src/jvmMain/kotlin/afield/app/server/browsing.kt — read the source

Top-level functions

Declaration Summary
fun registerAllBrowsingAdapters() Every BrowsableAdapters registration the server makes, in one place. A register*BrowsingAdapters() function defined anywhere in src/jvmMain must be called from here, or column()/tree() never see its types (the Assimp/glTF adapters sat unregistered from their introduction until 2026-10-28); InspectorTest scans the source and fails on one that is not. Idempotent.
fun installInspectorHandler()
fun registerExampleRoots()
fun registerExampleAdapters()

Browsable

interface Browsable : LiveGraphInspectable

Anything you want to expose in the inspector implements this. A dir is also this object's LiveGraphInspectable edges, so graphPaths(...) reaches the same children the inspector lists.

Declaration Summary
fun heading(): String
fun dir(): Map<String, Any?>?
override fun liveGraphChildren(): Iterable<LiveGraphEdge>
fun html(): String?
fun version(): Long

HasBrowsable

interface HasBrowsable

Wrapper: keep all inspector logic inside this method. If null, treat as null.

LeafRenderers

object LeafRenderers

Register custom HTML renderers for non-Browsable values (leaf fallback).

Declaration Summary
fun <T : Any> register(klass: KClass<T>, renderer: (T, Browsable?, String?) -> String?)
fun render(v: Any, parent: Browsable?, title: String?): String?

RootsProvider

typealias RootsProvider = () -> Map<String, Any?>

Inspector

object Inspector

Declaration Summary
const val DEFAULT_PAGE Entries per open/more reply. A directory larger than this is paged; see handle.
fun setRoots(provider: RootsProvider)
fun linkPathFor(obj: Any, browsePath: String = "/browse", uid : String? = null, options: GraphWalkOptions = GraphWalkOptions()): String Make a deep-link URL (path) for an object. Multiple calls can produce different tokens; tokens are process-local and may expire if the object is GC'ed. If you need absolute URLs, prepend your own origin, e.g. "http://localhost:9092" + linkPathFor(obj).
fun linkFor(obj: Any, browsePath: String = "/browse", uid : String = Uuid.random().toString(), options: GraphWalkOptions = GraphWalkOptions()): String Convenience alias naming matches your phrasing.
fun changed(obj: Any) Notify watchers that a Browsable changed. Accepts Browsable, HasBrowsable, or raw object.
fun handle(text: String, clientKey: Any, push: (String) -> Unit): String
fun handle(text: String): String
const val QUERY_LIMIT Matches listed per query reply.

Clock

class Clock : Browsable

Declaration Summary
override fun heading(): String
override fun html(): String
override fun version(): Long

Heap

class Heap : Browsable

Declaration Summary
override fun heading(): String
override fun html(): String
override fun version(): Long

Numbers

class Numbers(private val n: Int) : Browsable

Declaration Summary
override fun heading(): String
override fun dir(): Map<String, Any?>

ThreadList

class ThreadList : Browsable

Declaration Summary
override fun heading(): String
override fun html(): String

AdapterFn

typealias AdapterFn = (Any, Browsable?, String?) -> Browsable?

BrowsableAdapters

object BrowsableAdapters

Declaration Summary
fun <T : Any> register(klass: KClass<T>, adapter: (T, Browsable?, String?) -> Browsable?)
fun adapt(v: Any, parent: Browsable?, title: String?): Browsable?

ToStringHighlighter

object ToStringHighlighter

Declaration Summary
fun render(src: String): String

chat.kt

The server side of the Discussion thread that a slideshow step or a box can show: two RPC handlers that carry messages between Field's clients and an outside chat service.

A thread is addressed by an id and a number, and the pair is a name rather than a location: withChat(id) on a step can hand it a template id, so that every document made from the same template shares one thread, and Box.withChat passes the box's own id. load-chat-messages fetches a thread and replies only to the client that asked for it; post-chat-message sends one and then broadcasts chat-message-posted to every client of the session, so any open widget reloads the thread. Field's browser never calls the service itself.

Field ships no such service. The Python one was removed from the repository in October 2026, so unless something compatible is listening on CHAT_SERVER_URL the Discussion UI appears, loads an empty thread and drops what is posted into it. The rest of a slideshow is unaffected by that: the requests run outside slideshow execution, a failed load is answered as an empty list, and a failed post is logged and not answered at all. dev_docs/GUIDE_CHAT_QUICKSTART.md lists what a replacement backend would have to serve.

src/jvmMain/kotlin/afield/app/server/chat.kt — read the source

Top-level functions

Declaration Summary
fun chat() Install Field's guide-chat RPC bridge.

controllers.kt

The phone controller surface: the /controllers routes that let a second device see and drive the selected box's controls and outputs.

installControllerRoutes mounts the controller page, a /controllers/controls and /controllers/outputs JSON view, and the /controllers/ws socket, which carries control edits and touch gestures back into Field. The recordController... and broadcastControllerControlUpdates functions keep connected controllers current as output, selection and control values change in the editor.

src/jvmMain/kotlin/afield/app/server/controllers.kt — read the source

Top-level functions

Declaration Summary
fun Route.installControllerRoutes(preferences: () -> FieldPreferences)
fun recordControllerOutputMessage(info: SessionInformation?, message: Message)
fun recordControllerOutputMessage(targetDocument: String, message: Message)
fun recordControllerSelection(info: SessionInformation, selection: ControllerSelectionState)
fun broadcastControllerControlUpdates(controls: List<ControllerControl>, targetDocument: String = "")
fun documentSelection(info: SessionInformation): ControllerSelectionState? What info's pages last said was selected — the boxes, and the property their editor shows — or null if none has said. The newest report wins when several pages are open on one document.

dependencies.kt

One websocket handler, dependency-graph, which is how the dependency layer asks what it draws.

The reply is PropertyDependencies' record for the document of the page that asked: every producer to consumer flow it has seen, and every read that found nothing. Sending it with action = "clear" forgets that document's record first, which is the palette's Clear Dependency Record.

Nothing calls dependencyGraphHandlers; the startup scan finds it by its @RunsAt annotation and runs it while handlers are being installed. A page with no document session yet, or a record that fails to build, is answered with an empty graph carrying the reason rather than with an error, so the layer can say why it is empty.

src/jvmMain/kotlin/afield/app/server/dependencies.kt — read the source

Top-level functions

Declaration Summary
fun dependencyGraphHandlers() dependency-graph: the dependency layer's view of PropertyDependencies for the asking page's document — every producer → consumer flow and every unanswered read — or, with action = "clear", forget the document's record first.

docsAnnotations.kt

Notes written on a documentation page, and the /field-docs/ mirror those pages are read through.

Three things live here. The access policy and its guards, which every docs route in Field is checked against. The annotation notebook: the types a note is made of, the store that keeps them in one JSON file, and the /api/docs-annotations… routes the panel reads and writes. And the mirror itself, which by default serves this checkout's built site when there is one and otherwise proxies the published site, injecting the annotation panel into each page either way. AFIELD_DOCS_MIRROR_SOURCE pins it to one or the other.

Access control

Two groups of routes are installed here, into the same Ktor application as an SSL connector that binds 0.0.0.0 (see Server.kt, httpsHost), because Field genuinely does serve browsers and the visionOS shell on other devices:

  • the annotation routes, /api/docs-annotations…. A note is not a private notepad: it is the prompt the revision pass hands its agents, so whoever can write one can steer a run the human later starts. Loopback and same-site by default, with two hatches (AFIELD_DOCS_ANNOTATIONS_ALLOW_REMOTE, AFIELD_DOCS_ANNOTATIONS_ALLOW_CROSS_SITE) that re-open them to other devices and other websites respectively;
  • the revision pass, /api/docs-improvement/pass… and /docs-pass-site/… (docsPass.kt), which runs coding agents with write access to the checkout docsImprovementCheckout names. Loopback and same-site with both hatches forced shut, so a hatch named for annotations can never open it.

What this does not claim to defend against is another process running as the same user on the same machine. It can reach loopback, and in any case already has whatever access the coding agents would be given. The boundary drawn here is this machine's own user, not this machine.

src/jvmMain/kotlin/afield/app/server/docsAnnotations.kt — read the source

DocsAnnotationAnchor

data class DocsAnnotationAnchor(val exact: String = "", val prefix: String = "", val suffix: String = "", val startOffset: Int = 0, val endOffset: Int = 0, val xRatio: Double? = null, val yRatio: Double? = null, val sourceCommit: String = "", val docsSource: String = "", val heading: String = "", val region: String = "")

Declaration Summary
val exact: String
val prefix: String
val suffix: String
val startOffset: Int
val endOffset: Int
val xRatio: Double?
val yRatio: Double?
val sourceCommit: String The commit the page in front of the human was built from, read by the panel out of the site's field-docs-build.json. tools/docs_notes.py matches the quote against the page's Markdown at that commit, which is what lets a note written days before anyone syncs it still land on the sentence that was actually read. Empty for the published site when it predates that file.
val docsSource: String local or published: which copy of the page the note was written on.
val heading: String The rendered heading the selection sits under, a second anchor when the quote is short.
val region: String content, or chrome for a selection in the site header or navigation rather than the page.

DocsAnnotationEntry

data class DocsAnnotationEntry(val id: String = "", val kind: String = "comment", val author: String = "human", val text: String = "", val atEpochMs: Long = 0, val runId: String = "", val commit: String = "", val changedFiles: List<String> = emptyList(), val handoff: String = "")

One entry in a note's thread: a human reply, an agent's question, resolution or memory, or the harness recording a run that left the note untouched. Written by tools/docs_notes.py and by the routes below; the panel renders them.

Declaration Summary
val id: String
val kind: String comment, memory, question, reply, resolution, reopen, run, handoff, blocked or unblocked.
val author: String human, harness, or agent:<name>.
val text: String
val atEpochMs: Long
val runId: String
val commit: String
val changedFiles: List<String>
val handoff: String For handoff, blocked and unblocked: the id of the handoff note concerned.

DocsAnnotation

data class DocsAnnotation(val id: String = "", val pageUrl: String, val note: String, val tags: List<String> = emptyList(), val anchor: DocsAnnotationAnchor = DocsAnnotationAnchor(), val createdAtEpochMs: Long = 0, val updatedAtEpochMs: Long = 0, val resolvedAtEpochMs: Long? = null, val resolution: String = "", val kind: String = "note", val thread: List<DocsAnnotationEntry> = emptyList(), val source: JsonObject? = null, val handoff: JsonObject? = null, val recordedAtCommit: String = "", val recordedInCheckout: String = "")

The note is shared with tools/docs_notes.py, which reads and writes this same file. Every top-level key that tool writes must be a property here, because DocsAnnotationStore rewrites the whole file from these data classes and ignoreUnknownKeys would silently drop anything else; tools/test_docs_notes.py (KotlinSchemaTest) fails when the two disagree. What only the tool understands lives inside source, which is kept verbatim.

Declaration Summary
val id: String
val pageUrl: String
val note: String
val tags: List<String>
val anchor: DocsAnnotationAnchor
val createdAtEpochMs: Long
val updatedAtEpochMs: Long
val resolvedAtEpochMs: Long?
val resolution: String
val kind: String note — a request, in the revision queue; memory, which an agent left and nobody works; or handoff, a larger task an agent queued as a document for a separate, interactive pass.
val thread: List<DocsAnnotationEntry>
val source: JsonObject? Where the note lives in the source: file, current line range, and the git blame origins that carry it forward through later commits. Owned by tools/docs_notes.py sync; the server only preserves it, and drops it when a human moves the note's anchor.
val handoff: JsonObject? For a handoff: its document (path, under dev_docs/handoffs/), scope (project or crosscutting), blocking, state (open, taken, done, dropped), the page it was filed from and the notes it came from. Owned by tools/docs_notes.py; preserved here.
val recordedAtCommit: String HEAD of recordedInCheckout when the note was first saved; the fallback when DocsAnnotationAnchor.sourceCommit is empty.
val recordedInCheckout: String The checkout whose Field recorded this note.

DocsAnnotationUpsert

data class DocsAnnotationUpsert(val annotation: DocsAnnotation, val reply: String = "")

reply is how the panel answers an agent's question without a route of its own: post the note's id with the reply, and it is appended to the thread as the human's, which puts the note back in the queue (see awaiting_human in tools/docs_notes.py).

DocsAnnotationResolve

data class DocsAnnotationResolve(val resolved: Boolean = true, val resolution: String = "")

docsPass.kt

The documentation revision pass, as a page: /docs-pass.html.

Everything a pass does is tools/docs_notes.py pass …, and works the same from a terminal: make a worktree, drain the notes queue with one agent per page, show the handoffs and the diff, rebuild the docs, commit, land on main, remove the worktree. This file only runs those commands for the page, one long job at a time, keeping the complete output on disk and the last part of it in memory for the page to poll.

Every route here is loopback and same-site and nothing else: a browser on this machine, on one of Field's own pages. The boundary is this machine's own user rather than this machine, and it is what stops the two real attacks, another device on the network and a web page open in your browser. There is no feature switch and no token, because a token scrolls out of the terminal it is printed in, and the page is used from inside Field itself. What that gives up is a backstop for a future change to the CORS or same-site code; the checks themselves are the ones DocsAnnotationRouteSecurityTest pins.

Both annotation hatches are forced shut for these routes. With only two checks left, AFIELD_DOCS_ANNOTATIONS_ALLOW_REMOTE, which is set for taking notes from a tablet, must not turn out to let the tablet start agents, commit, and move main.

src/jvmMain/kotlin/afield/app/server/docsPass.kt — read the source

DocsPassAction

data class DocsPassAction(val action: String, val jobs: Int = 4, val agent: String = fieldAgent(), val tags: List<String> = emptyList(), val pages: List<String> = emptyList(), val mode: String = "never", val path: String = "", val id: String = "", val state: String = "done", val text: String = "", val abandon: Boolean = false, val batch: String = "")

One request from the page. Only the fields its action uses are read, and all of them are checked.

Declaration Summary
val action: String
val jobs: Int
val agent: String
val tags: List<String>
val pages: List<String>
val mode: String
val path: String
val id: String
val state: String
val text: String
val abandon: Boolean
val batch: String A handoff batch: decisions, navigation, captures, site, code or elsewhere.

DocsPassJob

data class DocsPassJob(val name: String = "", val running: Boolean = false, val startedAtEpochMs: Long = 0, val finishedAtEpochMs: Long = 0, val exitCode: Int? = null, val logTail: String = "", val logFile: String = "")

Declaration Summary
val name: String
val running: Boolean
val startedAtEpochMs: Long
val finishedAtEpochMs: Long
val exitCode: Int?
val logTail: String
val logFile: String

DocsPassStatusResponse

data class DocsPassStatusResponse(val workingDirectory: String, val job: DocsPassJob, val status: JsonElement, val statusError: String? = null)

What GET /api/docs-improvement/pass answers. Classes rather than buildJsonObject, whose put("…") calls read as route declarations to ServerSurfaceLedgerTest's discovery.

Declaration Summary
val workingDirectory: String
val job: DocsPassJob
val status: JsonElement docs_notes.py pass status as it printed it, or null when it failed.
val statusError: String?

DocsPassQuickResult

data class DocsPassQuickResult(val exitCode: Int, val output: String)

A quick action's outcome: the command's exit code and everything it printed.

docsSourceFile.kt

A repo source file, from this checkout, for the docs pane.

The docs build links a code span that is exactly a tracked repo path under src/, tools/, server/ or public/ to GitHub (link_repo_source_paths_html in public/docs_hooks.py). Inside Field's own docs pane that is the wrong copy: the file is on this machine, and may be ahead of main. So docsAnnotations.js, which the /field-docs/ mirror injects into each page, intercepts the click and shows the file this route returns.

It is a read of source text, which is exactly the exposure GET /api/source has without a guard (see installFieldApiSameSiteGuard in Server.kt). This one is guarded like the revision pass's routes: loopback and same-site, with both annotation hatches forced shut, so it answers only a browser on this machine, on one of Field's own pages. It serves the four roots the docs link and nothing else: no dev_docs/, no config/, no build output, no path that leaves its root through .. or a symlink.

src/jvmMain/kotlin/afield/app/server/docsSourceFile.kt — read the source

DocsSourceFileResponse

data class DocsSourceFileResponse(val path: String, val text: String, val truncated: Boolean = false)

Declaration Summary
val path: String
val text: String
val truncated: Boolean

documentGuides.kt

src/jvmMain/kotlin/afield/app/server/documentGuides.kt — read the source

Top-level functions

Declaration Summary
fun documentGuideHandlers()
fun Route.installDocumentGuideRoutes() Assets are served only from an already-open document, with the same canonical-path boundary as reads.

dynamicRouting.kt

A registry that maps URI prefixes to directories, so the server can serve files from folders chosen at runtime.

The /local route in Server.kt looks up a request path in dynamicRoutes, longest matching prefix first, and serves the file from the directory registered for that prefix.

src/jvmMain/kotlin/afield/app/server/dynamicRouting.kt — read the source

Top-level values

Declaration Summary
val dynamicRoutes

DynamicDirectoryRoutes

class DynamicDirectoryRoutes

Prefix-to-directory table consulted by the server's /local route.

Declaration Summary
fun addRoute(prefix: String, directory: File)
fun getRoutes(): Map<String, File>

executing.kt

src/jvmMain/kotlin/afield/app/server/executing.kt — read the source

Top-level functions

Declaration Summary
fun executing()

fieldAudioProxy.kt

src/jvmMain/kotlin/afield/app/server/fieldAudioProxy.kt — read the source

Top-level functions

Declaration Summary
fun Route.installFieldAudioProxyRoutes() A same-origin bridge for browser-facing Field Audio UI, limited to an allowlist of paths per method; anything else answers 404.

fileproxy.kt

File proxy boxes: a box whose contents mirror a text file, given by an absolute path or by a path under the workspace or the document folder.

refreshFileProxyBox and refreshFileProxyBoxes reload a proxy from disk, and writeFileProxyContents writes an edit back, reporting the outcome as a FileProxyWriteResult. A workspace or document proxy's path is checked to stay inside that folder; an absolute one is not. fileProxyHandlers installs the create-file-proxy-box handler the editor uses to make one.

src/jvmMain/kotlin/afield/app/server/fileproxy.kt — read the source

Top-level functions

Declaration Summary
fun refreshFileProxyBox(box: Box, notifyMissing: Boolean = false)
fun refreshFileProxyBoxes(info: afield.app.server.SessionInformation, notifyMissing: Boolean = false)
fun writeFileProxyContents(box: Box, contents: String): FileProxyWriteResult Write contents through to the file a proxy box stands for.
fun fileProxyHandlers()

FileProxyWriteResult

enum class FileProxyWriteResult

What writeFileProxyContents did, for a caller that has to tell the cases apart.

Constants: NotAFileProxy, Refused, Written.

h2explorer.kt

src/jvmMain/kotlin/afield/app/server/h2explorer.kt — read the source

Top-level functions

Declaration Summary
fun installH2ExplorerHandler()

importing.kt

Server handlers that import boxes from a dropped metadata.edn or Jupyter notebook, and export Python cells back to .ipynb.

importing installs four message handlers: import-metadata-edn and import-metadata-edn-nested add another document's boxes to the current one with fresh ids, import-ipynb turns notebook cells into boxes (importNotebookCells), and export-ipynb writes a Python root's cells out as a notebook (exportNotebookCells).

src/jvmMain/kotlin/afield/app/server/importing.kt — read the source

Top-level functions

Declaration Summary
fun importing() Handler for importing metadata.edn files from the frontend.
fun importMetadataAsNest(ednText: String, anchorX: Double, anchorY: Double, nestName: String = "", info: SessionInformation = thisSessionInfo.get()): List<Box> Parse ednText, remap its ids to fresh UUIDs, and nest the top-level imported boxes inside a new nest-parent box anchored at document-space (anchorX, anchorY). Returns the created boxes with the nest parent first.
fun importNotebookCells(notebookJson: String, fileName: String, anchorX: Double, anchorY: Double, info: SessionInformation = thisSessionInfo.get()): List<Box> Parse notebookJson and materialize its cells as boxes in info. Returns the created boxes with the python-root parent first. Pure with respect to client sync (callers invoke doResync); safe to call from tests without live sessions.
fun exportNotebookCells(pythonRoot: Box, info: SessionInformation = thisSessionInfo.get()): String Serialize the python-root column headed by pythonRoot as an nbformat-4 notebook JSON string. Inverse of importNotebookCells. Pure (no client sync); safe to call from tests without live sessions.

inspecting.kt

A two-way debug inspector for a box: Box.inspect shows a set of objects in an iframe at /inspect and lets the page edit their fields.

The shape of what is shown is described by a list of TypeDefs. extractMappedData walks the roots against those definitions and records a setter for each editable path, and DebugSocketBridge sends the extract to the page and applies the edits it sends back. initInspecting installs the raw socket handler that routes inspector messages to the right bridge.

src/jvmMain/kotlin/afield/app/server/inspecting.kt — read the source

Top-level functions

Declaration Summary
fun extractMappedData(roots: List<Any?>, specs: List<TypeDef>): ExtractResult
fun applyPathUpdate(setters: Map<String, List<(Any?) -> Unit>>, path: String, newValue: Any?)
fun ExtractResult.toMessage(debugUid: String): String
fun initInspecting()
fun Box.inspect(channel: String = "debug", name: String = "inspect", a: Any?, typeDefs: List<TypeDef>): DebugSocketBridge
fun initDemo(): Pair<MutableMap<String, Any>, List<TypeDef>>

Top-level values

Declaration Summary
val knownDebugInspectors

TypeDef

data class TypeDef(val path: String, val outputPath: String? = null, val type: String? = null, val options: List<String>? = null, val min: Number? = null, val max: Number? = null, val step: Number? = null, val readOnly: Boolean? = null, val priority: Int? = null, val units: String? = null, val unitsHtml: String? = null, val getter: ((Any?, String) -> Any?)? = null, val setter: ((Any?, String, Any?) -> Unit)? = null, val action: ((Any?, String) -> Unit)? = null, val actionTitle: String? = null)

Declaration Summary
val path: String
val outputPath: String?
val type: String?
val options: List<String>?
val min: Number?
val max: Number?
val step: Number?
val readOnly: Boolean?
val priority: Int?
val units: String?
val unitsHtml: String?
val getter: ((Any?, String) -> Any?)?
val setter: ((Any?, String, Any?) -> Unit)?
val action: ((Any?, String) -> Unit)?
val actionTitle: String?

ExtractResult

data class ExtractResult(val data: Any, val typeDefs: List<TypeDef>, val setters: MutableMap<String, MutableList<(Any?) -> Unit>>)

Declaration Summary
val data: Any
val typeDefs: List<TypeDef>, val setters: MutableMap<String, MutableList<(Any?) -> Unit>>

DebugSocketBridge

class DebugSocketBridge(private val roots: List<Any?>, private val specs: List<TypeDef>, var sendFn: (String) -> Unit)

Declaration Summary
fun sendInitial(debugUid: String)
fun handleMessage(text: String)

keyframeBrowsing.kt

The server half of the keyframe collection browser: one /ws raw channel, no new HTTP route.

The page itself is keyframe_browser.html, served by the static mount over src/commonMain/resources, so nothing here widens Field's HTTP surface — which matters, because every verbless route(…) { handle { … } } is an unauthenticated all-methods endpoint and there is a census test counting them (FieldApiCorsExposureTest). A raw channel rides the socket the client already has, the way the h2 explorer and the log stream do.

What crosses the wire

Structure as JSON, presentation as HTML. The collection summary, the slot bands and the version rows are typed payloads the page lays out itself; individual values come over as rendered HTML from .h, which is what reusing the value visualizers buys: a Vector3f arrives with its axis widget already drawn, and a type that registers its own printer gets it for free.

Read-only

There is an apply op, because putting a saved look back is the reason you were browsing. There is no edit op. Everything on disk is append-only by construction — saveKey never overwrites a numbered version and an unreadable manifest is quarantined rather than replaced — and an in-browser edit button would break that. Getting a modified look back out goes through updateCode, which emits pasteable Kotlin.

src/jvmMain/kotlin/afield/app/server/keyframeBrowsing.kt — read the source

Top-level functions

Declaration Summary
fun registerKeyframeBrowser(root: Path, pattern: String, scope: KeyframeScope? = null): String Register a directory + glob for browsing and return the token the iframe passes back on every request.
fun installKeyframeBrowserHandler()

logstream.kt

The log archive behind the /logs iframe: LogStream keeps recent log records (still printing them to stdout) and streams them to the page.

The page can ask for a filtered subset of the archive and then receive new entries live. installLogStreamHandler installs the socket handler it uses.

src/jvmMain/kotlin/afield/app/server/logstream.kt — read the source

Top-level functions

Declaration Summary
fun installLogStreamHandler()

LogStream

object LogStream : LogSink

Capture all log events (while still emitting to stdout) and expose a lightweight streaming API similar to browsing.kt. Designed for an iframe widget that can request subsets of the log archive and stream future entries live.

Declaration Summary
data class Stored(val seq: Long, val event: LogEvent)
data class Filter(val channels: List<String>? = null, val messageContains: String? = null, val messageRegex: String? = null, val blockId: String? = null, val includeBlockChildren: Boolean = false, val sinceEpochMs: Long? = null, val afterSeq: Long? = null, val limit: Int = 500, val tail: Boolean = true)
data class Subscribe(val subId: String, val filter: Filter)
data class Unsubscribe(val subId: String)
data class Ping(val subId: String? = null)
data class Ready(val type: String = "logs/ready", val subId: String, val replayed: Int, val nextSeq: Long)
data class LogPush(val type: String = "logs/event", val subId: String, val seq: Long, val channel: String, val message: String, val timestampEpochMs: Long, val timestampIso: String, val indent: String, val blockId: String? = null, val boundary: String? = null)
data class ErrorPush(val type: String = "logs/error", val subId: String? = null, val message: String)
override fun log(event: LogEvent)
fun addSubscription(subId: String, filter: Filter, sender: (String) -> Unit)
fun unsubscribe(subId: String)
fun ensureSinkRegistered()

offlineImport.kt

src/jvmMain/kotlin/afield/app/server/offlineImport.kt — read the source

Top-level functions

Declaration Summary
fun unpackDocumentZip(zip: ByteArray, targetDir: File): Int Unpacks a document zip, as the docs pages ship them, into targetDir, for /api/import-offline-doc: metadata.edn together with its sidecars and every folder beside it (saved chats, study revisions, recorded conversations), which an ednText import leaves behind.

refactorPass.kt

The server side of the refactor-pass review panel: two routes that drive tools/refactor_pass.py in the docs-improvement checkout, one job at a time.

GET /api/refactor-pass answers status, a review item, a change or the delivery diff by running the CLI and parsing its JSON; POST starts an action, or cancels the running one. Both refuse a request that is not a same-site docs client, because the answers carry private workspace source.

No part of a request reaches a shell. The browser sends an action name and fields, and refactorPassArguments rebuilds the argument list from a fixed set of actions, so a field that fails its length, character-class or absolute-path check yields no command at all. A started job streams its output to a log file under the checkout's .field-backdoor/refactor-pass/jobs, keeping the last 24,000 characters in memory for the panel to poll; a query that has not answered in a minute is killed along with its descendants and reported as a timeout.

src/jvmMain/kotlin/afield/app/server/refactorPass.kt — read the source

rendererBrowsing.kt

src/jvmMain/kotlin/afield/app/server/rendererBrowsing.kt — read the source

Top-level functions

Declaration Summary
fun registerRendererBrowsingAdapters() One-time registration of the Renderer/graphics Browsable adapters, so column("r", renderer) and tree("r", renderer) open the renderer as targets (in draw order), modules with their shaders and bindings, textures, CPU span timings and profiler state. Called from installInspectorHandler at server start; idempotent.

signalViewer.kt

The HTTP side of the frontend signal viewer: one route that answers range queries over a SignalViewer a box is currently showing.

GET /signal-viewer/{id} takes a start, an end and a points count and returns that much of the signal as JSON, which is how the plot zooms and pans without the box running again. The id names an entry in the viewer's weak registry, so once a box replaces its output the viewer it was showing becomes collectable and the route answers 410 Gone rather than resampling a signal whose evaluators have been released.

Sampling is confined to two threads and abandoned after twelve seconds, and the window call is handed a check that throws once the client has gone, so an abandoned zoom stops between sample batches instead of finishing a query nobody will read.

src/jvmMain/kotlin/afield/app/server/signalViewer.kt — read the source

Top-level functions

Declaration Summary
fun Route.installSignalViewerRoutes() Read-only, opaque source IDs; never accepts or compiles code from a browser request.

templating.kt

src/jvmMain/kotlin/afield/app/server/templating.kt — read the source

Top-level functions

Declaration Summary
fun templating()

Top-level values

Declaration Summary
val templateServers: List<String> by lazy Remote metadata-template servers, read once from AFIELD_TEMPLATE_SERVERS or, when the environment variable is absent, afield.templateServers.

RemoteTemplate

data class RemoteTemplate(val name: String, val has_edn: Boolean)

TemplateMetadata

data class TemplateMetadata(val name: String, val lastModified: Long, val boxDocumentation: String, val source: String = "template")

Declaration Summary
val name: String
val lastModified: Long
val boxDocumentation: String
val source: String

terminal.kt

A real terminal in the browser: /terminal.html (xterm.js) over the /pty/<id> WebSocket, with a pseudo-terminal (pty4j) on this side.

Its first job is running an interactive agent (claude or codex) on one handoff batch of the documentation revision pass, started from that batch on /docs-pass.html with one sentence of direction, so the agent works through the batch's handoffs and the human answers it in place instead of copying a command into a terminal of their own.

This hands out a process on this machine, so the boundary is the revision pass's and nothing looser: loopback and same-site, with the annotation hatches forced shut, on every route here and on the WebSocket upgrade. And the client never names a command or a directory: it picks one of the TerminalLaunch kinds, and the server builds the argv and finds the directory. There is no "run this shell line".

Protocol on /pty/<id>

Server to client, binary frames are terminal output, the scrollback first on every connect so a reload re-attaches; a text frame {"type":"exit","code":N} ends it. Client to server, binary or text frames are keystrokes, except a text frame that is a JSON object with "type":"resize", which carries cols and rows. Closing the page detaches; the process keeps running until it exits or is killed through POST /api/terminal/<id>/kill.

src/jvmMain/kotlin/afield/app/server/terminal.kt — read the source

TerminalLaunch

data class TerminalLaunch(val kind: String, val text: String = "", val batch: String = "", val cols: Int = 120, val rows: Int = 36)

What the page may start. kind is claude (in the checkout, text as an optional first message), handoff-batch (in batch's open session worktree, text as the human's direction), land-watch (tools/land.py status --watch in the checkout, for /land.html; text is ignored; plain glyphs, since xterm.js has no Nerd Font), or refactor-batch, which refactorPass.kt plans instead and which needs an imported refactor plan.

TerminalInfo

data class TerminalInfo(val id: String, val kind: String, val title: String, val directory: String, val startedAtEpochMs: Long, val running: Boolean, val exitCode: Int? = null, val batch: String = "")

Declaration Summary
val id: String
val kind: String
val title: String
val directory: String
val startedAtEpochMs: Long
val running: Boolean
val exitCode: Int?
val batch: String The handoff batch a handoff-batch terminal works on, so the page can show it there.

upstreamcheck.kt

Field is distributed as source: you clone it and you build it. So "is there an update" is not a question about a version manifest and a zip, it is a question about your checkout and its remote — and git already answers it. This asks, and reports. It never writes to the working tree; pulling is yours to do, because only you know whether your checkout has work in it.

src/jvmMain/kotlin/afield/app/server/upstreamcheck.kt — read the source

Top-level functions

Declaration Summary
suspend fun checkUpstream(repoDir: File = File("."), remote: String = "origin", timeoutSeconds: Long = 20): UpstreamStatus Asks remote what it has on this branch, and — only if that differs from HEAD — fetches so the new commits can be described. The cheap case costs one git ls-remote; nothing is fetched, and no ref is moved, when you are already up to date.
fun updateCheckMessages(status: UpstreamStatus, maxNotices: Int = 4): List<String> The lines to show for status, most important first. Notices come out whole and unabbreviated — a commit only carries one when someone decided every puller needs to read it.

Top-level values

Declaration Summary
const val NOTICE_TRAILER The trailer a commit carries when it has something to say to everyone who pulls it.

UpstreamCommit

data class UpstreamCommit(val sha: String, val subject: String, val notices: List<String>)

One commit that origin has and this checkout does not.

Declaration Summary
val sha: String
val subject: String
val notices: List<String> The commit's NOTICE_TRAILER values, in the order they appear. Usually empty.

UpstreamStatus

sealed interface UpstreamStatus

Declaration Summary
data class UpToDate(val remoteRef: String, val ahead: Int) : UpstreamStatus HEAD is at the remote branch's tip, or ahead of it.
data class Behind(val remoteRef: String, val behind: Int, val ahead: Int, val commits: List<UpstreamCommit>) : UpstreamStatus The remote branch has behind commits this checkout does not.
data class Unavailable(val reason: String) : UpstreamStatus The question could not be asked, and why: no git, no checkout, no remote, no network.

PersonalLayer.kt

Loads the gitignored src/personal jar in its own classloader between core Field and the REPL, so box code can use personal classes and the layer can be replaced without restarting.

PersonalLayer owns the loader; PersonalReload.kt handles the objects an old layer leaves behind after a reload.

src/jvmMain/kotlin/afield/app/settings/PersonalLayer.kt — read the source

PersonalLayer

object PersonalLayer

The classloader layer that holds src/personal.

Declaration Summary
const val CLASSPATH_PROPERTY Set by requiredRunJvmArgs in build.gradle.kts; File.pathSeparator-separated.
val roots: List<File> by lazy The roots making up the layer, or empty when there is no personal code — which is the normal state for anyone but Marc, and also what you get before the first ./gradlew personalJar.
val isPresent: Boolean
var generation: Int How many times advance has run. Stamped into each layer's name and into the files it reads, so a stale object can be attributed to a specific reload in an error message.
val activeRoots: List<File> The files the live layer actually reads: a per-generation copy of roots.
fun wrap(base: ClassLoader): ClassLoader The layer built on top of base, creating it on first use.
fun advance(): Int? Retire every live layer and move to the next generation. The next wrap/current builds a fresh layer over a fresh copy of the jar; callers are responsible for disposing the engines that were using the old one (see reloadPersonal).
val hasRetiredLayers: Boolean @Synchronized Whether advance has retired a layer that is still listed. PersonalReload.kt's staleness checks return early when it is false, so nothing walks the retired list on the normal path.
fun retiredGenerationOf(type: Class<*>?): Int? The generation that owned type, when type came from a layer a later reload retired, which is what makes an object holding it stale. Null for everything else, including classes from the live layer.
val current: ClassLoader The layer over the loader that loaded core Field.

PersonalLayerSmokeTest.kt

Guards the PersonalLayer boundary: src/personal is off the app -cp and reachable only through its own classloader.

Each of these fails silently rather than loudly if it regresses — box code stops resolving personal symbols with an ordinary "unresolved reference", which looks like a user typo; and completion just returns fewer candidates. Hence a test rather than trust:

  1. personal really is off -cp (otherwise the layer is decorative: the classes would resolve from the app loader anyway),
  2. PersonalLayer.wrap is idempotent (Server.main installs the layer as the context classloader, so KI2's wrap(contextClassLoader) must not stack a second layer on top of the first),
  3. and the K2 REPL both compiles and runs against it. Compilation is the subtle half: it works only because dependenciesFromClassloader(wholeClasspath = true) walks the parent chain collecting each loader's URLs, which is behaviour of the Kotlin scripting library, not of anything here.

Skips (exit 0) when there is no src/personal, which is the normal state for everyone but Marc.

src/jvmMain/kotlin/afield/app/settings/PersonalLayerSmokeTest.kt — read the source

PersonalLayerSmokeTest

object PersonalLayerSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

PersonalReload.kt

Reloading PersonalLayer at runtime, and coping with the objects the old one left behind.

reloadPersonal retires the live layer so the next engine compiles and runs against a freshly built afield-personal.jar. The hard part is not the swap, it is that every already-evaluated snippet holds classes from the layer that just died: an object made by gen-0 kiwi.Solver and an object made by gen-1 kiwi.Solver are unrelated types, and mixing them produces the JVM's least helpful error, class kiwi.Solver cannot be cast to class kiwi.Solver.

Two halves, a scan up front and a diagnosis at the point of failure:

  • scanForStaleClasses is a shallow, best-effort look at what an engine is holding, so the common case — a box with a personal object in a top-level val — gets caught and reset up front. It is incomplete; its own docs list what it does not look at.
  • staleLayerDiagnosis catches what the scan missed, at the moment it actually breaks, and says so in English. Because recovery is a one-click reset rather than a debugging session, the scan does not have to be good — it only has to keep most boxes from visibly hiccuping.

Neither half re-runs anything. A box whose engine was recycled is marked (needsRerun) and left alone: Field does not execute user code that the user did not ask to execute, and a reload is not an exception to that — re-running a box can open windows, start strands and hit the network.

src/jvmMain/kotlin/afield/app/settings/PersonalReload.kt — read the source

Top-level functions

Declaration Summary
fun reloadPersonal(): PersonalReloadReport Retire the live personal layer and pick up whatever ./gradlew personalJar last built.
fun installPersonalReloadAction(root: Box)
fun scanForStaleClasses(box: Box): String? A one-peel look for classes belonging to a retired layer, returning a human-readable description of the first thing found, or null.
fun scanEngineForStaleClasses(ki: KotlinScriptEngine): String? The engine half of scanForStaleClasses — its declarations, without a box's property bag.
fun staleLayerDiagnosis(box: Box, throwable: Throwable): String? If throwable looks like a box tripping over classes from a retired layer, an explanation fit to show in place of the JVM's — and the box is flagged needsRerun on the way past, since that is exactly the state it is in.

Top-level values

Declaration Summary
var Box.needsRerun: String? Why this box needs re-running, or null if it doesn't. Set when the box's engine is discarded out from under it; cleared when the box is next executed (see executeInBox).
const val NEEDS_RERUN_PROPERTY

PersonalReloadReport

data class PersonalReloadReport(val retiredGeneration: Int?, val newGeneration: Int, val examined: Int, val reset: List<Pair<Box, String>>)

Summary of one reloadPersonal.

Declaration Summary
val retiredGeneration: Int?
val newGeneration: Int
val examined: Int
val reset: List<Pair<Box, String>>
val skipped: Int
override fun toString(): String

UserKeybindingsManager.kt

Reads, generates and live-reloads ~/.afield/keybindings.yaml, the user's keyboard shortcut overrides, and sends the resolved bindings to connected clients.

UserKeybindingsManager holds the state; installUserKeybindingsHandlers registers its websocket handlers.

src/jvmMain/kotlin/afield/app/settings/UserKeybindingsManager.kt — read the source

Top-level functions

Declaration Summary
fun installUserKeybindingsHandlers()

UserKeybindingsManager

object UserKeybindingsManager

~/.afield/keybindings.yaml — the user's shortcut overrides, watched and applied live exactly like theme.css (see UserThemeManager).

Declaration Summary
fun watchFile(file: File?) Watches file instead of ~/.afield/keybindings.yaml — or the default again, when null — and re-reads immediately.
val resolved: UserKeybindings The bindings in force right now, exactly as the clients were last told about them.
fun start()
fun sendTo(session: WebSocketSession?)
fun reloadNow() Re-reads the file now and pushes it to every client, whatever the watcher has seen.
fun applyInventory(reported: CommandInventory) Records what commands exist and makes sure the file mentions all of them: it is written from scratch when absent, and otherwise only ever appended to, so nothing the user wrote is lost.

util_settings.kt

Property delegates that keep a single String, Int or Double value across Field restarts, one small file per property name under .field/ in the working directory.

Declare a property by PersistentString { "default" } (or PersistentInt, PersistentDouble). The first read loads .field/<propertyName> through PersistOnExit.load and falls back to the default when the file is missing; a write records the value in PersistOnExit.needingPersistance. A daemon thread writes pending values about once a second, and a shutdown hook writes whatever is left on a graceful exit.

The file is named by the Kotlin property name alone, so two persistent properties with the same name share one file.

src/jvmMain/kotlin/afield/app/settings/util_settings.kt — read the source

PersistentString

class PersistentString(val defaultValue: (() -> String?) = { null })

Declaration Summary
var first
var value: String?
operator fun getValue(f: Any?, property: KProperty<*>): String?
operator fun setValue(f: Any?, property: KProperty<*>, v: String?)

PersistentInt

class PersistentInt(val defaultValue: (() -> Int?) = { null })

Declaration Summary
var first
var value: Int?
operator fun getValue(f: HasProperties, property: KProperty<*>): Int?
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Int?)

PersistentDouble

class PersistentDouble(val defaultValue: (() -> Double)? = { 0.0 })

Declaration Summary
var first
var value: Double?
operator fun getValue(f: HasProperties, property: KProperty<*>): Double
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Double)

PersistOnExit

object PersistOnExit

Declaration Summary
var needingPersistance
var checkThread
fun save(name: String, t: Any?)
inline fun <reified T> load(name: String, storageDirectory: File = File(System.getProperty("user.dir"), ".field")): T?

stdlib_collections.kt

Loop, collection and string shorthands, and the truthiness test for a raw property value.

10 { i -> ... } and (0..9) { i -> ... } call a block once per index and collect the results, and 20 alphas { a -> ... } does the same over evenly spaced values from 0.0 to 1.0 inclusive, which is the loop that parameterises a curve so that it closes. The rest are conversions and slices: list[2..5] for an inclusive copy, List.m and List.s for a mutable copy and a set, String.r for a compiled Regex, "ab" * 3 for repetition. isTrue reads an Any? as a yes or a no, and is true only for true, a non-zero Int and a non-empty String.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/data/collections/stdlib_collections.kt — read the source

Top-level functions

Declaration Summary
fun Any?.isTrue(): Boolean JavaScript-ish truthiness for a value out of a property bag, where you have an Any? and want a yes/no.
inline operator fun <T> IntRange.invoke(to: (Int) -> T): List<T> (0..9) { i -> … } — calls the block once per value in the range and collects the results.
inline operator fun <T> Int.invoke(to: (Int) -> T): List<T> 10 { i -> … } — calls the block for i in 0 until 10 and collects the results.
inline infix fun <T> Int.alphas(to: (Double) -> T): List<T> 10 alphas { a -> … } — calls the block with that many evenly spaced values from 0.0 to 1.0 inclusive, and collects the results.
fun Int.alphas(): SizedIterable<Double> This many values evenly spaced from 0.0 to 1.0 inclusive, as a lazy sized sequence.
operator fun String.times(ln: Int): String "ab" * 3 — the string repeated ln times. Zero or fewer gives the empty string.
operator fun <T> List<T>.get(range: IntRange) list[2..5] — a new list of the elements in that inclusive range, Python-slice syntax with Kotlin's range semantics.

Top-level values

Declaration Summary
val <T> List<T>.m: MutableList<T> This list as a MutableList — a copy, so editing it leaves the original alone.
val <T> List<T>.s: Set<T> This list as a Set, dropping duplicates and keeping first-seen order. A copy.
val String.r: Regex This string compiled as a Regex — "\\d+".r.

ContextResolution.kt

The graph walk that decides which box supplies a value read through fromContext, prop or @exported.

Every lookup starts at the asking box and moves up through its ancestors one generation at a time; the nearest match wins. A ContextResolution.Query says what counts as a match and a ContextResolution.Policy says which parts of each box are searched, and in what order. ContextResolution.explain runs the same walk to the end of the graph and reports the winner, the path to it, and anything it shadowed, as a WhyAnswer.

src/jvmMain/kotlin/afield/document/context/ContextResolution.kt — read the source

ContextResolution

object ContextResolution

The one walk behind every "where does this value come from?" question a box can ask of the graph.

Declaration Summary
fun walk(box: Box, query: Query, policy: Policy, mechanism: Mechanism, explain: Boolean = false): Resolution? Resolve query from box under policy, or null if nothing up the graph matches.
fun peek(box: Box, query: Query, policy: Policy, mechanism: Mechanism): Resolution? walk without explain and without recording the outcome in PropertyDependencies: for a tool asking where a value would come from, which is not the box depending on it.
fun explain(box: Box, name: String? = null, type: String? = null): WhyAnswer "Why does box resolve name (or a value of type) where it does?" as JSON: the mechanism, the supplier, the path of boxes, the match kind, the shadowed and competing candidates, and which sourced preamble line — if any — declared the name. The entry point behind the backdoor's why, the MCP tool field_why and the AI tool WhyDoesThisResolve; see ContextWhy. Those three send WhyAnswer.json unchanged; in a box, .h renders it as a Markdown card and .h2 explores it.
class Search(val resolution: Resolution?, val ancestorsExamined: Int, val boxAncestorsExamined: Int = ancestorsExamined) A walk's answer plus how many ancestors it examined. boxAncestorsExamined leaves out the document root and metaroot, which every box has above it; that is the count Inheritance reports, since "any of its 3 parent boxes" for a box with one parent sent people looking for two boxes that are not on the canvas (backlog N1023-2).
fun isStructuralAncestor(box: Box): Boolean Whether box is a document root or metaroot: above every box, never drawn as a parent.
fun search(box: Box, query: Query, policy: Policy, mechanism: Mechanism, explain: Boolean = false, record: Boolean = !explain): Search walk, also reporting how many ancestors were examined before it stopped.
enum class Mechanism Which user-facing mechanism asked — recorded on the Resolution so an explanation can name it.
enum class MatchKind What kind of slot supplied the value.
enum class OwnVariables How much of the asking box's own variable table a walk may use.
enum class SearchOrder The order the two slot kinds are searched in, within every box on the walk.
data class Policy(val selfCandidate: Boolean, val ownVariables: OwnVariables, val order: SearchOrder) Which parts of each box a walk looks at.
class Query(val description: String, val names: List<String>?, val acceptProperty: (Any?) -> Boolean, val acceptVariable: (KotlinVariableInfo) -> Boolean, val acceptSelf: ((Box) -> Boolean)? = null) What counts as a match.
class Candidate(val value: Any?, val supplier: Box, val matchKind: MatchKind, val matchedName: String?, val depth: Int, val path: List<Box>) One slot that matched a query, and how the walk got there.
class Resolution internal constructor(val mechanism: Mechanism, val query: String, val consumer: Box, winner: Found, private val predecessor: Map<Box, Box?>, val shadowed: List<Candidate>, val competing: List<Candidate>) The answer to one walk: the winning slot, the route to it, and — when asked for — what lost.

ContextWhy.kt

Builds the JSON answer to "why did this value resolve here?" for the backdoor, MCP and AI tools.

ContextWhy.explain takes a box and a name or a type. The answer names the supplying box, the path of parents to it, whether the value was a property or a live variable, and what it shadows. For a name declared in the box's preamble it also names that preamble line and the box it came from.

src/jvmMain/kotlin/afield/document/context/ContextWhy.kt — read the source

ContextWhy

object ContextWhy

"Why did this value resolve here?" — ContextResolution.explain, the answer as JSON, shared by the backdoor's why command, the MCP tool field_why and the in-app AI tool WhyDoesThisResolve.

Declaration Summary
fun explain(box: Box, name: String? = null, type: String? = null): JsonObject
class Declaration(val text: String, val tag: String?, val suppliedBy: Box?, val inOwnPreamble: Boolean) One line of the box's gathered preamble that declares Box.<name>, and where its tag says it came from.
fun isOfType(value: Any, type: String): Boolean Whether value's class, one of its superclasses or one of their interfaces is called type (a fully-qualified or simple name).
fun plain(json: JsonElement): Any? json as plain maps, lists and primitives — what a Jackson-serialised AI tool result carries.

ContextWhyTools.kt

The AI tool WhyDoesThisResolve, which lets the in-app assistant ask where a box's value comes from.

It takes a name or a type and, optionally, a box (the focused one by default), and returns the same explanation as ContextResolution.explain. installContextWhyTools registers it.

src/jvmMain/kotlin/afield/document/context/ContextWhyTools.kt — read the source

Top-level functions

Declaration Summary
fun installContextWhyTools() Offer WhyDoesThisResolve to any conversation focused on a box.
fun whyDoesThisResolve(box: Box, name: String?, type: String?): WhyDoesThisResolveResult WhyDoesThisResolve for box.

WhyDoesThisResolve

data class WhyDoesThisResolve(@JsonPropertyDescription("The name the code reads, e.g. `_speed`, `renderer`. Give this or `type`.") val name: String? = null, @JsonPropertyDescription("A class name, fully qualified or simple (e.g. `afield.graphics.vulkan.Renderer`), to resolve by type.") val type: String? = null, @JsonPropertyDescription("The box asking, by uid or name; the focused box when omitted.") val box: String? = null)

The AI4 tool that explains where a box's inherited or context value comes from (ContextResolution.explain).

Declaration Summary
val name: String?
val type: String?
val box: String?

WhyDoesThisResolveResult

data class WhyDoesThisResolveResult(val summary: String, val explanation: Map<String, Any?>) : SummarisesForToolLog

WhyDoesThisResolve's answer: one sentence, and the whole explanation.

Declaration Summary
override fun toolLogSummary(): String

Mesh_context.kt

The two calls that find a value somewhere up a box's parent chain: fromContext by type, fromContextWithNameOrNull by name.

Both are inline wrappers over ContextResolution's walk, which does the searching. What they add is the reified type argument and the lookup of the box that is asking, so outside a running box there is nothing to search: fromContext throws unless it was given a default, and fromContextWithNameOrNull returns the default or null.

Neither searches the asking box's own top-level variables unless includeLocalVariables is set. That is what keeps val x = fromContext<X>() resolving from a parent on every later run instead of finding the answer it left behind last time.

src/jvmMain/kotlin/afield/document/context/Mesh_context.kt — read the source

Top-level functions

Declaration Summary
inline fun <reified A> fromContext(defaultValue: A? = null, includeLocalVariables: Boolean = false): A Resolve a value of type A from the box context: this box first, then its ancestors, breadth-first.
inline fun <reified A> fromContextWithNameOrNull(name: String, defaultValue: A? = null, includeLocalVariables: Boolean = false): A? Named sibling of fromContext: resolve the value called name (and assignable to A) from the box context.

WhyAnswer.kt

The value a box gets back when it asks where one of its inputs comes from.

ContextResolution.explain and why.<name> both return a WhyAnswer. Printing it gives a one-sentence summary, .h shows it as a card, and .h2 lays out its fields to explore. The full JSON answer stays available as WhyAnswer.json.

src/jvmMain/kotlin/afield/document/context/WhyAnswer.kt — read the source

Top-level functions

Declaration Summary
fun registerWhyH2() The .h2 rendering of a WhyAnswer: its fields to explore, its summary sentence as the headline.

WhyAnswer

class WhyAnswer(val json: kotlinx.serialization.json.JsonObject) : ToHTML

One answer from ContextResolution.explain, with the fields a person reads first pulled out.

Declaration Summary
val query: String What was asked: a name or a type.
val found: Boolean Whether anything up the graph supplies it.
val supplier: String? The box whose value is used, or null when nothing supplies it.
val matchKind: String? Where in the supplier it was found: PROPERTY, VARIABLE or SELF.
val value: String? The value, as text.
val path: List<String> The boxes from this one to the supplier.
val competing: List<String> Boxes as near as the supplier holding a different value — an ambiguity settled by parents order.
val shadowed: List<String> Every other box that also answers, farther away or later in order.
val summary: String The one-sentence explanation.
val markdown: String The answer as a short Markdown table and the summary sentence.
val summaryMarkdown: String summary as Markdown: the sentence already writes names and code between backticks, so those spans are kept as inline code and only the text between them is escaped.
val fields: Map<String, Any?> The fields markdown shows, for .h2 to page through; the whole answer is under json.
override fun toHTML(): String
override fun toString(): String

why.kt

why.speed, a completable way to ask where a box's prop-declared value comes from.

Every prop<T>(name) also declares name on Why, so typing why. in a box lists the names it inherits; each one returns a WhyAnswer. For any other name, or to ask by type, call ContextResolution.explain directly.

src/jvmMain/kotlin/afield/document/context/why.kt — read the source

Top-level values

Declaration Summary
val Box.why: Why why.speed: where this box's speed comes from, as ContextResolution.explain answers it.

Why

class Why(val inside: Box)

The namespace behind Box.why; its members are the val Why.<name> by Whying() lines prop writes.

Whying

class Whying

The delegate prop generates for Why: the property's own name is the question.

Declaration Summary
operator fun getValue(f: Why, property: KProperty<*>): WhyAnswer

Boxes.kt

The Box model: a box's properties, its children/parents graph, and the property sets that decide what is saved and what is sent to the browser.

Box holds its properties in a property bag and implements HasProperties and HasHierarchy. Box.connect and Box.disconnect edit the edge sets (children by default). The companion's ioPersistentProperties lists what is written to metadata.edn, ioSeparateProperties maps the properties written as sidecar files to their extensions (code to kt, shader stages to glsl), and uiResyncProperties lists what is pushed to connected clients when it changes.

src/jvmMain/kotlin/afield/document/graph/Boxes.kt — read the source

Used in: AI4's built-in tools, Keyboard shortcuts, Keyframes: capturing, persisting and replaying live state

Top-level functions

Declaration Summary
fun booleanPropertyValue(raw: Any?): Boolean raw as a yes/no: a Boolean as itself, a string by what it spells (true, yes, on, 1, any case), a number when it is not zero. Anything else, and null, is false.
fun textPropertyValue(box: Box, property: String, value: String): Any The value to store when property of box is set from text — the backdoor's set, the browser's set-string. Text spelling a Boolean (true/false, yes/no, on/off, 1/0) becomes that Boolean for a property that is one: a known Boolean property, an is…Root language marker, or one that already holds a Boolean. Everything else is stored as the text it is, so a code or notes property that happens to read "true" is left alone.
fun attachRootToMetaroot(root: Box) Attach a document under its workspace projects, with the global metaroot above those scopes.
fun detachRootFromMetaroot(root: Box)
fun doResync()
fun syncExecutionStatus()
fun Any?.isTrue(): Boolean
fun box(u: String, createIfMissing: Boolean = true, initIfCreated: Boolean = true, root: SessionInformation = thisSessionInfo.get(), onCreated: ((Box) -> Unit)? = null): Box
fun makeKI(box: Box, reason: String = "makeKI call", superseded: () -> Boolean = { false }): KotlinScriptEngine Build a Kotlin engine for box and queue its stdlib bootstrap on the main runloop.
suspend fun makeKIHere(box: Box, reason: String = "makeKIHere call"): KotlinScriptEngine A fresh engine for box, ready for its code: the box's spare from afield.runtime.kotlin.SpareEngines when it has one, otherwise one built and bootstrapped here.
inline fun Box.visitParents(v: (Box) -> Unit)
fun <T : MutableMap<String, *>> Box.gather(name: String): T
inline fun <reified T> Box.findWithNameOrNull(start: Box, name: String): T?

Top-level values

Declaration Summary
var needsResync Pending (box, property name) pairs that doResync will send to the box's connected clients.
var Box.offline: Boolean
var Box.preamble: String
var Box.introduction: String
var Box.attic: String
var Box.notes: String
var Box.autorun: Boolean Whether opening the document runs this box (SessionInformation.autorunBoxes).
var Box.layers: String Comma-separated membership used by the frontend and server visibility intersection.
var Box.layerFrames: MutableMap<String, Rect> Persisted frame overrides keyed by layer; updated when the session leaves an active layer.
var Box.templateId: String?
const val METAROOT_ID
val metaroot: Box by lazy
var actionTable
val currentActionProperty: ThreadLocal<String>
var intro
val introImports: List<String> intro as individual import statements — the exact set every box gets for free, in a form other subsystems can pass around.

HasProperties

interface HasProperties

Declaration Summary
fun setPropertyRaw(name: String, value: Any, doResync: Boolean = true)
fun getPropertyRaw(name: String, default: Any? = null): Any?
fun removePropertyRaw(name: String, doResync: Boolean = true) Erases name from the bag, leaving it unset rather than set to null.

HasHierarchy

interface HasHierarchy : HasProperties

Declaration Summary
var children: MutableSet<Box>
var parents: MutableSet<Box>

CachedLaunchReturn

data class CachedLaunchReturn(val code: String, val returnValue: Any)

Declaration Summary
val code: String
val returnValue: Any

RunningState

data class RunningState(val watermark: Int, val mode: String)

Declaration Summary
val watermark: Int
val mode: String

Box

open class Box(val id: String, val output: DefaultOutput = DefaultOutput()) : HasOutput by output, HasProperties

Hierarchical Field object identified by a stable id and backed by output.

Declaration Summary
lateinit var inside: SessionInformation
fun isInsideInitialized()
val properties: MutableMap<String, Any?>
val markers: MutableMap<String, Marker>
var ki: KotlinScriptEngine by kiDelegate
val liveKi: KotlinScriptEngine? This box's engine only if one was ever built. Reading ki would build one, which for a box that has never run is both wasteful and misleading — callers that are inspecting engines (see scanForStaleClasses) want to skip such boxes, not create work for them.
var ke: BoxExecution
fun disposeKi() Dispose this box's Kotlin engine (the K2 REPL compilation state: FIR sessions, project environment, ProtoBuf metadata) if it was ever created. Each box owns its own engine, and until now nothing ever disposed it, so every deleted or detached box leaked its whole compilation state (~tens of MB each) -- fatal for long-lived sessions that create/run/delete many boxes (e.g. the docs capture loop). Safe to call repeatedly; a later ki access lazily recreates a fresh engine.
fun replaceKi(engine: KotlinScriptEngine) Install engine as this box's engine, disposing the one it replaces and forgetting what that one had run — the single way to swap engines, and what the reset action, execute-all and afield.workspace.copyBox all go through. Assigning ki directly leaks the old engine's K2 compilation state.
var frame: Rect
var tint: String A colour the canvas lays over this box, to show some state of it: a CSS colour such as "hsla(28, 80%, 56%, 0.2)" or "rgba(255, 184, 108, 0.15)", or "" for none.
var viewport: Rect What the canvas's main pane shows: the world rect (-x, -y, w, h), in world units at any zoom. Sent by the browser; onViewportChange callbacks fire when it moves. A box that has not been told yet (made after the document loaded) reads its document's latest value.
val viewports: List<Rect> Every pane's visible world rect (x, y, w, h) on a split canvas, the main (bottom-right) pane last; a single rect when the canvas is not split, and empty before a browser has reported.
var name: String
var code: String? No code property on an untouched box is distinct from an explicitly empty script.
var customization: String
val thisBox: Box This receiver as an explicit Box value, useful where the implicit box context is hidden.
var runGeneration: MutableMap<String, Int>
var fragmentGeneration: Int
var jobs: MutableList<Job>
val cachedLaunchReturns: MutableMap<String, CachedLaunchReturn>
val currentRunningStates: MutableMap<String, RunningState>
val runScopes: BoxRunScopes This box's open RunScopes — the owners of what each run of it registered. stopTrackedJobs closes them all; a later run of a strand closes that strand's earlier ones.
override var children: MutableSet<Box>
override var parents: MutableSet<Box>
var visible: Boolean
val root: Box
fun connect(to: Box, property: String = "children")
fun disconnect(to: Box, property: String = "children")
override fun setPropertyRaw(name: String, value: Any, doResync: Boolean)
override fun getPropertyRaw(name: String, default: Any?): Any?
override fun removePropertyRaw(name: String, doResync: Boolean)
override fun toString(): String
fun resyncAll() Queues every non-null UI-resyncable property on this box for the next resync pass.
fun resyncOnly(names: Collection<String>)
fun resyncAllExcept(names: Set<String>)
fun setFrameAnimated(newFrame: Rect, duration: Double = 500.0) Animates the frame change over the specified duration (in milliseconds)

nesting.kt

Nesting: drawing one box inside another, through the nested and nestedIn properties.

A nested box is also connected to its container, so it inherits the container's imports and marked preamble lines. Setting nested or nestedIn keeps both sides and the edge in step; nestInto also converts the frame to and from parent-relative coordinates, and refuses a cycle. reconcileNestingEdges re-derives missing edges for boxes loaded from disk. The behaviour is covered by NestingConnectsTest.

src/jvmMain/kotlin/afield/document/graph/nesting.kt — read the source

Used in: Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun reconcileNestingEdges(boxes: Collection<Box>): Int Re-derive the container edges that nesting implies, over boxes, and return how many edges were added.
fun Box.addNested(child: Box)
fun Box.removeNested(child: Box)
fun Box.clearNested()
fun Box.nestInto(parent: Box?): Boolean Nest this box inside parent — or, with null, un-nest it — converting the frame to and from parent-relative coordinates, and moving the connection edge along with the containment.
fun Box.isNestedWithin(ancestor: Box): Boolean Is this box ancestor itself, or nested somewhere inside it?
fun collectAllNestedBoxes(boxes: Set<Box>): Set<Box> Recursively collects all nested boxes from the given set of boxes. This ensures that when saving templates or copying boxes, all nested children are included.
fun formatBoxCountMessage(selectedCount: Int, totalCount: Int, actionVerb: String = ""): String Creates a notification message describing the number of boxes and nested boxes.

Top-level values

Declaration Summary
var Box.nested: MutableSet<Box>
var Box.nestedIn: Box?

stdlib_graph.kt

Finding boxes by name, connecting them, and hanging scratch state off one.

named is how one box gets hold of another, searching the whole document from the root and skipping the tombstoned boxes a delete leaves behind. parent += child and parent -= child connect and disconnect, and connection is the edge property inheritance walks. Marker is a named property bag attached to a box, obtained with __m and created on first use, for state that belongs to part of a box rather than to the box itself; it lives in memory only and is not saved with the document.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/document/graph/stdlib_graph.kt — read the source

Top-level functions

Declaration Summary
fun Set<Box>.named(s: String, ignoreDeleted: Boolean = true) The first box in this set called s. Throws NoSuchElementException when there is none.
fun Box.named(s: String, ignoreDeleted: Boolean = true) The box called s in this document — the usual way for one box to get hold of another.
operator fun Box.plusAssign(child: Box) parent += child — connects child to this box, making it a child in the box graph.
operator fun Box.minusAssign(child: Box) parent -= child — breaks the connection to child, without deleting either box.
fun Box.__m(s: String, onCreation: Marker.() -> Unit = {}) The Marker called s on this box, created on first use — the get-or-create for per-box scratch state.

Marker

data class Marker(val name: String, val box: Box) : HasProperties, HasHierarchy

A named property bag attached to a box — somewhere to hang state that belongs to part of a box rather than to the box itself.

Declaration Summary
val properties This marker's state, as a plain map. In memory only; nothing here is persisted with the document.
override fun setPropertyRaw(name: String, value: Any, doResync: Boolean) Stores a value under name. The doResync flag is ignored — a marker has no client to notify.
override fun getPropertyRaw(name: String, default: Any?): Any? The value stored under name, or default if this marker has never been given one.
override fun removePropertyRaw(name: String, doResync: Boolean) Erases name. The doResync flag is ignored — a marker has no client to notify.
override var children: MutableSet<Box> Always empty: a marker is a leaf. Assigning to it does nothing.
override var parents: MutableSet<Box> Just the owning box, so a parent walk reaches it. Assigning to it does nothing.

HistoryMetrics.kt

Per-version activity and screenshot records for a document, stored in its history_metrics.edn beside metadata.edn.

Each record is keyed by version index. A save adds the activity the flight recorder saw since the previous save (per-box counts included), a diff adds its edit statistics, and a captured screenshot adds a reference to its image file. The history browser reads these back through HistoryMetrics.activityByVersion to annotate its timeline.

src/jvmMain/kotlin/afield/document/history/HistoryMetrics.kt — read the source

HistoryMetrics

object HistoryMetrics

Reads and writes a document's history_metrics.edn: activity per save, edit statistics per version, and screenshot references.

Declaration Summary
fun recordCurrentSave(documentPath: String, timestamp: Long = System.currentTimeMillis())
fun recordEditStats(documentPath: String, versionIndex: Int, stats: HistoryDiffStats)
fun activityByVersion(documentPath: String): Map<Int, HistoryActivityStats>
fun addScreenshotRef(documentPath: String, versionIndex: Int, channel: String, path: String, timestamp: Long = System.currentTimeMillis(), width: Int = 0, height: Int = 0)
fun storeScreenshotDataUrl(documentPath: String, channel: String, dataUrl: String, versionIndex: Int = currentVersionIndex(documentPath), width: Int = 0, height: Int = 0, timestamp: Long = System.currentTimeMillis()): HistoryScreenshotRef?
fun reserveScreenshotFile(documentPath: String, versionIndex: Int = currentVersionIndex(documentPath), channel: String, timestamp: Long = System.currentTimeMillis(), width: Int = 0, height: Int = 0, extension: String = "png"): Pair<File, HistoryScreenshotRef>?
fun addScreenshotRef(documentPath: String, versionIndex: Int, ref: HistoryScreenshotRef)
fun currentVersionIndex(documentPath: String): Int

HistoryScreenshotCapture.kt

src/jvmMain/kotlin/afield/document/history/HistoryScreenshotCapture.kt — read the source

HistoryScreenshotCapture

object HistoryScreenshotCapture

Declaration Summary
fun captureFourWindowsOnSave(documentPath: String, versionIndex: Int = HistoryMetrics.currentVersionIndex(documentPath), timestamp: Long = System.currentTimeMillis())

history.kt

A document's version history on disk: listing, diffing, searching, naming and restoring the saved versions under its backups/ folder, plus the RPC handlers behind the history browser.

HistoryManager owns the version list and every operation on it; installHistoryHandlers registers the messages history.html sends (list-history-versions, diff-history-version, restore-history-version and the rest). Per-version activity and screenshot records live alongside in HistoryMetrics.

src/jvmMain/kotlin/afield/document/history/history.kt — read the source

Top-level functions

Declaration Summary
fun installHistoryHandlers() Install RPC handlers for document history operations

HistoryManager

object HistoryManager

Reads, compares, searches, and restores the saved versions of one document directory.

Declaration Summary
data class VersionInfo(val index: Int, val file: File, val timestamp: Long, val boxIds: Set<String>, val boxCount: Int, val byteSize: Long) Data class representing a single version in document history
data class Transition(val fromIndex: Int, val toIndex: Int, val timestamp: Long) Data class representing a transition between versions
data class VersionsData(val transitions: MutableList<Transition> = mutableListOf(), val names: MutableMap<Int, String> = mutableMapOf()) Data class for the versions.edn file format
data class VersionDiffSummary(val versionIndex: Int, val boxIds: Set<String>, val textProps: Map<String, Map<String, String>>, val comparableProps: Map<String, Map<String, String>>)
fun onVersionSaved(documentPath: String, boxIds: Set<String>, timestamp: Long = System.currentTimeMillis()) Called by saving.kt when a new version is saved. This updates the cache incrementally without reparsing all files.
fun listVersions(documentPath: String): List<VersionInfo> List all backup versions for a document directory. Uses cached data if available, otherwise scans from disk.
fun boxIdsForVersion(documentPath: String, version: VersionInfo): Set<String>
fun restoreVersion(documentPath: String, sessionInfo: SessionInformation, targetVersionIndex: Int): Boolean Restore the indexed version into sessionInfo. Returns false for an invalid index or a synchronous snapshot/restore failure. SavingContext.save writes the resulting state in the background, so a later disk-write failure is reported by the saving subsystem rather than by this return value. The RPC layer sends a generic error for synchronous failures.
fun diffVersionAgainstLive(documentPath: String, sessionInfo: SessionInformation, targetVersionIndex: Int, boxFilter: Set<String> = emptySet(), propertyFilter: Set<String> = emptySet()): HistoryVersionDiff?
fun diffVersionAgainstPrevious(documentPath: String, targetVersionIndex: Int, boxFilter: Set<String> = emptySet(), propertyFilter: Set<String> = emptySet()): HistoryVersionDiff?
fun streamRailLineStatsAgainstLive(documentPath: String, sessionInfo: SessionInformation, targetVersionIndexes: List<Int>, boxFilter: Set<String> = emptySet(), propertyFilter: Set<String> = emptySet(), shouldContinue: () -> Boolean = { true }, emit: (Int, HistoryLineStats) -> Unit)
fun cherryPickVersion(documentPath: String, sessionInfo: SessionInformation, targetVersionIndex: Int, selections: List<HistoryCherryPickSelection>, includeTopology: Boolean = true): CherryPickHistoryResult?
fun setVersionName(documentPath: String, versionIndex: Int, name: String): Boolean Set a friendly name for a version
fun getVersionNames(documentPath: String): Map<Int, String> Get names map
fun getTransitions(documentPath: String): List<Transition> Get the version transitions for visualization
fun computeDiffStats(documentPath: String, versions: List<VersionInfo>): List<HistoryDiffStats?> Compute line diff summaries between consecutive versions for UI display.
fun computeDiffStat(documentPath: String, previous: VersionInfo, current: VersionInfo): HistoryDiffStats
fun loadVersionDiffSummary(documentPath: String, version: VersionInfo): VersionDiffSummary
fun computeDiffStat(previous: VersionDiffSummary, current: VersionDiffSummary): HistoryDiffStats
fun searchVersionTextChanges(documentPath: String, query: String): List<Int>
fun searchVersionTextChangesAgainstLive(documentPath: String, sessionInfo: SessionInformation, query: String): List<Int>
fun blameLineHistory(documentPath: String, uid: String, property: String, lineNumber: Int, lineText: String = "", propertyText: String = ""): List<Int> The versions that changed the line at lineNumber of propertyText (the editor's text for uid.property), oldest first, following the line back through edits above it. A version is listed when it touched the line; the trace stops at the version that inserted it.
fun selectionChangeVersions(documentPath: String, uid: String, property: String, startLine: Int, endLine: Int, selectedText: String = "", propertyText: String = ""): List<Int>
fun selectionTraceVersions(documentPath: String, uid: String, property: String, startLine: Int, endLine: Int, selectedText: String = "", propertyText: String = ""): List<Int>
fun boxStory(documentPath: String, uid: String, property: String = "code"): HistoryBoxStoryResult The story of uid.property across every saved version, oldest first: consecutive versions holding the same text collapse into one HistoryBoxStoryEntry, so a box edited three times over forty saves reads as four entries. Versions where the box is absent are their own entries with exists = false. The history panel's This box list is built from this.
fun propertyTextAtVersion(documentPath: String, versionIndex: Int, uid: String, property: String): String?
fun lineChurnForProperty(documentPath: String, uid: String, property: String, liveText: String, maxVersions: Int = 160): HistoryLineChurnResult
fun clearCache(documentPath: String) Clear the version cache for a document. Call this when the document is closed or if you need to force a rescan.

DocumentShutdownFlush.kt

Saves pending document edits when Field shuts down, so a Ctrl-C does not lose the last few seconds of work.

src/jvmMain/kotlin/afield/document/persistence/DocumentShutdownFlush.kt — read the source

DocumentShutdownFlush

object DocumentShutdownFlush

Saves, on the way out, the document edits the autosave loop has not reached yet.

Declaration Summary
const val BUDGET_MS
fun install()
fun flushPendingDocumentSaves(): Int Saves every document with pending changes, on this thread. Returns how many it saved.
fun saveIfPending(info: SessionInformation, inline: Boolean, reason: String, log: Boolean = true): Boolean The autosave loop's save of info, if it needs one: writesToDisk, not loading, and either something marked pending or a PersistedFingerprint that differs from the last one matching disk. inline runs the write on this thread; otherwise it goes to the misc pool. log prints a line on stderr (the shutdown and detach saves do; the 5 s loop does not). Returns whether a save was made.
val noOpSkips How many saves saveIfPending skipped because the marked edits changed nothing; for tests and measurement.
const val UNMARKED_CHANNEL Channel of the unmarked-edit tripwire's lines; one per (box, property) a mutator changed without marking.
var lastUnmarked: List<Pair<String, String>> The last tripwire report, for tests: (box id, property) pairs.

FileWatchManager.kt

Reloads an open document's properties when their sidecar files are edited outside Field.

A FileWatchManager watches one document folder and tells Field's own saves apart from external edits by content hash, so editing a box's .kt or .glsl sidecar in another editor updates the box in the running session.

src/jvmMain/kotlin/afield/document/persistence/FileWatchManager.kt — read the source

FileWatchManager

class FileWatchManager(private val sessionInfo: SessionInformation)

Keeps one open document synchronized with external edits to its workspace directory.

Declaration Summary
fun start() Establish the current sidecars as the baseline, register the workspace, and start the event loop plus the five-second orphan scan. Calling this while already running is a no-op.
fun stop() Cancel both jobs, discard all baselines and save signatures, and close the watch service. Calling this when not running is a no-op; a stopped instance cannot be started again.
fun markInternalSave(file: File, written: String? = null) Record a content hash of the file after a Field save so the resulting watch event is not mistaken for an external edit. written is the text Field wrote, when the caller has it; a file whose size and mtime match the last record keeps that record's hash rather than being re-read. Missing files are ignored.
fun markMetadataWrite(file: File, boxIds: Collection<String>, written: Boolean, text: String? = null) Record that Field has just saved metadata.edn holding boxIds: those become the on-disk baseline, and, if written (the save changed the file), its signature is registered as markInternalSave does. SavingContext calls this straight after writing the file, on the save thread, because the rest of a save (Markdown mirror, history, sidecar bookkeeping) took ~900 ms on a loaded host, and the file's own watch event arrived before a registration made at the end, was taken as external, and tombstoned a box created during the save.

LinalgSerialization.kt

JSON serializers for Field's vector and quaternion types, and the FieldJson instance that uses them.

Each serializer writes its value as a flat JSON array of floats (two, three or four for the vectors, four for Quaternionf, eight for DualQuaternionf) and rejects an array of the wrong length on the way back. FieldSerializersModule registers them as contextual serializers.

src/jvmMain/kotlin/afield/document/persistence/LinalgSerialization.kt — read the source

Top-level values

Declaration Summary
val FieldSerializersModule
val FieldJson

Vector2fSerializer

object Vector2fSerializer : KSerializer<Vector2f>

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: Vector2f)
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): Vector2f

Vector3fSerializer

object Vector3fSerializer : KSerializer<Vector3f>

JSON serializer for Field's mutable Vector3f, encoded as [x, y, z] floats.

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: Vector3f)
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): Vector3f

Vector4fSerializer

object Vector4fSerializer : KSerializer<Vector4f>

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: Vector4f)
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): Vector4f

QuaternionfSerializer

object QuaternionfSerializer : KSerializer<Quaternionf>

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: Quaternionf)
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): Quaternionf

DualQuaternionfSerializer

object DualQuaternionfSerializer : KSerializer<DualQuaternionf>

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: DualQuaternionf)
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): DualQuaternionf

PersistedFingerprint.kt

The content hash that catches a document edit dirty marking did not see.

Field decides whether to save by marking: a write through setPropertyRaw, removePropertyRaw, connect or disconnect tells the session there is something to write. A collection mutated in place and a write made while the document is still loading go through none of them. So the autosave tick, the detach save and the shutdown flush also hash what they would write and compare that against the hash from the last time the document matched disk, saving when the two differ.

The per-property entries behind the hash are kept as well, which lets a tripwire name the (box id, property) pairs that changed without being marked. Each one names a mutator that should have marked its own write.

src/jvmMain/kotlin/afield/document/persistence/PersistedFingerprint.kt — read the source

PersistedFingerprint

object PersistedFingerprint

A content hash of a document's persistent properties, for "has this changed since the last save?"

Declaration Summary
fun of(info: SessionInformation): Long
fun of(directory: String, boxes: Collection<Box>): Long
fun entries(info: SessionInformation): Map<Pair<String, String>, Long> Per (box id, property) hashes; of is their sum. Kept so the tripwire can name what changed.
fun entries(directory: String, boxes: Collection<Box>): Map<Pair<String, String>, Long>
fun record(info: SessionInformation, entries: Map<Pair<String, String>, Long>?) Records entries as what disk holds now (after a load or a save).
fun unmarkedChanges(saved: Map<Pair<String, String>, Long>, now: Map<Pair<String, String>, Long>, marked: Set<Pair<String, String>>): List<Pair<String, String>> The tripwire (backlog N1022-2): the (box id, property) keys whose hash differs between saved and now (added, removed or changed) and that nothing marked in marked. Each one is a mutator that bypassed Box.notePersistentChange; the save still writes it, but the mutator should be fixed.
fun hashesByContent(value: Any?): Boolean Whether deep of value is a function of its content (no identity hash anywhere inside).

SafeSaveAtomicSmokeTest.kt

Smoke for File.safeSave's atomic = true path, which is what keeps a file readable while it is being rewritten. The operation-transcript writer (OperationTranscript.checkpoint and friends) uses it for operations.yaml: the text goes to a temp file in the same directory and is then renamed into place, so a concurrent reader — another thread, or another process sharing the workspace — only ever sees a complete file.

concurrentAtomicWritesNeverTear is the case that exercises that. Six writer threads replace one file 120 times each, with payloads of very different lengths, while a reader parses it in a tight loop. A torn or interleaved write splices one payload into another and SnakeYAML reports expected <block end>; under atomic writes every read parses to a list.

src/jvmMain/kotlin/afield/document/persistence/SafeSaveAtomicSmokeTest.kt — read the source

SafeSaveAtomicSmokeTest

object SafeSaveAtomicSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

edn.kt

An EDN reader and printer, the text format of a document's metadata.edn.

Edn.read and Edn.write convert between EDN text and plain Kotlin values (maps, lists, strings, numbers), and EdnValue is the parsed tree in between. Tagged values such as #afield/rect are handled by registering a type with Edn.CustomTypes.

src/jvmMain/kotlin/afield/document/persistence/edn.kt — read the source

EdnValue

sealed class EdnValue

Declaration Summary
object Nil : EdnValue()
data class Bool(val value: Boolean) : EdnValue()
data class Number(val token: String) : EdnValue()
data class Str(val value: String) : EdnValue()
data class ListVal(val values: List<EdnValue>) : EdnValue()
data class VectorVal(val values: List<EdnValue>) : EdnValue()
data class SetVal(val values: Set<EdnValue>) : EdnValue()
data class MapVal(val values: Map<EdnValue, EdnValue>) : EdnValue()
data class Symbol(val name: String) : EdnValue()
data class Keyword(val name: String) : EdnValue()
data class Tagged(val tag: String, val value: EdnValue) : EdnValue()

Edn

object Edn

Declaration Summary
interface NumberStrategy Strategy for mapping EDN numeric tokens to JVM numeric types.
object DefaultNumberStrategy : NumberStrategy
var defaultNumberStrategy: NumberStrategy Global default strategy used when you don't pass a NumberStrategy explicitly.
class CustomTypes Registry for custom tagged types.
fun write(value: Any?, custom: CustomTypes? = null): String Serialize a single Kotlin value to an EDN string.
fun writeList(values: List<Any?>, custom: CustomTypes? = null): String Serialize a list of values as an EDN vector.
fun read(edn: String, custom: CustomTypes? = null, numberStrategy: NumberStrategy = defaultNumberStrategy): Any? Deserialize an EDN string into Kotlin values using the given strategies.
fun readList(edn: String, custom: CustomTypes? = null, numberStrategy: NumberStrategy = defaultNumberStrategy): List<Any?> Deserialize a top-level EDN vector or list into List<Any?>.

saving.kt

Reads and writes a document as metadata.edn plus sidecar files, and keeps backups of what it replaces.

SavingContext is the canonical reader and writer of the version 2 document format: it saves a set of boxes to metadata.edn, writing large text properties such as code or fragment to <uid>_<property>_.<ext> sidecars, and loads a document folder (or EDN text) back into boxes. File.safeSave is the file write underneath it, which keeps the previous version in a backups/ directory and can write atomically through a temp file and a rename.

src/jvmMain/kotlin/afield/document/persistence/saving.kt — read the source

Used in: What a document is on disk

Top-level functions

Declaration Summary
fun File.safeSave(text: String, createBackup: Boolean = true, maxBackups: Int? = null, atomic: Boolean = false): Boolean Safely saves text to a file, creating backups of previous versions.

Box_transfer

data class Box_transfer(val id: String, val properties: Map<String, Pair<String, JsonElement>>)

BoxTransferEdn

data class BoxTransferEdn(val id: String, val properties: Map<String, Any?>)

InlineFileSnapshot

data class InlineFileSnapshot(val boxId: String, val property: String, val path: String, val inline: String)

One sidecar-backed property as stored inline in a document's metadata.edn.

Declaration Summary
val boxId: String
val property: String
val path: String
val inline: String

InlineMetadata

data class InlineMetadata(val paths: List<String>, val metadataEdn: String)

A document's metadata.edn as Field's codec would write it, with the inline fallbacks refreshed.

SavingContext

class SavingContext

Declaration Summary
fun exportEdnPackage(boxes: Set<Box>, directory: String): String
fun exportDocumentMarkdown(boxes: Set<Box>, directory: String): String
fun listTemplates(dir: String): List<String>
fun cloneInto(dir: String): Set<Box>
fun cloneTemplateInto(path: String): Set<Box> cloneInto for inserting a template into an open document. The template's own root and metaroot entries are left out: a template that is a whole document (a workspace document, or a bundled template folder saved with its root) would otherwise write its root's frame, children, drawing and bookmarks over the receiving document's root and add a stray copy of its metaroot. Its top-level boxes still name root as their parent, which is the receiving document's root. A metadata.json template has no such entries to drop and goes through cloneInto.
var altered
fun toTransfer(boxes: Set<Box>, directory: String): List<BoxTransferEdn> Serializes each box's persistent properties for save. Only properties whose name matches one of Box.persistentPropertiesFor directory (regex-matched) are written; others are dropped. Box.ioNeverPersistedProperties then vetoes what survived, which is how Monaco view state stays off disk despite mergePersistentProperties re-adding ".*_editorState" from every older metadata.edn this process loads.
fun checkDifference(p: String, contents: String, createBackup: Boolean = true, maxBackups: Int? = null): Boolean
fun save(fn: String, boxes: Set<Box>, message: String? = null, onComplete: (() -> Unit)? = null, inline: Boolean = false) Save boxes to disk, optimized to minimize main thread blocking.
fun saveAsTemplate(templateName: String, boxes: Set<Box>, templatesDir: String) Save boxes as a standalone template file (metadata.edn without subdirectory)
fun load(fn: String, filterId: (String) -> String = { it }, upgradeOnLoad: Boolean = true, initIfCreated: Boolean = true): Set<Box>
fun resolveStoredValue(value: Any?, directory: String, preferInlineFiles: Boolean = false): Any?
fun readInitialBoxFrames(fn: String, filterId: (String) -> String = { it }): List<InitialBoxFrame>
fun updateRootMetadata(directory: String, metadata: String): Boolean
fun resolveStoredValueWithBoxes(value: Any?, directory: String, filterId: (String) -> String = { it }, allowedRefIds: Set<String>? = null, root: SessionInformation? = null, preferInlineFiles: Boolean = false): Any?
fun readEdnTransfers(directory: String): List<BoxTransferEdn>
fun readInlineFileSnapshots(directory: String): List<InlineFileSnapshot> Lists the inline fallbacks carried by a document without resolving them through loose files.
fun refreshInlineFilesFromDisk(directory: String, allowInlineOnly: Boolean = false): List<String> Refreshes every #afield/file-inline fallback from its loose file using Field's EDN codec.
fun canonicalInlineMetadata(directory: String, allowInlineOnly: Boolean = false): InlineMetadata What refreshInlineFilesFromDisk would write, without writing it.
fun readEdnTransfersFromString(ednText: String, recordInto: String? = null): List<BoxTransferEdn> Reads EDN text; recordInto is the document its header's persistent-property names belong to.
fun loadFromEdnText(ednText: String, directory: String, filterId: (String) -> String = { it }, allowedRefIds: Set<String>? = null, preferInlineFiles: Boolean = false, initIfCreated: Boolean = true): Set<Box>
fun fromTypedPair(element: Pair<String, JsonElement>, directory: String, filterId: (String) -> String = { it }, createBoxes: Boolean = true, initIfCreated: Boolean = true): Any?
fun ensureDirectory(fn: String): String

PropertyBag.kt

The thread-safe, insertion-ordered map that holds every Box's properties.

PropertyBag below explains the guarantees: single-key operations are serialised, and keys, values and entries iterate snapshots that write removals through to the live map. The view behaviour is pinned by PropertyBagViewTest.

src/jvmMain/kotlin/afield/document/properties/PropertyBag.kt — read the source

PropertyBag

class PropertyBag : MutableMap<String, Any?>

The property bag behind every Box — an insertion-ordered, null-tolerant map that is safe to read, write and iterate from more than one thread at once.

Declaration Summary
override val size: Int
override fun isEmpty(): Boolean
override fun containsKey(key: String): Boolean
override fun containsValue(value: Any?): Boolean
override fun get(key: String): Any?
override fun put(key: String, value: Any?): Any?
override fun remove(key: String): Any?
override fun putAll(from: Map<out String, Any?>)
override fun clear()
override val keys: MutableSet<String> The keys, in insertion order: iterated from a snapshot, removed through to the live map.
override val values: MutableCollection<Any?> The values, in key insertion order — read-only.
override val entries: MutableSet<MutableMap.MutableEntry<String, Any?>> The entries, in insertion order — this is what makes every filter/forEach over a box's properties safe. Iterated from a snapshot; iterator().remove() and MutableEntry.setValue reach the live map.
override fun toString(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

PropertyDependencies.kt

Records which boxes read values held by other boxes, per document, and saves that record as dependencies.edn beside metadata.edn.

PropertyDependencies below describes what is recorded and when it is written; the tests are PropertyDependenciesTest.

src/jvmMain/kotlin/afield/document/properties/PropertyDependencies.kt — read the source

PropertyDependencies

object PropertyDependencies

Which boxes read values that other boxes hold — recorded as box code runs, kept per document, and saved beside metadata.edn as dependencies.edn.

Declaration Summary
const val FILE_NAME
const val COUNT_FLUSH_INTERVAL_MS
val clock Orders accesses against each other and against propDown changes (ReactiveRuns): every recorded access and every change takes the next value. Milliseconds are too coarse for that.
val sessionStartedAt: Long When this process started recording: an edge last seen before this was remembered from disk.
var enabled: Boolean
enum class Access
data class Key(val reader: String, val supplier: String, val name: String, val mechanism: String, val access: Access)
class Edge(val key: Key, @Volatile var firstSeen: Long, @Volatile var lastSeen: Long, initialCount: Long, @Volatile var matchKind: String?, @Volatile var matchedName: String?)
class Store(val documentPath: String)
fun storeFor(documentPath: String): Store The record for the document at documentPath, loading dependencies.edn on first use.
fun storeFor(box: Box): Store? The record for box's document, or null for a box that is in no document.
fun recordNamedRead(reader: Box, supplier: Box, name: String) Record that reader found name in supplier's properties by name, as a shader's sampler binding does (Box.findWithNameOrNull). A propDown write to it then reruns reader.
fun recordWrite(holder: HasHierarchy, name: String, mechanism: ContextResolution.Mechanism) Record that the executing box wrote name into holder through an inherited delegate. A write into the executing box itself (local shadowing) is not a dependency and is skipped.
fun retireReadsBefore(box: Box, runStartSeq: Long) box has just finished a full run (not a fragment) that began at runStartSeq: every access it made before then and did not repeat during the run is now historical. A completed run replaces the box's live dependencies; reads its frames {} loops and callbacks make later revive theirs.
data class Flow(val from: String, val to: String, val names: List<String>, val count: Long, val sessionCount: Long, val lastSeen: Long, val historical: Boolean = false) One producer → consumer arrow: from's value reached to's code, under any of names.
fun flows(store: Store, rootId: String? = null, includeHistorical: Boolean = false): List<Flow> Every producer → consumer flow in store, merged per pair. Historical edges are left out unless includeHistorical; then an arrow made only of them is marked Flow.historical.
fun upstreamOf(box: Box): List<String> The boxes that supplied box's inherited values, as far as this document has seen.
fun rememberedSuppliers(store: Store, reader: String, name: String, rootId: String?): List<String> The boxes that supplied name to reader on any earlier read — what an upstream auto-run would execute when reader misses name. Suppliers of live reads come first, historical ones last.
fun displayName(query: String): String afield.graphics.vulkan.Renderer → Renderer; a property name stays as it is.
fun graph(info: SessionInformation): DependencyGraph The wire form the dependency layer draws.
fun flush(info: SessionInformation, force: Boolean = false) Write info's record if it changed: at once for a new edge, otherwise at most every COUNT_FLUSH_INTERVAL_MS. Called from the autosave loop; force (shutdown) writes any change, on the calling thread.
fun forget(documentPath: String) Drop the in-memory record for documentPath (tests, and a document closing).

props.kt

The prop / rootProp macro pair — the typed, inheritable box-property slot.

prop and rootProp exist only as the declarations that make prop<Int>(foo) resolve; neither is meant to run, and calling one throws MacroRegistry.MacroNotExpandedException. The real work happens at expansion time in macro_prop and macro_root_prop, which are handed the type argument and the name, and return an Insert that rewrites the call site to the bare name (or root.name) and pushes two declarations into the box's preamble: a var Box.<name> by Inheritance<T>(), a val Once.<name> by Onceing<T>() and a val Why.<name> by Whying() (so why.<name> explains where the value comes from). All three generated lines carry the trailing empty-comment marker that makes the preamble gather copy them down into child boxes, and an import for T is added via kotlinImportStatementFor. macro_root_prop differs only in targeting this.root, so the accessors land on the document root rather than on the calling box.

src/jvmMain/kotlin/afield/document/properties/props.kt — read the source

Top-level functions

Declaration Summary
fun <T> Box.prop(name: String) Declares a typed, inheritable property on this box: prop<Double>(speed) and thereafter speed is a variable that children inherit and the property inspector can see.
fun <T> Box.rootProp(name: String) prop, but the accessors land on the document root rather than on this box — so every box in the document shares the one slot, wherever it is read from.
fun Box.macro_prop(contents: List<String>): Insert? Macro expansion for prop<T>(name): rewrites the call to the bare name and pushes the delegate declarations into the box's preamble.
fun Box.macro_root_prop(contents: List<String>): Insert? Macro expansion for rootProp<T>(name) — the same declarations as macro_prop, but the Insert targets this.root and the call site becomes root.<name>.

reactiveProps.kt

propUp, propDown and propUpDown: prop with re-execution attached.

All three declare the same typed, inherited slot prop does (a var Box.<name> in the preamble, copied down into children), and read and write it the same way. What they add:

  • Up — a read that finds no value anywhere up the graph runs the box that supplies it, then reads again. The supplier is the box that declared the property (or supplied it before), nearest first, and the run happens inside the read, so a chain of them unwinds in order.
  • Down — a write that changes the value reruns, after the writing box's run has finished, every box that read the old value, in dependency order.

Declare it where the value is written (Down is decided by the writer's declaration, Up by the reader's), and the declaration reaches the children as prop's does. The scheduling is afield.runtime.lifecycle.ReactiveRuns; what it did is in ReactiveRuns.recentEvents().

src/jvmMain/kotlin/afield/document/properties/reactiveProps.kt — read the source

Top-level functions

Declaration Summary
fun <T> Box.propUp(name: String) prop, and a read that finds nothing runs the box that supplies the value first: propUp<Double>(speed).
fun <T> Box.propDown(name: String) prop, and changing the value reruns the boxes that read it: propDown<Double>(speed).
fun <T> Box.propUpDown(name: String) propUp and propDown together: propUpDown<Double>(speed). A macro placeholder.
fun Box.macro_reactive_prop(contents: List<String>, mode: Reactive, macroName: String): Insert? Macro expansion for propUp / propDown / propUpDown: macro_prop's declarations, with ReactiveInheritance.
fun <T> Box.rootPropUp(name: String) rootProp with propUp's pull: rootPropUp<Double>(speed), read and written as root.speed. A macro placeholder.
fun <T> Box.rootPropDown(name: String) rootProp with propDown's reruns: changing root.speed reruns the boxes that read it. A macro placeholder.
fun <T> Box.rootPropUpDown(name: String) rootPropUp and rootPropDown together. A macro placeholder.
fun Box.macro_reactive_root_prop(contents: List<String>, mode: Reactive, macroName: String): Insert? Macro expansion for rootPropUp / rootPropDown / rootPropUpDown: macro_reactive_prop's declarations on the document root, as macro_root_prop does for rootProp, with the call site becoming root.<name>. The declaring box is recorded, so the root's slot reruns its readers (Down) and a missing value runs the declaring box (Up) while that box's code still says so.

Reactive

enum class Reactive(val up: Boolean, val down: Boolean)

Which re-executions a ReactiveInheritance property triggers.

Constants: UP, DOWN, UP_DOWN.

ReactiveInheritance

class ReactiveInheritance<T>(val mode: Reactive, val defaultValue: T? = null)

The delegate behind propUp / propDown / propUpDown: Inheritance (the same walk, the same local write), plus a pull on a missing read and a change notice on a changing write, per mode.

Declaration Summary
operator fun getValue(f: Box, property: KProperty<*>): T The value from this box or the nearest ancestor. Under Reactive.up, a miss first runs the supplier and reads again.
operator fun setValue(f: HasHierarchy, property: KProperty<*>, v: T) Writes the value locally, as Inheritance does. Under Reactive.down, a value that differs from the one there (by equals) reruns its readers once the writing box's run has finished. Assigning null erases the property.

stdlib_properties.kt

The property delegates that prop and @exported generate against.

Delegation stores a value in the receiver's own property bag rather than in a Kotlin field, which is what makes such a property survive a re-run and show up in the property inspector. Inheritance walks up parents until some ancestor has the property, throwing MissingInheritedPropertyException with a message naming the property and the box when nothing does; InheritanceFromContext makes the same walk but prefers each box's live Kotlin variables over raw properties. inheritedPropertyRaw is that lookup without a KProperty to perform it through, for readers such as FieldML that have no generated delegate to read it with.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/document/properties/stdlib_properties.kt — read the source

Used in: Running code in a box

Top-level functions

Declaration Summary
fun inheritedPropertyRaw(from: Box, name: String): Any? The lookup Inheritance performs, without a KProperty to perform it through: this box's own raw property, then breadth-first up parents until some ancestor has one. null means nothing up the graph has it.

Delegation

class Delegation<T>(var onGet: ((HasProperties, currentValue: T) -> Unit)? = null, var hook: ((inside: HasProperties, currentValue: T, incomingValue: T) -> T)? = null, val defaultValue: (() -> T)? = null)

The property delegate behind prop and @exported: reads and writes go to the receiver's own property bag under the property's name, not to a Kotlin field.

Declaration Summary
var onGet: ((HasProperties, currentValue: T) -> Unit)?
var hook: ((inside: HasProperties, currentValue: T, incomingValue: T) -> T)?
val defaultValue: (() -> T)?
operator fun getValue(f: HasProperties, property: KProperty<*>): T Reads the property from f's bag, filling in defaultValue on the first read if it is unset.
operator fun setValue(f: HasProperties, property: KProperty<*>, v: T) Writes the property into f's bag, after letting hook rewrite the incoming value.

MissingInheritedPropertyException

class MissingInheritedPropertyException(message: String) : IllegalStateException(message)

Thrown when an inherited (prop-style) property has no value anywhere up the box graph and no default.

Inheritance

class Inheritance<T>(val defaultValue: T? = null)

The property delegate that makes a value inherited: reading it looks in this box, then breadth-first up through parents until some ancestor has it.

Declaration Summary
operator fun getValue(f: Box, property: KProperty<*>): T The value from this box or the nearest ancestor that has it.
operator fun setValue(f: HasHierarchy, property: KProperty<*>, v: T) Writes the value locally, shadowing any ancestor's value rather than modifying it. A write into a box other than the executing one (root.speed = 3.0 under rootProp) is recorded in PropertyDependencies, which is how a later reader's producer is known.

InheritanceFromContext

class InheritanceFromContext<T>(val defaultValue: T? = null)

Inheritance, but it can also see the live Kotlin variables of the boxes it walks — so a plain val speed = 3.0 in a parent box is inheritable, with no property or annotation needed.

Declaration Summary
operator fun getValue(f: Box, property: KProperty<*>): T The value from this box or the nearest ancestor, preferring live Kotlin variables over properties.
operator fun setValue(f: HasHierarchy, property: KProperty<*>, v: T) Writes the value locally as a raw property, shadowing whatever an ancestor had; a write into another box is recorded as Inheritance.setValue's is.

KitDedicatedHost.kt

The compatibility route for a project that already has its own launcher, workspace and document conventions and wants to appear in Welcome's kit library without adopting Field's. Such a kit declares a host block in its field-kit.json naming a launcher script, a loopback HTTP endpoint and the workspace that launcher owns, and Field then treats it as something to start rather than something to load: none of its runtime declarations join the calling host's classpath, and its documents keep whatever versioning scheme the project already uses.

KitDedicatedHosts.open is the whole action, and it runs only from an explicit Open kit host or Create in kit host in Welcome, never from adding a kit. It probes /api/instance-info at the declared endpoint first and reuses a host that is already up, refusing outright when a different workspace answers there; otherwise it runs the launcher from the kit's source directory with Field's own port and path variables stripped from the environment, appends its output to logs/<id>.log under the kits directory, and polls for up to three minutes. Creating a document is then a POST to that host's own starting-point API, so the running project copies the seed by its own rules and hands back the URL to open.

Two checks hold the arrangement together. KitPackages.read calls KitDedicatedHosts.validate on every manifest, so a non-loopback endpoint, a query string, user info in the URL, or a launcher path escaping the package is refused when the kit is added rather than when it is run. And KitDedicatedHosts.open compares the launcher's hash in the source directory against the one in the installed snapshot, so the script that executes is the script that was there when the kit was added.

src/jvmMain/kotlin/afield/document/workspace/KitDedicatedHost.kt — read the source

KitDedicatedHost

data class KitDedicatedHost(val launcher: String, val endpoint: String, val workspace: String, val environment: Map<String, String> = emptyMap())

Compatibility for an existing project launcher. Its original workspace and runtime remain authoritative.

Declaration Summary
val environment: Map<String, String>

KitDedicatedHosts

object KitDedicatedHosts

Starts an existing local project without rewriting its launcher, manifests or existing documents.

Declaration Summary
fun validate(root: File, host: KitDedicatedHost)
fun open(kit: InstalledKit, startingPoint: String? = null): String The launcher is invoked only by an explicit Run/Create action, never by installation.

KitFirstOpen.kt

A document copied from a kit's starting point runs its roles, in the kit's launch order, the first time it opens, so it does something without field_kit.py activate.

openStartingPointIntoWorkspace leaves MARKER in a copy that holds a kit record; the server's document-ready hook (afield.app.server.installKitFirstOpen) calls rolesToLaunch and launches what it returns, once: the marker is consumed whether or not a launch succeeds, so a role that fails is not relaunched on every open. Later opens run only what autorun asks for, as for any document.

src/jvmMain/kotlin/afield/document/workspace/KitFirstOpen.kt — read the source

KitFirstOpen

object KitFirstOpen

Finds the roles a freshly copied kit document launches on its first open.

Declaration Summary
const val MARKER The one-time marker, in the document folder beside metadata.edn.
fun rolesToLaunch(documentDir: File, boxes: Collection<Box>): List<Box> The role boxes of the document in documentDir, in launch order, when it carries MARKER; the marker is deleted. Empty for any other document, and for roles no box plays.

KitInstances.kt

A document copied from a kit's starting point, the record of what it received, and the three-way comparison that updates it. KitInstances.create copies a starting point into the workspace, puts the new document in a kit-<id> workspace project, and writes .field-kit-instance.json: the kit id, the package and runtime revisions, the starting point, the project, and a hash of every file the copy began from. Those hashes are the baseline; the document's files are its own from then on.

KitInstances.inspect reads three hashes for each path (the baseline, the document's file now, and the same path in the installed starting point) and labels the difference update, edited-here, remove or conflict, a conflict being both sides changed since the baseline. KitInstances.migrate then applies that plan or none of it. It refuses outright while any path conflicts, copies every file it is about to write or delete into backups/kit-<uuid> first, restores from those copies if a write fails partway, and rewrites the record last, so a failed update leaves the document on the revision it already had.

A document reads shared resources from the package revision it recorded rather than the newest installed one (KitInstances.sharedResources), which is also why .field-kit-instance.json has to travel with the document's other files: without it a host has no kit to compare the document against, and it opens as an ordinary workspace document. The equivalent for a role kit inside a single document is Kits.

src/jvmMain/kotlin/afield/document/workspace/KitInstances.kt — read the source

KitInstanceRecord

data class KitInstanceRecord(val kit: String, val revision: String, val runtimeRevision: String, val startingPoint: String, val project: String, val files: Map<String, String>)

The seed revision and file baselines this document actually received.

Declaration Summary
val kit: String The kit id this document came from, which must still be installed for an update to be offered.
val revision: String The package revision the copy was taken from. Shared resources and the starting point are read back through this revision, so a newer installed one does not change an existing document.
val runtimeRevision: String The runtime revision the copy was taken against; KitRuntime.activate refuses any other.
val startingPoint: String Which of the kit's starting points was copied, by its key in KitPackageManifest.startingPoints.
val project: String The kit-<id> workspace project this document was added to.
val files: Map<String, String> Path to content hash for every file the copy began from, which is the baseline arm of the three-way comparison in KitInstances.inspect. KitInstances.migrate rewrites it on a successful update.

KitInstanceChange

data class KitInstanceChange(val path: String, val state: String)

One path and what an update would do to it: update, edited-here, remove or conflict.

Declaration Summary
val path: String The document-relative path, which is also its path inside the starting point.
val state: String update for a file only the kit changed, edited-here for one only the document changed, remove for one absent from the installed starting point, and conflict for one both changed since the baseline.

KitInstanceInspection

data class KitInstanceInspection(val document: String, val record: KitInstanceRecord, val availableRevision: String, val changes: List<KitInstanceChange>, val conflicts: List<String>)

An update preview: the revision on offer, every path that differs, and the conflicts that block it.

Declaration Summary
val document: String The document folder's name.
val record: KitInstanceRecord The record as it stands, so a caller can compare KitInstanceRecord.revision with availableRevision.
val availableRevision: String The installed revision an update would move to, and empty when the kit is not installed at all.
val changes: List<KitInstanceChange> Every path that differs, sorted, and empty both when the document already matches the installed starting point and when there was nothing to compare it against.
val conflicts: List<String> What blocks an update, which KitInstances.migrate refuses while this is non-empty. Ordinarily these are the conflict paths from changes; when the kit is not installed, or the recorded starting point is gone from the installed revision, it is instead a single sentence saying so.

KitInstances

object KitInstances

Starting-point copies and all-or-nothing three-way file migration. Runtime snapshots remain immutable.

Declaration Summary
const val RECORD The filename of the record, in the document folder beside metadata.edn. A document carrying one is a kit instance everywhere Field asks, so it has to travel with the document's other files.
const val FIRST_OPEN The filename of the marker create leaves in a fresh copy. The document-ready hook deletes it on the first open, and decides what to launch from RECORD rather than from this.
fun record(document: File): KitInstanceRecord? The kit record in document, or null for an ordinary workspace document. A record whose kit id or revision is not of the right shape throws rather than reading as absent.
fun files(seed: File): Map<String, String> Relative path to content hash for every file under seed that a copy would carry, sorted by path and leaving out backups, version history, the kit markers and the manifest itself. This is what a baseline is made of and what a fresh starting point is compared against.
fun create(workspace: File, id: String, startingPoint: String, name: String): DuplicateDocumentResponse Copies one of an installed kit's starting points into workspace as a new document called name, falling back to the starting point's own key when that is blank. Creates the kit-<id> project and seeds its preferences the first time the kit is used, then writes the record and the first-open marker. Refuses a kit this host has not loaded, and a dedicated-host kit, whose documents are made in its own host; a failure part way deletes the half-made document.
fun inspect(document: File): KitInstanceInspection What an update to the installed revision would do, without doing any of it. Compares three hashes per path and labels each difference, reporting a path both sides changed as a conflict. Throws for a document with no record; a kit that is not installed, or a recorded starting point the installed revision does not have, comes back as a blocked inspection rather than an error.
fun migrate(document: File): KitInstanceInspection Caller must close/unload a live document first. Validate the whole plan before writing any file.
fun sharedResources(document: File): File? The kit's shared resources folder as this document sees it, which is the one inside the package revision the record names rather than the newest installed one. Null for a document with no record and for a kit that declares no KitPackageManifest.sharedResources.

KitPackages.kt

The registry of kit packages installed on this machine, and the immutable snapshot each registration copies out of a package's source folder. A kit package is a directory holding a field-kit.json that names one or more starting points, each of them an ordinary saved Field document, beside optional Kotlin sources, JARs, pinned Maven coordinates, native libraries and a lifecycle class. public/docs/kits.md is the user's page. The smaller role-kit model, which describes shared box code and assets inside one document, is Kits.

KitPackages.read validates a manifest and KitPackages.install copies the package to packages/<id>/<revision>/ under ~/.afield/kits (afield.kits.dir or AFIELD_KITS_DIR moves that root). The revision is a SHA-256 over the path and content hash of every declared file, so registering unchanged source resolves to the snapshot that already exists, and a document goes on reading the revision it recorded after a newer one arrives. Installation reads JSON and copies bytes; it runs nothing the package ships.

Two files carry the registry to the next start of Field. registry.json lists the InstalledKit snapshots, and runtime.properties is a launch profile that build.gradle.kts reads to assemble the extension classpath, resolve the pinned dependencies, extend java.library.path and pass -Dafield.kit.packages and -Dafield.kit.revisions to the host, which is where KitRuntime picks the same set up again. A kit registered inside a running host therefore reports restart-required from KitPackages.statuses until its revision turns up in KitPackages.active; those statuses are what Welcome's Kits area lists.

KitPackages.child is the containment rule every path here goes through: package-relative, no .., and canonicalised, so neither a path nor a symlink inside the package can reach a file outside it. Because one host loads every installed runtime together, installing also refuses a set of kits that disagree about a dependency's version, about the value of a runtime system property, or about the contents of two native libraries with the same filename.

src/jvmMain/kotlin/afield/document/workspace/KitPackages.kt — read the source

KitPackageManifest

data class KitPackageManifest(val schema: String = "field.kit.package.v1", val id: String, val title: String, val description: String = "", val version: String, val guide: String = "", val runtime: KitPackageRuntime = KitPackageRuntime(), val startingPoints: Map<String, KitStartingPoint> = emptyMap(), val sharedResources: String = "", val preferences: Map<String, String> = emptyMap(), val host: KitDedicatedHost? = null)

A self-contained runtime package. Each starting point is an ordinary Field document directory.

Declaration Summary
val schema: String Pinned at field.kit.package.v1. KitPackages.read refuses a manifest declaring any other schema.
val id: String Names the kit wherever one is named: the packages/<id>/ snapshot folder, the registry entry, the kit-<id> workspace project and -Dafield.kit.revisions. A letter or digit, then up to 127 more characters from A-Za-z0-9._-.
val title: String The kit's name in Welcome's kit library, which read requires to be non-blank.
val description: String Prose for the library entry, and what GET /api/kits/guide renders when guide is blank.
val version: String The kit author's own version string. read requires it to be non-blank and nothing else reads it: a snapshot is keyed by InstalledKit.revision, a hash of the package's contents.
val guide: String Package-relative path to a markdown file served as the kit's guide. read requires the file to exist when this is set, and description stands in when it is blank.
val runtime: KitPackageRuntime The classpath, dependency, native-library and lifecycle declarations, which reach a host through the runtime.properties launch profile rather than the registry, so registering a kit adds nothing to the host that registered it.
val startingPoints: Map<String, KitStartingPoint> The documents this kit offers to copy, keyed by a name of the same shape as id. read requires at least one, and requires each KitStartingPoint.path to hold a Field document.
val sharedResources: String Package-relative directory of files the kit's documents read at run time. KitInstances.sharedResources resolves it against the revision a document recorded, not against the newest installed one.
val preferences: Map<String, String> Seeds preferences.json for the kit-<id> project. KitInstances.create writes it when it creates that project, so a later revision's changed preferences do not reach a project that already exists.
val host: KitDedicatedHost? Set for a kit that brings its own launcher and workspace (KitDedicatedHost). Such a kit contributes no runtime to the calling host: KitRuntime.start skips it and the launch profile records an empty KitPackageRuntime in its place.

KitPackageRuntime

data class KitPackageRuntime(val extensionDirs: List<String> = emptyList(), val jars: List<String> = emptyList(), val dependencies: List<String> = emptyList(), val nativeDirectories: Map<String, List<String>> = emptyMap(), val systemProperties: Map<String, String> = emptyMap(), val lifecycle: String = "")

Runtime declarations are consumed at the next build/restart, never injected into a running host.

Declaration Summary
val extensionDirs: List<String> Package-relative directories of Kotlin sources and resources compiled into the personal class layer by the next build. read requires each to hold a kotlin/ or a resources/ folder.
val jars: List<String> Package-relative JAR files added to the extension classpath; read requires each to exist.
val dependencies: List<String> Maven coordinates the launcher's Gradle build resolves. read refuses a SNAPSHOT or a latest version, and installing refuses a set of kits asking for two versions of one group:artifact.
val nativeDirectories: Map<String, List<String>> Native library directories per platform, keyed as KitPackages.platform spells it (macos-arm64, linux-x64 and so on). Only the running platform's entry is written to the launch profile, and two kits may not supply different bytes under one library filename.
val systemProperties: Map<String, String> System properties the launcher passes to the host, with ${kit} replaced by the snapshot directory. read rejects a key beginning afield.kit or naming a classpath property, and two kits may not ask for different values of one key.
val lifecycle: String Fully qualified name of a KitLifecycle class, instantiated from the personal class layer when the host starts. Empty leaves the kit with a lifecycle whose four methods do nothing.

KitStartingPoint

data class KitStartingPoint(val title: String, val path: String, val description: String = "", val launch: List<String> = emptyList())

A named, versioned seed; launch names boxes to launch in order on first open and explicit activation.

Declaration Summary
val title: String This starting point's name in the kit library, which read requires to be non-blank.
val path: String Package-relative directory of the seed document, which read requires to hold a metadata.edn or a metadata.json.
val description: String Optional prose about this starting point.
val launch: List<String> Box names to launch in this order, read back by KitRuntime.launchBoxes. read requires them non-blank and distinct, and launching requires exactly one live box per name.

InstalledKit

data class InstalledKit(val id: String, val revision: String, val runtimeRevision: String, val directory: String, val source: String)

One immutable package snapshot registered on this machine. Old revisions remain for existing instances.

Declaration Summary
val id: String The manifest's KitPackageManifest.id, which is also the registry key this entry replaces.
val revision: String Hash over the path and content of every file the manifest declares, and the name of the snapshot folder. Registering unchanged source resolves to the snapshot that already exists.
val runtimeRevision: String Hash over the runtime declarations and the files they name alone, so two revisions that differ only in a starting point share it. KitRuntime.activate refuses a document recorded against another one.
val directory: String Canonical path of the installed snapshot, under packages/<id>/<revision>/ in the kits directory.
val source: String The folder the package was registered from, which Welcome's Update registers again, and matches against the bundled starter kits to leave out one that is already installed.

KitPackageStatus

data class KitPackageStatus(val installed: InstalledKit, val manifest: KitPackageManifest?, val state: String, val message: String = "")

One registered kit as Welcome lists it: the registry entry, its manifest, and whether this host runs it.

Declaration Summary
val installed: InstalledKit The registry entry this status is about.
val manifest: KitPackageManifest? The snapshot's manifest, or null when reading it threw, which is also when state is error.
val state: String active when this host loaded this revision, error when startup or a read failed, and restart-required for a registered revision this host has not loaded.
val message: String The failure, when state is error, and empty otherwise.

KitPackages

object KitPackages

Registry and immutable snapshots used by Welcome and by the launcher's Gradle runtime profile.

Declaration Summary
const val MANIFEST The manifest filename a kit package folder is recognised by.
val directory: File Where the registry, the launch profile and the installed snapshots live: the afield.kits.dir system property, else AFIELD_KITS_DIR, else ~/.afield/kits. Read on each access, so a property set after start takes effect, and the folder is created when something writes to it rather than here.
fun read(root: File): KitPackageManifest Reads field-kit.json from root and validates every declaration in it, throwing with the first problem it finds rather than returning a partial manifest. Each path is checked for containment and for the file or folder it promises, so a manifest that reads is one whose package is complete.
fun child(root: File, path: String): File Canonical containment applies to symlinks too; installed snapshots never depend on outside files.
fun hash(file: File): String The SHA-256 of a file's bytes as 64 hex characters, read in 64 KiB blocks so package size does not matter.
fun runtimeRevision(root: File): String The InstalledKit.runtimeRevision a package folder would register under, covering the runtime declarations and the files they name. Reads and validates the manifest on the way, so it throws on a package read would reject.
fun hashText(text: String): String The SHA-256 of a string's bytes as hex. Its one use is the identity of a WorkspaceProjectScope's box, which hashes the project directory's canonical path.
fun installed(): List<InstalledKit> The registry as registry.json has it, one entry per kit id, and empty when nothing is registered.
fun install(source: File): InstalledKit Validate and copy a package before publishing its new registry entry. No code executes here.
fun remove(id: String) Drops a kit from the registry and rewrites the launch profile without it. The installed snapshots stay on disk and so do the documents made from them, so a host that has already loaded the kit goes on running it until it restarts.
fun writeJson(file: File, value: Any) Writes value as pretty-printed JSON through a uniquely named temporary file and an atomic move, so a reader of file sees either the previous contents or the new ones. Parent directories are created.
fun active(): Map<String, String> The revision this host actually loaded for each kit id, parsed from the afield.kit.revisions the launcher passed in as id@revision pairs. Empty in a host the launcher did not start, which is what leaves every registered kit reporting restart-required.
fun statuses(errors: Map<String, String> = emptyMap()): List<KitPackageStatus> One KitPackageStatus per registered kit, which is what Welcome's Kits area lists. Pass KitRuntime.errors as errors to have a kit whose startup threw report error with its message; a kit whose snapshot will not read reports error too, with a null manifest.
fun platform(): String How this machine is spelled in KitPackageRuntime.nativeDirectories: one of macos, windows or linux, a hyphen, then arm64 or x64. Anything that is not macOS or Windows reads as linux, and anything that is not AArch64 reads as x64.

KitRestarts.kt

Builds the classpath a kit needs before stopping the Field that asked for it, so a build that fails leaves Welcome running and able to say why. Adding a kit package registers a snapshot but loads nothing: its Kotlin extensions, pinned dependencies and native libraries reach a host only through the launch profile KitPackages writes, which is why Restart Field in Welcome's kit area has to compile and resolve before the restart rather than after it.

KitRestarts.prepare checks that this installation has both a launcher (field.sh, or field.ps1 on Windows) and a Gradle wrapper, then runs gradlew jvmJar dumpFieldLaunch -PfieldServerOnly=true on a daemon thread, adding --offline when AFIELD_OFFLINE is set and appending to logs/restart.log under the kits directory. The current host keeps serving throughout. A non-zero exit, or a build still going after twenty minutes, leaves KitRestarts.status at phase failed with the message and the log path and no restart happens; only a clean build invokes the ready callback, which flushes pending document saves, stops the server and relaunches the launcher with --server-only. Welcome polls GET /api/kits/restart-status while the build runs, and the Electron or browser window it polled from reconnects to the new host once the restart completes.

src/jvmMain/kotlin/afield/document/workspace/KitRestarts.kt — read the source

KitRestartStatus

data class KitRestartStatus(val running: Boolean = false, val phase: String = "idle", val message: String = "", val log: String = "")

The phase Welcome is showing: idle, building, restarting or failed, with its log path.

KitRestarts

object KitRestarts

Prepare the next classpath before stopping Field, so a failed kit build leaves Welcome usable.

Declaration Summary
var status
fun prepare(checkout: File, ready: () -> Unit)

KitRuntime.kt

Loads each installed kit runtime once per host, then attaches and detaches documents against it. KitRuntime.start reads the package directories the launcher captured in afield.kit.packages, instantiates each manifest's runtime.lifecycle class from the personal class layer and calls KitLifecycle.startup. A kit whose startup throws is cleaned up, recorded in KitRuntime.errors and shown as an error in Welcome rather than failing the host. Opening a document that carries a KitInstanceRecord calls KitRuntime.activate, which resolves the document's WorkspaceProjectScope and runs KitLifecycle.activate inside its root box context; tearing that session down calls KitLifecycle.deactivate.

Those package directories come from the profile Gradle read at launch, not from the registry as it stands now, so a kit installed or updated since this host started is invisible here until a restart. KitRuntime.activate enforces the matching condition directly: a document records the runtime revision it was created against, and activation refuses when this host loaded a different one, naming both.

KitRuntime.launchBoxes answers what a starting point asks Field to run. It reads launch from the revision the document recorded rather than from the newer compatible one this host may hold, so a kit that has since renamed or reordered its boxes cannot change what an existing document launches, and it insists on exactly one live box per name instead of guessing. A shutdown hook deactivates every live document and calls KitLifecycle.cleanup in reverse load order, with four seconds to finish.

Reloading the personal layer (reloadPersonal()) goes through suspendForReload and resumeAfterReload: every kit is deactivated and cleaned up, the layer is replaced, and the kits start again from the new classes, reactivating the documents that were open. The package directories themselves are still the launch profile's, so installing or removing a kit needs a restart, as does a native library a kit has already loaded.

src/jvmMain/kotlin/afield/document/workspace/KitRuntime.kt — read the source

Used in: Kits and Starting Points

KitLifecycle

interface KitLifecycle

Implement in a kit's Kotlin extension; startup/cleanup belong to the host, activation to a document.

Declaration Summary
fun startup(context: KitHostContext) Called once when the host loads this kit, before any of its documents open. Throwing here cleans the kit up and leaves it reporting an error in Welcome, rather than failing the host.
fun activate(context: KitDocumentContext) Called as each of the kit's documents opens, inside that document's root box context, so a box API that reads the current box works. Throwing deactivates the document again and leaves it inactive.
fun deactivate(context: KitDocumentContext) Called as a document's session goes away. A failure here is logged and does not stop the teardown.
fun cleanup(context: KitHostContext) Called as the host shuts down or reloads the personal layer, after every document has been deactivated, and in reverse load order across kits. Shutdown allows four seconds for all of them.

KitHostContext

data class KitHostContext(val kit: InstalledKit, val directory: File)

The immutable runtime revision loaded by this process.

KitDocumentContext

data class KitDocumentContext(val host: KitHostContext, val document: SessionInformation, val project: WorkspaceProjectScope)

The ordinary Field document and its project's shared parent/resources/preferences.

KitRuntime

object KitRuntime

Runtime lifecycle for the exact package paths captured by the launcher, independent of pending registry changes.

Declaration Summary
val errors: Map<String, String> A snapshot of why each kit is not running, keyed by kit id, and by the package directory instead for a kit whose manifest would not read far enough to have one. KitPackages.statuses takes this to turn a registered kit into an error row in Welcome.
fun start() Loads every kit the launcher captured in afield.kit.packages and calls KitLifecycle.startup on each, in the order the launch profile lists them. Returns at once if the kits are already started, skips a dedicated-host kit, and records a kit that throws in errors instead of propagating it.
fun suspendForReload(): List<SessionInformation>? The documents active now, for resumeAfterReload; deactivates them and cleans up every kit. Null when the kits were never started, so there is nothing to resume.
fun resumeAfterReload(documents: List<SessionInformation>?) Starts the kits again, from the personal layer as it is now, and reactivates documents. A document that will not reactivate is reported in errors and left inactive.
fun activate(info: SessionInformation) Runs KitLifecycle.activate for a document that carries a KitInstanceRecord, inside its root box context. Does nothing for an ephemeral document, for one already active, or for one with no record, and throws when the kit is not loaded or when this host holds a different runtime revision from the one the document recorded, naming both. A failed activation is deactivated again and recorded in errors before the error is rethrown.
fun deactivate(info: SessionInformation) Runs KitLifecycle.deactivate for a document activate made active, and drops it from the active set. A document that is not active is ignored, and a lifecycle that throws is logged rather than raised, so one kit cannot hold up closing a document.
fun shutdown() Deactivates every live document, then calls KitLifecycle.cleanup on each kit in reverse load order, logging rather than raising a failure so one kit cannot strand the rest. Leaves the kits unstarted, so a later start loads them again from the classes as they then are.
fun launchBoxes(info: SessionInformation): List<Box>? The boxes a kit document's starting point asks Field to run, in the order it names them, and null for a document with no KitInstanceRecord. The names come from the revision the document recorded rather than from the newer compatible one this host may hold, and a name matched by anything other than exactly one live box throws rather than guessing. scheduleAutorunBoxes takes this list to keep the ordinary autorun sweep off these boxes; what launches them is the first open of a copy and Welcome's explicit activation.

Kits.kt

Kits package the shared parts of one kind of document so many documents can be installed from, and migrated to, the same source. A kit is what every document of one kind receives: the code of the boxes that play its roles (providers), the sources those boxes hold in other properties (a rule set, a recipe), and the asset files they read from the document's files/ folder. It is a directory with a kit.json beside those files; tools/field_kit.py makes one from a document, rehashes it, installs it in a document, migrates a document to a newer revision and publishes it as a Welcome starting point. public/docs/kits.md is the user's page.

{
  "schema": "field.kit.v1", "name": "rooms", "roleProperty": "kitRole",
  "launchOrder": ["rules", "plan"],
  "roles": {"rules": {"name": "Rules", "properties": {
      "code":       {"file": "roles/rules/code.kt",       "sha256": "…"},
      "roomRules":  {"file": "roles/rules/roomRules.json", "sha256": "…"},
      "rulesAudit": {"file": "roles/rules/roomRules.json", "sha256": "…", "frozen": true}}}},
  "assets": {"palette.json": "…"},
  "revision": "…"
}

A box plays a role when its KitManifest.roleProperty holds the role's key. Each property is installed from file in the kit, and sha256 is the hash of that file's bytes, which is also the hash of the property's text (SourceStudy.hash). An asset palette.json comes from the kit's files/palette.json and lands in the document's files/palette.json. The revision is the hash of everything else, so two kits with the same revision install the same bytes.

The document owns a copy. A document on a kit records the manifest it was last installed from in files/kit.json (its record), with the kit's directory. Nothing changes a document's copy behind its back: a newer kit is only a difference Kits.inspect reports, until a migration installs it. Kits.inspect compares three hashes for every property and asset: the record's, the document's, and the kit's. The document matching the kit is current; matching the record (and not the kit) means the kit changed it, which a migration installs; matching neither means the document's own edit, which a migration refuses to overwrite. A frozen property is a copy the document must not edit, such as the baseline an audit checks candidates against; a migration replaces it whenever it differs from the kit, and says so.

src/jvmMain/kotlin/afield/document/workspace/Kits.kt — read the source

Top-level functions

Declaration Summary
fun Box.showKit(kit: File? = null, channel: String = "kit"): KitInspection Show which kit this document runs, and every property and asset that differs from it, in a box output (channel, by default kit). kit is a kit directory to compare against; by default, the one the document's record names.

Kits

object Kits

Reads kit manifests and document records, and compares a document's role boxes and assets against them.

Declaration Summary
const val RECORD The record's path inside a document folder: files/kit.json, readable from a box as "kit.json".file.
const val DEFAULT_ROLE_PROPERTY The role property a kit uses unless its manifest names another. Field persists it.
fun record(documentDir: File): KitManifest? The record of the document in documentDir, or null when the document is not on a kit.
fun read(file: File): KitManifest Read a manifest: a kit's kit.json, a kit directory, or a document's record.
fun declare(kit: KitManifest) Have Field save every property kit installs, as sidecar files named by the kit file's extension. A property Field does not know is neither autosaved nor written; inspecting a document or opening one that holds a record calls this.
fun inspect(boxes: Collection<Box>, documentDir: File, kit: File? = null): KitInspection What the document in documentDir, holding boxes, runs and how it differs from its kit. kit is the kit to compare against (a directory or its kit.json); null means the one the record names. A document with no record compares against kit alone, as before installing it.
fun fileHash(file: File): String SHA-256 of a file's bytes, the hash a kit records for it.

KitProperty

data class KitProperty(val file: String, val sha256: String, val frozen: Boolean = false)

One property a kit installs on a role's box: the kit file it comes from, that file's hash, and whether it is a frozen copy.

KitRole

data class KitRole(val name: String, val properties: Map<String, KitProperty>)

A role: the box name a new document gives it, and the properties the kit installs on that box.

KitManifest

data class KitManifest(val name: String, val roleProperty: String, val launchOrder: List<String>, val roles: Map<String, KitRole>, val assets: Map<String, String>, val revision: String, val directory: String?)

A parsed kit.json or document record. directory is where the kit lives, when known.

Declaration Summary
val roles: Map<String, KitRole>, val assets: Map<String, String>, val revision: String, val directory: String?)

KitItemState

enum class KitItemState

How a document's copy of one kit property or asset stands against its record and its kit.

Constants: CURRENT, KIT_CHANGED, DOCUMENT_EDITED, FROZEN_STALE, REMOVED_FROM_KIT.

KitItem

data class KitItem(val role: String?, val property: String?, val asset: String?, val box: String?, val boxName: String?, val recorded: String?, val document: String?, val kit: String?, val frozen: Boolean, val state: KitItemState)

One property (role and property) or asset, with the three hashes: recorded, document and kit (null when absent, or when the kit is not known).

Declaration Summary
val recorded: String?, val document: String?, val kit: String?, val frozen: Boolean, val state: KitItemState)
val label: String rules · roomRules, or the asset's path.
val display: String How a person names it: Rules · roomRules (the box's name), or files/palette.txt.

KitInspection

data class KitInspection(val kit: String?, val recordedRevision: String?, val kitRevision: String?, val kitDirectory: String?, val kitFound: Boolean, val items: List<KitItem>, val problems: List<String>, val roleProperty: String = Kits.DEFAULT_ROLE_PROPERTY, val launchOrder: List<String> = emptyList(), val packageInstance: KitInstanceInspection? = null)

What Kits.inspect found: the kit's name, the recordedRevision the document runs, the kitRevision its kit is at now (null when the kit was not found), every items entry, and problems that stop a migration whatever the items say.

Declaration Summary
val kitFound: Boolean, val items: List<KitItem>, val problems: List<String>
val roleProperty: String
val packageInstance: KitInstanceInspection? The document's kit package, when it was created from one; Box.showKit fills it in.
fun inState(state: KitItemState): List<KitItem> Items in state.
val migrationRefusals: List<String> Whether a migration to the kit would go ahead: something to do, and no document edit or problem in the way.
fun summary(): String One paragraph, for the agent and the card.

VirtualFileSystem.kt

A read-only view of box text in saved documents as addressable files, so tools that expect files can read a box's code or shaders without opening Field. AfieldVirtualFileSystem names each text property afield://<document>/<uid>/<property>, lists them from a workspace directory (an afield: URI whose path is that directory) and reads them back from metadata.edn or metadata.json, resolving sidecar files and skipping tombstoned boxes.

Only properties in AfieldVirtualFileSystem.DEFAULT_PROPERTIES are exposed by default: code and the GLSL and Slang shader stages, each with a language label. collectVirtualFiles combines listing roots and reading explicit URIs across several VirtualFileSystems, dispatched by URI scheme; the AI4 filestore CLI in afield.ai.knowledge uses it to upload document text.

src/jvmMain/kotlin/afield/document/workspace/VirtualFileSystem.kt — read the source

Top-level functions

Declaration Summary
fun listVirtualFileRefs(roots: List<URI>, fileSystems: List<VirtualFileSystem>): List<VirtualFileRef> Lists every root against whichever of fileSystems claims its scheme, concatenated in the order the roots are given. A root with no scheme, or one no file system claims, is skipped without comment.
fun resolveVirtualFiles(uris: List<URI>, fileSystems: List<VirtualFileSystem>): List<VirtualTextFile> Reads each URI through whichever of fileSystems claims its scheme, keeping the ones that produced text. A URI that resolves to nothing is dropped, so the result may be shorter than uris and a caller that needs to know which failed has to compare them.
fun collectVirtualFiles(roots: List<URI>, uris: List<URI>, fileSystems: List<VirtualFileSystem>): List<VirtualTextFile> Reads everything the roots list and everything uris names, as one set of files with duplicate URIs collapsed. The explicit URIs come first, so an address given both ways is read once. This is the way in for a caller that wants document text without caring which file system produced it.

VirtualFileRef

data class VirtualFileRef(val uri: String, val fileName: String, val language: String)

One addressable file: its afield: uri, a display fileName and a language label.

Declaration Summary
val uri: String The address to read this file back by, which for AfieldVirtualFileSystem is afield://<document>/<uid>/<property>.
val fileName: String A name to show. AfieldVirtualFileSystem puts the whole URI here, since a box property has no filename.
val language: String What the text is written in, as a label for a reader rather than a parser: Kotlin, or a stage such as GLSL (fragment) or Slang (compute). Falls back to the property name for a property with no label.

VirtualTextFile

data class VirtualTextFile(val ref: VirtualFileRef, val content: String)

One virtual file, read whole.

Declaration Summary
val ref: VirtualFileRef Where this text came from, carried along so a consumer can attribute or re-read it.
val content: String The property's text, exactly as the document stores it.

VirtualFileSystem

interface VirtualFileSystem

A source of addressable read-only text files, chosen by the scheme of the URI being asked about.

Declaration Summary
val scheme: String The URI scheme this file system answers for, matched without regard to case.
fun list(root: URI): List<VirtualFileRef> Every file under root, which is a scheme-specific place to look rather than a single file.
fun read(uri: URI): VirtualTextFile? The text at uri, or null when it names nothing this file system can produce.

AfieldVirtualFileSystem

class AfieldVirtualFileSystem(private val roots: List<Path>, private val properties: Set<String> = DEFAULT_PROPERTIES, private val languageByProperty: Map<String, String> = DEFAULT_LANGUAGE_BY_PROPERTY) : VirtualFileSystem

The afield: file system, which reads box text straight out of saved documents on disk without a running Field. The two halves take their scope from different places: list reads whichever workspace directory the root URI names, while read resolves a document name against the roots this was constructed with.

Declaration Summary
override val scheme: String afield, the scheme of every URI this reads and every one it hands back.
override fun list(root: URI): List<VirtualFileRef> Every exposed text property of every saved document directly under root, whose path (or host) names a workspace directory. Reads metadata.edn, or metadata.json when there is no EDN, and leaves out tombstoned boxes. A root that is not a directory lists nothing rather than failing, and the listing is not limited to the constructor's roots, which govern read alone.
override fun read(uri: URI): VirtualTextFile? The text of one box property, found by looking for the document name under each of the constructor's roots in turn and taking the first that holds a document. Sidecar-backed properties are resolved to their contents. Null throughout for anything it cannot produce: another scheme, too few path segments, a property outside the exposed set, an unknown document or box, a tombstoned box, and a property whose stored value is not text.

WebPublication.kt

src/jvmMain/kotlin/afield/document/workspace/WebPublication.kt — read the source

WorkspaceLastDocument.kt

Where Field remembers the document you had open, keyed by workspace rather than by checkout.

This is the store behind lastFn, which is what a window opened without --document falls back to. The name is kept in a file named for the SHA-256 of the workspace's canonical path, under last-documents in the machine-wide user config directory, so two workspaces opened from one checkout remember different documents and one workspace opened from two checkouts remembers the same one.

Both directions are checked against the disk rather than trusted. A remembered name comes back only while a document by that name is still there, a name is written only if it resolves to something inside the workspace, and a name holding a path separator or naming one of the app's own routes is refused at both ends. The write goes to a temporary file beside the target and is moved into place, atomically where the filesystem supports it.

src/jvmMain/kotlin/afield/document/workspace/WorkspaceLastDocument.kt — read the source

WorkspaceProjectScope.kt

The shared box and the persistent preferences a workspace project gives every document in it. The folder side of a project lives in dir.kt: a directory under the workspace's _projects/ with a contents.edn membership list, a preferences.json and a files/ folder, listed by listWorkspaceProjects. This file is the part of it that is live in a running host. Each WorkspaceProjectScope owns an invisible Box whose id is project: followed by a hash of the project directory, and attachRootToMetaroot parents a document's root box beneath the scopes it belongs to instead of directly beneath the global metaroot. A property set on that box is therefore inherited by every box in every document of the project, which is how a kit shares settings and runtime objects between its documents.

From box code projectScopes() is the way in: projectScopes().single().setPreference("theme", "dark") writes a preference that outlives the host, and WorkspaceProjectScope.preferences reads a snapshot of them back. A project box never reaches a document's metadata.edn, because persistence filters it and the metaroot out of the saved parents and children. A document whose project is absent attaches to the metaroot instead, with nothing dangling in its saved graph.

WorkspaceProjectScopes keeps one scope per project directory for the life of the host, so two documents in one project see the same box identity rather than two copies of it. Documents created by KitInstances.create land in a kit-<id> project and ordinarily have exactly one scope, but nothing stops a document from belonging to several.

src/jvmMain/kotlin/afield/document/workspace/WorkspaceProjectScope.kt — read the source

Top-level functions

Declaration Summary
fun Box.projectScopes(): List<WorkspaceProjectScope> Shared project scopes above this document's root; a kit instance ordinarily has exactly one.

WorkspaceProjectScope

class WorkspaceProjectScope internal constructor(val directory: File)

One workspace project's shared parent, persistent preferences and shared files.

Declaration Summary
val name: String The project's name, which is its folder's name under the workspace's _projects/.
val files: File The project's shared files/ folder, created on first access, for assets its documents read.
val root: Box The invisible box every document of this project is parented beneath, so a property set here is inherited by every box in all of them. Its id is project: followed by a hash of the project directory's canonical path, and persistence keeps it out of a document's saved graph.
fun preferences(): Map<String, Any?> A snapshot; write a preference with setPreference so it survives a host restart.
fun setPreference(name: String, value: Any?) Writes one preference to preferences.json and republishes the whole set on root as projectPreferences. A null value removes the preference instead. A blank name is refused, and the write replaces the file, so the last writer across two hosts on one workspace wins.

WorkspaceProjectScopes

object WorkspaceProjectScopes

Stable scope identity per workspace/project, rebuilt from project membership on document attachment.

Declaration Summary
fun get(workspace: File, name: String): WorkspaceProjectScope The scope for a project of workspace, made on first ask and the same instance afterwards, so every document of the project shares one box identity. Refuses a project the workspace does not list.
fun forDocument(document: File): List<WorkspaceProjectScope> The scopes a document belongs to, by project name, read from the membership lists of the workspace it sits in. A kit instance ordinarily has one, an ordinary document none, and nothing stops several.
fun isProjectRoot(box: Box): Boolean Whether box is a live project scope's own box. Saving asks in order to drop it from a document's stored parents and children, and reattaching a root asks in order to find the parents to replace. Identity against the cache rather than the id alone, so a box whose id merely begins project: is not mistaken for one.

copy.kt

Duplicating boxes inside a document: copyBox clones one box, copyBoxes clones a selection and rewires its connections, and nextCopyName names the results.

copyBox copies the persistent properties except the graph edges (parents, children, nested, nestedIn), gives the copy a fresh uuid, a (copy) or (copy n) name and its own engine, connects it under root, and by default offsets its frame by 80 in x and y. copyBoxes also takes every box nested inside the selection and skips tombstoned ones. Connections between copied boxes point at the copies; connections to boxes outside the selection are kept, so a copy stays wired to the same outside parents and children. Only the top-level boxes of the selection are offset, so nested copies keep their place inside their copied container.

src/jvmMain/kotlin/afield/document/workspace/copy.kt — read the source

Top-level functions

Declaration Summary
fun nextCopyName(name: String): String Returns the next copy-style name for name.
fun copyBoxes(boxes: List<Box>): List<Box> Copies boxes and everything nested in them, as described above, and returns the new boxes.
fun copyBox(b: Box, offsetFrame: Boolean = true): Box A copy of b in the same document, offset by 80 in x and y when offsetFrame is true.

dir.kt

The server side of the workspace browser: listing, searching, tagging and copying the documents under a workspace folder.

simpleDir and welcomeDir list documents for the file browser and the welcome page, startingPoints and templateStartingPoints list the transcripts and templates a new document can start from, and openStartingPointIntoWorkspace and duplicateWorkspaceDocument copy one into the workspace. searchWorkspace searches document contents, updateDocumentTags and exportTaggedDocumentsZip manage tags, and createWorkspaceProject and its neighbours group documents into projects stored under the workspace's _projects folder.

src/jvmMain/kotlin/afield/document/workspace/dir.kt — read the source

Top-level functions

Declaration Summary
fun registerTemplate(dir: String)
fun simpleDir(base: String): List<Entry>
fun parseRecentDocuments(raw: String?): List<String>
fun updateRecentDocuments(raw: String?, documentName: String, maxEntries: Int = 200): String
fun welcomeDirCandidates(base: String, recents: List<String>): List<WelcomeDirCandidate> Cheap first pass over the workspace: stat every document directory and order it the way the welcome screen wants (recency first, then most-recently-modified). No metadata is parsed here, so this stays fast even for large workspaces and lets callers stream summaries in final order.
fun summarizeWelcomeCandidate(candidate: WelcomeDirCandidate, now: Long = System.currentTimeMillis(), savingContext: SavingContext = SavingContext()): WelcomeEntry? Expensive per-document step: parse (or reuse a cached parse of) the document metadata to obtain its box count and tags. Returns null if the document can't be read.
fun welcomeDir(base: String, recents: List<String>): List<WelcomeEntry>
fun startingPointsDir(): File? Locate the bundled "starting points" workspace — the materialized agent transcripts. These are curated, CI-verified example documents that ship with Field (not in the user's workspace), so the welcome screen and its search can offer them as ready-to-open scenes. Resolution: -Dafield.startingPoints.dir override, else the repo-relative dir walked up from the working dir. Returns null when not found (e.g. a packaged build without the docs assets) — callers degrade to simply not offering starting points.
fun startingPoints(): List<WelcomeEntry> Welcome cards for the bundled starting points, tagged kind = "starting-point". Empty when none.
fun templatesStartingPointsDir(): File? Locate the bundled document-template directory (templates/), whose self-contained document subfolders (each carrying a metadata.edn/metadata.json) are surfaced on the welcome screen as a distinct "Templates" group of starting points. Resolution mirrors startingPointsDir: -Dafield.templates.dir override, else the repo-relative dir walked up from the working dir. Returns null when not found (e.g. a packaged build without the bundled templates).
fun personalTemplatesDir(): File Where a person's own starting points live: ~/.afield/templates, or -Dafield.personalTemplates.dir, or AFIELD_PERSONAL_TEMPLATES_DIR. tools/field_kit.py publish writes a kit's starting point here.
fun templateStartingPoints(): List<WelcomeEntry> Welcome cards for the bundled document templates, tagged kind = "template". Empty when none.
fun searchTemplateStartingPoints(rawQuery: String, maxDocuments: Int = 50, maxMatchesPerDocument: Int = 5, maxScanMillis: Long = 3_000L, cancellationCheck: () -> Unit = {}): WorkspaceSearchResponse Text-search the bundled document templates, tagging every result kind = "template".
fun searchStartingPoints(rawQuery: String, maxDocuments: Int = 50, maxMatchesPerDocument: Int = 5, maxScanMillis: Long = 3_000L, cancellationCheck: () -> Unit = {}): WorkspaceSearchResponse Text-search the bundled starting points, tagging every result kind = "starting-point".
fun projectsDir(base: String): File
fun listWorkspaceProjects(base: String): List<ProjectEntry>
fun createWorkspaceProject(base: String, name: String, documents: List<String> = emptyList()): ProjectMutationResponse
fun addDocumentToWorkspaceProject(base: String, projectName: String, filename: String): ProjectMutationResponse
fun deleteWorkspaceProject(base: String, name: String): ProjectMutationResponse
fun searchWorkspace(base: String, rawQuery: String, maxDocuments: Int = 50, maxMatchesPerDocument: Int = 5, maxScanMillis: Long = 3_000L, cancellationCheck: () -> Unit = {}): WorkspaceSearchResponse
fun normalizeDocumentTags(tags: List<String>): List<String>
fun resolveWorkspaceDocumentDir(base: String, filename: String): File
fun updateDocumentTags(base: String, filename: String, tags: List<String>): DocumentTagsUpdateResponse
fun openStartingPointIntoWorkspace(base: String, name: String): DuplicateDocumentResponse Copy a bundled starting point (a transcript or a BUNDLED_DOCUMENT_STARTING_POINTS document) into the user's workspace under a fresh, non-colliding name and return it, so the welcome screen can offer "open" on a read-only bundled scene without ever mutating the bundle. Reuses the same recursive copy duplicateWorkspaceDocument uses (skipping backups/.git/versions.edn).
fun copyKitStartingPoint(base: String, name: String, sourceDir: File): DuplicateDocumentResponse Copy a validated kit starting point through the ordinary document-copy path.
fun duplicateWorkspaceDocument(base: String, sourceFilename: String, newFilename: String): DuplicateDocumentResponse
fun exportTaggedDocumentsZip(base: String, rawTag: String): TaggedDocumentsZip

Top-level values

Declaration Summary
val defaultImportClassBlacklist
val BUNDLED_DOCUMENT_STARTING_POINTS: Map<String, String> Saved documents offered as starting points as they are, rather than materialized from a transcript: name to path relative to public/docs/assets. A transcript cannot carry what these are worth opening for (a conversation's study revisions, held variants, tool I/O and chat), so the welcome screen lists, searches and copies the document itself. public/docs/agent_transcripts.md gives each one an entry under its bundled section, which AgentTranscriptIndexGuard holds equal to this map.

Entry

class Entry

One document in a workspace listing, as simpleDir returns it.

Declaration Summary
var filename: String
var numBoxes: Int
var lastModified: Long
var boxes: MutableList<Pair<String, String>>
var metadata: MutableList<Pair<String, List<String>>>

WelcomeEntry

data class WelcomeEntry(val filename: String, val numBoxes: Int, val sizeBytes: Long, val lastModifiedEpochMs: Long, val lastModifiedAgoMs: Long, val tags: List<String> = emptyList(), val runningSessionCount: Int = 0, val runningExecutionCount: Int = 0, val kind: String = "document", val canvasThumbnail: String = "", val fourThumbnail: String = "")

Declaration Summary
val filename: String
val numBoxes: Int
val sizeBytes: Long
val lastModifiedEpochMs: Long
val lastModifiedAgoMs: Long
val tags: List<String>
val runningSessionCount: Int
val runningExecutionCount: Int
val kind: String
val canvasThumbnail: String
val fourThumbnail: String

ProjectEntry

data class ProjectEntry(val name: String, val documents: List<String>, val sizeBytes: Long, val lastModifiedEpochMs: Long, val lastModifiedAgoMs: Long, val kind: String = "project")

Declaration Summary
val name: String
val documents: List<String>
val sizeBytes: Long
val lastModifiedEpochMs: Long
val lastModifiedAgoMs: Long
val kind: String

RunningDocumentEntry

data class RunningDocumentEntry(val documentName: String, val sessionCount: Int, val ephemeral: Boolean = false, val openUrl: String)

Declaration Summary
val documentName: String
val sessionCount: Int
val ephemeral: Boolean
val openUrl: String

GcPauseStats

data class GcPauseStats(val monitoring: Boolean = false, val collectorCount: Int = 0, val observedCount: Long = 0, val sampleCount: Int = 0, val sampleCapacity: Int = 0, val medianPauseMs: Double = 0.0, val p95PauseMs: Double = 0.0, val maxPauseMs: Long = 0, val latestPauseMs: Long = 0)

Declaration Summary
val monitoring: Boolean
val collectorCount: Int
val observedCount: Long
val sampleCount: Int
val sampleCapacity: Int
val medianPauseMs: Double
val p95PauseMs: Double
val maxPauseMs: Long
val latestPauseMs: Long

RunloopTimingStats

data class RunloopTimingStats(val running: Boolean = false, val frame: Long = 0, val totalSamples: Long = 0, val sampleCount: Int = 0, val sampleCapacity: Int = 0, val ticksPerSecond: Double = 0.0, val latestIntervalMs: Double = 0.0, val intervalMedianMs: Double = 0.0, val intervalP95Ms: Double = 0.0, val intervalP99Ms: Double = 0.0, val intervalMaxMs: Double = 0.0, val latestWorkMs: Double = 0.0, val workMedianMs: Double = 0.0, val workP95Ms: Double = 0.0, val workP99Ms: Double = 0.0, val workMaxMs: Double = 0.0, val intervalsOver1Ms: Int = 0, val intervalsOver4Ms: Int = 0, val intervalsOver8Ms: Int = 0, val intervalsOver16Ms: Int = 0)

Declaration Summary
val running: Boolean
val frame: Long
val totalSamples: Long
val sampleCount: Int
val sampleCapacity: Int
val ticksPerSecond: Double
val latestIntervalMs: Double
val intervalMedianMs: Double
val intervalP95Ms: Double
val intervalP99Ms: Double
val intervalMaxMs: Double
val latestWorkMs: Double
val workMedianMs: Double
val workP95Ms: Double
val workP99Ms: Double
val workMaxMs: Double
val intervalsOver1Ms: Int
val intervalsOver4Ms: Int
val intervalsOver8Ms: Int
val intervalsOver16Ms: Int

RunloopSchedulingStatus

data class RunloopSchedulingStatus(val idleStrategy: String = "spin-then-park", val spinIterations: Int = 0, val parkNanos: Long = 0, val affinityRequested: Boolean = false, val affinityCore: Int = -1, val affinityApplied: Boolean = false, val affinityThreadId: Long = -1, val affinityMessage: String = "")

Declaration Summary
val idleStrategy: String
val spinIterations: Int
val parkNanos: Long
val affinityRequested: Boolean
val affinityCore: Int
val affinityApplied: Boolean
val affinityThreadId: Long
val affinityMessage: String

RuntimeAotCacheStatus

data class RuntimeAotCacheStatus(val state: String = "unknown", val summary: String = "unknown", val cachePath: String = "", val autoGenerate: Boolean = false, val attempted: Boolean = false, val detail: String = "")

Declaration Summary
val state: String
val summary: String
val cachePath: String
val autoGenerate: Boolean
val attempted: Boolean
val detail: String

RuntimeInfo

data class RuntimeInfo(val javaVersion: String = "", val javaRuntimeName: String = "", val javaRuntimeVersion: String = "", val javaVmName: String = "", val javaVmVendor: String = "", val javaVmVersion: String = "", val javaVendor: String = "", val javaHome: String = "", val runtimeVersion: String = "", val osName: String = "", val osVersion: String = "", val osArch: String = "", val processId: Long = -1, val uptimeMs: Long = 0, val inputArguments: List<String> = emptyList(), val availableProcessors: Int = 0, val maxMemoryBytes: Long = 0, val totalMemoryBytes: Long = 0, val freeMemoryBytes: Long = 0, val aotCacheStatus: RuntimeAotCacheStatus = RuntimeAotCacheStatus(), val gcPauseStats: GcPauseStats = GcPauseStats(), val runloopTimingStats: RunloopTimingStats = RunloopTimingStats(), val runloopSchedulingStatus: RunloopSchedulingStatus = RunloopSchedulingStatus())

Declaration Summary
val javaVersion: String
val javaRuntimeName: String
val javaRuntimeVersion: String
val javaVmName: String
val javaVmVendor: String
val javaVmVersion: String
val javaVendor: String
val javaHome: String
val runtimeVersion: String
val osName: String
val osVersion: String
val osArch: String
val processId: Long
val uptimeMs: Long
val inputArguments: List<String>
val availableProcessors: Int
val maxMemoryBytes: Long
val totalMemoryBytes: Long
val freeMemoryBytes: Long
val aotCacheStatus: RuntimeAotCacheStatus
val gcPauseStats: GcPauseStats
val runloopTimingStats: RunloopTimingStats
val runloopSchedulingStatus: RunloopSchedulingStatus

CompletionDocDraftSummary

data class CompletionDocDraftSummary(val cachePath: String = "", val exists: Boolean = false, val draftCount: Int = 0, val fileCount: Int = 0, val outsideCount: Int = 0, val message: String = "")

The KDoc completion drafted for undocumented declarations, as the welcome page shows it. The drafts are worked by the docs revision pass (tools/docs_notes.py import-drafts, then a pass on /docs-pass.html), not by a tool of their own.

Declaration Summary
val cachePath: String
val exists: Boolean
val draftCount: Int Drafts for declarations under src/ (any source set, src/personal included).
val fileCount: Int
val outsideCount: Int Drafts for declarations outside src/ (dependency jars): never imported.
val message: String

AiToolUseLogSummary

data class AiToolUseLogSummary(val logDir: String = "", val exists: Boolean = false, val logCount: Int = 0, val recentLogCount: Int = 0, val sizeBytes: Long = 0, val lastModifiedEpochMs: Long = 0, val reportPath: String = "", val dryRunPatchPath: String = "", val message: String = "")

Declaration Summary
val logDir: String
val exists: Boolean
val logCount: Int
val recentLogCount: Int
val sizeBytes: Long
val lastModifiedEpochMs: Long
val reportPath: String
val dryRunPatchPath: String
val message: String

AgentWorktreeProposal

data class AgentWorktreeProposal(val id: String, val label: String = "", val path: String = "", val branch: String = "", val baseBranch: String = "", val baseCommit: String = "", val createdAtEpochMs: Long = 0, val updatedAtEpochMs: Long = 0, val status: String = "created", val taskName: String = "", val reportPath: String = "", val dryRunPatchPath: String = "", val message: String = "")

Declaration Summary
val id: String
val label: String
val path: String
val branch: String
val baseBranch: String
val baseCommit: String
val createdAtEpochMs: Long
val updatedAtEpochMs: Long
val status: String
val taskName: String
val reportPath: String
val dryRunPatchPath: String
val message: String

AgentWorktreeActionRequest

data class AgentWorktreeActionRequest(val id: String)

Declaration Summary
val id: String

AgentWorktreeActionResponse

data class AgentWorktreeActionResponse(val success: Boolean, val proposal: AgentWorktreeProposal? = null, val message: String = "")

Declaration Summary
val success: Boolean
val proposal: AgentWorktreeProposal?
val message: String

AiToolUseImprovementStatus

data class AiToolUseImprovementStatus(val logs: AiToolUseLogSummary = AiToolUseLogSummary(), val worktrees: List<AgentWorktreeProposal> = emptyList(), val running: Boolean = false, val status: String = "idle", val runId: String = "", val taskName: String = "", val dryRun: Boolean = true, val startedAtEpochMs: Long = 0, val finishedAtEpochMs: Long = 0, val exitCode: Int? = null, val message: String = "", val logTail: String = "", val reportMarkdown: String = "", val dryRunPatch: String = "", val transcriptPath: String = "", val viewStamp: String = "")

Declaration Summary
val logs: AiToolUseLogSummary
val worktrees: List<AgentWorktreeProposal>
val running: Boolean
val status: String
val runId: String
val taskName: String
val dryRun: Boolean
val startedAtEpochMs: Long
val finishedAtEpochMs: Long
val exitCode: Int?
val message: String
val logTail: String
val reportMarkdown: String
val dryRunPatch: String
val transcriptPath: String
val viewStamp: String Changes whenever the transcript, report or patch does: the page fetches AiToolUseImprovementView again then.

AiToolUseImprovementView

data class AiToolUseImprovementView(val viewStamp: String = "", val transcriptHtml: String = "", val reportHtml: String = "", val patchHtml: String = "")

The run's transcript, report and patch as the AI4 chat panel renders them (aiChatHtml.kt), for ai-chat-view.html.

Declaration Summary
val viewStamp: String
val transcriptHtml: String
val reportHtml: String
val patchHtml: String

AiToolUseImprovementRunRequest

data class AiToolUseImprovementRunRequest(val dryRun: Boolean = true, val proposalCount: Int = 1)

Declaration Summary
val dryRun: Boolean
val proposalCount: Int

AiToolUseImprovementClearRequest

data class AiToolUseImprovementClearRequest(val recentOnly: Boolean = true)

Declaration Summary
val recentOnly: Boolean

WelcomeDocsResponse

data class WelcomeDocsResponse(val documents: List<WelcomeEntry>, val lastOpened: String, val defaultDocument: String, val allTags: List<String> = emptyList(), val runningDocuments: List<RunningDocumentEntry> = emptyList(), val startingPoints: List<WelcomeEntry> = emptyList(), val templates: List<WelcomeEntry> = emptyList(), val currentWorkspace: String = "", val defaultWorkspace: String = "", val aiSettings: AiSettingsStatus = AiSettingsStatus(), val preferences: FieldPreferences = FieldPreferences(), val projects: List<ProjectEntry> = emptyList(), val runtimeInfo: RuntimeInfo = RuntimeInfo(), val pythonSettings: PythonSettingsStatus = PythonSettingsStatus(configured = false, source = "not loaded", kind = "", path = "", configPath = "", message = "not loaded"), val completionDocDrafts: CompletionDocDraftSummary = CompletionDocDraftSummary(), val aiToolUseImprovement: AiToolUseImprovementStatus = AiToolUseImprovementStatus(), val codeUse: CodeUseSummary = CodeUseSummary(enabled = false, databasePath = "", queueSize = 0, droppedEvents = 0, message = "not loaded"), val semanticSearch: SemanticRetrievalSettingsStatus = SemanticRetrievalService.settingsStatus())

Declaration Summary
val documents: List<WelcomeEntry>
val lastOpened: String
val defaultDocument: String
val allTags: List<String>
val runningDocuments: List<RunningDocumentEntry>
val startingPoints: List<WelcomeEntry>
val templates: List<WelcomeEntry>
val currentWorkspace: String
val defaultWorkspace: String
val aiSettings: AiSettingsStatus
val preferences: FieldPreferences
val projects: List<ProjectEntry>
val runtimeInfo: RuntimeInfo
val pythonSettings: PythonSettingsStatus
val completionDocDrafts: CompletionDocDraftSummary
val aiToolUseImprovement: AiToolUseImprovementStatus
val codeUse: CodeUseSummary
val semanticSearch: SemanticRetrievalSettingsStatus

MavenDependencyEntry

data class MavenDependencyEntry(val coordinate: String, val resolved: Boolean = false, val jarCount: Int = 0, val latestVersion: String = "", val status: String = "", val resolvedAtEpochMs: Long = 0)

Declaration Summary
val coordinate: String
val resolved: Boolean
val jarCount: Int
val latestVersion: String
val status: String
val resolvedAtEpochMs: Long

MavenClasspathStatus

data class MavenClasspathStatus(val repositories: List<String> = emptyList(), val dependencies: List<MavenDependencyEntry> = emptyList(), val resolvedJars: List<String> = emptyList(), val attachedJarCount: Int = 0, val annotationSnippet: String = "", val message: String = "")

Declaration Summary
val repositories: List<String>
val dependencies: List<MavenDependencyEntry>
val resolvedJars: List<String>
val attachedJarCount: Int
val annotationSnippet: String
val message: String

MavenRepositoryRequest

data class MavenRepositoryRequest(val url: String)

Declaration Summary
val url: String

MavenDependencyRequest

data class MavenDependencyRequest(val coordinate: String)

Declaration Summary
val coordinate: String

MavenResolveRequest

data class MavenResolveRequest(val coordinates: List<String> = emptyList())

Declaration Summary
val coordinates: List<String>

MavenUpgradeResponse

data class MavenUpgradeResponse(val upgraded: List<String> = emptyList(), val status: MavenClasspathStatus)

Declaration Summary
val upgraded: List<String>
val status: MavenClasspathStatus

MavenSearchResult

data class MavenSearchResult(val groupId: String, val artifactId: String, val latestVersion: String, val packaging: String = "", val versionCount: Int = 0, val timestampEpochMs: Long = 0, val coordinate: String)

Declaration Summary
val groupId: String
val artifactId: String
val latestVersion: String
val packaging: String
val versionCount: Int
val timestampEpochMs: Long
val coordinate: String

MavenSearchResponse

data class MavenSearchResponse(val query: String, val results: List<MavenSearchResult>, val message: String = "")

Declaration Summary
val query: String
val results: List<MavenSearchResult>
val message: String

FieldPreferences

data class FieldPreferences(val profiling: Boolean = false, val framePresentationLog: Boolean = false, val logShadercSource: Boolean = true, val enableVR: Boolean = false, val enableHDR: Boolean = false, val openXRDebugInput: Boolean = true, val validate: Boolean = true, val quietMode: Boolean = false, val autoOpenLastDocumentInDesktop: Boolean = false, val boxInterrupt: Boolean = true, val editorPropertyStickiness: String = "none", val oscEditorEnabled: Boolean = false, val oscEditorPort: Int = 9001, val oscEditorFloatAddress: String = "/field/float", val oscEditorLaunchAddress: String = "/field/launch", val oscEditorStopAddress: String = "/field/stop", val oscEditorDebounceMs: Int = 200, val oscEditorMaxExecutionMs: Int = 750, val oscEditorDeltaMode: Boolean = false, val importClassBlacklist: List<String> = defaultImportClassBlacklist)

Declaration Summary
val profiling: Boolean
val framePresentationLog: Boolean
val logShadercSource: Boolean
val enableVR: Boolean
val enableHDR: Boolean
val openXRDebugInput: Boolean
val validate: Boolean
val quietMode: Boolean
val autoOpenLastDocumentInDesktop: Boolean
val boxInterrupt: Boolean Compile box code so a box that never returns can be interrupted (afield.runtime.lifecycle.BoxInterrupt).
val editorPropertyStickiness: String
val oscEditorEnabled: Boolean
val oscEditorPort: Int
val oscEditorFloatAddress: String
val oscEditorLaunchAddress: String
val oscEditorStopAddress: String
val oscEditorDebounceMs: Int
val oscEditorMaxExecutionMs: Int
val oscEditorDeltaMode: Boolean
val importClassBlacklist: List<String>

FieldPreferencesUpdateRequest

data class FieldPreferencesUpdateRequest(val preferences: FieldPreferences)

Declaration Summary
val preferences: FieldPreferences

FieldPreferencesUpdateResponse

data class FieldPreferencesUpdateResponse(val preferences: FieldPreferences, val message: String)

Declaration Summary
val preferences: FieldPreferences
val message: String

OscEditorLearnRequest

data class OscEditorLearnRequest(val port: Int, val timeoutMs: Long = 10_000, val ignoreAddresses: List<String> = emptyList())

Declaration Summary
val port: Int
val timeoutMs: Long
val ignoreAddresses: List<String>

OscEditorLearnResponse

data class OscEditorLearnResponse(val address: String, val message: String = "")

Declaration Summary
val address: String
val message: String

WorkspaceSwitchRequest

data class WorkspaceSwitchRequest(val path: String)

Declaration Summary
val path: String

AiProviderModelSettings

data class AiProviderModelSettings(val flagshipModel: String = "", val fastModel: String = "", val fallbackModel: String = "")

Declaration Summary
val flagshipModel: String
val fastModel: String
val fallbackModel: String

AiSettingsStatus

data class AiSettingsStatus(val hasOpenAiPlatformKey: Boolean = false, val hasOpenRouterApiKey: Boolean = false, val provider: String = "openai", val source: String = "", val configPath: String = "", val openRouterConfigPath: String = "", val baseUrl: String = "", val openRouterBaseUrl: String = "", val flagshipModel: String = "", val fastModel: String = "", val fallbackModel: String = "", val modelsByProvider: Map<String, AiProviderModelSettings> = emptyMap(), val localBaseUrl: String = "", val hasLocalApiKey: Boolean = false, val environmentOverrides: List<String> = emptyList())

Declaration Summary
val hasOpenAiPlatformKey: Boolean
val hasOpenRouterApiKey: Boolean
val provider: String
val source: String
val configPath: String
val openRouterConfigPath: String
val baseUrl: String
val openRouterBaseUrl: String
val flagshipModel: String
val fastModel: String
val fallbackModel: String
val modelsByProvider: Map<String, AiProviderModelSettings>
val localBaseUrl: String
val hasLocalApiKey: Boolean
val environmentOverrides: List<String>

AiSettingsUpdateRequest

data class AiSettingsUpdateRequest(val provider: String = "openai", val openAiPlatformKey: String = "", val openRouterApiKey: String = "", val openRouterBaseUrl: String = "", val flagshipModel: String = "", val fastModel: String = "", val fallbackModel: String = "", val localBaseUrl: String = "", val localApiKey: String = "")

Declaration Summary
val provider: String
val openAiPlatformKey: String
val openRouterApiKey: String
val openRouterBaseUrl: String
val flagshipModel: String
val fastModel: String
val fallbackModel: String
val localBaseUrl: String
val localApiKey: String

AiSettingsUpdateResponse

data class AiSettingsUpdateResponse(val hasOpenAiPlatformKey: Boolean, val hasOpenRouterApiKey: Boolean, val provider: String, val source: String, val configPath: String, val openRouterConfigPath: String, val baseUrl: String, val flagshipModel: String, val fastModel: String, val fallbackModel: String, val message: String, val localBaseUrl: String = "", val hasLocalApiKey: Boolean = false, val environmentOverrides: List<String> = emptyList())

Declaration Summary
val hasOpenAiPlatformKey: Boolean
val hasOpenRouterApiKey: Boolean
val provider: String
val source: String
val configPath: String
val openRouterConfigPath: String
val baseUrl: String
val flagshipModel: String
val fastModel: String
val fallbackModel: String
val message: String
val localBaseUrl: String
val hasLocalApiKey: Boolean
val environmentOverrides: List<String>

LocalAiConnectionRequest

data class LocalAiConnectionRequest(val baseUrl: String, val apiKey: String = "", val model: String = "")

LocalAiConnectionStatus

data class LocalAiConnectionStatus(val connected: Boolean, val models: List<String> = emptyList(), val modelAvailable: Boolean = false, val message: String)

Declaration Summary
val connected: Boolean
val models: List<String>
val modelAvailable: Boolean
val message: String

WorkspaceSwitchResponse

data class WorkspaceSwitchResponse(val workspace: String, val current: Boolean, val restarting: Boolean, val message: String)

Declaration Summary
val workspace: String
val current: Boolean
val restarting: Boolean
val message: String

WelcomeChangelogEntry

data class WelcomeChangelogEntry(val hash: String, val summary: String, val committedEpochMs: Long)

Declaration Summary
val hash: String
val summary: String
val committedEpochMs: Long

WelcomeChangelogResponse

data class WelcomeChangelogResponse(val entries: List<WelcomeChangelogEntry>)

Declaration Summary
val entries: List<WelcomeChangelogEntry>

DocumentTagsUpdateRequest

data class DocumentTagsUpdateRequest(val filename: String, val tags: List<String>)

Declaration Summary
val filename: String
val tags: List<String>

DocumentTagsUpdateResponse

data class DocumentTagsUpdateResponse(val filename: String, val tags: List<String>, val changed: Boolean)

Declaration Summary
val filename: String
val tags: List<String>
val changed: Boolean

DuplicateDocumentRequest

data class DuplicateDocumentRequest(val sourceFilename: String, val newFilename: String)

Declaration Summary
val sourceFilename: String
val newFilename: String

DuplicateDocumentResponse

data class DuplicateDocumentResponse(val filename: String, val numBoxes: Int, val sizeBytes: Long, val tags: List<String> = emptyList())

Declaration Summary
val filename: String
val numBoxes: Int
val sizeBytes: Long
val tags: List<String>

StartingPointOpenRequest

data class StartingPointOpenRequest(val name: String)

Declaration Summary
val name: String

RevealDocumentRequest

data class RevealDocumentRequest(val filename: String)

Declaration Summary
val filename: String

RevealDocumentResponse

data class RevealDocumentResponse(val filename: String, val path: String, val message: String)

Declaration Summary
val filename: String
val path: String
val message: String

ProjectCreateRequest

data class ProjectCreateRequest(val name: String, val documents: List<String> = emptyList())

Declaration Summary
val name: String
val documents: List<String>

ProjectAddDocumentRequest

data class ProjectAddDocumentRequest(val projectName: String, val filename: String)

Declaration Summary
val projectName: String
val filename: String

ProjectDeleteRequest

data class ProjectDeleteRequest(val name: String)

Declaration Summary
val name: String

ProjectMutationResponse

data class ProjectMutationResponse(val project: ProjectEntry, val changed: Boolean, val message: String = "")

Declaration Summary
val project: ProjectEntry
val changed: Boolean
val message: String

TaggedDocumentsZip

data class TaggedDocumentsZip(val filename: String, val bytes: ByteArray, val documentCount: Int)

Declaration Summary
val filename: String
val bytes: ByteArray
val documentCount: Int

WorkspaceSearchMatch

data class WorkspaceSearchMatch(val boxName: String, val property: String, val line: Int, val snippet: String)

Declaration Summary
val boxName: String
val property: String
val line: Int
val snippet: String

WorkspaceSearchResult

data class WorkspaceSearchResult(val filename: String, val numBoxes: Int, val lastModifiedEpochMs: Long, val lastModifiedAgoMs: Long, val tags: List<String> = emptyList(), val matches: List<WorkspaceSearchMatch>, val kind: String = "document", val documents: List<String> = emptyList())

Declaration Summary
val filename: String
val numBoxes: Int
val lastModifiedEpochMs: Long
val lastModifiedAgoMs: Long
val tags: List<String>
val matches: List<WorkspaceSearchMatch>
val kind: String
val documents: List<String>

WorkspaceSearchResponse

data class WorkspaceSearchResponse(val query: String, val documents: List<WorkspaceSearchResult>, val totalDocuments: Int, val totalMatches: Int, val truncated: Boolean)

Declaration Summary
val query: String
val documents: List<WorkspaceSearchResult>
val totalDocuments: Int
val totalMatches: Int
val truncated: Boolean

WelcomeDirCandidate

data class WelcomeDirCandidate(val filename: String, val dir: File, val metadataFile: File, val lastModifiedEpochMs: Long, val sizeBytes: Long)

A workspace document discovered by welcomeDirCandidates, described only by cheap stat metadata. Turning one into a WelcomeEntry (box count + tags) requires parsing its metadata file, which summarizeWelcomeCandidate does lazily.

Declaration Summary
val filename: String
val dir: File
val metadataFile: File
val lastModifiedEpochMs: Long
val sizeBytes: Long

metadata.kt

Per-box YAML metadata and the tags read from it. A box's metadata property holds YAML text, saved as the <uid>_metadata_.yaml sidecar; tagsForBox reads one key of it as a list of strings, and tagDatabase scans every document folder under a workspace root for those sidecars and collects each box's tags.

tagsForBox returns null when the YAML is not a map or lacks the key; tagDatabase skips a sidecar whose YAML is not a map. A YAML syntax error is not caught in either and propagates.

src/jvmMain/kotlin/afield/document/workspace/metadata.kt — read the source

Top-level functions

Declaration Summary
fun idFromFilename(fn: String) The box uid a sidecar filename begins with, taken as everything before its first underscore.
fun tagDatabase(root: String): List<Pair<BoxReference, List<String>>> Every tagged box under a workspace root, as a box and the tags its metadata sidecar gives it. Reads each document folder's <uid>_metadata_.yaml files and takes their tags key, accepting a single value as a one-element list. A sidecar whose YAML is not a map, or which has no tags, is left out rather than reported; a YAML syntax error propagates, and so does a root that is not a directory.
fun parseMetadata(s: String) A box's YAML metadata as a map. Nothing is caught here: empty text loads as null and fails the cast, and YAML that is a list or a scalar does the same, so a caller that does not know the text is a map should wrap this.
fun tagsForBox(b: Box, key: String? = null) tagsForString over a box's own metadata property. The key has no useful default: left out, the lookup asks for a null key and the result is null for all but the rare YAML that has one.
fun tagsForString(s: String, key: String? = null): List<String>? One key of a YAML map as a list of strings, with a single value read as a one-element list and everything stringified. Null when the text is not a map, when the key is absent, and when its value is null, so a caller cannot tell those apart; callers in this repo pass "tags" and treat null as no tags.

Top-level values

Declaration Summary
var Box.metadata: String The box's YAML metadata text; empty by default.

BoxReference

data class BoxReference(val document: String, val id: String)

One box in one document, as tagDatabase names the boxes it found.

Declaration Summary
val document: String The absolute path of the document folder, not the document's name.
val id: String The box uid, read off the sidecar's filename by idFromFilename.

provisional.kt

Provisional edits: proposed text for a box property, typically from AI4, held per document until someone accepts or rejects it. Entries are kept in memory, keyed by document, box uid and property, and every change to the entries sends the document's whole ProvisionalSet to its browsers as set-provisional.

acceptProvisional writes the proposed text into the box, records the acceptance (with any AiProvisionalArchive markdown attached by setProvisionalArchive) in the flight recorder, and drops the entry; rejectProvisional only drops it. acceptAllProvisional and rejectAllProvisional do the same for a whole document. When the decided property is a shader source (GLSL, Slang or stackSL) and the box already has the matching live shader module, that module is rebuilt afterwards, once per box and shader kind, on the runloop; rejecting reloads it too.

src/jvmMain/kotlin/afield/document/workspace/provisional.kt — read the source

Top-level functions

Declaration Summary
fun markProvisional(box: Box, property: String, contents: String? = null) Proposes contents for one of a box's properties, against the document the box belongs to. With no contents it proposes the property's current text, which makes accepting a no-op on the box and leaves the proposal as a thing to approve rather than a change to apply. Does nothing when the box belongs to no document, and when the property is absent or does not hold text.
fun setProvisional(document: String, uid: String, property: String, contents: String) Proposes contents for a box property by document path and box uid, replacing any proposal already held for that property, and sends the document's whole set to its browsers. Neither the box nor the property has to exist yet; a proposal for a box the session does not have is dropped when it is decided.
fun setProvisionalArchive(document: String, uid: String, property: String, archive: AiProvisionalArchive) Attaches the reasoning behind a proposal to one box property, to be recorded if that property is accepted. An archive with blank markdown is ignored. Archives are held apart from the proposals, and one is dropped when its own proposal is decided, so an archive attached to a property with no proposal stays until the document's proposals are cleared as a whole.
fun setProvisionalArchive(document: String, entries: List<ProvisionalEntry>, archive: AiProvisionalArchive) Attaches one archive to every property in entries, which is how a single conversation covers the several boxes it edited. Each entry gets the same markdown, so accepting any one of them records the whole of it.
fun clearProvisional(box: Box, property: String): Boolean Drops the proposal for one of a box's properties without applying it, and reports whether there was one. False for a box that belongs to no document. This is removeProvisional addressed by box; note that the other function of this name takes a document path and drops every proposal in it.
fun removeProvisional(document: String, uid: String, property: String): Boolean Drops one proposal and its archive without applying it, reporting whether there was one to drop. The document's set is sent to its browsers on every path through this, including the ones that found nothing, so a browser showing a stale proposal is corrected either way.
fun clearProvisional(document: String) Drops every proposal and archive held for a document, applying none of them, and tells its browsers. Takes a document path, unlike the overload taking a box and a property, which drops exactly one.
fun provisionalSet(document: String): ProvisionalSet A document's proposals as the wire type the frontend receives, which is provisionalEntries in an envelope.
fun provisionalEntries(document: String): List<ProvisionalEntry> Every proposal held for a document, each naming a box uid, a property and the proposed text. Empty for a document with none. The order follows the concurrent maps these are held in, so treat it as arbitrary.
fun acceptProvisional(document: String, uid: String, property: String): Boolean Writes one proposal into its box, records the acceptance with any archive attached to it, and drops the proposal. Reports false and changes nothing when there is no such proposal; when the box is not in the current session it also drops the proposal, since nothing could accept it later. A decided shader property has its live module rebuilt afterwards.
fun recordAcceptedProvisionalValue(document: String, uid: String, property: String, acceptedValue: String): Boolean Records an acceptance of acceptedValue, with any archive attached to the property, and does nothing else: the proposal stays and the box is not written. The editor sends this when someone takes part of a proposal hunk by hunk, so what was accepted is the editor's text rather than the proposal verbatim. Reports false when the proposal or the box is gone.
fun rejectProvisional(document: String, uid: String, property: String): Boolean Drops one proposal without writing it, and reports whether there was one. A rejected shader property has its live module rebuilt too, so the box goes back to running the source it had before the proposal.
fun acceptAllProvisional(document: String) Writes every proposal a document holds into its box and records each acceptance, then clears the document. A proposal whose box is not in the session is cleared along with the rest without being applied. Each box and shader kind is rebuilt once, however many of its properties were accepted.
fun rejectAllProvisional(document: String) Drops every proposal a document holds, writing none of them, and rebuilds the live shader modules of the boxes whose shader sources were among them.
fun sendProvisionalSet(document: String) Sends a document's whole proposal set to every browser open on it, as set-provisional. The set is sent whole rather than as a delta, so this is also how a browser is corrected after any change.

AiProvisionalArchive

data class AiProvisionalArchive(val markdown: String, val source: String = "AI4")

Markdown kept beside a provisional entry and recorded with it when the entry is accepted.

Declaration Summary
val markdown: String The reasoning to keep with the acceptance, usually the conversation that produced the edit. Blank markdown is not stored at all, so an archive has to carry something to survive.
val source: String Who proposed the edit. Both archives built in the tree leave it at AI4, and an acceptance is recorded from markdown alone, so nothing reads this yet.

stdlib_buffers.kt

add for FloatBuffer and IntBuffer: append components at the current position and return the buffer, so writes chain.

The vertex-writing idiom, buf.add(x, y).add(x2, y2). There are fixed-arity overloads for two and three floats and for a Vec2, Vec3 or Vec4, and a vararg one that converts any Number, which boxes each argument and is therefore the wrong one for a per-frame loop over thousands of vertices. IntBuffer.add is the index-buffer counterpart, truncating rather than rounding; it is called add because IntBuffer.put(index, data) already means writing at an absolute index.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/stdlib_buffers.kt — read the source

Top-level functions

Declaration Summary
fun FloatBuffer.add(x: Float, y: Float): FloatBuffer Appends two floats at the buffer's current position, advances it by two, and returns the buffer so calls chain.
fun FloatBuffer.add(a: Vector2f): FloatBuffer Appends a Vec2's two components at the current position, advances by two, and returns the buffer.
fun FloatBuffer.add(a: Vector3f): FloatBuffer Appends a Vec3's three components at the current position and advances by three. Chainable.
fun FloatBuffer.add(a: Vector4f): FloatBuffer Appends a Vec4's four components at the current position and advances by four. Chainable.
fun FloatBuffer.add(x: Float, y: Float, z: Float): FloatBuffer Appends three float components and returns this buffer for chaining.
fun FloatBuffer.add(vararg x: Number): FloatBuffer Appends any number of values, each converted to Float, and returns the buffer.
fun IntBuffer.add(vararg x: Number): IntBuffer Appends any number of values, each converted to Int (truncating, not rounding), and returns the buffer for chaining — the index-buffer counterpart of FloatBuffer.add.

stdlib_remotejs.kt

Running JavaScript in the browser from inside a box.

inBrowser evaluates a string as JavaScript in every browser session attached to this workspace, in the context of the calling box. It is fire-and-forget: nothing comes back and there is no error channel, so a mistake in the code fails silently in the browser console. It is the way to reach the box's own DOM, or something on the page Kotlin has no handle on.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/lang/remotejs/stdlib_remotejs.kt — read the source

Top-level functions

Declaration Summary
fun Box.inBrowser(code: String) Evaluates code as JavaScript in every browser session attached to this workspace, in the context of this box.

stdlib.kt

Matrix construction, terse numeric conversions and half-float packing.

mat4 builds a transform from a base matrix, a translation, a rotation and a scale, all four optional, so mat4() is the identity; mat3 is its linear part, for normals and 2D transforms, and ignores the translation it will accept. HasMatrix4f is what anything able to hand out a 4x4 pose implements. fa and ia are array literals for FloatArray and IntArray, and Number.f / .i / .l / .d are the conversions that keep Kotlin's numeric widening out of the way in graphics code. Vec2h, Vec3h and Vec4h name the packed fp16 vector types, and ByteBuffer.putf16 appends one half float, little-endian.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/math/linalg/stdlib.kt — read the source

Top-level functions

Declaration Summary
operator fun Matrix4f.set(n: Int, v: Float) Writes one element of the matrix by a single row-major index: n / 4 is the row and n % 4 the column.
fun mat4(base: Matrix4f? = null, t: Vector3f? = null, r: Quaternionf? = null, s: Vector3f? = null): Matrix4f Builds a 4×4 transform from any combination of a starting matrix, a translation, a rotation and a scale — everything optional, so mat4() is the identity.
fun mat3(base: Matrix3f? = null, t: Vector3f? = null, r: Quaternionf? = null, s: Vector3f? = null): Matrix3f Builds a 3×3 matrix from an optional starting matrix, rotation and scale — the linear part of mat4, for normals and 2D transforms.
fun fa(vararg f: Number): FloatArray A FloatArray from any numbers, converted for you — fa(1, 2.5, 3f). The terse array literal.
fun ia(vararg f: Number): IntArray An IntArray from any numbers, truncated toward zero rather than rounded — ia(1, 2.9) is [1, 2].
fun ByteBuffer.putf16(value: Float): ByteBuffer Appends value as a 16-bit half float, little-endian, two bytes, and returns the buffer for chaining.

Top-level values

Declaration Summary
val Number.f This number as a Float — 3.d.f, x.f — the shorthand that keeps Kotlin's numeric conversions out of the way in graphics code.
val Number.i This number as an Int, truncated toward zero: 1.9.i is 1 and (-1.9).i is -1.
val Long.i This Long as an Int, wrapping silently if it does not fit — the low 32 bits, not a clamp.
val Number.l This number as a Long, truncated toward zero if it was fractional.
val Number.d This number as a Double. Widening, so nothing is lost — except that a Float's error comes with it.

HasMatrix4f

interface HasMatrix4f

Implemented by anything that can hand out a 4×4 transform, so callers can take a pose without caring how it was stored.

Declaration Summary
val m: Matrix4f This object's transform as a 4×4 matrix. May be recomputed on every read.

Vec2h

typealias Vec2h = afield.graphics.vulkan.util.Vec2h

A pair of 16-bit half floats, for packed vertex attributes and uniforms. Half the bytes of a Vec2, a fraction of the precision — see ByteBuffer.putf16 for the range.

Vec3h

typealias Vec3h = afield.graphics.vulkan.util.Vec3h

Three 16-bit half floats — the packed transit form of a Vec3 for GPU buffers.

Vec4h

typealias Vec4h = afield.graphics.vulkan.util.Vec4h

Four 16-bit half floats — the packed transit form of a Vec4, and enough precision for a colour.

LanguageRuntimeSupport.kt

Shared machinery for a language runtime that converts a language root's children to its own execution.

TopologyWatcher runs a callback on the runloop whenever a session's box graph changes. applyRuntimeToggle does the conversion: each child of a language root gets the runtime's execution, a language label in its corner and the matching code_language. A child converts back to Kotlin only when it has no language-root parent left, so the order of connecting and disconnecting does not matter.

src/jvmMain/kotlin/afield/runtime/LanguageRuntimeSupport.kt — read the source

Top-level functions

Declaration Summary
fun applyRuntimeToggle(boxes: Collection<Box>, isRoot: (Box) -> Boolean?, runtimeClass: KClass<out BoxExecution>, newExecution: (Box) -> BoxExecution, language: String, customization: String, notifyCategory: String, onAttach: (Box) -> Unit = {}, onDetach: (Box) -> Unit = {})

TopologyWatcher

class TopologyWatcher(private val label: String, private val intervalMs: Long = 5L, private val onTopologyChanged: (Collection<Box>) -> Unit)

Polls every session's topology generation on the runloop and calls back with its boxes when it changes.

Declaration Summary
fun start()

MainLanguageRegistry.kt

The registry that lets a box choose the language its children run in, such as Python instead of Kotlin.

A language runtime registers itself with registerMainLanguageRuntime. Every registered runtime then gets a "Set Main Language: ..." command in the palette, next to "Set Main Language: Kotlin". Running one on a selected box records the choice in the box's persistent mainLanguage property and converts the box's children to that runtime. Choosing Kotlin clears the property and converts them back.

src/jvmMain/kotlin/afield/runtime/MainLanguageRegistry.kt — read the source

Top-level functions

Declaration Summary
fun registerMainLanguageRuntime(runtime: MainLanguageRuntime)
fun refreshMainLanguageRuntimes(boxes: Collection<Box>) Apply every registered language root synchronously.
fun Box.isMainLanguage(id: String): Boolean
fun installMainLanguageProperty()
fun installMainLanguageActions(root: Box)

Top-level values

Declaration Summary
var Box.mainLanguage: String?

MainLanguageRuntime

data class MainLanguageRuntime(val id: String, val label: String, val runtimeClass: KClass<out BoxExecution>, val setLegacyRoot: (Box, Boolean) -> Unit, val applyTopology: (Collection<Box>) -> Unit)

One language a box can give its children: an id, a palette label, and the hooks that convert them.

Declaration Summary
val id: String
val label: String
val runtimeClass: KClass<out BoxExecution>
val setLegacyRoot: (Box, Boolean) -> Unit
val applyTopology: (Collection<Box>) -> Unit

AaCompletionClient.kt

The HTTP bridge from Field's Kotlin completion to the out-of-process K2 Analysis-API completion sidecar.

AaCompletionClient is consulted only when the AA completion backend is active, and returns null on any failure so the caller falls back to the in-process K1 completer.

src/jvmMain/kotlin/afield/runtime/completion/AaCompletionClient.kt — read the source

AaCompletionClient

object AaCompletionClient

Stage C client for the out-of-process K2 Analysis-API completion endpoint (/complete on the code-use semantic sidecar). Used only when KotlinEngineConfig.activeCompletionBackend == AA; KotlinInterface2.complete falls back to the K1 delegate whenever this returns null (no sidecar URL configured, HTTP/timeout failure, or the sidecar reports failure). The AA classpath stays out of the live runtime: this is a thin HTTP bridge, mirroring CodeUseTelemetryService.

Declaration Summary
const val EXPECTED_TYPE_MATCH extraInfo key marking a candidate whose type fits the type expected at the caret.
const val DECLARED_IN Where a candidate is DECLARED (kotlin.Any, afield.audio.synth, afield.graphics.render.ComputeMesh). Read by KotlinCompletionSupport.classifyMetadata to categorise the row by its own declaration rather than by the receiver of the panel it appears in — see DECLARATION_PROXIMITY.
const val DECLARATION_PROXIMITY own | supertype | universal: how close the declaration is to the receiver's own type. The client (fieldSuggest.js) turns it into a ranking term, so ComputeMesh's own members sort above inherited ones, which sort above the names declared on Any/T that are true of everything (also, apply, let, and T.onBeat).
fun kotlinStdlibRoots(): List<String> Just the core kotlin-stdlib jar(s) from Field's classpath — the analysis scope for "stdlib-only" completion (KotlinJS / head boxes, which have no JVM classpath, only the Kotlin stdlib + their own declarations). Excludes -jdk7/-jdk8 so JVM-only extensions don't leak into completions for code that runs in a JS head. Empty if none found (caller then skips AA).
data class Outcome(val result: CompletionResult?, val detail: String) Outcome of an AA completion attempt. result non-null means AA served the completion; null means the caller should fall back to K1. detail always explains what happened (for logging).
fun complete(snippet: String, cursor: Int, preamble: String, implicitReceivers: List<String>, declarations: String = "", timeoutMs: Long = 4000, classpathRootsOverride: List<String>? = null, jsTarget: Boolean = false, replImports: List<String> = emptyList()): Outcome
data class LoweringError(val region: String, val factory: String, val message: String, val line: Int, val excerpt: String) One error in the lowered file, with the region of the lowering it belongs to.
data class Lowering(val success: Boolean = false, val message: String = "", val text: String = "", val preambleInjected: Boolean = false, val preambleBlank: Boolean = true, val stubCount: Int = 0, val errors: List<LoweringError> = emptyList()) What /lower reports: the file completion would analyse, and every error in it.
fun lower(snippet: String, cursor: Int, preamble: String, implicitReceivers: List<String>, declarations: String = "", replImports: List<String> = emptyList(), includeText: Boolean = false, timeoutMs: Long = 300_000): Lowering? Type-check the lowering for a request instead of completing it — the invariant behind CompletionLoweringProbe. Takes the same arguments as complete, because a lowering checked under a different request than the live one isn't checked. Null when there is no sidecar or the call failed.

CommonMemberPrior.kt

A ranking prior for the completion popup taken from a type's CommonMembers annotation, so that an empty-prefix popup such as a bare f. lists the members that matter first.

CommonMemberPrior.rankOf gives a member's declared position and CommonMemberPrior.scoreOf turns it into popup points, below a user's own recency and above the category weight.

src/jvmMain/kotlin/afield/runtime/completion/CommonMemberPrior.kt — read the source

CommonMemberPrior

object CommonMemberPrior

Resolves a receiver type's CommonMembers declaration into the ranking prior the popup consumes (Metadata.commonMemberRank, scored by COMMON_MEMBER_BASE in fieldSuggest.js).

Declaration Summary
const val BASE Score for the first-listed member. Below recency's ~62 ceiling, above CATEGORY_WEIGHT's 28.
const val STEP Points shed per position, so a declared order survives into the popup without swamping it.
fun rankOf(receiverQualifiedName: String?, memberName: String?): Int 1-based position of memberName in receiverQualifiedName's list, or 0 if either is absent.
fun scoreOf(rank: Int): Int Turns a rank into the client-side ranking term. Rank 0 scores nothing; the list decays by STEP.
fun ranksFor(receiverQualifiedName: String?): Map<String, Int> Declared common members of receiverQualifiedName, name to 1-based position. Empty if none.
fun declaredNames(type: Class<*>): List<String> Names declared directly on type's own annotation, in declared order — what the guard test checks against the class's actual members. A superclass's list is not included: a class is only answerable for the list it writes.

CompletionAaE2EProbe.kt

Runs Field's completion chain end to end against the Analysis-API sidecar, inside a live Field JVM: a box's gathered preamble and this JVM's classpath go out over HTTP and the candidates come back.

The probe switches the active backend to AA, builds a parent and a child box so the child completes with an inherited preamble, and asks for candidates at a series of carets. Every check names the backend that answered, read from KotlinInterface2.lastCompletionBackend, so an answer the K1 fallback produced counts as a failure however good its candidates look. The fallback offers every name in position, thousands of them, which is enough to satisfy a check for a present candidate while the sidecar is still building its session.

A sidecar is required, and the smoke runner supplies one. By hand, point the probe at one with -Dafield.completion.sidecarUrl, AFIELD_CODE_USE_SEMANTIC_SIDECAR_URL, or a build/completion-corpus/sidecar-url.txt; with no sidecar it exits 3 and never prints its AA E2E DONE marker, so the smoke gate reads an unreachable chain as a failure rather than as a skip. A cold sidecar indexes the whole classpath before it can answer anything, which takes minutes on the first request of its life and almost nothing once a running Field or an earlier probe has paid for it, so waiting for the session is itself one of the checks. A run whose candidate list came back truncated fails too: a receiver index too small for the classpath in front of it drops real members, and nothing in the answer says which.

src/jvmMain/kotlin/afield/runtime/completion/CompletionAaE2EProbe.kt — read the source

CompletionAaE2EProbe

object CompletionAaE2EProbe

STAGE D live end-to-end: exercises the REAL KI2 → AA-sidecar completion chain in a running Field JVM. Sets activeCompletionBackend = AA, builds a parent→child box pair with an inherited preamble (as CompletionContextProbe does), and calls KotlinInterface2.complete() — which POSTs the box's gathered preamble + implicit Box receiver + this JVM's classpath to the sidecar /complete.

Declaration Summary
fun main(args: Array<String>)

CompletionContextProbe.kt

src/jvmMain/kotlin/afield/runtime/completion/CompletionContextProbe.kt — read the source

CompletionContextProbe

object CompletionContextProbe

PHASE 0 context-dependent completion expectations (see dev_docs/COMPLETION_ENGINE.md). These can't be (string, cursor) fixtures — they depend on the box's INHERITED PREAMBLE, i.e. on where the box sits in the tree. Both mechanisms land in the inherited preamble:

Declaration Summary
fun main(args: Array<String>)

CompletionCorpus.kt

A reproducible corpus of real boxes for measuring Kotlin completion, harvested with their macros already expanded so each box sees the preamble it has at run time.

CompletionCorpus reads the bundled starting points and templates, expands each box's macros without evaluating its code, and checks the result in as fixtures that the completion probes measure against.

src/jvmMain/kotlin/afield/runtime/completion/CompletionCorpus.kt — read the source

CompletionCorpus

object CompletionCorpus

The completion corpus: real boxes, with the preamble they have AT RUNTIME.

Declaration Summary
const val ROOT_ID The id a document's root box has; document.md lists it as a parent without a record of its own.
val defaultDirs: List<String> Where the bundled starting points and templates live, relative to the repo root.
const val FIXTURE_DIR Checked-in harvest output.
data class BoxRec(val id: String, val parents: List<String>, val props: MutableMap<String, String> = mutableMapOf(), val childLanguage: String? = null) One box, as stored in a document.md: its parents and its named text properties. childLanguage is the language this box confers on its children when it is a language root (mainLanguage, or the legacy isPythonRoot / isRemoteJSRoot flags), lowercased; null for an ordinary box.
data class DocRec(val name: String, val dir: File, val boxes: LinkedHashMap<String, BoxRec>)
data class Case(val doc: String, val box: String, val code: String, val preamble: String, val receivers: List<String> = listOf("afield.document.graph.Box")) One corpus entry: a box's code, and the preamble it has once its macros have expanded.
fun sha256(s: String): String
fun parseDocument(file: File): DocRec? Parse a document.md — the machine-readable mirror Field writes beside metadata.edn. Only what this corpus needs is read: each box's parent list (for gatherPreambleUpwards) and its property texts. Not an EDN parser: the mirror exists so tools don't need one.
fun documents(dirs: List<String> = defaultDirs): List<DocRec> Every document under dirs (repo-relative or absolute), sorted, for determinism.
fun boxGraph(doc: DocRec): Map<String, Box> Reconstruct a document's box graph as detached Boxes carrying its text properties. Enough for gatherPreambleUpwards (which walks parents and reads preamble) and for macro expansion (which reads vertex/fragment/… and writes preamble). Nothing is registered anywhere, so these boxes are garbage once the caller drops them.
fun kotlinBoxes(doc: DocRec): List<BoxRec> Boxes whose code is Kotlin — the only ones this corpus is about.
fun harvest(ki: KotlinInterface2, dirs: List<String> = defaultDirs, onProblem: (String, Throwable) -> Unit = { _, _ -> }): List<Case> Expand every box's macros and read back its inherited preamble.
fun typesDeclaredWithoutAnImport(preamble: String): List<String> The types this preamble declares a property of but never brings into scope.
fun writeFixtures(cases: List<Case>, dir: File = File(FIXTURE_DIR)): Int Write cases as reviewable fixtures: one manifest line per box, and one .preamble.kt file per box that has a preamble. The preamble is the harvested part — the part no static read of the corpus can produce — so it is what gets checked in; the code is left where it already lives and referenced by a content hash, so there is exactly one copy of it in the repo and a corpus edit is detectable.
data class Drift(val label: String, val what: String) A fixture that no longer matches the corpus it was harvested from.
fun readFixtures(dir: File = File(FIXTURE_DIR), dirs: List<String> = defaultDirs): Pair<List<Case>, List<Drift>> Read the checked-in fixtures back, joined to the code they were harvested from. The join is verified: a box that has moved, vanished or been edited shows up in Drift rather than being measured against a preamble that belongs to different code.

CompletionCorpusHarvest.kt

Builds the checked-in completion fixtures under tools/completion-corpus/boxes, and on a plain run guards them instead of rewriting them.

The fixtures are real boxes, taken from the agent transcripts and templates in CompletionCorpus.defaultDirs, each with the preamble its parents give it. That preamble is what makes a completion measurement about Field rather than about Kotlin, and recovering it means reading whole documents and expanding their macros, so the probes downstream read it from disk instead: CompletionLoweringProbe measures against these files rather than harvesting its own.

This file's two jobs share one entrypoint, because a generator and the guard that watches for drift have to agree on what the fixtures should contain. AFIELD_HARVEST_WRITE=1 writes; anything else compares, prints a DRIFT line per difference and fails. AFIELD_LOWERING_CORPUS and AFIELD_HARVEST_DIR move the input and output directories.

A harvest resolves imports against the repository rather than against this machine, so a class in src/personal that shares a simple name with one in src/ cannot change what a fixture says a box can see. Running with AFIELD_IMPORT_HERMETIC=0 where that collision exists is refused rather than recorded; dev_docs/HERMETIC_COMPLETION_INPUTS.md has the history.

src/jvmMain/kotlin/afield/runtime/completion/CompletionCorpusHarvest.kt — read the source

CompletionCorpusHarvest

object CompletionCorpusHarvest

Harvests CompletionCorpus fixtures — and, run without AFIELD_HARVEST_WRITE=1, guards them.

Declaration Summary
fun main(args: Array<String>)

CompletionCorpusProbe.kt

What Kotlin completion offers at a caret, read against what it ought to offer there.

Each fixture is a fragment of box code with | where the caret sits, and a list of candidates the engine is expected to produce (goals) or to keep out (anti-goals). The matchers look for a candidate by call form, by class, by exact text, by exact text ignoring case, or in the named-argument form an editor offers for a parameter still to be filled in; they read the candidate set and not its order, because the order a reader sees is decided in the browser by fieldSuggest.js and is covered separately by FieldSuggestPresentationTest.

A run prints each goal as met or unmet, with the rank of the candidate that met it, and ends with a met/unmet summary. It always exits 0, so the unmet set is a reading rather than a gate. Completion is asked for through KotlinInterface2 itself, so the answers come from the engine a box talks to and not from a stand-in.

The fixtures here are written out in this file. The box-shaped fixtures that CompletionCorpusHarvest generates, with a real inherited preamble, are what CompletionLoweringProbe reads instead.

src/jvmMain/kotlin/afield/runtime/completion/CompletionCorpusProbe.kt — read the source

CompletionCorpusProbe

object CompletionCorpusProbe

PHASE 0 characterization corpus for completion, written as a specification of the behaviour wanted rather than as a snapshot of the behaviour there is.

Declaration Summary
fun main(args: Array<String>)

CompletionDiffK1Probe.kt

A probe that dumps the K1 completer's raw candidates over the shared completion fixtures, for diffing against the K2 Analysis-API sidecar's output.

Run as the kotlin.completion-diffdump.probe smoke test; it writes build/completion-corpus/k1-candidates.json, which tools/completion_diff.py compares with the sidecar's dump fixture by fixture.

src/jvmMain/kotlin/afield/runtime/completion/CompletionDiffK1Probe.kt — read the source

CompletionDiffK1Probe

object CompletionDiffK1Probe

STAGE B (K1 side of the differential). Runs the REAL production K1 completion path (KotlinInterface2.complete → K1 delegate, intro-warmed) over the shared TSV corpora and dumps the raw candidates per fixture to build/completion-corpus/k1-candidates.json. The AA side is dumped out-of-process by the sidecar spike (--json-out); tools/completion_diff.py reads both and diffs.

Declaration Summary
data class K1DumpCandidate(val text: String, val displayText: String, val tail: String, val icon: String)
data class K1DumpFixture(val id: String, val member: String, val isDot: Boolean, val prefix: String, val source: String, val candidates: List<K1DumpCandidate>)
fun main(args: Array<String>)

CompletionDocDrafts.kt

How many KDoc drafts completion has cached, for the welcome page.

Nothing here works the drafts. tools/docs_notes.py import-drafts makes each one under src/ a kdoc-draft note on its declaration, and the docs revision pass (/docs-pass.html) handles them like the human's own notes. See dev_docs/agents/COMPLETION_DOC_POLISH_DESIGN.md.

src/jvmMain/kotlin/afield/runtime/completion/CompletionDocDrafts.kt — read the source

CompletionDocDrafts

object CompletionDocDrafts

Counts the cached KDoc drafts that belong to declarations under src/, for the welcome page.

Declaration Summary
fun summary(): CompletionDocDraftSummary

CompletionDocScrapeProbe.kt

Asks how completion does on Field's own documentation: the fixtures are real doc code with the caret put back where the author was typing.

tools/scrape_completion_corpus.py walks the Kotlin fences in public/docs, cuts each sampled line at a member access, and records the member the page actually used into tools/completion-corpus/scraped.tsv as a label, the code with a ‸ for the caret, and that member's name. This probe replays each one through KotlinInterface2 and reports whether the member came back and at what rank, with the top candidates printed under it so an answer can be read rather than just scored.

Alongside the rank it reports whether the receiver type resolved at all, judged by how many candidates came back: more than two hundred is read as completion having fallen back to offering every name in scope. The two readings belong together, because a member can sit at a good rank in a list that was never about the receiver.

It is a reading and not a gate: it always exits 0. With the scraped file absent it says so and still prints its done marker, so the smoke entry shows that the probe ran rather than that it had anything to run on.

src/jvmMain/kotlin/afield/runtime/completion/CompletionDocScrapeProbe.kt — read the source

CompletionDocScrapeProbe

object CompletionDocScrapeProbe

PHASE 0 (exploratory): run REAL doc code through completion. Fixtures are scraped from diffstatic{_runner} kotlin blocks in public/docs, with a caret dropped mid-member-access and the remainder of the line deleted (tools/scrape_completion_corpus.py → tools/completion-corpus/scraped.tsv, columns: label, base64(code with ‸ caret), intendedMember).

Declaration Summary
fun main(args: Array<String>)

CompletionInteractionLogger.kt

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.

src/jvmMain/kotlin/afield/runtime/completion/CompletionInteractionLogger.kt — read the source

CompletionInteractionLogger

object CompletionInteractionLogger

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

Declaration Summary
fun record(event: CompletionInteractionEvent, box: Box, documentName: String, documentPath: String)

CompletionLoweringProbe.kt

src/jvmMain/kotlin/afield/runtime/completion/CompletionLoweringProbe.kt — read the source

CompletionLoweringProbe

object CompletionLoweringProbe

Measures the two things completion quality actually rests on, over a corpus of REAL boxes.

Declaration Summary
fun main(args: Array<String>)

GlslCompletionSupport.kt

Completion and hover context for GLSL shader properties (fragment, vertex, compute and the rest), including Field's {% ... %} and {{ ... }} shader template syntax.

GlslCompletionSupport.complete builds an inventory of the symbols visible in the shader (built-ins, declarations and struct members from the box's own source and its shader template) and offers root names, receiver.member completions, or template tags and expressions depending on the caret. GlslCompletionSupport.describeContext answers the matching symbol-context request.

src/jvmMain/kotlin/afield/runtime/completion/GlslCompletionSupport.kt — read the source

GlslCompletionSupport

object GlslCompletionSupport

Declaration Summary
fun isGlslProperty(property: String): Boolean
fun complete(box: Box, request: CompletionRequest): Completion
fun describeContext(box: Box, request: SymbolContextRequest): SymbolContextResponse

KotlinCompletionSupport.kt

src/jvmMain/kotlin/afield/runtime/completion/KotlinCompletionSupport.kt — read the source

KotlinSourceIndex.kt

An on-disk index of the Kotlin definitions (top-level, nested and extension) under Field's source roots, with each one's KDoc and completion category, which completion looks up by symbol name.

KotlinSourceIndex.startBackgroundSync scans src/jvmMain/kotlin, src/personal/kotlin and src/generatedLinalg/kotlin on a low-priority daemon thread and writes kotlin_source_index.json under afield.ai.knowledge.FilestoreDir. Lookups go through KotlinSourceIndex.getKDocForSymbol and KotlinSourceIndex.getCompletionCategoryForSymbol; KotlinSourceIndex.scanDefinitions is the scan alone, with no cache or network.

src/jvmMain/kotlin/afield/runtime/completion/KotlinSourceIndex.kt — read the source

Top-level functions

Declaration Summary
fun syncKotlinSourceIndex()

KotlinSourceFileRecord

data class KotlinSourceFileRecord(val path: String, val contentHash: String, val fileId: String?, val bytes: Long, val modifiedEpochMs: Long)

One scanned source file: its path, content hash and size.

Declaration Summary
val path: String
val contentHash: String
val fileId: String?
val bytes: Long
val modifiedEpochMs: Long

KotlinDefinitionRecord

data class KotlinDefinitionRecord(val name: String, val kind: String, val filePath: String, val line: Int, val fileId: String?, val contentHash: String, val kdoc: String? = null, val completionCategory: String? = null)

Declaration Summary
val name: String
val kind: String
val filePath: String
val line: Int
val fileId: String?
val contentHash: String
val kdoc: String?
val completionCategory: String?

KotlinSourceIndexDb

data class KotlinSourceIndexDb(val version: Int = 1, val root: String, val apiKeyHash: String?, val updatedAtEpochMs: Long, val files: List<KotlinSourceFileRecord>, val definitions: List<KotlinDefinitionRecord>, val vectorStoreId: String? = null, val vectorStoreFileHash: String? = null)

Declaration Summary
val version: Int
val root: String
val apiKeyHash: String?
val updatedAtEpochMs: Long
val files: List<KotlinSourceFileRecord>
val definitions: List<KotlinDefinitionRecord>
val vectorStoreId: String?
val vectorStoreFileHash: String?

KotlinSourceIndex

object KotlinSourceIndex

Declaration Summary
var latestIndex: KotlinSourceIndexDb? The index in memory — but only while it still belongs to the directory FilestoreDir currently resolves to.
fun loadIndex(): KotlinSourceIndexDb?
fun startBackgroundSync()
fun getKDocForSymbol(symbolName: String): String? Get KDoc for a symbol from the index.
fun getCompletionCategoryForSymbol(symbolName: String, sourceName: String? = null, sourcePath: String? = null, declaredIn: String? = null): String? The completion category for one candidate.
fun scanDefinitions(roots: List<Path>, relativeTo: Path): List<KotlinDefinitionRecord> The definitions roots declare — the deterministic half of runSync, with no network or cache.

descriptions.kt

src/jvmMain/kotlin/afield/runtime/completion/descriptions.kt — read the source

Top-level functions

Declaration Summary
fun findDefinition(a: Any?, search: Collection<*>?): String

util_completion.kt

src/jvmMain/kotlin/afield/runtime/completion/util_completion.kt — read the source

CompletionCategory

annotation class CompletionCategory(val value: String)

CommonMembers

annotation class CommonMembers(vararg val value: String)

The handful of members that answer "what do you actually do with one of these", in the order a person reaches for them — a ranking prior for the completion popup, declared where the type is.

BoxBodyRun.kt

Which boxes are executing their body right now, as distinct from a frame loop or callback the body started: BoxBodyRun counts each box's nested body runs, and a source-backed block reads it to refuse re-entering its own box.

src/jvmMain/kotlin/afield/runtime/execution/BoxBodyRun.kt — read the source

BoxBodyRun

object BoxBodyRun

Which boxes' bodies are executing right now: set by execution around a box's run or launch, and not by its frame loop or callbacks, which run after the body has returned.

Declaration Summary
inline fun <T> during(box: Box, block: () -> T): T Run block as the body of box.
fun isRunning(id: String): Boolean Whether the box with id is running its body (rather than a frame loop or a callback).

ExecutionVerdict.kt

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.

src/jvmMain/kotlin/afield/runtime/execution/ExecutionVerdict.kt — read the source

PreambleFailureStopsExecutionTest.kt

src/jvmMain/kotlin/afield/runtime/execution/PreambleFailureStopsExecutionTest.kt — read the source

PreambleFailureStopsExecutionTest

object PreambleFailureStopsExecutionTest

Declaration Summary
fun main(args: Array<String>)

ReturnValueRegistry.kt

What happens to the value a box's code returns: ReturnValueRegistry offers it to each registered ReturnValueHandler in turn, and the first one that accepts it keeps it running.

The handlers registered here run a returned Sequence one element per frame (registerSequenceReturnValue) and a returned frames { } block (registerFramesReturnValue). Several values can be returned together with launches or +, which builds a LaunchBundle. Each launch carries a per-strand generation, so relaunching or stopping a strand ends what it started before.

src/jvmMain/kotlin/afield/runtime/execution/ReturnValueRegistry.kt — read the source

Top-level functions

Declaration Summary
fun launches(vararg items: Any?): LaunchBundle
operator fun LaunchBundle.plus(other: Any?): LaunchBundle
operator fun Frames.plus(other: Any?): LaunchBundle
fun registerSequenceReturnValue()
fun registerFramesReturnValue()

ReturnValueHandler

typealias ReturnValueHandler = (inside: Box, returnValue: Any, strand: String, generation: Int, launchEpoch: Int, notify: (running: Boolean, strand: String, watermark: Int) -> Unit) -> Boolean

A handler that can launch a return value from box execution.

LaunchBundle

class LaunchBundle internal constructor(val items: List<Any?>)

Declaration Summary
override fun toString()

ReturnValueRegistry

object ReturnValueRegistry

Declaration Summary
val handlers
fun launch(inside: Box, returnValue: Any?, strand: String = "default", notify: (running: Boolean, strand: String, watermark: Int) -> Unit): Boolean

execution.kt

How a Kotlin box runs: KotlinExecution compiles and evaluates a box's code, launches what it returns, and routes its printed output, errors and completions back to the editor.

BoxExecution is the contract a language engine implements: run a fragment or the whole box, launch it on a strand, continue, stop, and answer completion and hover requests. KotlinExecution is the Kotlin implementation. configureOutputProxies connects a box's print, error and display(...) output to the client that ran it. improveMessage and suggestionsFor turn compiler diagnostics into the inline hints shown under an error.

src/jvmMain/kotlin/afield/runtime/execution/execution.kt — read the source

Top-level functions

Declaration Summary
fun installKEActions(root: Box) Adds the "Reset Kotlin Environment for Box" action to a new root: it stops the boxes it is run on and gives each a fresh Kotlin engine.
fun configureOutputProxies(box: Box, reply: (Message) -> Unit, originSession: WebSocketSession? = thisSession.get())
fun improveMessage(b: Box, errorMessage: String, code: String? = null, diagnostic: ScriptDiagnostic? = null): String
fun suggestionsFor(b: Box, errorMessage: String, code: String? = null, diagnostic: ScriptDiagnostic? = null): List<OutputSuggestion> Structured, actionable fixes for an error message — the data behind the Apply / Ask AI4 buttons in the error output area. Reuses the same ranking engine as improveMessage (suggestResolvableSymbols, numericMismatchHint) but returns OutputSuggestions instead of baked HTML. Position (OutputSuggestion.line/OutputSuggestion.column) comes from the diagnostic so the client can locate and replace the offending token.

BoxExecution

interface BoxExecution

Declaration Summary
fun executeFragment(inside: Box, execute: Execute, reply: ((Message) -> Unit))
suspend fun executeFragmentAndWait(inside: Box, execute: Execute, reply: ((Message) -> Unit))
fun executeAll(inside: Box, execute: Execute, reply: ((Message) -> Unit))
fun launchAll(inside: Box, execute: Execute, code: String = execute.code, strand: String = "default", reply: ((Message) -> Unit))
fun continueAll(inside: Box, execute: Execute, code: String = execute.code, strand: String = "default", reply: ((Message) -> Unit))
fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: ((Message) -> Unit))
fun stop(inside: Box)
fun complete(inside: Box, completionRequest: CompletionRequest, reply: ((Message) -> Unit))
fun describeSymbolContext(inside: Box, request: SymbolContextRequest): SymbolContextResponse
val languageName: String

DebugProbeSegment

data class DebugProbeSegment(val rawText: String, val expandedText: String, val localLine: Int, val startLine: Int, val endLine: Int)

Declaration Summary
val rawText: String
val expandedText: String
val localLine: Int
val startLine: Int
val endLine: Int

KotlinExecution

class KotlinExecution : BoxExecution

Declaration Summary
override fun executeFragment(inside: Box, execute: Execute, reply: ((Message) -> Unit))
override suspend fun executeFragmentAndWait(inside: Box, execute: Execute, reply: ((Message) -> Unit))
override fun executeAll(inside: Box, execute: Execute, reply: ((Message) -> Unit))
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: ((Message) -> Unit))
override fun continueAll(inside: Box, execute: Execute, code: String, strand: String, reply: ((Message) -> Unit))
suspend fun executeInBox(box: Box, code: String, reply: ((Message) -> Unit)?, originSession: WebSocketSession?, ln: Int, property: String = "code", runSetupBlocks: Boolean = true, requestId: String = "", executionMode: String = "fragment", sourceText: String = code, accepted: () -> Boolean = { true }, finishedHandler: () -> Unit = {}, runScope: RunScope? = null, handleReturn: (Any?) -> Unit)
fun launchReturnValue(inside: Box, returnValue: Any?, strand: String = "default", notify: (running: Boolean, strand: String, watermark: Int) -> Unit)
override fun stop(inside: Box)
override fun describeSymbolContext(inside: Box, request: SymbolContextRequest): SymbolContextResponse
fun resolveDebugProbeSegment(box: Box, allCode: String, line: Int): DebugProbeSegment?
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override val languageName: String

playlist.kt

Playlist functionality for Field4

This module provides helpers for executing boxes in a defined order based on their playlist property. Boxes with a playlist property (a Double) will be executed in order from lowest to highest value. Between each begin() call, executePlaylist waits pauseList frame() calls (default 1).

User-facing dev doc: dev_docs/Playlist.md (a pointer stub); this KDoc is the source of truth.

src/jvmMain/kotlin/afield/runtime/execution/playlist.kt — read the source

Top-level functions

Declaration Summary
fun Box.findBoxesWithPlaylist(): List<Box> Finds all boxes (recursively) that have the playlist property set
fun Box.executePlaylist(strand: String = "default"): Frames Executes all boxes with the playlist property in order (sorted by playlist value, low to high), calling begin(strand) on each.
fun Box.runPlaylist(strand: String = "default"): Frames Helper function to execute playlist from root This can be called from any box and will execute the playlist starting from root

Top-level values

Declaration Summary
var Box.playlist by afield.document.properties.Delegation<Double>() Extension property to get/set the playlist order for a box
var Box.pauseList by afield.document.properties.Delegation<Double>() The number of frame() calls executePlaylist makes after begin() before advancing to the next box. Defaults to 1 when unset; coerced to a minimum of 1.

stdlib_execution.kt

Starting and stopping box code from inside box code, and the two block markers the editor reads.

begin runs another box's whole code property as if its run key had been pressed, on a named strand, so a second launch on the same strand replaces the first; end stops every strand that box is running and disposes any Field 4 renderer it owns. start and animate open a Frames scope, the second re-entering its body every frame. aside holds a block an ordinary run skips and option-return executes, which is where one-off experiments and debug dumps can live instead of in a comment; cell marks a block as one notebook-style cell and at runtime only calls it.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/runtime/execution/stdlib_execution.kt — read the source

Top-level functions

Declaration Summary
fun Box.begin(strand: String = "default") Runs this box's whole code property, as if someone had pressed the run key on it.
fun Box.end() Stops everything this box is running — every strand, not just the default one — and disposes any Field 4 renderer it owns, closing that window.
fun start(a: suspend Frames.() -> Unit): Frames Starts a new Frames scope and runs a inside it.
fun start(a: suspend () -> Unit): Frames Thin wrapper around frames that starts a frame-based animation from a plain suspend lambda (no Frames receiver). The body is responsible for advancing frames if it needs multi-frame behavior.
fun animate(block: suspend Frames.() -> Unit): Frames Creates an animation loop from a lambda. The lambda will be executed every frame when this function is launched.
inline fun Box.aside(name: String = "", block: () -> Unit) A block that runs only when you explicitly ask for it — put the cursor inside and press option/alt-return — and is skipped by an ordinary run of the box.
inline fun <T> Box.cell(block: () -> T) Marks a block as one notebook-style cell, so the editor can treat it as a unit for auto-execution and re-running.

Top-level values

Declaration Summary
var Box._currentlyExecutingFragment: Boolean True while this box is running a fragment — a selected region or an alt-return block — rather than its whole body.

CompiledSnippetTest.kt

Smoke test of KotlinInterface.CompiledSnippet: compile a snippet once, then evaluate it many times.

It covers a basic compile and evaluate, repeated evaluations of one compiled snippet, snippets that read variables, and the compile-error and evaluation-error paths. It is a runnable object rather than a JUnit class, run by the smoke catalog as core.compiled-snippet.smoke.

src/jvmMain/kotlin/afield/runtime/kotlin/CompiledSnippetTest.kt — read the source

CompiledSnippetTest

object CompiledSnippetTest

Entry point and case list for the compiled-snippet smoke test.

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

DataFramePluginCostProbe.kt

Measures what wiring the Kotlin DataFrame compiler plugin adds to the cost of building a box's Kotlin engine.

Field builds one KotlinInterface2 per box, so a compiler plugin registered into the compilation state is paid for by every box in a document rather than by the boxes that use DataFrame. This entrypoint builds three engines in a row, times each one's construction and its first evaluation, and prints the per-engine total:

run-jvm-main afield.runtime.kotlin.DataFramePluginCostProbe --arg off

--arg on is the default and --arg off disables the plugin, by setting the afield.dataframePlugin property that KotlinInterface2.compilerPluginArgs reads through a lazy. The property has to be set before the first engine is built, which is why this is a jvm-main entrypoint rather than something a box can call. The other half of the cost, the dataframe-core jar every engine takes into its classpath snapshot, is fixed at build time, so the difference between an on run and an off run isolates the plugin's share. dev_docs/DATAFRAME_PLUGIN.md records the readings, and the different fix each of the two costs would need if one ever came to dominate.

src/jvmMain/kotlin/afield/runtime/kotlin/DataFramePluginCostProbe.kt — read the source

DataFramePluginCostProbe

object DataFramePluginCostProbe

What does wiring the DataFrame compiler plugin cost every other box?

Declaration Summary
fun main(args: Array<String>)

DataFramePluginSpike.kt

Spike that measures what the Kotlin DataFrame compiler plugin can do inside Field's K2 REPL.

The plugin hangs off the compiler's function-call refinement extension, which the compiler documents as "highly unstable and not recommended to use". At a call to a @Refine-annotated function it interprets the arguments statically, computes the resulting schema, and rewrites the call into a run block that casts it to DataFrame<Token> over local classes, because the extension runs at body resolution and cannot create top-level declarations. The column accessors are not declarations anywhere: they are a per-call-site local type plus an implicit extension receiver injected by the plugin.

The probes ask how far that survives the REPL:

  • within one snippet: a dataFrameOf(...) and a column access in the same evaluation, which shows whether refinement fires at all;
  • a refined operation: add("c") { a + b }, a schema that changes, with the new column named only in the returned type;
  • across snippets: the same two statements split across two evaluations of one engine, the boundary a box rerun has (boxes have separate engines, see SharedTypes). The receiver injection only looks inside callable and code-fragment containers and a REPL snippet is neither, so the injected receiver is expected not to survive; the schema state rides on the FIR declaration itself, which is the mechanism that could carry it anyway;
  • a shared @DataSchema interface, and one declared in a snippet.

Upstream's REPL work targets the Jupyter kernel, where cross-cell persistence comes from runtime codegen of real top-level @DataSchema interfaces and the plugin's top-level generator is switched off to avoid duplicating them (KT-77437). A within-snippet-only result here matches that design.

The two invariants, that the plugin is passed to the compiler and its runtime is on the classpath, fail the spike; the probes report either way.

src/jvmMain/kotlin/afield/runtime/kotlin/DataFramePluginSpike.kt — read the source

DataFramePluginSpike

object DataFramePluginSpike

Runs the DataFrame plugin invariants and probes and prints what each probe found.

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

ExportedTypeNames.kt

Renders the source-level type name that @exported writes into a consuming box's preamble.

@exported shares a value between boxes by two channels: the value itself crosses at runtime, while its type crosses as text, a generated val Box._x by InheritanceFromContext<T>() line spliced into the other box's preamble and recompiled by that box's own engine. This object computes the T.

Each box has its own KotlinInterface2 with its own REPL history and snippet classloader chain, so a type is usable in the consuming box only if two things hold:

  • It resolves at compile time. Every engine's compile classpath is derived from the shared base classloader, so a type is nameable when that loader can reach it. Library types, afield. types and @DependsOn jars qualify; a class declared in another box's snippet does not, since it exists only in that box's snippet loader.
  • It is the same class at run time. Snippet loaders are children of the shared loader, so a name that resolves there resolves to one Class for every box. The check is on class identity, so a shadowed name cannot produce a declaration that compiles and then throws ClassCastException where it is used.

A type that fails either test renders as ExportedTypeNames.OPAQUE (Any?). A preamble line that does not compile breaks every later compile in the consuming box, so a lost type costs less than a wrong one.

Type arguments are walked recursively and each unnameable argument is erased on its own to *. List<Double> stays List<Double>, and a list of a box-local class becomes kotlin.collections.List<*>. ExportedTypeNamesSmokeTest covers these cases.

User types declared in one box cannot pass these tests; sharing them needs a compilation unit on the base classloader, which SharedTypes is about.

src/jvmMain/kotlin/afield/runtime/kotlin/ExportedTypeNames.kt — read the source

ExportedTypeNames

object ExportedTypeNames

Type-name rendering for @exported; render is the entry point.

Declaration Summary
const val OPAQUE: String The fallback when a type cannot be named in another box. Always valid, always useless.
fun render(type: KType, loader: ClassLoader?): String The source text to use for type in another box's preamble, or OPAQUE if it cannot be named there.

ExportedTypeNamesSmokeTest.kt

Smoke for ExportedTypeNames, which decides the type text @exported splices into another box's preamble.

Two properties pull in opposite directions, and both are asserted here:

  • Precision. Library and afield. types survive with their generic arguments intact, so List<Double> reaches the consuming box as List<Double> rather than as Any?.
  • Safety. A type the consuming box cannot name degrades to Any? rather than being emitted and hoped for. A preamble line that fails to compile breaks every later compile in that box, so a false accept costs more than a false reject.

testUnnameableArgumentIsErasedNotEmitted is where the two meet: an argument the consuming box cannot name collapses to a star projection, leaving List<*>, so the outer type still lands and no unresolvable name reaches the preamble.

Pure renderer assertions: no REPL and no engine, so this stays fast. Runnable-object test style, matching WitherSmokeTest.

src/jvmMain/kotlin/afield/runtime/kotlin/ExportedTypeNamesSmokeTest.kt — read the source

ExportedTypeNamesSmokeTest

object ExportedTypeNamesSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

ExportedTypeNamesPublicProbe

class ExportedTypeNamesPublicProbe

Public probe for ExportedTypeNamesSmokeTest; nameable from the app classloader.

ExportedTypeNamesSuspendProbe

class ExportedTypeNamesSuspendProbe

Carries a suspend function type, which typeOf cannot express directly.

Declaration Summary
val fn: suspend (Int) -> String

FieldKtParser.kt

Kotlin source to a parse tree, without compiling it.

Several things in Field need to look at the shape of box code rather than run it: reformat it, find which imports it names, rewrite a call, translate it to JavaScript. Compiling is the expensive way to get that, and it fails on code that does not yet make sense, which is most code while it is being typed. This file holds the one parser they share: a KotlinCoreEnvironment built once and reused, and a parse that returns the tree or throws on the first syntax error.

It is derived from ktfmt's own parser; the Apache 2.0 notice at the top of the source file is ktfmt's.

src/jvmMain/kotlin/afield/runtime/kotlin/FieldKtParser.kt — read the source

FieldKtParser

object FieldKtParser

Parses Kotlin source as a script (which box code is) and returns its parse tree, for Field code that wants PSI without compiling: the comment pass in formatter.kt, the import analysis in boxImports.kt, the labelled-argument macro, the RemoteJS instrumenter, and the pattern-rewrite smoke test.

Declaration Summary
val env: KotlinCoreEnvironment The environment used to open a KtFile.
fun parse(code: String): KtFile

Kotlin24SyntaxSmokeTest.kt

Smoke test that a box compiles Kotlin 2.4 syntax: collection literals, positional destructuring with square brackets, and name-based destructuring with renamed entries.

One snippet on a fresh KotlinInterface2 declares a data class, a [1, 2, 3] list literal and both destructuring forms, and the test checks the result string. It runs in the smoke catalog as core.kotlin-2-4-syntax.smoke.

src/jvmMain/kotlin/afield/runtime/kotlin/Kotlin24SyntaxSmokeTest.kt — read the source

Kotlin24SyntaxSmokeTest

object Kotlin24SyntaxSmokeTest

Compiles and evaluates the Kotlin 2.4 syntax snippet; main fails if the result differs.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

KotlinEngineDisposalSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinEngineDisposalSmokeTest.kt — read the source

KotlinEngineDisposalSmokeTest

object KotlinEngineDisposalSmokeTest

Disposing a box's KotlinInterface2 while one of its compiles is queued or running.

Declaration Summary
fun main(args: Array<String>)

KotlinEngineReleaseSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinEngineReleaseSmokeTest.kt — read the source

KotlinEngineReleaseSmokeTest

object KotlinEngineReleaseSmokeTest

A disposed Kotlin engine is garbage: nothing static keeps its compiler alive.

Declaration Summary
fun main(args: Array<String>)

KotlinInterface.kt

The older K1 implementation of KotlinScriptEngine, built on the K1 REPL compiler.

KotlinInterface2 is the K2 replacement. createKotlinScriptEngine decides which of the two a box gets.

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinInterface.kt — read the source

KotlinInterface

class KotlinInterface(val propertyClasses: Map<String, KClass<*>> = emptyMap(), val properties: Map<String, Any?> = emptyMap(), override val implicitReceivers: List<KotlinType> = emptyList(), override var classLoader: ClassLoader? = null, val filePrefix: String = "EVAL_") : KotlinScriptEngine

Declaration Summary
val propertyClasses: Map<String, KClass<*>>
override val implicitReceivers: List<KotlinType>
override var classLoader: ClassLoader?
override var sourceProperty: String
var evaluator: BasicJvmReplEvaluator
var compiler: KJvmReplCompilerWithIdeServices
override var compilerConfig: ScriptCompilationConfiguration
val contextUpdater: ContextUpdater
override val context: KotlinContext
override var associatedBox: Any?
override fun dispose() Release the compiler's project and everything it analysed.
override val isDisposed: Boolean
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration>
var id
var cache
suspend fun compile(s: String): ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>>
suspend fun evaluate(e: LinkedSnippet<KJvmCompiledScript>, updateContext: Boolean = false): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
suspend fun evaluate(e: LinkedSnippet<KJvmCompiledScript>, updateContext: Boolean = true, implicits: List<Any>): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
fun String.findNthSubstring(s: String, n: Int, start: Int = 0): Int
fun Int.toSourceCodePositionWithNewAbsolute(code: SourceCode, newCode: SourceCode): SourceCode.Position?
override suspend fun complete(s: String, cursor: Int): CompletionResult
override fun completeAndWait(s: String, cursor: Int): CompletionResult
override fun applyMacros(box: Box, s: String, skipMacros: Set<String>, previewOnly: Boolean): String
override fun findLastImport(s: String): Int
override fun parseAndRewrite(s: String, rules: List<(PsiElement, String) -> String>): String
override var execNumber
override suspend fun exec(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit, implicits: List<Any>): ResultValue?
override fun execAndWait(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit, implicits: List<Any>): ResultValue?
inner class CompiledSnippet internal constructor(private val compiled: LinkedSnippet<KJvmCompiledScript>, private val defaultImplicits: List<Any>) : PreparedSnippet K1 implementation of PreparedSnippet; see that interface for the evaluation contract.
override suspend fun compileSnippet(s: String, implicits: List<Any>, onCompileError: (List<ScriptDiagnostic>) -> Unit): CompiledSnippet? Compile a K1 CompiledSnippet; the shared contract is on the overridden interface method.
override fun compileSnippetAndWait(s: String, implicits: List<Any>, onCompileError: (List<ScriptDiagnostic>) -> Unit): CompiledSnippet? Blocking wrapper around compileSnippet.
class ContextUpdater(val context: KotlinContext, private val evaluator: BasicJvmReplEvaluator, private val ki: KotlinInterface)

KotlinInterface2.kt

The K2 implementation of KotlinScriptEngine: the engine that compiles and runs a box's Kotlin.

Each box gets its own KotlinInterface2, with its own REPL history and snippet classloader. It compiles snippets with the K2 compiler, evaluates them, tracks the variables and functions they declare, and answers completion requests in the editor. Redeclaring a plain var resets older mutable bindings of the same name and Kotlin type after evaluation; captured vals stay fixed.

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinInterface2.kt — read the source

KotlinInterface2

class KotlinInterface2(val propertyClasses: Map<String, KClass<*>> = emptyMap(), val properties: Map<String, Any?> = emptyMap(), override val implicitReceivers: List<KotlinType> = emptyList(), override var classLoader: ClassLoader? = null, val filePrefix: String = "EVAL_") : KotlinScriptEngine

Declaration Summary
val propertyClasses: Map<String, KClass<*>>
val properties: Map<String, Any?>
override val implicitReceivers: List<KotlinType>
override var classLoader: ClassLoader?
val filePrefix: String
override var sourceProperty: String
var evaluator: K2ReplEvaluator
var compiler: K2ReplCompiler
override var compilerConfig: ScriptCompilationConfiguration
val contextUpdater: ContextUpdater
override val context: KotlinContext
override var associatedBox: Any?
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration>
var id
var compileCount Snippets this engine has really compiled — each one grows the compiler's REPL history.
var compileNanos Total wall time of those compiles, in nanoseconds.
val rerunCacheHits: Int Reruns served by rerunCache instead of a compile.
suspend fun compile(s: String): ResultWithDiagnostics<LinkedSnippet<CompiledScript>>
suspend fun evaluate(e: LinkedSnippet<out CompiledScript>, updateContext: Boolean = false): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
suspend fun evaluate(e: LinkedSnippet<out CompiledScript>, updateContext: Boolean = true, implicits: List<Any>): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
fun String.findNthSubstring(s: String, n: Int, start: Int = 0): Int
fun Int.toSourceCodePositionWithNewAbsolute(code: SourceCode, newCode: SourceCode): SourceCode.Position?
var lastCompletionBackend: KotlinEngineConfig.CompletionBackend? Which backend actually served the most recent complete call, and the AaCompletionClient.Outcome detail that decided it. complete logs both; reading them here lets a test assert which backend answered, which a test of the AA chain needs: a K1 fallback answers a name position with thousands of names, so "the candidate I wanted is in the list" can be satisfied by the fallback alone and still read as a green AA result. CompletionAaE2EProbe asserts it.
var lastCompletionDetail: String
override suspend fun complete(s: String, cursor: Int): CompletionResult Completion delegates to the K1 IDE-services path. K2 remains the execution backend, but autocomplete needs compiler-grade language semantics rather than partial handwritten Analysis API coverage.
override suspend fun warmCompletion() Build the K1 completion delegate ahead of the first real completion. The bootstrap is a full intro+preamble compile (~seconds, longer the colder the JVM's K1 infra). If it instead fires lazily from inside a runloop-pumping wait it re-enters the K1 engine mid-compile and corrupts its analysis state (a "Rewrite at slice PACKAGE_TO_FILES" error). Warming here, on the non-reentrant runloop before any such wait, keeps every later completion light and safe.
override suspend fun warmCompletionBackend(timeoutMs: Long): String Poll the AA sidecar with the real request shape until it actually serves, so the next completion is the one a user gets rather than the K1 fallback.
override fun warmCompletionAsync() Schedule warmCompletion on the main runloop (the same non-reentrant dispatcher complete uses) so the ~1s K1 bootstrap is paid ahead of the first real completion/error-suggestion request instead of stalling it. The completionWarmInFlight guard collapses the burst of edit events into a single scheduled warm; once warm for the current preamble, warmCompletion returns near-instantly (mutex + fingerprint check), and it re-bootstraps automatically when the preamble changes.
override fun completeAndWait(s: String, cursor: Int): CompletionResult
override fun applyMacros(box: Box, s: String, skipMacros: Set<String>, previewOnly: Boolean): String
override fun findLastImport(s: String): Int
override fun parseAndRewrite(s: String, rules: List<(PsiElement, String) -> String>): String
override var execNumber
override suspend fun exec(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit, implicits: List<Any>): ResultValue?
override suspend fun bootstrap(s: String, implicits: List<Any>, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit): ResultValue? Run the stdlib bootstrap (intro) without waiting on the bootstrapReady gate, since this call is what opens it, and release it afterwards whatever happened, so nothing waits forever.
override fun armBootstrapGate()
override fun releaseBootstrapGate()
override fun execAndWait(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit, implicits: List<Any>): ResultValue?
inner class K2PreparedSnippet internal constructor(private val compiled: LinkedSnippet<out CompiledScript>, private val defaultImplicits: List<Any>) : afield.runtime.kotlin.PreparedSnippet K2 implementation of PreparedSnippet; see that interface for the evaluation contract.
override suspend fun compileSnippet(s: String, implicits: List<Any>, onCompileError: (List<ScriptDiagnostic>) -> Unit): K2PreparedSnippet?
override fun compileSnippetAndWait(s: String, implicits: List<Any>, onCompileError: (List<ScriptDiagnostic>) -> Unit): K2PreparedSnippet?
override fun dispose() Dispose the underlying K2 REPL compilation state. Call this when the KotlinInterface is no longer needed.
override val isDisposed: Boolean
class ContextUpdater(val context: KotlinContext, private val evaluator: K2ReplEvaluator, private val ki: KotlinInterface2)

KotlinLineNotes.kt

Line notes for a Kotlin box: each top-level val or var a run declared, and the value it held when the run finished.

The REPL hands back only a snippet's final value, but it also publishes the snippet's properties after every evaluation (box.ki.context.vars, the same map fromContext() searches), so a declaration's value can be read back by name once the run is over. A statement that declares nothing has no note: its value was never kept.

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinLineNotes.kt — read the source

KotlinLineNotes

object KotlinLineNotes

Reads a finished run's declarations out of the box's variable table and records them as notes.

Declaration Summary
fun record(box: Box, property: String, code: String, lineOffset: Int) Record what the declarations of code (box property, after box line lineOffset) came to.

KotlinScriptEngine.kt

The common interface every box's Kotlin engine implements, and the shared pieces both engines use.

KotlinScriptEngine is what a box's code runs through: compiling, evaluating, completion and context tracking. createKotlinScriptEngine returns the active implementation, KotlinInterface2 (K2) or the older KotlinInterface (K1). The file also holds the types both engines return, such as CompletionResult, KotlinContext and KotlinDebugScriptTracker, and the compiler options they share.

src/jvmMain/kotlin/afield/runtime/kotlin/KotlinScriptEngine.kt — read the source

Used in: What's new, Running code in a box, Agent transcript gallery

Top-level functions

Declaration Summary
suspend fun <T> KotlinScriptEngine.withSourceProperty(property: String, block: suspend () -> T): T
fun functionSignature(function: KFunction<*>): String
fun shortenType(name: String): String
fun getTokenBounds(buf: String, cursor: Int): CompletionTokenBounds
fun kotlinCompletionBlacklistPatternMatches(pattern: String, candidate: String): Boolean
fun isBlacklistedKotlinCompletionName(name: String, blacklistPatterns: List<String> = kotlinCompletionBlacklistPatterns): Boolean
fun isBlacklistedKotlinCompletionCandidate(variant: SourceCodeCompletionVariant, blacklistPatterns: List<String> = kotlinCompletionBlacklistPatterns): Boolean
fun getCompletionResult(code: String, cursor: Int, completions: List<SourceCodeCompletionVariant>): CompletionResult.Success
fun createKotlinScriptEngine(propertyClasses: Map<String, KClass<*>> = emptyMap(), properties: Map<String, Any?> = emptyMap(), implicitReceivers: List<KotlinType> = emptyList(), classLoader: ClassLoader? = null, filePrefix: String = "EVAL_"): KotlinScriptEngine Creates a KotlinScriptEngine using the currently configured backend.

Top-level values

Declaration Summary
val kotlinCompletionBlacklistPatterns: List<String>

KotlinScriptEngine

interface KotlinScriptEngine

Common interface for Kotlin scripting engines used by Field4.

Declaration Summary
val context: KotlinContext The mutable Kotlin REPL context for this engine: tracked variables and functions, its generation counter, and debug-script history.
val implicitReceivers: List<KotlinType> The types whose members this engine's scripts can name without a receiver — Box for every box (see makeKI). Completion needs this: at a caret with no dot, a bare frame is either a local or a member of one of these, and nothing else in the reply says so.
var associatedBox: Any?
var compilerConfig: ScriptCompilationConfiguration
var classLoader: ClassLoader?
var execNumber: Int
var sourceProperty: String
val compilationDispatcher: CoroutineDispatcher Dispatcher for CPU-bound compilation and macro-expansion work. K1 returns its single KotlinInterface.compilationThread (K1 is not thread-safe). K2 returns Dispatchers.Default, with each K2 REPL state serializing its own compiles.
suspend fun exec(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalException: (Throwable) -> Unit = { throw it }, implicits: List<Any> = emptyList()): ResultValue?
fun execAndWait(s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalException: (Throwable) -> Unit = {}, implicits: List<Any> = emptyList()): ResultValue?
suspend fun bootstrap(s: String, implicits: List<Any> = emptyList(), onCompileError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalException: (Throwable) -> Unit = { throw it }): ResultValue? Run the stdlib bootstrap (s, the intro imports) as this engine's first evaluation.
fun armBootstrapGate() Arm the engine's bootstrap-readiness gate (make it pending) so ordinary exec/evaluate block until bootstrap (or releaseBootstrapGate) opens it. Call this right after building an engine that will be bootstrapped asynchronously. Engines left un-armed (the default) never block — used directly in tests and ad-hoc snippet execution. No-op for engines without such a gate.
fun releaseBootstrapGate() Open the engine's bootstrap-readiness gate unconditionally, in case bootstrap never ran (e.g. the box was tombstoned before it could) — otherwise a later exec/evaluate would wait forever. Idempotent; a no-op for engines without such a gate.
suspend fun complete(s: String, cursor: Int): CompletionResult
suspend fun warmCompletion() Eagerly build whatever machinery complete needs (for K2 this is the K1 completion delegate — a full intro+preamble compile). Doing it up front, on the same main-runloop dispatcher complete uses, gets the one heavy compile out of the way outside any re-entrant runloop pump, so later completions are light and single-threaded-safe. Idempotent; a no-op for engines with no lazy completion bootstrap.
suspend fun warmCompletionBackend(timeoutMs: Long = 120_000): String Build the serving completion backend, not just the fallback.
fun warmCompletionAsync() Fire-and-forget proactive warm: schedule warmCompletion on the main-runloop dispatcher so the one heavy K1 bootstrap happens before the first completion/error-suggestion request rather than blocking it. Idempotent and self-de-duplicating (a no-op while a warm is already in flight, and cheap when already warm for the current preamble). Called on interactive code edits so the intelligent-error path is warm by run time. No-op for engines with no bootstrap.
fun completeAndWait(s: String, cursor: Int = s.length): CompletionResult
fun applyMacros(box: Box, s: String, skipMacros: Set<String> = emptySet(), previewOnly: Boolean = false): String
fun parseAndRewrite(s: String, rules: List<(PsiElement, String) -> String>): String
fun findLastImport(s: String): Int
suspend fun compileSnippet(s: String, implicits: List<Any> = emptyList(), onCompileError: (List<ScriptDiagnostic>) -> Unit = {}): PreparedSnippet? Compile s against this engine's current REPL state without evaluating it.
fun compileSnippetAndWait(s: String, implicits: List<Any> = emptyList(), onCompileError: (List<ScriptDiagnostic>) -> Unit = {}): PreparedSnippet? Blocking wrapper around compileSnippet.
fun dispose() Dispose any resources held by this engine. Not all implementations require disposal; default is a no-op.
val isDisposed: Boolean Whether dispose has run on this engine. Lets a caller that replaced or retired an engine — Box.replaceKi, Box.disposeKi — be checked for having actually released the one it dropped, which is the leak those two exist to close. False for an implementation with nothing to dispose.

PreparedSnippet

interface PreparedSnippet

Code compiled against one KotlinScriptEngine REPL state and reusable within that state.

Declaration Summary
suspend fun eval(updateContext: Boolean = false, implicits: List<Any> = emptyList(), onEvalError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalException: (Throwable) -> Unit = { throw it }): ResultValue?
fun evalAndWait(updateContext: Boolean = false, implicits: List<Any> = emptyList(), onEvalError: (List<ScriptDiagnostic>) -> Unit = {}, onEvalException: (Throwable) -> Unit = {}): ResultValue?

exported

annotation class exported

Annotation to mark functions/values that should be automatically exported to the box's preamble. When a declaration is annotated with @exported, its fully-typed signature will be added to the box's preamble after execution, making it available as a declared property.

rootExported

annotation class rootExported

metarootExported

annotation class metarootExported

CompletionStatus

enum class CompletionStatus(private val value: String)

Constants: OK, ERROR.

Declaration Summary
override fun toString(): String

CompletionResult

abstract class CompletionResult(private val status: CompletionStatus)

Declaration Summary
open class Success(val matches: List<String>, val bounds: CompletionTokenBounds, val metadata: List<SourceCodeCompletionVariant>, val text: String, val cursor: Int) : CompletionResult(CompletionStatus.OK)
class Empty(text: String, cursor: Int)
class Error(val errorName: String, val errorValue: String, val traceBack: String)

CompletionTokenBounds

data class CompletionTokenBounds(val start: Int, val end: Int)

KotlinContext

class KotlinContext(val vars: HashMap<String, KotlinVariableInfo> = LinkedHashMap(), val functions: HashMap<String, KotlinFunctionInfo> = HashMap(), var generation: Int = 0, val debugScripts: KotlinDebugScriptTracker = KotlinDebugScriptTracker(), val exportedVariables: MutableSet<String> = java.util.concurrent.ConcurrentHashMap.newKeySet())

Mutable bookkeeping for one Kotlin scripting context.

Declaration Summary
val vars: HashMap<String, KotlinVariableInfo>
val functions: HashMap<String, KotlinFunctionInfo>
var generation: Int
val debugScripts: KotlinDebugScriptTracker
val exportedVariables: MutableSet<String>

KotlinDebugScriptSnapshot

data class KotlinDebugScriptSnapshot(val sourceName: String, val sourceText: String, val normalizedSourceText: String = normalizeDebugSourceText(sourceText), val sourceHash: Int = normalizedSourceText.hashCode(), var className: String? = null, var updatedAtMs: Long = System.currentTimeMillis())

Declaration Summary
val sourceName: String
val sourceText: String
val normalizedSourceText: String
val sourceHash: Int
var className: String?
var updatedAtMs: Long

KotlinDebugScriptTracker

class KotlinDebugScriptTracker(private val maxEntries: Int = 64)

Declaration Summary
fun recordCompile(sourceName: String, sourceText: String): KotlinDebugScriptSnapshot
fun discard(snapshot: KotlinDebugScriptSnapshot)
fun attachLatestRuntimeClass(className: String?)
fun latestMatchingSource(sourceText: String): KotlinDebugScriptSnapshot?
fun latest(): KotlinDebugScriptSnapshot?
fun all(): List<KotlinDebugScriptSnapshot>

KotlinFunctionInfo

class KotlinFunctionInfo(val function: KFunction<*>, val line: Any) : Comparable<KotlinFunctionInfo>, ToHTML

Declaration Summary
val name: String
override fun toHTML(): String
fun toString(shortenTypes: Boolean): String
override fun toString(): String
override fun compareTo(other: KotlinFunctionInfo): Int
override fun hashCode(): Int
override fun equals(other: Any?): Boolean

KotlinVariableInfo

class KotlinVariableInfo(val value: Any?, val descriptor: KProperty<*>, val line: Any) : ToHTML

Declaration Summary
val name: String
val type: String
fun toString(shortenTypes: Boolean): String
override fun toString(): String
override fun toHTML(): String

KotlinEngineConfig

object KotlinEngineConfig

Controls which Kotlin scripting backend to use.

Declaration Summary
var rerunCompileCache: Boolean Re-evaluate, rather than recompile, an exact rerun of a snippet the engine already compiled (RerunCompileCache). On by default; -Dafield.kotlin.rerunCompileCache=false turns it off.
enum class Backend
var activeBackend: Backend
enum class CompletionBackend Completion candidate-generation backend, independent of the execution activeBackend. AA is the out-of-process K2 Analysis-API sidecar (AaCompletionClient → /complete) and is the DEFAULT (Stage D cutover): it matches-or-beats K1 recall, passes all anti-goals, and adds named-arg completion. It only engages when a sidecar URL is configured (afield.completion.sidecarUrl / AFIELD_CODE_USE_SEMANTIC_SIDECAR_URL); otherwise, and on any timeout / error / empty result, it falls through to the compiler-grade K1 delegate, so AA is never worse than K1. Force the legacy path with -Dafield.kotlin.completion=k1.
var activeCompletionBackend: CompletionBackend
var offloadCompilation: Boolean When true (default), compilation runs on a background dispatcher so the Runloop frame loop is not blocked while the Kotlin compiler works.
var logExpandedMacros: Boolean When true (the default), KotlinScriptEngine.applyMacros prints the post-macro-expansion source — the code actually handed to the Kotlin compiler — to the terminal whenever macro expansion changed the code (execute path only, never editor previews). Invaluable for debugging rewrite {}/unquote {}/builtin-macro output. Disable with -Dafield.macros.logExpanded=false.

MavenClasspathManager.kt

Adds Maven dependencies to the classpath that box code compiles against.

MavenClasspathManager keeps a list of repositories and dependency coordinates, resolves them to jars, searches Maven Central, and checks for newer versions. It stores its state on disk so the list survives a restart, and the server's Maven requests call into it.

src/jvmMain/kotlin/afield/runtime/kotlin/MavenClasspathManager.kt — read the source

MavenClasspathManager

object MavenClasspathManager

Declaration Summary
const val MAVEN_CENTRAL_REPOSITORY
fun status(message: String = ""): MavenClasspathStatus
fun addRepository(url: String): MavenClasspathStatus
fun removeRepository(url: String): MavenClasspathStatus
fun addDependency(coordinate: String): MavenClasspathStatus
fun removeDependency(coordinate: String): MavenClasspathStatus
fun resolve(coordinates: List<String> = emptyList(), addToList: Boolean = false): MavenClasspathStatus
fun upgradeDependencies(): MavenUpgradeResponse
fun search(query: String, rows: Int = 12): MavenSearchResponse

ReplBootstrapGateTest.kt

Smoke test of the bootstrap ordering of a KotlinInterface2 engine: the stdlib intro is evaluated before any ordinary snippet.

On one engine the K2 evaluator chains each snippet's classloader onto the previously evaluated snippet and emits the shared ReplState holder only into the first-evaluated one, so snippets have to evaluate in the order they compile. A box launched under a running renderer evaluates the intro asynchronously while its launch setup evaluates the inherited preamble, and the two can race.

The race depends on timing, so the test does not rely on threads to reproduce it. It runs three cases:

  1. It forces an out-of-order evaluation and checks that it evaluates with the right answer, which it does since the Kotlin 2.4.20 uplift.
  2. It checks that the engine's bootstrap gate (KotlinScriptEngine.armBootstrapGate and KotlinScriptEngine.bootstrap) holds an ordinary exec until the gate is released.
  3. It checks that a concurrent exec waits for the bootstrap to finish.
  4. It checks that a compile without evaluation (KotlinScriptEngine.compileSnippet, the checkBoxCodeCompiles path) waits for the bootstrap too.

Like CompiledSnippetTest it is a runnable object rather than a JUnit class.

src/jvmMain/kotlin/afield/runtime/kotlin/ReplBootstrapGateTest.kt — read the source

ReplBootstrapGateTest

object ReplBootstrapGateTest

Runs the four bootstrap-ordering cases; main fails if any of them does.

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

ReplRerunCompileCacheSmokeTest.kt

Smoke test of the rerun compile cache: a rerun of an unchanged snippet is evaluated again without compiling, its body really runs, and every change that would make a recompile bind differently compiles.

The launch path runs the regathered preamble and then the box code through KotlinInterface2.exec on every rerun; this test drives the same calls on one engine. It covers an edited body, a changed preamble, a changed expansion, the snippet evaluated last, a different property, a new engine, a disposed engine, and the cache switched off with KotlinEngineConfig.rerunCompileCache.

src/jvmMain/kotlin/afield/runtime/kotlin/ReplRerunCompileCacheSmokeTest.kt — read the source

ReplRerunCompileCacheSmokeTest

object ReplRerunCompileCacheSmokeTest

Runs each rerun-cache case in turn on its own engine.

Declaration Summary
var bodyRuns Incremented by snippet bodies, so a test can see that a re-evaluated body ran.
fun main(args: Array<String>)

ReplSnippetDeclarationsSmokeTest.kt

Smoke test of what a box may declare at its top level and still compile under the K2 REPL.

Each case is a shape of ordinary box code that the stock 2.4 K2 REPL fails to compile or load, and that Field's Kotlin fork (and, for the last group, Field's build of the Wither plugin) handles:

  1. A named function that touches the implicit Box receiver, such as fun foo(a: Double) = a + _qqq. The fork stores the receivers in the snippet class's implicit-receiver fields, as the .kts script lowering does. Cases A, B, and case L, which checks that a later run binds foo(12.0) to the live declaration.
  2. An expression-bodied function that reaches the receiver, such as fun f(a: Double) = println(a). Only an inferred return type takes this resolution path, so the same body in braces is a separate shape. Cases S, T, U.
  3. A delegated property whose provider declares provideDelegate, such as var x by keyCamera(). Cases N, Q.
  4. A destructuring declaration with a _ entry, such as val (a, _) = p. The fork builds the _ entry as a local of the snippet's evaluation body rather than a class property. Cases V to Y, with Z1 to Z4 covering the name-based forms that reach the same path.
  5. A statement-level call to a function with no callableId: a local function, or a member of a local or anonymous class, which Wither's IR pass has to tolerate. Cases LF1 to LF9.
  6. A class or object that reaches the box receiver, such as class C { fun g() { println("hi") } } -- a bare println or display is a member of the box. The REPL lowering makes no class capturing, so the read had no snippet this and crashed with "Unexpected script transformation state"; the fork now reads the snippet's own instance from the REPL state. Cases CR1 to CR6; CR6 constructs the class from a later snippet.

One case is still open: a declaration from an earlier snippet can shadow a later one of the same name, and the test records that behaviour rather than asserting a fix.

Each case drives the real engine: a fresh KotlinInterface2 with an implicit Box receiver, the stdlib intro, then standardExecutionPractice, the same preamble-then-body snippet sequence a launched box runs through.

src/jvmMain/kotlin/afield/runtime/kotlin/ReplSnippetDeclarationsSmokeTest.kt — read the source

ReplSnippetDeclarationsSmokeTest

object ReplSnippetDeclarationsSmokeTest

Runs every top-level declaration case on a fresh engine; main exits non-zero if any case failed.

Declaration Summary
fun main(args: Array<String>)
class ProvideDelegateProvider Needs provideDelegate, the shape keyCamera() / orbitControls() have.
class PlainDelegate(private var v: Int = 7) The same value through a plain delegate — no provideDelegate.
fun provideDelegateProvider()
fun plainDelegate()

ReplVariableRebindingSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/kotlin/ReplVariableRebindingSmokeTest.kt — read the source

ReplVariableRebindingSmokeTest

object ReplVariableRebindingSmokeTest

Real K2 closure checks for declaration-time resets and the snapshots they must preserve.

Declaration Summary
fun main(args: Array<String>)

RerunCompileCache.kt

Compile once, re-evaluate on rerun: one K2 engine's record of the snippets it has compiled.

A box that reruns with exactly the same expanded source (a propDown reader re-launched by its writer, the regathered preamble in front of it) can evaluate the snippet it already has instead of compiling it again.

On Kotlin 2.5.0-Beta1's K2ReplEvaluator, re-evaluating an old LinkedSnippet is not the same as recompiling its source, in two ways, and the cache refuses both:

  1. The body does not run when the snippet is re-evaluated straight after its own previous evaluation: the evaluator reuses the class it already loaded, and a K2 snippet's body runs once per loaded class. (lst.add(1); lst.size gave 1 twice and the list kept one element.) With any other snippet evaluated in between the class is reloaded and the body runs. So a hit is refused when the cached snippet is the evaluator's last one (KotlinInterface2 checks).
  2. Stale bindings. A snippet binds each name it uses to the snippet class that declared it when it was compiled. If a later snippet redeclares that name, a recompile binds the new declaration and a re-evaluation keeps the old one (q * 2 stayed 10 after val q = 6). So every entry records which earlier snippet classes it references, and is refused once any name those classes declare has been redeclared by a later compile.

Mutable declarations and their readers also compile afresh: cached evaluation can reload historical backing classes, leaving captured mutable slots outside the evaluator's current history.

Also refused, conservatively: sources that carry @file: annotations (DependsOn refines the compile configuration) — and a compiled import invalidates every entry, since imports may change how already-compiled names would resolve now.

Everything here runs under the engine's compile mutex.

src/jvmMain/kotlin/afield/runtime/kotlin/RerunCompileCache.kt — read the source

ShadowedSnippetNameChecker.kt

src/jvmMain/kotlin/afield/runtime/kotlin/ShadowedSnippetNameChecker.kt — read the source

ShadowedSnippetNameDiagnostics

object ShadowedSnippetNameDiagnostics : KtDiagnosticsContainer()

A compiler warning for box code: a name the box declares at its top level that, inside a lambda, resolves to a member of the lambda's receiver instead.

Declaration Summary
val FIELD_SHADOWED_BY_RECEIVER by warning2<PsiElement, String, String>()
const val MESSAGE_MARK Text every rendered message of this checker carries, which is how a compile result's reports are filtered for it.
override fun getRendererFactory(): BaseDiagnosticRendererFactory

ShadowedSnippetNameCheckerPlugin

class ShadowedSnippetNameCheckerPlugin : CompilerPluginRegistrar()

The plugin entry point the compiler finds through the services jar ShadowedSnippetNameCheckerPlugin.jar writes.

Declaration Summary
override val pluginId: String
override val supportsK2: Boolean
override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration)

ShadowedSnippetNameSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/kotlin/ShadowedSnippetNameSmokeTest.kt — read the source

ShadowedSnippetNameSmokeTest

object ShadowedSnippetNameSmokeTest

Smoke test of ShadowedSnippetNameDiagnostics: a name the box declares that a lambda's receiver shadows gets one warning on the line where the receiver's member was used, and the uses that do mean the box's declaration, or say which receiver they want, get none.

Declaration Summary
fun main(args: Array<String>)

SharedTypes.kt

Lets several boxes in one document use the same Kotlin class, with its real type.

Call sharedTypes on a box with Kotlin declarations as source. SharedTypes compiles them as ordinary classes onto a classloader every box shares, so a data class declared there can be used, and named, in any box. By default, publishing restarts the other boxes that have already run, because a loaded class cannot be replaced.

src/jvmMain/kotlin/afield/runtime/kotlin/SharedTypes.kt — read the source

Top-level functions

Declaration Summary
fun Box.sharedTypes(source: String, refreshOthers: Boolean = true): String Declare types that every box in this document can name and use.

SharedTypes

object SharedTypes

Declares Kotlin types that every box in a document can name.

Declaration Summary
const val ROOT_NAME Directory name under the workspace holding every generation.
const val SUGGESTED_PACKAGE Package the demo and docs assume; nothing here enforces it.
data class Publication(val ok: Boolean, val generation: Int, val dir: File?, val classFiles: List<String>, val refreshedBoxes: List<String>, val log: String, val shadowed: List<String> = emptyList()) What a publish did, in a form a box can print.
fun compileTo(source: String, outDir: File, moduleName: String = "shared-types"): Pair<List<File>, String> Compile source to outDir as ordinary Kotlin, in a subprocess.
fun publish(source: String, workspace: File, except: Box? = null, refreshOthers: Boolean = true, session: SessionInformation? = null): Publication Compile source into a fresh generation, put it on the shared classloader, and invalidate the engines that could not otherwise see it.

SharedTypesClasspathSpike.kt

Spike that measures whether a shared declarations directory can carry user-defined types between boxes.

Each box has its own KotlinInterface2, so a type declared in one box's snippet is invisible to every other box (see ExportedTypeNames). The proposed fix compiles such types as ordinary Kotlin into a directory hung off the process-wide shared classloader, from which every engine derives its compile classpath. Runtime identity follows, since snippet loaders are children of the shared loader. Compile-time visibility is the open question, because dependenciesFromClassloader snapshots the classpath into a JvmDependency(files) rather than holding a live view of the loader.

The spike runs three probes:

  • A, the measurement: a directory that was on the classpath snapshot but empty at the time. Does a class file that appears in it later resolve in an engine that already exists? If it does, publishing a shared type is writing a file; if not, publishing has to rebuild the consuming engines.
  • B, an invariant: a fresh engine sees the directory, the fallback the design rests on.
  • C, an invariant: named arguments, toString, copy and destructuring of a data class work across the trip, which needs real Kotlin metadata rather than only a loadable JVM class.

It fails on B or C and reports A either way.

src/jvmMain/kotlin/afield/runtime/kotlin/SharedTypesClasspathSpike.kt — read the source

SharedTypesClasspathSpike

object SharedTypesClasspathSpike

Runs probes A, B and C and prints the outcome of A.

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

SharedTypesSmokeTest.kt

End-to-end smoke for SharedTypes: publishing user-declared Kotlin types that every box can name.

The assertions cover the whole arc the feature exists for:

  • testPublishProducesLoadableClasses — publish compiles and lands on the shared loader.
  • testPublishedTypeIsExportable — the payoff for @exported. A published type passes ExportedTypeNames, so it crosses between boxes with its real type where a box-declared class can only ever cross as Any?. This is the join between the two halves of the work.
  • testFreshEngineGetsFullFidelity — named arguments, copy, destructuring and the data-class toString all survive, which only happens because these are real compiled classes.

measureRepublish is a measurement rather than an assertion, in the style of SharedTypesClasspathSpike: it reports whether a second publish of the same class name actually takes effect. It may well not — gen-1 was added to the classloader before gen-2, and URL search order is first-wins — and if so that is a real constraint on the feature, not a test bug.

src/jvmMain/kotlin/afield/runtime/kotlin/SharedTypesSmokeTest.kt — read the source

SharedTypesSmokeTest

object SharedTypesSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

SnippetClassReceiverCrash.kt

src/jvmMain/kotlin/afield/runtime/kotlin/SnippetClassReceiverCrash.kt — read the source

SpareEngines.kt

A box's next Kotlin engine, built while the box is idle.

Running a box gives it a fresh REPL, and building one costs a few hundred milliseconds that the person who pressed the key waits through. SpareEngines moves that wait off the keypress: once a run has its verdict, the box's next engine is built on a background thread, and the next run takes the one already standing by. The run still gets a fresh engine; only the waiting moves.

Engines are tens of megabytes, so only a handful of boxes hold one at a time, the least recently prepared giving way first. A spare is dropped when its box is retired, and refused if the shared class loader has gained jars since the spare was built, because an engine fixes its compile classpath when it is created. -Dafield.kotlin.spareEngines=0 turns the whole thing off.

src/jvmMain/kotlin/afield/runtime/kotlin/SpareEngines.kt — read the source

SpareEngines

object SpareEngines

Holds a pre-built Kotlin engine per box, so the next execute-all does not wait for one.

Declaration Summary
val capacity: Int
suspend fun take(box: Box): KotlinScriptEngine? The box's spare engine, if it has one and it is still current; it is no longer the spare. A spare still being built is waited for: building another beside it would only compete with it.
fun prepare(box: Box) Build box's next engine in the background, unless it has one or one is being built.
fun discard(box: Box) Drop box's spare, and supersede one being built (it is disposed when its build returns).

SpareEnginesSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/kotlin/SpareEnginesSmokeTest.kt — read the source

SpareEnginesSmokeTest

object SpareEnginesSmokeTest

Running a box takes the engine SpareEngines built after its previous run, and the run still starts from a fresh REPL.

Declaration Summary
fun main(args: Array<String>)

WitherSmokeTest.kt

Smoke for the experiment that wires the Wither compiler plugin (kyay10/wither) into the K2 REPL (KotlinInterface2). Wither is a FIR + IR plugin: a call to io.github.kyay10.wither.with(x) turns x into an implicit receiver for the rest of the enclosing block, and its IR pass rewrites the receiver-less member accesses that follow.

testReceiverInjection is the assertion that decides the run: a bare greet() with no explicit receiver only resolves if Wither injected the Greeter receiver. Without the plugin the snippet fails to compile (unresolved reference 'greet'), so a green run shows that the plugin is registered into the REPL's compilation config and working end to end.

testPlainEvalStillWorks is the regression half: turning the plugin on must not disturb ordinary snippet evaluation.

Runnable-object test style, matching ReplBootstrapGateTest.

Red from 2026-08-25 to 2026-10-04: Wither's IR pass never recognised its own with

testReceiverInjection failed with an IrErrorCallExpression reaching codegen:

Unresolved reference: this@R|<local>/<with holder>|
AssertionError: Unexpected IR element found during code generation ... or it should have been lowered

fir2ir emitting that error call is expected. Wither's FIR half binds the injected receiver to a synthetic holder that fir2ir cannot translate, and its IR half (WitherReceiverTransformer) exists to patch it: it removes each with(...) statement, stores the argument in a temporary, and replaces each <with holder> error call with a read of that temporary. The bug was that the pass never matched the with(...) call. The IR dump in the failure still held

CALL 'public final fun with (vararg values: kotlin.Any?): kotlin.Unit [companion] declared in io.github.kyay10.wither.WithKt'

The callee comes from libs/wither-annotations.jar, and in IR its parent is the JVM file facade class, not the package fragment. A temporary probe in the pass printed parent=IrClassImpl parentOrigin=FILE_CLASS isTopLevel=true kotlinFqName=io.github.kyay10.wither. Wither matched on callableId against the package-level CallableId(io.github.kyay10.wither, with); under a facade class that id is io/github/kyay10/wither/WithKt.with, so the two were never equal. (Since 5052517 the pass refused any parent that was not a package fragment, which fails the same way.) ~/temp/wither field/kotlin-2.5.0-beta1 @ face220 accepts both top-level shapes and compares package plus name.

Measured 2026-10-04 on Kotlin 2.5.0-Beta1, same worktree and harness, changing only libs/wither-plugin.jar:

  • the jar on main before the swap (sha1 2869851…): XFAIL, with CALL still in the IR, got: null;
  • 5052517 (1e6f4c6…): XFAIL, same IR, same got: null;
  • face220 (be020c9…): passes; demo() returns hi-W.

With face220, kotlin.repl-snippet-declarations.smoke, kotlin.repl.provide-delegate and kotlin.repl-local-fun-statements.box also pass. Those runs used the versions of those tests that expect 5052517's has no callableId fix.

Not measured: why the jar set from before the 2.4.20 uplift passed. The earlier diagnosis in this KDoc blamed Fir2IrVisitor.visitThisReceiverExpression for producing the error call. That is true, but fir2ir is supposed to produce it, as described above. Nobody checked how the older compiler parented the callee.

When rebuilding the jar: JAVA_HOME=<jdk-22> ./gradlew --configure-on-demand :compiler-plugin:jar in ~/temp/wither (class file 66; the compiler host is a JDK 22). The build is reproducible, so compare shasum with libs/.

src/jvmMain/kotlin/afield/runtime/kotlin/WitherSmokeTest.kt — read the source

WitherSmokeTest

object WitherSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

boxImports.kt

Which of a box's imports it needs.

A box is compiled with the intro preamble prepended (Boxes.kt), so an import already provided by that preamble adds nothing, and agents write such lines all the time. This reads a box's own import lines and says, for each one, whether it is redundant (the intro already provides it), unused (nothing in the code names it), needed, or kept because the evidence is not good enough to drop it. formatBoxImports removes the first two kinds and sorts the rest; nothing here ever adds an import.

The rules are conservative, because a wrongly dropped import is a compile error:

  • An explicit import a.b.C (or a.b.C as D) is needed when its name appears as an identifier in the code, outside comments and strings. Operator-convention names (plusAssign, getValue, invoke, ...) are always kept: code uses them without writing them.
  • It is redundant when the intro has the same line, or a.b.* and no other visible package (including Kotlin/JVM defaults) offers a competing C. An aliased import is never redundant.
  • A star import a.b.* is redundant when the intro has the same star. It is unused only on the evidence k2ImportEvidence collects: K2 resolved every reference in the box and none of them lives in a.b. Without that evidence a star import is kept.

src/jvmMain/kotlin/afield/runtime/kotlin/boxImports.kt — read the source

Top-level functions

Declaration Summary
fun boxImports(code: String): List<BoxImport> The box's own import lines, in order.
fun referencedNames(code: String): Set<String>? Every identifier the code refers to, outside import and package lines, comments and strings.
fun analyzeBoxImports(code: String, introText: String = intro, resolution: K2ImportEvidence? = null, otherCandidates: (String) -> Collection<String> = ::classpathCandidates): List<ImportFinding> The verdict for each of code's imports, against the box intro introText.
fun introProvidedType(name: String, introText: String = intro): String? The type a box already sees under the simple name name through introText's imports, as a fully qualified name, or null when there is none: what a class generated with that name (a shader block's, say) would hide.
fun formatBoxImports(code: String, findings: List<ImportFinding> = analyzeBoxImports(code)): String code with its redundant and unused imports removed and the remaining import lines sorted, in place of the original block. The rest of the code is untouched, comment lines among the imports included; returns code itself when nothing would change.
fun k2ImportEvidence(code: String, timeoutMs: Long = 10_000L): K2ImportEvidence? Ask the code-use K2 sidecar what code resolves to, for analyzeBoxImports. The box's own import lines go to the sidecar as imports and the rest as the snippet, which is how it expects box code. Null when no sidecar answered (AFIELD_CODE_USE_SEMANTIC_SIDECAR_URL unset, or it timed out): the source-index fallback is a guess about names, not a resolution, so it is no evidence here.
fun formatBoxKotlin(code: String, useK2: Boolean = true): String What Format Kotlin does to a box's code: drop its redundant and unused imports (with K2 evidence when the code-use sidecar answers, by name alone when it does not), then format with ktfmt.

BoxImport

data class BoxImport(val line: Int, val text: String, val path: String, val alias: String?)

One import line of a box.

Declaration Summary
val star: Boolean
val pkg: String
val name: String? The name the import brings into scope: the alias, the last segment, or null for a star.

ImportVerdict

enum class ImportVerdict

Constants: NEEDED, REDUNDANT, UNUSED, KEPT.

ImportFinding

data class ImportFinding(val import: BoxImport, val verdict: ImportVerdict, val reason: String)

K2ImportEvidence

data class K2ImportEvidence(val resolvedFqns: Set<String>, val unresolvedCount: Int)

What K2 resolved in a box's code: every resolved reference's fully qualified name, and how many references it could not resolve. Produced by k2ImportEvidence; null when no analyzer ran.

formatter.kt

Reformats a box's Kotlin or GLSL code.

formatCodeProperty picks the formatter by property name: formatKotlinCode for code, which runs ktfmt and then restores Field-specific layout such as fragment breaks and comment blocks, and formatGlslCode for shader properties.

src/jvmMain/kotlin/afield/runtime/kotlin/formatter.kt — read the source

Top-level functions

Declaration Summary
fun formatKotlinCode(code: String, blockifyComments: Boolean = true): String
fun joinAssignmentBreaks(code: String): String Undo ktfmt's break after = when the right-hand side is one multi-line call or block:
fun restoreStatementSpacing(input: String, formatted: String): String Take out the blank lines ktfmt adds between top-level statements of a script.
fun collapseOneLineLambdaAssignments(code: String): String Put x = {\n e\n} back on one line as x = { e }.
fun restoreFragmentBreaks(code: String): String
fun blockifyLineCommentRuns(code: String): String
fun formatCodeProperty(property: String, code: String): String?
fun formatGlslCode(code: String): String

importHelp.kt

Finds which classes a bare name could mean, so Field can write the import line for it.

A class-path index, built once by initializeClassCache, maps simple class names to fully qualified ones. findClassesNamed, classesWithPrefix and kotlinImportStatementFor answer completion and auto-import from it, filtered by the user's import blacklist. ImportResolution controls whether that answer depends on the local machine.

src/jvmMain/kotlin/afield/runtime/kotlin/importHelp.kt — read the source

Top-level functions

Declaration Summary
fun findClassesNamed(name: String): List<String>?
fun isImportableClassName(name: String, blacklist: List<String> = kotlinCompletionBlacklistPatterns): Boolean
fun classesWithPrefix(name: String, hideDollarSignClasses : Boolean = true): List<Pair<String, String>>
fun kotlinImportStatementFor(name: String): String?
fun initializeClassCache()

Top-level values

Declaration Summary
val cache: Map<String, List<String>> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) Every simple name on the classpath, including the personal layer.

ImportResolution

object ImportResolution

The two inputs, both outside the checked-in source, that decide what kotlinImportStatementFor answers.

Declaration Summary
var hermetic: Boolean AFIELD_IMPORT_HERMETIC=1 turns this on for a whole JVM without a code change.
fun describe(): String One line naming both inputs, for a harvest banner or a bug report.
fun divergences(names: Collection<String>): List<Divergence> For each of names, the import this machine would emit when it differs from the hermetic answer.
data class Divergence(val name: String, val hermetic: String?, val machine: String?, val candidates: List<String>) One name whose import depends on something outside the checkout.
data class Census(val simpleNames: Int, val resolvable: Int, val ambiguous: Int, val unimportable: Int, val ambiguousNames: List<String>) How many simple names on this classpath an import can actually be emitted for.
fun personalLayerCasualties(): List<String> Every type name that this machine's personal layer has taken away.
fun census(index: Map<String, List<String>> = index(), blacklist: List<String> = blacklist()): Census

LanguageRoots.kt

The roots of the small languages that draw in a window — RectSL and StackSL — made and given a window by the language, so that starting one takes a command rather than Kotlin.

adopt is "Make a RectSL Scene" and "Make a StackSL Program": the selected box goes beneath a root of that language — the one already above it, or a new one placed just above it — and becomes a scene.

A box marked "Set Main Language: RectSL" (or StackSL) is the root its scenes draw beneath. When a scene starts and there is no running window above it, ensure opens one on that root — a Window and a Renderer held in the root's window and renderer properties, exactly where prop<Renderer>(renderer) would have put them, with the render loop running as the root's job. So the root shows as running, stopping it closes the window (and, as for any renderer box, stops the scenes drawing in it first), and closing the window stops them too; running a scene again reopens it.

A root that holds Kotlin of its own is left to open its own window: that Kotlin may be about to, and two render loops on one renderer would fight. ensure only says so.

src/jvmMain/kotlin/afield/runtime/languages/LanguageRoots.kt — read the source

LanguageRoots

object LanguageRoots

Finds, makes and opens windows on the roots of RectSL and StackSL scenes.

Declaration Summary
fun rootOf(box: Box, language: String): Box? The nearest box above box, walking up its parents, marked with main language language.
fun ensure(box: Box, language: String, label: String, dimensions: Rect): String? Make sure box has a window to draw in: the running renderer above it, or a new window on its language root, titled after the root and placed at dimensions. Call on the main thread, when the box starts — never from a loop that follows it, or closing the window would reopen it at once.
fun adopt(boxes: List<Box>, language: String, rootName: String, sceneName: String, property: String, starter: String): List<Box> Put each of boxes beneath a language root and return the scenes to start: a box already beneath one stays there, a box that is not gets a new root named rootName placed just above it, and a selected root gets a new scene beneath it. Each scene is marked autorun, so it plays when its document opens, and given starter in property if that is empty. Call on the main thread.

LanguageSpecs.kt

The text conventions of the languages a box property can hold: each LanguageSpec says how to wrap a property's source before it is compiled and how to unwrap it again.

Kotlin box code is wrapped by wrapScript into a fun SCRIPT() block between preamble and postamble markers, keeping leading import and @file lines above it, and unwrapScriptLenient takes it back out. GLSL specs normalise the preamble to #version 450 followed by the source's #extension lines; the ray-tracing stages use #version 460 and always add GL_EXT_ray_tracing. Slang specs only trim trailing whitespace.

glslSpecForProperty and shaderSpecForProperty map a property name to its spec, case-insensitively: vertex, fragment, mesh, task, compute and the ray-tracing stages (raygen, miss, closesthit, anyhit, intersection, callable) are GLSL, and the same names with an _s suffix, plus vertex_fragment, are Slang. NANO_ASM is the spec for the Nano assembly language.

src/jvmMain/kotlin/afield/runtime/languages/LanguageSpecs.kt — read the source

Top-level functions

Declaration Summary
fun wrapScript(script: String): String Wraps top-level Kotlin script into a fun SCRIPT() block with preamble/postamble markers. Keeps leading import/@file lines at top.
fun unwrapScriptLenient(wrapped: String): String Lenient unwrap for Kotlin script output.
fun glslSpecForProperty(property: String): LanguageSpec?
fun shaderSpecForProperty(property: String): LanguageSpec?

Top-level values

Declaration Summary
val KOTLIN_SCRIPT: LanguageSpec
val GLSL_VERTEX: LanguageSpec
val GLSL_FRAGMENT: LanguageSpec
val GLSL_MESH: LanguageSpec
val GLSL_TASK: LanguageSpec
val GLSL_COMPUTE: LanguageSpec
val GLSL_RAY_TRACING: LanguageSpec
val GLSL_MISS: LanguageSpec
val GLSL_CLOSEST_HIT: LanguageSpec
val GLSL_ANY_HIT: LanguageSpec
val GLSL_INTERSECTION: LanguageSpec
val GLSL_CALLABLE: LanguageSpec
val SLANG_VERTEX: LanguageSpec
val SLANG_FRAGMENT: LanguageSpec
val SLANG_MESH: LanguageSpec
val SLANG_TASK: LanguageSpec
val SLANG_COMPUTE: LanguageSpec
val SLANG_VERTEX_FRAGMENT: LanguageSpec
val SLANG_RAY_TRACING: LanguageSpec
val SLANG_MISS: LanguageSpec
val SLANG_CLOSEST_HIT: LanguageSpec
val SLANG_ANY_HIT: LanguageSpec
val SLANG_INTERSECTION: LanguageSpec
val SLANG_CALLABLE: LanguageSpec
val NANO_ASM: LanguageSpec Language spec for the Nano micro-computer assembly language. Nano is an 8-bit VM with 4 registers (r0–r3) and 32 bytes of RAM, designed for emergence and artificial-life simulations.

LanguageSpec

data class LanguageSpec(val name: String, val wrap: (String) -> String, val unwrap: (String) -> String)

A language's name and the pair of functions that wrap its source for compiling and unwrap it again.

Declaration Summary
val name: String
val wrap: (String) -> String
val unwrap: (String) -> String

MalformedAI

class MalformedAI(message: String) : RuntimeException(message)

Thrown when a model's output still carries a reply marker such as <help> or <cursor>, so it is refused rather than applied.

BoxInterrupt.kt

Getting out of box code that will not return: a stall watchdog, and the bytecode rewriting that gives the interrupt somewhere to land.

The whole public surface is BoxInterrupt: request an interrupt, poll for one, instrument a freshly compiled snippet, and the nested watchdog that notices a Runloop which has stopped making frames. The rest of the file is the ASM class and method visitors that insert the poll call, and nothing outside the file uses them. dev_docs/RUNAWAY_BOX_INTERRUPT.md has the design.

src/jvmMain/kotlin/afield/runtime/lifecycle/BoxInterrupt.kt — read the source

BoxInterrupt

object BoxInterrupt

A stall watchdog and a cooperative escape, for box code that never returns.

Declaration Summary
class BoxInterruptedError(val reason: String) : Error("Box interrupted: $reason") Thrown out of box code by poll. An Error, so catch (e: Exception) in a box does not swallow it.
val onInterrupted Listeners told about every interrupt that actually fired (the UI and tests).
var enabled: Boolean Whether newly compiled box code polls, and whether the Watchdog may interrupt. Off, the watchdog still reports stalls. Code compiled while it was on stays interruptible until it is recompiled.
fun applyPreference(on: Boolean) Apply the persisted preference (FieldPreferences.boxInterrupt), unless the env/property override is set.
fun poll() Called by instrumented box code at every method entry and loop back-edge. Must stay cheap: the common path is one opaque read and a null check. Opaque, not volatile: a volatile read on every back-edge is an acquire barrier that stops the JIT vectorising the loop (a[i](workspace.md#stdlib-kt--i) += i over an IntArray ran ~20x slower), while an opaque read still cannot be hoisted out of the loop, so a request is seen within an iteration or two. The compare-and-set below is the real fence.
fun request(reason: String, thread: Thread? = Runloop.mainThread): Boolean Ask thread (by default the Runloop thread) to leave box code at its next poll. Returns false when there is no such thread. Safe to call from any thread, including while the Runloop is blocked. A request is withdrawn by the Watchdog once the Runloop makes progress again, so a request that found no box code to land in cannot fire later in an unrelated box.
fun isPending(): Boolean True while a request has not yet landed or been withdrawn.
fun culprit(stack: Array<StackTraceElement>): String? Box label that owns the innermost snippet frame in stack, or null.
fun instrument(script: CompiledScript, boxLabel: String) Rewrite the newest snippet of script so its code polls. Idempotent per compiled module; a module of an unknown shape is left alone (and logged once).
fun instrumentClass(bytes: ByteArray, boxLabel: String): ByteArray? bytes rewritten to poll, recording its class as boxLabel's; null if it is not rewritable (then the class runs unchanged, just not interruptibly).
object Watchdog Notices that Runloop.frame has stopped advancing. After reportAfterMs it logs the stall once, with the culprit box and the Runloop thread's stack; after interruptAfterMs (0 = never) it requests an interrupt. Runs on its own daemon thread; never touches Runloop state beyond reading the frame counter and the thread's stack.

LifecycleLedger.kt

The ledger of everything a box has registered with a longer-lived subsystem, and the handles that release it.

Audio patches, control callbacks, Runloop hooks, render modules and feeder threads are each claimed as an Ownership through LifecycleLedger, tagged with the box executing at the time. The run scopes in RunScope.kt decide when each one is released; lifecycleCensus counts what is still live per box, which is how a test checks that a stopped or retired box left nothing behind.

src/jvmMain/kotlin/afield/runtime/lifecycle/LifecycleLedger.kt — read the source

Top-level functions

Declaration Summary
fun lifecycleCensus(boxes: Collection<Box>, includeUnattributed: Boolean = true): JsonObject What each box of boxes still holds: jobs, running strands, stop hooks, ledger ownerships by kind and by ReleasePolicy, and whether its K2 engine is live — the lifecycle-census backdoor command's answer. policies maps every kind present (and every known kind) to its policy.

Ownership

class Ownership internal constructor(val kind: String, val key: String, val ownerId: String?, val strand: String?, val policy: ReleasePolicy, slot: Any?, live: (() -> Boolean)?, release: (() -> Unit)?) : AutoCloseable

One live thing some box registered with a subsystem that outlives it: an audio patch, a control callback, a Runloop hook, a module added to a render target, a feeder thread.

Declaration Summary
val kind: String
val key: String
val ownerId: String?
val strand: String?
val policy: ReleasePolicy
val isLive: Boolean False once closed, superseded, or once the subsystem itself reports the thing gone.
override fun close() Take the thing back out of its subsystem, unless a newer registration now holds its slot.
override fun toString(): String

LifecycleLedger

object LifecycleLedger

Every live Ownership in the process, so a box's leftovers can be counted.

Declaration Summary
fun claim(kind: String, key: String, slot: Any? = null, owner: Box? = RunScope.executingBox(), scope: RunScope? = RunScope.currentFor(owner), live: (() -> Boolean)? = null, policy: ReleasePolicy = ReleasePolicy.forKind(kind, hasSlot = slot != null), release: (() -> Unit)? = null): Ownership Record that owner (by default the executing box) now holds a kind thing called key.
fun entries(): List<Ownership> Every ownership still live, pruning the ones whose subsystem has dropped them on its own.

LedgerSlot

class LedgerSlot(val owner: Any, val key: Any?)

A slot for LifecycleLedger.claim: key within the container owner, compared by the container's identity. A Pair(map, key) would not do, since a map's equals and hashCode are over its contents and change as it is written.

Declaration Summary
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

LifecycleKinds

object LifecycleKinds

Kind names used in the ledger, so the census and its smoke agree on spelling.

Declaration Summary
const val AUDIO_PATCH
const val PATSYNTH_FEEDER
const val CONTROL_CALLBACK
const val RUNLOOP_PRE_RUN
const val RUNLOOP_POST_RUN
const val RENDER_MODULE
const val STREAM_PROCESS

OwnedCallbackRegistry

class OwnedCallbackRegistry<T : Any>(private val name: String)

A registry of UI control callbacks — sliders, checkboxes, colour pickers — keyed by the id the browser posts back, whose every registration is an Ownership in the LifecycleLedger.

Declaration Summary
fun register(id: String, callback: T, owned: Boolean = true): Ownership Install callback under id, recorded against the executing box; returns its ownership.
fun unregister(id: String) Remove whatever is registered under id.
operator fun get(id: String): T? The callback registered under id, if any.

Phased.kt

Frame-by-frame animation for box code: return a Frames and Field runs it once per Runloop frame.

frames, eachFrame, loop and forever build a Frames from a suspending body that calls frame() to wait for the next frame; Box.launchFrames runs one on the Framer dispatcher. The file also holds Generator, a small coroutine-based generator built with generator or generator2.

src/jvmMain/kotlin/afield/runtime/lifecycle/Phased.kt — read the source

Used in: Frames and the animation loop

Top-level functions

Declaration Summary
fun frames(block: suspend Frames.() -> Unit): Frames Lowest-level Frames builder: the body runs once and is responsible for calling Frames.frame itself to advance/suspend frames (e.g. while (frame()) { ... }).
fun forever(block: suspend Frames.() -> Boolean): Frames
fun loop(block: suspend Frames.() -> Unit): Frames Runs block repeatedly. Unlike eachFrame, it auto-advances one frame after each pass only if the body did not itself advance the frame (via Frames.frame/Frames.delay).
fun Frames.end(block: suspend Frames.() -> Unit): Frames
fun eachFrame(block: suspend Frames.() -> Unit): Frames Runs block once per frame while Frames.frame returns true. This is what animate { } (stdlib.kt) delegates to.
fun Box.launchFrames(frames: Frames, strand: String = "frames", mode: String = "frames", reportRunning: Boolean = true): Job Run frames on Field's Runloop with this box as the active context.
fun Box.launchFrames(strand: String = "frames", mode: String = "frames", reportRunning: Boolean = true, block: suspend Frames.() -> Unit): Job
operator fun Frames.plus(other: Frames): Frames Run two Frame programs in parallel. Each child advances as the parent advances, and cancellation of the parent stops both children.
fun <In, Out> generator2(block: suspend GeneratorScope<In, Out>.() -> Unit): Generator<In, Out> sequence { ... }-style helper.
fun <Out> generator(block: suspend GeneratorScope<Unit, Out>.() -> Unit): Generator<Unit, Out> Convenience overload when you don't need to send inputs. Input type is Unit, so yield(out) just returns Unit.
fun <In> gather(block: suspend GeneratorScope<In, Boolean>.() -> Unit): Generator<In, Boolean> Convenience overload when you don't need to send outputs. Input type is Unit, so yield(out) just returns Unit.
operator fun <In, Out> Generator<In, Out>.plus(other: Generator<In, Out>): Generator<In, Out> Sequential composition: (this + other)
operator fun <In, A, B> Generator<In, A>.rem(other: Generator<In, B>): Generator<In, Pair<A, B>> Parallel / lockstep composition: (this % other)
fun <In, Out, Out2> Generator<In, Out>.map(m: (Out) -> (Out2))
infix fun <In, Out, Out2> Generator<In, Out>.pipe(g: Generator<Out, Out2>)

Frames

class Frames

A frame-based animation. Returning a Frames from launched box code makes Field run its body once per frame on the Runloop (see Box.launchFrames and the Frames return-value handler in ReturnValueRegistry.registerFramesReturnValue).

Declaration Summary
var job: Job?
var atEnd
var body
var shouldend
var frameNumber
override fun toString(): String
suspend fun wait()
suspend fun end()
suspend fun delay(ms: Int)
suspend fun frame(): Boolean

Framer

class Framer : CoroutineDispatcher()

Declaration Summary
override fun dispatch(context: CoroutineContext, block: Runnable)
fun frame(): Int Runs what was dispatched before this call; what those blocks dispatch waits for the next.

GeneratorScope

class GeneratorScope<In, Out>(private val onYield: (Out, Continuation<In>) -> Unit)

Scope available inside generator { ... }.

Declaration Summary
suspend fun yield(value: Out): In
suspend fun yield(): In Convenience yield() without value — yields true. Only valid for generators whose output type is Boolean; any other output type will fail at runtime with a ClassCastException.

Generator

class Generator<In, Out> internal constructor(private val block: suspend GeneratorScope<In, Out>.() -> Unit)

Python-style generator that supports yield(out) / send(input)-like behavior.

Declaration Summary
fun start(): Out? Starts the coroutine and runs until the first yield. Returns the first yielded value, or null if the generator completed without yielding anything.
fun next(input: In): Out? Resume after a yield, providing the next input. Returns the next yielded value, or null if the generator has completed.
val isCompleted: Boolean

ReactiveRuns.kt

Reactive properties: the runs that propUp pulls from upstream and propDown pushes downstream.

A read that finds no value runs the box that supplies it before returning; a write that changes a value reruns the boxes that read the old one. ReactiveRuns holds both mechanisms and their guards.

src/jvmMain/kotlin/afield/runtime/lifecycle/ReactiveRuns.kt — read the source

ReactiveRuns

object ReactiveRuns

The runs that propUp and propDown cause: pulling a missing value from upstream, and pushing a changed one downstream.

Declaration Summary
val launchImmediately Set around a pull's launchAll: run the launch on the runloop now, not in the launch queue.
var enabled: Boolean
var pullTimeoutMs: Long
const val PULL_REPEAT_WINDOW_MS
const val MAX_RUNS_PER_CASCADE
fun recentEvents(): List<String> What the pulls and cascades did, oldest first, each line numbered.
fun clearEvents()
fun runStarted(box: Box, mode: String)
fun runFinished(box: Box, ok: Boolean, mode: String)
fun isQuiet(): Boolean No cascade draining and none waiting to: for tests that need the downstream reruns to be over.
fun isRunning(box: Box): Boolean
suspend fun awaitNotBlocked(box: Box) Hold a run of box while box is blocked in a pull, so it does not start inside its own read.
class PullOutcome(val satisfied: Boolean, val tried: List<String>, val problems: List<String>)
fun pull(reader: Box, asking: Box, name: String, satisfied: () -> Boolean): PullOutcome Run the boxes that supply name to asking (read by reader), nearest first, until satisfied; see the class comment for how, and for the guards.
fun stamp(): Long The next value of the shared clock: launches stamp themselves with it when they are queued.
fun pulledSince(box: Box, stamp: Long): Boolean True when a pull ran box successfully, launched after stamp.
fun relaunch(box: Box, entrypoint: String) Relaunches box's code on the strand it is running on (else default), recorded under entrypoint; for callers outside a cascade, such as the file watcher's onExternalEdit.
fun suppliersFor(asking: Box, reader: Box, name: String): List<Box> The boxes that supply name to asking (read by reader), nearest first, never including reader, the root, or a deleted box. asking itself comes first when it is not the reader and declares name — named("other").speed is asking other for its own value.
class Change(val holder: Box, val name: String, val seq: Long, val writer: Box?) writer's code changed name on holder; seq orders it against reads.
fun declaredDown(box: Box, name: String) box's code declared name with propDown / propUpDown; recorded when the macro expands.
fun declaredDownOnRoot(declarer: Box, root: Box, name: String) declarer's code declared name on root with rootPropDown / rootPropUpDown.
fun noteRawWrite(holder: Box, name: String, old: Any?, new: Any?) A write that did not go through the propDown delegate (a control bound with syncToProperty, which calls setPropertyRaw): if holder declared name Down and the value changed, its readers rerun as they would after holder.name = v.
fun isDeclaredDown(box: Box, name: String): Boolean Whether box still declares name Down. A declaration is recorded when the macro expands and is dropped here, lazily, once the box is deleted (tombstoned) or its code no longer contains a propDown / propUpDown of that name — so editing the macro out, or deleting the box, stops raw writes from rerunning readers.
fun forgetDown(box: Box) Forgets every Down declaration of box, and its run records (which otherwise held the deleted Box for the life of the JVM); for box deletion. A run still in flight keeps its state so its end is still seen.
fun forgetDocument(filename: String) Forgets everything recorded for the document at filename — run records, pull stamps, Down declarations — so a closed document leaves no reactive state (or Box references) behind. Runs still in flight keep their state, as in forgetDown. Called from document cleanup.
fun noteChange(holder: Box, name: String) A propDown value changed: hold it to the end of the writer's run, or send it on now.
fun orderNext(waiting: List<String>, adjacency: Map<String, Set<String>>): String? Of waiting (in priority order), the first that no other waiting id reaches through adjacency; the first of all when every one is reachable from another (a cycle).

RunOwnedListener.kt

A box's callbacks on a long-lived object, registered so that stopping or rerunning the box takes them off again.

runOwnedListener subscribes under an owner object and a name, and both halves of that pair matter: registering the same name again closes the previous subscription rather than adding a second one, and the owner is compared by identity, so two objects with equal contents keep separate slots. The registering box's RunScope owns the result, which is what releases it on Stop, on a rerun, and when the run is retired; calling it from outside a box run fails.

Events arrive with the registering box's context and run scope restored, so a callback behaves as if it were still inside the run. One that arrives after the scope stopped, or after a later registration took the same name, is dropped rather than delivered. Orca's listeners and PuppetBrowser's page events are registered this way.

src/jvmMain/kotlin/afield/runtime/lifecycle/RunOwnedListener.kt — read the source

RunScope.kt

Run scopes: what a box's run owns, and which of stop, re-run or retire releases it.

Each run of a box gets a RunScope; registrations made while it executes are owned by that scope and carry a ReleasePolicy. BoxRunScopes holds a box's scopes, so a re-run supersedes the last run's scope, a stop releases only what is running, and retiring the box releases everything.

src/jvmMain/kotlin/afield/runtime/lifecycle/RunScope.kt — read the source

Used in: Beat and pitch analysis, Corpus microscope: from recordings to related windows, Short-time spectral spaces

Top-level functions

Declaration Summary
fun <T> RunScope.withCurrent(block: () -> T): T Run block with this scope as the thread's current RunScope, so what it registers is owned here.

ReleasePolicy

enum class ReleasePolicy

Which of the three box events — stop, re-run, retire — takes a registration back out.

Constants: KEYED, ACCUMULATING, RUNNING.

OwnedEntry

class OwnedEntry internal constructor(val resource: AutoCloseable, val policy: ReleasePolicy)

Something a RunScope owns, with the policy that says which event releases it. onRelease listeners run once, after resource is closed by a scope — how once learns that what its cached value holds is gone.

Declaration Summary
val isReleased: Boolean True once a scope has released resource.
val isLive: Boolean Not yet released, and (for an Ownership) still held by its subsystem.
fun onRelease(listener: () -> Unit) Run listener when this is released; at once if it already has been.
override fun toString(): String

RunScope

class RunScope internal constructor(val box: Box, val strand: String, val kind: String) : AutoCloseable

One run of one box on one strand, and the owner of everything that run started.

Declaration Summary
val box: Box
val strand: String
val kind: String
val id: Long Opening order across the process; a strand's older scopes have smaller ids.
val job: CompletableJob Cancelled by stop, supersede and close; a parent for coroutines that belong to this run.
val isClosed: Boolean True once this scope has been superseded or retired; it owns nothing after that.
val isStopped: Boolean True once a stop (or a close) has reached this run scope. The parent scope never reports stopped.
val element: CoroutineContext A coroutine context element that makes this the current scope wherever the coroutine runs.
fun <T : AutoCloseable> own(resource: T, policy: ReleasePolicy = defaultPolicy(resource)): T Take ownership of resource under policy — by default an Ownership's own, and ReleasePolicy.RUNNING for anything else. Returns resource. If this scope has already ended, resource is released now when the event that ended it releases policy, and kept otherwise.
fun own(policy: ReleasePolicy = ReleasePolicy.RUNNING, release: () -> Unit): AutoCloseable Own a release action under policy; returns the AutoCloseable that runs it, which disown accepts.
fun ownJob(job: Job): Job Cancel job when stop, supersede or retire reaches this scope. Returns job.
fun disown(resource: AutoCloseable): Boolean Stop owning resource without closing it — for something that has ended on its own.
val liveCount: Int How many owned things are still live (ledger ownerships the subsystem still holds, and the rest).
fun liveCount(policy: ReleasePolicy): Int How many owned things under policy are still live.
override fun close() Retire: release everything owned, newest first, and cancel job. Idempotent; one failure does not strand the rest.
override fun toString(): String

BoxRunScopes

class BoxRunScopes internal constructor(private val box: Box)

A box's RunScopes: at most a few open per strand, the newest of which is the strand's current run, plus the one parent scope that no run supersedes. See RunScope for when they open and close.

Declaration Summary
fun begin(strand: String, kind: String): RunScope Open a new scope for a run of strand; older scopes of the strand stay open until supersede.
fun supersede(newer: RunScope) Supersede every scope of newer's strand that was opened before it: newer has replaced them.
val parent: RunScope The box-level scope no re-run supersedes. What it owns lasts until a stop, for ReleasePolicy.RUNNING things, or until the box retires, for everything else. A superseded run's ReleasePolicy.KEYED registrations move here, and once.x { … } computes with it current, so a value made once is not taken apart by the next run.
fun ambient(): RunScope? The newest open RUN_STRAND scope that no stop has reached, opening one if there is none — where a fragment run, another language's execution, and code with no installed scope register. Null for a deleted box, so a straggler registering after deletion owns nothing (the ledger still attributes it).
fun stopAll(): Int Stop's part of the teardown: every scope, the parent included, releases what is ReleasePolicy.RUNNING. Returns how many run scopes there were.
fun closeAll(): Int Retire's part: close every scope, the parent last, releasing everything. Returns how many run scopes there were.
fun snapshot(): List<RunScope> The open run scopes, oldest first, then the parent if it is open.
val liveCount: Int How many owned things across all open scopes are still live.
val liveRunningCount: Int How many ReleasePolicy.RUNNING things across all open scopes are still live: what a stop would release.

RunawayBoxInterruptSmokeTest.kt

src/jvmMain/kotlin/afield/runtime/lifecycle/RunawayBoxInterruptSmokeTest.kt — read the source

RunawayBoxInterruptSmokeTest

object RunawayBoxInterruptSmokeTest

Box code that never returns is thrown out of, and Field carries on (BoxInterrupt).

Declaration Summary
fun main(args: Array<String>)

Runloop.kt

The main Field loop: Runloop ticks preRun, the Framer dispatcher and postRun once per frame on one thread.

Runloop.runLoop is the single instance and Runloop.frame its frame counter. inMain launches a coroutine onto that loop, Runloop.miscPool is the shared pool for background work, and AnimationClock is the time source that frame-based code reads, which becomes frame-indexed during a frame-synchronous video capture. RateLimiter coalesces bursts of work to a fixed rate. The Runloop is not the audio thread; see FieldAudioEngine for that.

src/jvmMain/kotlin/afield/runtime/lifecycle/Runloop.kt — read the source

Used in: Profiling, Frames and the animation loop

Top-level functions

Declaration Summary
fun inMain(block: suspend CoroutineScope.() -> Unit) Launch block as a coroutine on the Runloop's Framer dispatcher.
fun inMain(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit) Explicit inherited metadata for box-owned work, including its first dispatch.

AnimationClock

object AnimationClock

Time source for frame-based programs that also need a duration.

Declaration Summary
class FrameToken internal constructor(internal val state: FrameState)
fun nowSeconds(): Double
fun isFrameIndexed(): Boolean

Runloop

class Runloop

Declaration Summary
var frame Volatile: the stall watchdog and BoxInterrupt.poll read it from other threads / later frames.
val main
val preRun
val postRun
var shouldSleep
var limiter
fun timingStats(): RunloopTimingStats
fun schedulingStatus(): RunloopSchedulingStatus
fun start()

RateLimiter

class RateLimiter(rate: Double, private val maxLatency: Double)

Declaration Summary
fun submit(block: () -> Unit)
operator fun invoke(block: () -> Unit)

RuntimeIdentity.kt

RuntimeIdentity records which compiled classes and native libraries produced a result, as hashes, and throws RuntimeIdentityMismatch from RuntimeIdentity.verify when a host has loaded different code.

src/jvmMain/kotlin/afield/runtime/lifecycle/RuntimeIdentity.kt — read the source

NativeLibraryIdentity

data class NativeLibraryIdentity(val path: String, val sha256: String)

A native library's file name is its key in a RuntimeIdentity; path is where it was loaded from, kept for messages.

RuntimeIdentityMismatch

class RuntimeIdentityMismatch(message: String, val differences: List<String>) : IllegalStateException(message)

RuntimeIdentity.verify found code other than the pinned code; differences has one line per class or library.

RuntimeIdentity

data class RuntimeIdentity(val classes: Map<String, String>, val natives: Map<String, NativeLibraryIdentity> = emptyMap())

Which compiled classes and native libraries a result was made with, as hashes, so that a host which has loaded different code is rejected by RuntimeIdentity.verify rather than returning a different answer.

Declaration Summary
fun write(file: File): File Write this identity as JSON to file.
fun differences(loaded: RuntimeIdentity): List<String> One line per pinned class or library that loaded lacks or holds differently.

SimulationClock.kt

A manually stepped coroutine clock, so delay and withTimeout inside a box follow simulation time.

withSimulationClock opens a SimulationClock inside ordinary frames code; each SimulationClock.advance moves simulation time forward and resumes the coroutines that are due, within a resume budget.

src/jvmMain/kotlin/afield/runtime/lifecycle/SimulationClock.kt — read the source

Used in: Profiling

Top-level functions

Declaration Summary
suspend fun <T> withSimulationClock(stepSeconds: Double = 1.0 / 60.0, maxStepsPerAdvance: Int = 8, maxResumesPerStep: Int = 1024, block: suspend SimulationClock.() -> T): T Scope a simulation to the current box coroutine. Use this inside an ordinary frames program; the existing launch/stop/relaunch controls own it. Closing cancels and drains all children.

SimulationAdvance

data class SimulationAdvance(val steps: Int, val resumed: Int, val deferred: Int, val droppedSeconds: Double)

Work performed by SimulationClock.advance; discarded elapsed time is reported explicitly.

SimulationClock

class SimulationClock(parentContext: CoroutineContext, val stepSeconds: Double = 1.0 / 60.0, val maxStepsPerAdvance: Int = 8, val maxResumesPerStep: Int = 1024) : CoroutineScope, AutoCloseable

A coroutine scope with a manually stepped dispatcher. It creates no thread and registers no new box launcher. Use withSimulationClock inside ordinary frames; standard launch children inherit the box, source and parent job. delay and withTimeout then use simulation time. advance and close belong to the creating thread; dispatch and cancellation may enqueue from another thread.

Declaration Summary
val stepSeconds: Double
val maxStepsPerAdvance: Int
val maxResumesPerStep: Int
override val coroutineContext
var tick: Long
val time: Double
val pendingResumes: Int
val pendingTimers: Int
suspend fun sleep(seconds: Double) Suspend for simulation seconds, with resolution stepSeconds. Zero still yields one tick.
fun runCurrent(): Int Run ready children without advancing time, for example to establish their initial delays.
fun advance(seconds: Double, onStep: (Double) -> Unit = {}): SimulationAdvance Advance fixed ticks, discarding excess catch-up time rather than creating an unbounded backlog.
fun advance(seconds: Double, beforeStep: (Int, Int) -> Unit, onStep: (Double) -> Unit): SimulationAdvance Prepare a shared snapshot before each tick's coroutines, then integrate in onStep. beforeStep receives the zero-based step index and total steps in this advance, allowing observed input motion to be distributed over catch-up steps. It sees the preceding time; the clock then advances one tick, resumes due children, and calls onStep.
override fun close()

SimulationReset.kt

SimulationReset, a way for UI or agent threads to ask a running simulation to reset at its next frame boundary, with several requests before that boundary coalesced into one reset.

src/jvmMain/kotlin/afield/runtime/lifecycle/SimulationReset.kt — read the source

SimulationReset

class SimulationReset

Coalesced reset requests, consumed explicitly at an application's frame boundary. This is not a launcher: create it in normal box setup and consume it inside the existing frames/loop coroutine. Requests can arrive from UI/agent threads.

Declaration Summary
var completed: Long
var failure: String?
var epoch: Any
val pending: Boolean
fun request(): Long Returns a ticket. Multiple requests before the next boundary share one reset.
fun atFrame(frame: Long, reset: () -> Unit): Boolean At most once per frame id. A failed callback is reported and is not retried silently; request again after repairing it. No state rollback is implied. A request arriving during the callback remains pending for the next frame.

cached.kt

cached, which hands a box back the value it made on its last run for the same keys, and closes the old value when the keys change.

src/jvmMain/kotlin/afield/runtime/lifecycle/cached.kt — read the source

Top-level functions

Declaration Summary
inline fun <reified T : Any> Box.cached(vararg keys: Any?, noinline make: () -> T): T Returns the T this box made last time for the same keys, making one with make only when there is none:

BoxCache

object BoxCache

The per-box store behind cached; public only so the inline cached can reach it.

Declaration Summary
fun size(box: Box): Int How many values box has cached; for tests and inspection.

di.kt

Startup phases: functions annotated RunsAt are found by a classpath scan and called by runAll at a named phase.

The phase names are the constants at the top of this file (early, plugins, installHandlers, newRoot, ...). runAll calls every function registered for a phase in priority order, passing those arguments its parameters ask for. The same scan fills labelledArgumentFunctions.

src/jvmMain/kotlin/afield/runtime/lifecycle/di.kt — read the source

Top-level functions

Declaration Summary
fun runAll(time: String, vararg args: Any?)

Top-level values

Declaration Summary
const val plugins Startup phase: install Ktor plugins.
const val early
const val installHandlers
const val templatesRegistration
const val newRoot
const val macroRegistration
const val returnValueRegistration
val labelledArgumentFunctions: MutableSet<String> Names of the functions annotated @LabelsArgumentsFromSource, filled by the same scan as fieldSourceCache and read when macros are registered (afield.runtime.macros.registerLabelledArgumentMacros).
val fieldSourceCache: Map<String, List<Pair<Int, Method>>> by lazy(LazyThreadSafetyMode.SYNCHRONIZED)

RunsAt

annotation class RunsAt(val value: String, val priority: Int = 0)

Marks a function to be called by runAll at phase value, lower priority first.

util_lifecycle.kt

LazyVar, a lazy value that can also be assigned, and its lazyVar factory.

Assigning the value before the first read means its initializer never runs, and LazyVar.discard forgets the value so the next read builds it again. A value that is dropped without being cached goes to an onDiscard callback, so something that needs releasing is released once.

src/jvmMain/kotlin/afield/runtime/lifecycle/util_lifecycle.kt — read the source

Top-level functions

Declaration Summary
fun <T> lazyVar(mode: LazyThreadSafetyMode = LazyThreadSafetyMode.SYNCHRONIZED, onDiscard: ((T) -> Unit)? = null, initializer: () -> T): LazyVar<T> Convenience factory mirroring lazy { }; see LazyVar for onDiscard.

LazyVar

class LazyVar<T> private constructor(private val initializer: () -> T, private val mode: LazyThreadSafetyMode, private val onDiscard: ((T) -> Unit)?) : ReadWriteProperty<Any?, T>

A var-friendly lazy: if never explicitly set, the first get() runs initializer and caches it. If explicitly set via the setter, initializer will not run.

Declaration Summary
override fun getValue(thisRef: Any?, property: KProperty<*>): T
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) Never waits for an initializer in flight: a value set now wins over the one it is building.
fun discard(): Boolean Forget the value, so the next get() runs the initializer again, without waiting for an initializer running on another thread. The cached value, if there was one, goes to onDiscard here, on the calling thread. An initializer already running hands its result to onDiscard when it returns, on its own thread, and does not cache it.
val generation: Int How many times discard has run. An initializer that reads this when it starts can tell later whether a discard has overtaken it, before its result is even returned (see Box.buildKi).
val isInitialized: Boolean Returns true iff a value (either set or computed) is currently cached.
fun valueOrNull(): T? The cached value, or null without running the initializer.

LabelledArguments.kt

A macro that labels a function's positional arguments with their own source text, so showMeshGrid(cube, ring) compiles as showMeshGrid("cube" to cube, "ring" to ring).

Annotate a function with LabelsArgumentsFromSource to opt it in; LabelledArgumentsTest covers the rewrite.

src/jvmMain/kotlin/afield/runtime/macros/LabelledArguments.kt — read the source

Top-level functions

Declaration Summary
fun registerLabelledArgumentMacros() Registers a labelArgumentsFromSource rewrite for every function annotated LabelsArgumentsFromSource.
fun labelArgumentsFromSource(callee: String, arguments: List<String>): Insert? The call callee``(``arguments``) with each unlabelled positional argument written as "<its source>" to <it>, or null when every argument is already labelled (which is also what ends the expander's re-run loop on the rewritten call).

LabelsArgumentsFromSource

annotation class LabelsArgumentsFromSource

Labels a function's positional arguments with their own source text, before the box compiles.

PatternRewrite.kt

Pattern-based rewrite rules for box code, written in the box itself with a rewrite { from { } to { } } block and applied to the source before it compiles.

The rules are matched on the parsed syntax tree, not on text, so from { max(x, y) } matches any call of max with two arguments whatever their spelling. PatternRewrite describes the phases, guards and hygiene rules.

src/jvmMain/kotlin/afield/runtime/macros/PatternRewrite.kt — read the source

PatternRewrite

object PatternRewrite

A structural source-to-source rewriter that runs as a preprocessor in front of kotlinc, bootstrapped on the existing PSI-based macro pass (see afield.runtime.kotlin.KotlinScriptEngine.applyMacros).

Declaration Summary
data class PatternRule(val pattern: KtElement, val repl: KtElement, val metavars: Map<String, MetaKind>, val guard: Guard, val provide: KtElement?, val aiPrompt: KtStringTemplateExpression?, val patternText: String, val name: String? = null) A collected rule: pattern PSI, replacement PSI, metavar declarations, guard, and side clauses.
sealed interface Guard A structural guard over captured metavariables (see where / distinct / isLiteral …).
data class AiRequest(val key: String, val prompt: String, val toText: String) A cached AI generation request produced by an ai("…") clause when its rule fires.
data class ExpandResult(val text: String, …`-wrapped). */ The result of expand: rewritten text plus side contributions for the caller to route.
sealed interface Strat A controllable term-rewriting strategy: a small Stratego/ELAN-style control program parsed from a strategy {} block and interpreted over the whole-file source text (see StrategyRunner). Leaves name rules; combinators compose them. A strategy either succeeds (yielding a possibly-changed source) or fails (does not apply) — the latter is what makes Choice and fail-fast Seq meaningful. Everything is spliced/re-parsed on PSI, exactly like the default fixpoint path.
fun expand(source: String, parse: (String) -> KtFile, maxIterations: Int = 64, evalUnquote: ((String) -> String?)? = null, ambientRuleSources: List<String> = emptyList(), onDiagnostic: (String) -> Unit = {}): ExpandResult Expand every rewrite {} rule in source. parse turns text into a KtFile (the caller supplies the PSI environment — the REPL's project in production, or a standalone parser in tests). onDiagnostic receives a human-readable message on a guard-vocabulary error or if the iteration cap is hit; when the cap is hit, expansion stops and the best-effort-expanded text so far is returned (never hangs, never throws into the box).
fun stableKey(content: String): String A stable, content-derived key (safe inside a `/*…
fun buildUnquoteSnippet(scalarBinds: Map<String, PsiElement>, blockBody: String): String Build the Kotlin snippet an unquote { … } block evaluates to. Each scalar metavar is bound as a String val holding its captured source text, so the block can manipulate captures as strings (unquote { x.reversed() }, unquote { simplify(x) }, …); the block's result is toString()-ed and spliced.
fun atticHasAiBlock(attic: String, key: String): Boolean True iff attic already contains a cached block for key (a cache hit — skip generation).
fun appendAiBlock(attic: String, key: String, prompt: String, code: String): String Append a generated block for key to attic, returning the new attic text.

RewriteDsl.kt

The rule-authoring surface for the pattern-rewrite layer (see PatternRewrite).

These declarations are erased: a rewrite { … } block is collected and deleted from the source by PatternRewrite.expand before the K2 REPL ever compiles it, so none of these functions is ever actually called at run time. They exist only so a rule authored in a live box - parses, - is importable (import afield.runtime.macros.*), and - reads sensibly (the names rewrite / from / to / Expr resolve to something).

The whole point (see PatternRewrite) is that a rule body never needs to type-check — only parse. PSI parsing runs no name resolution, so from { max(x, y) } parses into a valid tree even though max, x, y resolve to nothing. If one of these functions is somehow reached at run time it means expansion did not happen, which is a bug — hence the throw.

Surface:

rewrite { x: Expr, y: Expr ->
    from { max(x, y) }
    to   { if (x >= y) x else y }
}
The lambda's value parameters (x: Expr, y: Expr) are the metavariable declarations: inside from/to, a name reference is a hole iff its name is in that parameter set, and its declared type names the MetaKind. No unquote/$/, escape is needed — the known metavar set is the escape.

src/jvmMain/kotlin/afield/runtime/macros/RewriteDsl.kt — read the source

Used in: Rewriting your own syntax, Agent transcript gallery, Spatial artificial life

Top-level functions

Declaration Summary
fun rewrite(rule: () -> Unit): Unit Declare a rewrite rule. Collected and deleted before compilation; see PatternRewrite.
fun rule(name: String, rule: () -> Unit): Unit Declare a named rewrite rule, so a strategy block can refer to it by name. Otherwise identical to rewrite — same from/to/where/provide/ai body, same erase-before-compile treatment. The name is read textually by the collector (a string literal), so — like the metavar types — it never needs to type-check.
fun strategy(block: () -> Unit): Unit A controllable term-rewriting strategy over the box's named rules (see PatternRewrite.Strat).
fun once(name: String): Strategy Fire rule name at the single first (outermost) match — at most once.
fun topdown(name: String): Strategy Fire rule name at the single outermost (shallowest) match site.
fun bottomup(name: String): Strategy Fire rule name at the single innermost (deepest) match site — differs from topdown on nested matches (given f(f(a)), topdown rewrites the outer call, bottomup the inner one).
fun attempt(s: Any?): Strategy Run s once; if it does not apply, fall back to identity. Never fails. (Spelled attempt because try is a Kotlin hard keyword and cannot be a call name.)
fun repeat(s: Any?, n: Int): Strategy Iterate s to a fixpoint (or at most n times). Never fails.
fun repeat(s: Any?): Strategy Iterate s to a fixpoint (unbounded within the engine's iteration cap). Never fails.
fun seq(vararg s: Any?): Strategy Run each strategy in order, threading the result; fails as soon as one fails. See also then.
fun choice(vararg s: Any?): Strategy Run the first alternative that succeeds; fails only if none apply.
infix fun Any?.then(other: Any?): Strategy Infix sequencing: a then b runs a, then b on its result (fail-fast).
fun from(pattern: () -> Unit): Unit The pattern (left-hand side) of a rewrite rule. Its body is quasiquoted, never evaluated.
fun to(replacement: () -> Unit): Unit The replacement (right-hand side) of a rewrite rule. Its body is quasiquoted, never evaluated.
fun <T> unquote(block: () -> T): T Procedural unquote: inside a to {} body, unquote { … } runs Kotlin at expansion time and splices the block's toString() result into the emitted source as Kotlin source code. Each metavariable is available inside the block as a String holding its captured source text, so captures can be transformed as strings. The block runs in the box's REPL context, so it can call box-scoped helpers.
fun provide(preamble: () -> Unit): Unit An optional preamble contribution. When the rule fires, the declarations in this block are metavar-substituted (no hygiene rename — these are meant to define stable box-level names) and merged into the box preamble under a stable, content-derived key, so repeated runs deduplicate and the segment only re-executes when its content changes (see the `/*prefix
fun ai(prompt: String): Unit An optional cached AI request. When the rule fires, the (metavar-substituted) prompt is resolved once against an AI model and the generated code is cached into the box attic under a stable key; subsequent runs with the same prompt reuse the cached block with no model call. The to {} side forwards to whatever the generated code defines. Example:
fun where(guard: () -> Boolean): Unit An optional guard: the rule fires only if the (structural) predicate over the captured metavariables holds. The body is a single boolean expression built from the predicates below, combined with &&, \|\|, !, and parentheses — for example:
fun distinct(vararg metavars: Any?): Boolean True iff all listed metavariable captures are pairwise structurally distinct.
fun identical(a: Any?, b: Any?): Boolean True iff the two captures are structurally identical.
fun isLiteral(metavar: Any?): Boolean True iff the capture is a literal (numeric/boolean/char constant or a string template).
fun isIdentifier(metavar: Any?): Boolean True iff the capture is a bare identifier / simple name reference.
fun isCall(metavar: Any?): Boolean True iff the capture is a function or constructor call.
fun isCallTo(metavar: Any?, calleeName: String): Boolean True iff the capture is a call whose callee has the given calleeName. Handy for the type-directed coercion idiom — wrap every f(x) in a runtime adapter but guard against re-wrapping the adapter you just introduced, so the fixpoint terminates:

MetaKind

sealed interface MetaKind

The kind of syntax a metavariable is allowed to bind. Used for early rejection + readability.

Expr

object Expr : MetaKind

Any expression (KtExpression). The common case.

Ident

object Ident : MetaKind

A simple name reference (KtNameReferenceExpression).

Stmt

object Stmt : MetaKind

A statement-position element. Treated like Expr in v1.

TypeRef

object TypeRef : MetaKind

A type reference (KtTypeReference).

Vararg

object Vararg : MetaKind

A variadic hole: binds a run of call arguments (zero or more). Only meaningful inside an argument list. In from it consumes the matching arguments; in to it expands to them, comma-separated. Example:

Strategy

class Strategy internal constructor()

Opaque result of a strategy combinator. Erased — a strategy block never actually runs.

ai_macro.kt

The ai("...") macro, which asks a language model to write the body of the function it is called in and caches the result in the box's attic.

The containing function must declare its parameter types and return type; the signature and the prompt go to the model through AiMacroBridge, and the generated function is stored between // @ai-gen: and // @ai-gen-end: comment lines so it is reused rather than regenerated (parseAtticBlock, appendAtticBlock, removeAtticBlock). resolvePatternRewriteAi serves the ai(...) side contributions of PatternRewrite rules through the same cache.

src/jvmMain/kotlin/afield/runtime/macros/ai_macro.kt — read the source

Top-level functions

Declaration Summary
fun parseAtticBlock(attic: String, functionName: String): AtticBlock?
fun removeAtticBlock(attic: String, functionName: String): String
fun appendAtticBlock(attic: String, block: AtticBlock): String
fun extractParamNames(parameterList: String): String
fun stripAiGenMetadata(attic: String): String
fun Box.macro_ai(args: List<String>, containingFunction: ContainingFunction?): Insert?
fun Box.resolvePatternRewriteAi(req: PatternRewrite.AiRequest) Resolve a pattern-rewrite ai("…") clause (see PatternRewrite). If the box attic already holds a cached block for this request's stable key, do nothing — a cache hit, no model call. Otherwise generate the code once via AiMacroBridge, append it to the attic keyed by PatternRewrite.AiRequest.key, and leave _attic stale so that checkAtticAndExecute compiles and executes the freshly generated code on this run.
fun registerAiMacro()

AtticBlock

data class AtticBlock(val functionName: String, val signature: String, val prompt: String, val body: String)

Declaration Summary
val functionName: String
val signature: String
val prompt: String
val body: String

AiMacroBridge

object AiMacroBridge

Declaration Summary
val bridge: LlmBridge
fun getOrCreateThread(boxId: String, contextCode: String): LlmThreadState
fun clearThread(boxId: String)

macros.kt

The call-rewriting macro pass that box code goes through before it compiles: a call such as prop(...), moduleHere(...) or _println(...) is replaced by source text generated for the box that contains it.

MacroRegistry maps a call name to its rewrite and returns an Insert describing the replacement. The rest of the file supports that pass: gathering the preamble a box inherits from its parents (gatherPreambleUpwards, with conflicts reported by gatherPreambleUpwardsReport), tagging preamble lines with the uid of the box that supplied them, and splitting a box at //%- fragment breaks (isFragmentBreak, fragmentBreakIndices) so that compiler diagnostics can be moved back to the right line (rebaseFragmentDiagnostics).

src/jvmMain/kotlin/afield/runtime/macros/macros.kt — read the source

Used in: Running code in a box

Top-level functions

Declaration Summary
fun rebaseFragmentDiagnostics(diagnostics: List<ScriptDiagnostic>, lineOffset: Int): List<ScriptDiagnostic> Compiler locations are relative to the snippet after a //%- break. Editor locations are relative to the whole box. Discard the snippet's absolute position, which may also have changed during macro expansion; line and column remain useful in the original source.
suspend fun standardExecutionPractice(ki: KotlinScriptEngine, s: String, onCompileError: (List<ScriptDiagnostic>) -> Unit = { lprintln("error,macros", "onCompileError default handler $it") }, onEvalError: (List<ScriptDiagnostic>) -> Unit = { lprintln("error,macros", "onEvalError default handler $it") }, onEvalException: (Throwable) -> Unit = { lprintln("error,macros", "onEvalException default handler $it"); throw it }, implicits: List<Any> = emptyList(), runSetupBlocks: Boolean = true, onSetupCompileError: (String, List<ScriptDiagnostic>) -> Unit = { _, diagnostics -> onCompileError(diagnostics) }, onSetupEvalError: (String, List<ScriptDiagnostic>) -> Unit = { _, diagnostics -> onEvalError(diagnostics) }, onSetupEvalException: (String, Throwable) -> Unit = { _, throwable -> onEvalException(throwable) }): List<ResultValue?>
fun searchUpwards(f: HasHierarchy, name: String, found: (String?) -> Unit)
fun gatherPreambleUpwards(startingAt: Box): String The text a box actually compiles against: its own preamble in full, plus — from every ancestor — the import lines and the lines tagged with the trailing inherit marker (an empty block comment; spelled out rather than written here, because a comment marker inside a KDoc nests). prop, @exported and the FieldML host bridge all tag what they mean to publish; moduleHere's generated shader accessors do not, so they stay in the box whose shader declared them. Documented for users in public/docs/running_code.md ("Property inheritance").
fun gatherPreambleUpwardsReport(startingAt: Box): GatheredPreamble The walk behind gatherPreambleUpwards, reporting what it dropped.
fun preambleSupplierTag(line: String): String? The uid in the from: tag just before a line's trailing inherit marker, or null.
fun stripPreambleSupplierTag(line: String): String line with its supplier tag removed; the inherit marker stays.
fun withPreambleSupplierTag(line: String, uid: String): String line, an inherit-marked line, tagged as supplied by uid (replacing any tag it had).
fun dropStaleSuppliedLines(preamble: String, box: Box): String preamble without the inherit-marked lines whose supplier tag names a box that is neither box nor one of its ancestors — what checkPreambleAndExecute persists, so a disconnected supplier's declarations leave the box instead of living in it for good.
suspend fun checkPreambleAndExecute(inside: Box, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit)
suspend fun checkIntroductionAndExecute(inside: Box, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit)
suspend fun checkAtticAndExecute(inside: Box, onCompileError: (List<ScriptDiagnostic>) -> Unit, onEvalError: (List<ScriptDiagnostic>) -> Unit, onEvalException: (Throwable) -> Unit)
fun isFragmentBreak(line: String): Boolean Whether line is a //%- (or `/*%-
fun fragmentBreakIndices(lines: List<String>): List<Int> Where a box's code splits into fragments, as line indices, ending with lines.size: every isFragmentBreak line, plus an implicit break before a top-level line that calls moduleHere when an earlier top-level once.vertex { } / once.fragment { } (or another shader stage) sits in the same fragment. moduleHere reads the box's shaders while it expands, and once.vertex only sets them when it runs, so without the break the module would be built on the default shaders.

Top-level values

Declaration Summary
const val PREAMBLE_SELF_SUPPLIER The supplier a declaration @exported writes into its own box names: itself, whoever that is.

PreambleConflict

data class PreambleConflict(val receiverAndName: String, val kept: String, val keptSupplier: String?, val discarded: List<Pair<String, String?>>)

Two inheritable declarations of one extension property (Box._x, Once.speed) whose texts differ — typically two ancestors @exported the same name with different types. kept is the one compiled; discarded were dropped from the gathered text rather than left to the REPL's shadowing order.

Declaration Summary
val receiverAndName: String
val kept: String
val keptSupplier: String?
val discarded: List<Pair<String, String?>>

GatheredPreamble

class GatheredPreamble(val text: String, val stale: List<String>, val conflicts: List<PreambleConflict>)

gatherPreambleUpwards's text plus what it left out: inherited lines whose supplier is no longer this box or one of its ancestors (stale) and conflicting declarations (conflicts).

MacroRegistry

object MacroRegistry

Declaration Summary
class MacroNotExpandedException(msg: String) : Exception(msg)
class MacroExpansionProblem(msg: String) : Exception(msg)
val callRewrite
val callRewriteWithContext
fun rewrite(box: Box, identifier: String, arguments: List<String>, containingFunction: ContainingFunction? = null): Insert?

ContainingFunction

data class ContainingFunction(val name: String, val parameterList: String, val returnType: String)

Declaration Summary
val name: String
val parameterList: String
val returnType: String

Insert

data class Insert(val start: String = "", val afterImports: String = "", val here: String = "", val standaloneHere: String? = null, val end: String = "", val preamble: String = "", val preambleAfterImports: String = "", val target: Box? = null)

Declaration Summary
val start: String
val afterImports: String
val here: String
val standaloneHere: String?
val end: String
val preamble: String
val preambleAfterImports: String
val target: Box?

source_block_macro.kt

src/jvmMain/kotlin/afield/runtime/macros/source_block_macro.kt — read the source

Top-level functions

Declaration Summary
fun Box.macro_sourceBlock(args: List<String>): Insert

BoxStudyBuilds.kt

The box surface for study builds: Box.studyBuilds attaches a named StudyBuilds runner to a box, and Box.showStudyBuilds shows its stage buttons, progress and diagnostics in an output channel.

The runner keeps its history under the document's studies directory, is replaced when the box runs again, and closes when the box stops. Runners on boxes of the same document can depend on each other's stages through dependsOn(runner, stage).

src/jvmMain/kotlin/afield/study/build/BoxStudyBuilds.kt — read the source

Used in: Let AI4 Wait for Long Builds, Studies

Top-level functions

Declaration Summary
fun Box.studyBuilds(name: String, stages: List<StudyBuildStage>, workers: Int = 2, gates: List<StudyBuildGate> = emptyList(), exclusive: Boolean = false, readInputs: () -> Map<String, String>): StudyBuilds Attach durable derived stages to this box, discoverable by every AI4 chat in its document.
fun Box.showStudyBuilds(name: String, channel: String = "builds") Show this named runner's live stage buttons, progress and selectable diagnostics in a box output.

StudyBuildFindings.kt

What a study build's reviewers said, collected from the boxes that own the review surfaces.

A build's outputs get reviewed where everything else does — the three-view panels and the geometry review surfaces a box owns — so this gathers the notes a build run left behind and a build card can show them without the reviewing machinery knowing that builds exist. A note is kept when it names one of the run ids asked for and its kind is an error, a warning or a question; notes are deduplicated by their source box and note id, first one wins.

What comes back points at the discussion rather than restating it: ids for the note, the view and the two boxes, a count of replies, and the first 240 characters of the text for a label. A source whose surfaces have stopped is read too, from the review notes saved on the box itself, which is how a finished run keeps its history; those are marked as not live, and their staleness is left unanswered rather than guessed.

src/jvmMain/kotlin/afield/study/build/StudyBuildFindings.kt — read the source

StudyBuildHelpers.kt

Helpers a StudyBuildStage uses to name what it runs: fileIdentity and classIdentity hash the source files or compiled classes behind a build, for its recipeVersion or an input, so results go stale when that code changes.

StudyBuildProcessResult is what a stage's external process reported: exit code, retained log and last progress message.

src/jvmMain/kotlin/afield/study/build/StudyBuildHelpers.kt — read the source

Used in: Studies

Top-level functions

Declaration Summary
fun fileIdentity(files: Iterable<File>, include: (File) -> Boolean = { !it.name.startsWith(".") && it.extension != "pyc" }): String A hash of the contents of files, each named by its file name, for a stage's recipeVersion or an input, so a build goes stale exactly when the code it runs changes. A directory contributes every file under it that include accepts (by default, not hidden files and not Python bytecode), named by its path inside the directory. Where the files live is not hashed, so moving the document keeps the identity.
fun fileIdentity(vararg files: File): String fileIdentity of files.
fun classIdentity(classes: Iterable<KClass<*>>): String A hash of the compiled code of classes, each with every class compiled inside it (nested classes, companions, anonymous objects), for a stage's recipeVersion. Name the classes that do the build's work rather than a whole provider, so editing unrelated code leaves results current. A class whose compiled code cannot be read, such as one defined in a box, is refused: identify its source with fileIdentity instead.
fun classIdentity(vararg classes: KClass<*>): String classIdentity of classes.

StudyBuildProcessResult

data class StudyBuildProcessResult(val exitCode: Int, val log: File, val lastMessage: String?)

What StudyBuildContext.runProcess saw: the exit code, the retained log, and the last progress message.

StudyBuildPanel.kt

The build card a box shows for one study build runner: every stage, with its buttons, progress and diagnostics.

showStudyBuilds mounts one of these per output channel. The card drives the real runner through Field's ordinary button transport, so a stage's Build and Cancel are the same controls any box output gets, and its HTML is built from the stage list by one function, which is what lets the panel re-publish only when that HTML has actually changed.

Reading the stages is not free — it re-reads the design's inputs and checks result files — so the card reads them twice a second only while a build is queued or running, or when StudyBuilds reports a new revision, and otherwise every five seconds, which is how long an unannounced edit to the design can take to show up.

src/jvmMain/kotlin/afield/study/build/StudyBuildPanel.kt — read the source

StudyBuilds.kt

Durable derived builds for a study: named stages whose results are fingerprinted against their inputs and recipe, kept on disk with their attempt history, and reported stale when either changes.

A StudyBuildStage declares an id, a recipeVersion, the input keys it reads and the stages it depends on; StudyBuilds runs those stages on worker threads under a directory it owns through a file lock. A StudyBuildGate adds an input that exists only while a check passes, and dependsOn(runner, stage) names a stage of another runner in the same document. Reading StudyBuilds.stages recomputes currency, StudyBuilds.start and StudyBuilds.cancel drive one attempt, and StudyBuilds.prune deletes attempts that no current result, running build or pin needs. From a box, attach a runner with Box.studyBuilds (BoxStudyBuilds.kt).

src/jvmMain/kotlin/afield/study/build/StudyBuilds.kt — read the source

Used in: Studies, Let AI4 Wait for Long Builds

Top-level functions

Declaration Summary
fun dependsOn(runner: String, stage: String): String The dependency name of stage in the runner called runner in the same document, for StudyBuildStage.dependencies, StudyBuildGate.dependencies and StudyBuildContext.dependencyFile.

StudyBuildStatus

enum class StudyBuildStatus

A build's execution state. Currency against the design is reported separately by StudyStageState.

Constants: QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED, OBSOLETE, INTERRUPTED.

StudyBuildProblem

data class StudyBuildProblem(val code: String, val message: String, val suggestions: List<String> = emptyList(), val details: Map<String, Any?> = emptyMap())

An actionable build failure, optionally naming entities and measurements in details.

Declaration Summary
val suggestions: List<String>

StudyBuildFailure

class StudyBuildFailure(val problem: StudyBuildProblem) : IllegalArgumentException(problem.message)

Throw from a stage to keep its structured diagnostic in the attempt history.

StudyBuildArtifact

data class StudyBuildArtifact(val path: String, val sha256: String, val bytes: Long)

One immutable result file, relative to its attempt directory and checked by SHA-256.

StudyBuildDependency

data class StudyBuildDependency(val jobId: String, val fingerprint: String, val artifacts: List<StudyBuildArtifact>)

The successful build of another stage consumed by an attempt.

StudyBuildResume

data class StudyBuildResume(val jobId: String, val files: List<StudyBuildArtifact>)

Files an attempt copied from the earlier attempt jobId with StudyBuildContext.resumeFrom, as copied.

StudyBuildPruneReport

data class StudyBuildPruneReport(val directory: File, val kept: List<String>, val deleted: List<String>, val freedBytes: Long, val dryRun: Boolean)

What StudyBuilds.prune kept and deleted in the runner at directory, and the bytes the deleted attempts held.

StudyBuildRecord

data class StudyBuildRecord(val id: String, val stage: String, val recipeVersion: String, val fingerprint: String, val inputs: Map<String, String>, val dependencies: Map<String, StudyBuildDependency>, val status: StudyBuildStatus = StudyBuildStatus.QUEUED, val progress: Double = 0.0, val message: String = "Queued", val cancelRequested: Boolean = false, val created: String = Instant.now().toString(), val updated: String = created, val artifacts: List<StudyBuildArtifact> = emptyList(), val facts: Map<String, Any?> = emptyMap(), val problem: StudyBuildProblem? = null, val request: Map<String, Any?> = emptyMap(), val gates: Map<String, Map<String, StudyBuildDependency>> = emptyMap(), val resumedFrom: List<StudyBuildResume> = emptyList())

Durable record of one attempt, including the exact named input values and dependency results. request is what StudyBuilds.start was asked for beyond the design, and is part of fingerprint. gates holds, for each StudyBuildGate the stage consumes, the upstream results the gate judged. resumedFrom lists what the attempt copied from earlier attempts.

Declaration Summary
val id: String, val stage: String, val recipeVersion: String, val fingerprint: String
val inputs: Map<String, String>, val dependencies: Map<String, StudyBuildDependency>
val status: StudyBuildStatus
val progress: Double
val created: String
val artifacts: List<StudyBuildArtifact>
val problem: StudyBuildProblem?
val request: Map<String, Any?>
val gates: Map<String, Map<String, StudyBuildDependency>>
val resumedFrom: List<StudyBuildResume>

StudyBuildProduct

data class StudyBuildProduct(val files: List<String>, val facts: Map<String, Any?> = emptyMap())

Files produced inside the attempt directory, and verification facts supplied by the recipe.

StudyBuildInputHelp

data class StudyBuildInputHelp(val label: String, val message: String)

Explain who supplies a stage's missing inputs. An optional stage with nothing proposed to test says so here, so it reads as idle rather than blocked.

StudyBuildNoticeKind

enum class StudyBuildNoticeKind

Presentation of retained result evidence, separate from execution and dependency currency.

Constants: INFO, WARNING, ERROR.

StudyBuildNotice

data class StudyBuildNotice(val message: String, val kind: StudyBuildNoticeKind = StudyBuildNoticeKind.WARNING, val view: StudyBuildView? = null)

A recipe's explanation of a result and the next action; never an acceptance override. view names a picture of the result to look at alongside the words: an agent that waited for the build is shown it when its chat resumes.

Declaration Summary
val view: StudyBuildView?

StudyBuildView

class StudyBuildView(val box: String, val caption: String = "", @get:com.fasterxml.jackson.annotation.JsonIgnore val show: (() -> Unit)? = null)

A view of a result: box (a uid or unique name) shows it once show has drawn it, and caption says what to look for. When a chat that awaited the build resumes (afield.ai.agent.StudyBuildAwaits), Field runs show, captures the box's output and attaches the picture to the resumed message, so the agent sees the result as well as reading its facts and notes. show runs off the Runloop and only displays; it must not change the design or the record. Only box and caption are reported in tool results.

Declaration Summary
override fun toString()

StudyBuildStage

class StudyBuildStage(val id: String, val label: String, val recipeVersion: String, val inputKeys: Set<String> = emptySet(), val dependencies: List<String> = emptyList(), val inputHelp: StudyBuildInputHelp? = null, val resultNotes: (StudyBuildRecord) -> List<StudyBuildNotice> = { emptyList() }, val failureNotes: (StudyBuildRecord) -> List<StudyBuildNotice> = { emptyList() }, val build: (StudyBuildContext) -> StudyBuildProduct)

A reusable build stage. inputKeys selects only the authored inputs this stage consumes, and may name StudyBuildGates; dependencies names other stages whose current results must exist first, either in this runner or, written dependsOn(runner, stage), in another runner of the same document. recipeVersion must change when the adapter or its relevant runtime changes. The runner does not infer code identity from a closure. inputHelp explains missing inputs without making the stage runnable or changing its fingerprint; a closed gate supplies its own explanation. resultNotes is a fast, pure presentation of a completed record's retained facts; it must not inspect mutable design state or change acceptance. It is shared by UI and agent tools and does not change build identity. Qualification gates belong in the build and input contracts. failureNotes is the same presentation for a FAILED record, usually over its StudyBuildRecord.problem details, under the same rules; it is opt-in, and its notes, views included, appear wherever result notes would. A failed record is never current: its notes appear beside the failure, never as the current result's. build runs on a worker and must use its captured context, not mutable live input.

Declaration Summary
val inputKeys: Set<String>
val inputHelp: StudyBuildInputHelp?
val resultNotes: (StudyBuildRecord) -> List<StudyBuildNotice>
val failureNotes: (StudyBuildRecord) -> List<StudyBuildNotice>
val build: (StudyBuildContext) -> StudyBuildProduct)

StudyBuildGate

class StudyBuildGate(val name: String, val inputKeys: Set<String> = emptySet(), val dependencies: List<String> = emptyList(), val check: (StudyBuildGateEvidence) -> String?)

An input called name that exists only while check finds nothing wrong. A stage that lists name in its inputKeys waits until then, and its card and agent inspection say what it waits for.

Declaration Summary
val check: (StudyBuildGateEvidence) -> String?)

StudyBuildGateEvidence

class StudyBuildGateEvidence internal constructor(val inputs: Map<String, String>, private val results: Map<String, StudyBuildRecord>, private val read: (String, StudyBuildRecord, StudyBuildArtifact) -> File)

What a StudyBuildGate judges: its named inputs and the current results of its dependencies.

Declaration Summary
fun result(dependency: String): StudyBuildRecord The current result of dependency, which must be one of the gate's dependencies.
fun facts(dependency: String): Map<String, Any?> The retained facts of dependency's current result.
fun file(dependency: String, path: String): File A result file of dependency, checked against its recorded hash.

StudyStageState

data class StudyStageState(val id: String, val label: String, val fingerprint: String?, val current: StudyBuildRecord?, val latest: StudyBuildRecord?, val reason: String?, val blockedBy: List<String> = emptyList(), val inputHelp: StudyBuildInputHelp? = null, val resultNotes: List<StudyBuildNotice> = emptyList(), val failureNotes: List<StudyBuildNotice> = emptyList())

A stage's current availability, its latest attempt, and its last published result when still valid. resultNotes are the current result's notes; failureNotes are latest's StudyBuildStage.failureNotes when it failed against the current inputs, the failure the card shows.

Declaration Summary
val current: StudyBuildRecord?, val latest: StudyBuildRecord?, val reason: String?, val blockedBy: List<String>
val inputHelp: StudyBuildInputHelp?
val failureNotes: List<StudyBuildNotice>

StudyBuildContext

class StudyBuildContext internal constructor(private val store: StudyBuilds, val attempt: StudyBuildRecord)

The frozen inputs a worker can read and its own output directory. checkCancelled cooperatively stops work; cancellation never claims that an uncooperative native operation has already stopped.

Declaration Summary
val inputs: Map<String, String>
val request: Map<String, Any?> What this attempt was started with beyond its inputs; empty unless StudyBuilds.start was given one.
val directory: File
fun file(path: String): File
fun checkCancelled()
fun progress(fraction: Double, message: String)
fun dependencyFile(stage: String, path: String): File Read a frozen dependency file; a changed file refuses instead of becoming silently current. stage is the name in the stage's dependencies, including dependsOn(runner, stage).
fun resumeFrom(jobId: String, paths: List<String>, sameDesign: Boolean = true): List<File> Copy paths (files, or directories with everything under them) from the finished attempt jobId of this runner into this attempt, at the same paths, and return the copies. Each copy is checked against its source; a file that was a result of that attempt must also still match its recorded hash. What was copied, with hashes, is recorded in StudyBuildRecord.resumedFrom. With sameDesign, the default, the earlier attempt must be of this stage and recipe and have built the same inputs and dependency results, so a checkpoint of a replaced design is refused.
fun runProcess(command: List<String>, progressFile: String? = null, stall: Duration? = null, deadline: Duration? = null, log: String = "process.log", environment: Map<String, String> = emptyMap(), progressSpan: ClosedFloatingPointRange<Double> = 0.0..1.0, check: Boolean = true): StudyBuildProcessResult Run an external program in this attempt's directory and wait for it, keeping its combined output in log. progressFile, a file the program rewrites with {"fraction": 0.4, "message": "..."}, becomes this attempt's progress, scaled into progressSpan. The program is stopped, with every process it started, when the build is cancelled, when deadline passes, or when stall passes with no change to the progress file (or, without one, no new output). A stop or, with check, a non-zero exit fails the attempt with a StudyBuildFailure naming the log.

StudyBuilds

class StudyBuilds(val directory: File, definitions: List<StudyBuildStage>, private val readInputs: () -> Map<String, String>, workers: Int = 2, gates: List<StudyBuildGate> = emptyList(), val exclusive: Boolean = false, private val upstream: (runner: String) -> StudyBuilds? = { null }) : AutoCloseable

Durable, asynchronous derived stages over a study's authoritative input snapshot.

Declaration Summary
fun stages(): List<StudyStageState> All stages against one current input snapshot, including blocked dependencies and stale outputs.
fun job(id: String): StudyBuildRecord A retained attempt, including obsolete, failed and interrupted attempts.
fun resultNotes(id: String): List<StudyBuildNotice> Explain a retained result using its stage's presentation adapter, without changing its verdict: StudyBuildStage.resultNotes for a succeeded or obsolete attempt, StudyBuildStage.failureNotes for a failed one, nothing for any other.
fun currentFile(stage: String, path: String): File A verified file from the current result, refusing stale results before a viewer consumes them.
fun history(offset: Int = 0, limit: Int = 20): List<StudyBuildRecord> Paged attempt history, newest first.
fun start(stage: String, expectedFingerprint: String, request: Map<String, Any?> = emptyMap()): StudyBuildRecord Queue a stage against the fingerprint from stages; a stale request changes nothing.
fun await(id: String, timeoutSeconds: Double = 600.0): StudyBuildRecord Block the calling thread until attempt id is no longer queued or running, or until timeoutSeconds pass, and return its record; after a timeout the build keeps running. For scripts and tests that run stages in sequence. A box that starts a long build should let its card or an agent's wait follow it instead of blocking the thread it runs on.
fun prune(pins: Set<String> = emptySet(), dryRun: Boolean = false): StudyBuildPruneReport Delete the attempts nothing needs any more, with their files. Kept are each stage's current result, its latest attempt and its latest success, every attempt still queued or running, every attempt in pins, and every attempt those name: their dependencies, the results their gates judged, the attempts they resumed from, and job IDs among their inputs and requests. Attempt directories with no record are deleted too. With dryRun nothing is deleted, and the report says what would be. Runners that name each other's attempts are pruned together with the companion prune.
fun cancel(id: String): StudyBuildRecord Signal cancellation. A running attempt remains running until its worker actually returns.
override fun close()

StudyEvidence.kt

Measures and lays out the frames a study renders: brightness, clipping, non-finite pixels, detail, palette, frame-to-frame motion and region-restricted differences, plus labelled contact sheets of tiles for a model to look at.

StudyEvidence holds the functions; StudyFrameStats, StudyMotion and StudyDifference are the numbers it returns. Measurements are taken from the program's raw output before the display clamp, so clipping is counted rather than hidden.

src/jvmMain/kotlin/afield/study/evidence/StudyEvidence.kt — read the source

StudyFrameStats

data class StudyFrameStats(val seconds: Double, val meanRgb: List<Double>, val luma: Double, val lumaSpread: Double, val clippedBelow: Double, val clippedAbove: Double, val nonFinite: Double, val detail: Double, val featureSize: Double?, val colourfulness: Double, val palette: List<StudySwatch>)

What a study can say about a picture in numbers, and how it lays pictures out for a model to look at. Every study that draws — StackSL, RectSL, a stack.auto slot — measures and lays out its frames here, so a measurement means the same thing in all of them.

Declaration Summary
val seconds: Double
val meanRgb: List<Double> Mean R, G, B in 0..1 as the window shows them — sRGB-encoded, see StudyEvidence.presented.
val luma: Double Mean luma (Rec. 709) of the presented image, and its standard deviation.
val lumaSpread: Double
val clippedBelow: Double Fraction of R/G/B values the program left below 0 or above 1 — what saturate or the frame's clamp throws away.
val clippedAbove: Double
val nonFinite: Double Fraction of pixels whose colour was NaN or infinite; the frame draws them black.
val detail: Double Mean luma gradient, in luma per frame width: near 0 is flat, tens is busy.
val featureSize: Double? Typical feature width as a fraction of the frame, from how often luma crosses its mean along rows and columns; null when flat.
val colourfulness: Double Hasler–Süsstrunk colourfulness on the 0..255 scale: under ~15 reads as grey, over ~80 as vivid.
val palette: List<StudySwatch> The frame's main colours by k-means, most common first, with the fraction of pixels each covers.

StudySwatch

data class StudySwatch(val hex: String, val fraction: Double)

StudyMotion

data class StudyMotion(val fromSeconds: Double, val toSeconds: Double, val meanLevels: Double, val changedFraction: Double)

How much consecutive frames differ: mean absolute change in 8-bit levels, and the fraction of pixels that moved by more than two levels.

StudyDifference

data class StudyDifference(val scope: String, val pixels: Int, val max: Double, val mean: Double, val changedFraction: Double, val atU: Double?, val atV: Double?)

A difference between two frames, restricted to a region or its complement.

Declaration Summary
val scope: String
val pixels: Int
val max: Double Largest per-channel difference, in 8-bit levels (or raw units when comparing stages).
val mean: Double
val changedFraction: Double Fraction of pixels differing by more than one level (or 1e-6 raw).
val atU: Double? Where the largest difference is, in uv.
val atV: Double?

StudyEvidence

object StudyEvidence

Declaration Summary
fun displayed(raw: CpuRaster): CpuRaster What a study frame draws for a raw raster: non-finite colour becomes black, the rest is clamped, alpha is one.
fun presented(raw: CpuRaster): CpuRaster What a person sees of a raw raster in the window: displayed, then the sRGB transfer function.
fun linearToSrgb(v: Float): Float The sRGB transfer function (IEC 61966-2-1), for a linear value in 0..1.
fun stats(raw: CpuRaster, seconds: Double): StudyFrameStats
fun motion(a: CpuRaster, b: CpuRaster, from: Double, to: Double): StudyMotion
fun difference(a: CpuRaster, b: CpuRaster, region: List<Double>?, lumaOnly: Boolean, shown: Boolean = false): Pair<StudyDifference, StudyDifference?> Displayed-colour difference between two same-size rasters, split into outside and inside an optional uv region (u0, v0, u1, v1). With lumaOnly only brightness is compared. A pixel counts as changed when it moved by more than one 8-bit level. With shown, a and b are already what the window shows (a capture, see fromPng) and are compared as they are.
fun rawDifference(a: CpuRaster, b: CpuRaster, region: List<Double>?): Pair<StudyDifference, StudyDifference?> Raw-value difference (largest component) — for comparing what two programs leave on the stack at a stage.
class Tile(val argb: IntArray?, val width: Int, val height: Int, val label: String, val caption: String = "", val error: String? = null) One picture on a sheet: pixels (or an error in their place), and up to two lines of caption.
fun tile(raster: CpuRaster?, label: String, caption: String = "", error: String? = null): Tile
fun normalizedTile(raw: CpuRaster, label: String, caption: String): Pair<Tile, Boolean> Show a raw raster as colour when it already is one, else stretched per channel into view.
fun sheet(title: String, subtitle: String, rows: List<List<Tile>>, tileSize: Int, rowLabels: List<String> = emptyList(), footer: String = "", tileHeight: Int = tileSize): ByteArray Lay rows of tiles out as one PNG: a title, a line of context, then each row of tiles tileSize wide and tileHeight tall (square unless a wide scene asks otherwise), with captions under each tile. Small renders are scaled up with nearest-neighbour so a model sees pixels rather than a blur.
fun differenceTile(a: CpuRaster, b: CpuRaster, label: String, caption: String, full: Double = 32.0, shown: Boolean = false): Tile A difference heat map: black where equal, through red to yellow-white at full levels.
fun rawDifferenceTile(a: CpuRaster, b: CpuRaster, label: String, caption: String, full: Double = 0.1): Tile differenceTile for raw stack values: white where the largest component differs by full or more.
fun format(v: Double): String
fun fromPng(png: ByteArray): CpuRaster A PNG — a capture of what a window or target shows — as a raster of the values it shows, 0..1 per channel, alpha one. Already presented: compare such rasters with shown = true.

StudyReport.kt

The common result type of Field's study tools: a revision, a map of measured facts, and the images that accompany them.

StudyReport is what the function, StackSL, RectSL, stack.auto and source-frames studies return to a model.

src/jvmMain/kotlin/afield/study/evidence/StudyReport.kt — read the source

StudyReport

data class StudyReport(val revision: String, val facts: Map<String, Any?>, override val toolImages: List<ToolImage> = emptyList()) : CarriesToolImages, SummarisesForToolLog

What every study tool answers with: the revision it is about, the facts it measured, and the pictures that go with them.

Declaration Summary
override val toolImages: List<ToolImage>
override fun toolLogSummary()

StudyVariants.kt

The parts of a live study's variant search that do not depend on what the study draws.

StudyVariants holds the candidates a study has tried, on disk under speculations/, until one is adopted, and picks the newest unadopted batch back up after a restart. StudyCompare does the picture side of Compare and Sweep: before | after | difference tiles with a check that nothing changed outside the allowed region, and a sweep that draws 2 to 9 values of one parameter.

src/jvmMain/kotlin/afield/study/search/StudyVariants.kt — read the source

Used in: Studies

StudyVariants

class StudyVariants(val directory: File, val extension: String)

The candidates a study has tried and is holding — checked, drawn, unpublished — until a person (in the study's card) or a model adopts one: the half of TryVariants / AdoptVariant that does not depend on what the study draws.

Declaration Summary
data class Held(val label: String, val note: String, val source: String) A held candidate: its label, the note it came with, and its complete source.
data class Candidate(val label: String, val note: String, val source: String, val edits: List<StudySourceEdit>) A candidate to try: a complete source, or exact edits to the live source; label and note as the model gave them.
class Trial(val rows: List<List<StudyEvidence.Tile>>, val labels: List<String>, val held: List<Held>, val facts: List<Map<String, Any?>>) What tryAll made of a list of candidates: one row of tiles and one label per candidate, what it holds, and the facts for the report.
fun resume(currentId: String) Pick up the newest batch still unadopted, if it was tried against currentId.
fun <T> tryAll(candidates: List<Candidate>, live: String, check: (String) -> StudyAdmission<T>, cancelled: () -> Boolean, frames: (String) -> List<CpuRaster>, timeLabel: (Int) -> String, present: (CpuRaster) -> CpuRaster = StudyEvidence::presented, facts: (value: T, frames: List<CpuRaster>) -> Map<String, Any?>): Trial Check and draw each of candidates — resolving edits against live — with check (the study's admission, publishing nothing) and frames; a refused candidate gets an error tile with the reason instead. facts says what the report records of an admitted one beside its label, note and warnings; timeLabel captions each frame's tile. present is what a frame looks like on a tile: StudyEvidence.presented for raw output, identity for a capture that already is what is shown.
fun hold(against: String, liveSourceHash: String, candidates: Collection<Held>, sheet: ByteArray): File Hold candidates in place of whatever was held, and write the batch to disk; returns its folder.
fun held(): List<Triple<String, String, String>> The held candidates as label, note, source — what a card offers to adopt.
fun pending(): List<Map<String, String>>? The held candidates' labels and notes, for an Inspect; null when there are none.
fun <T> adopt(revisions: SourceStudy<T>, expectedRevision: String, expectedSourceHash: String, label: String, description: String, publish: (SourceRevision<T>) -> StudyReport): StudyReport Publish the held candidate label through revisions (guards as for any edit), with publish turning the new revision into the study's report, then forget the batch and record the adoption.

StudyCompare

object StudyCompare

The picture-level halves of a study's Compare and Sweep, which do not depend on what it draws.

Declaration Summary
class Pixels(val rows: List<List<StudyEvidence.Tile>>, val results: List<Map<String, Any?>>, val preserved: Boolean, val tolerance: Double) Per-time before | after | difference tiles, the measurements, and whether everything outside the region held.
fun pixels(before: List<CpuRaster>, after: List<CpuRaster>, times: List<Double>, beforeLabel: String, afterLabel: String, region: List<Double>?, mode: String, timeLabel: (Double) -> String, captions: ((Double) -> Pair<String, String>)? = null, shown: Boolean = false): Pixels Compare before and after, frame by frame at times: displayed colour (rgb), brightness (luma), or raw values (stage, for what a program leaves on the stack). region (u0, v0, u1, v1) is where change is allowed; everything else is held to one 8-bit level, or 1e-6 raw. captions replaces each tile's time caption, for a stage comparison that names its stages. With shown, the frames are captures of what is shown (see StudyEvidence.fromPng) rather than raw output.
data class Swept(val value: Double, val raster: CpuRaster?, val error: String?) One value of a sweep: the frame drawn with it, or why it could not be.
fun sweep(values: List<Double>, draw: (Double) -> CpuRaster): List<Swept> Draw values (2..9 of them) with draw; a value that fails records its error instead. Cancellation propagates.

LiveStudy.kt

The standard study verbs, and the tools that offer them to an AI4 agent: inspect, edit, patch, set numbers, observe, compare, sweep, try variants, adopt a variant and restore a revision.

LiveStudy is the interface a study implements. StudyRequest holds the request shapes the verbs take, which a library study's own request classes implement; registerStudyTools wires those classes to a study once. From box code, Box.registerStudyTools makes the same verbs as box-authored tools named after a prefix, using the StudyCall request classes.

src/jvmMain/kotlin/afield/study/source/LiveStudy.kt — read the source

Used in: Studies

Top-level functions

Declaration Summary
fun registerStudyTools(scope: Any, gate: (ToolExecutionContext) -> Boolean, studyOf: (ToolExecutionContext) -> LiveStudy, classes: StudyToolClasses) Register a study's standard tools with AgentToolExtensions: one per class in classes, each offered when gate passes and run on the study studyOf finds for the call — usually the focused box's. scope is the registration's scope (see AgentToolExtensions, "Scopes"): a library's own id, so a document's same-named tool adds to it. A study registers its extras (a guide, a trace, an export) itself.
fun Box.registerStudyTools(prefix: String, study: LiveStudy, about: String = "", verbs: Set<StudyVerb> = StudyVerb.entries.toSet()): Map<StudyVerb, BoxTool> Give this document's AI4 agents the standard study verbs on study, as tools named after prefix the way the library's are: Inspect<prefix>Study, Edit<prefix>Study, Patch<prefix>Study, Set<prefix>Numbers, Observe<prefix>Study, Compare<prefix>Study, Sweep<prefix>Number, Try<prefix>Variants, Adopt<prefix>Variant and Restore<prefix>Revision — verbs of them.

StudyRequest

object StudyRequest

What a study's standard tool requests have in common, whatever the language.

Declaration Summary
interface Revisioned A request naming a revision (current, or blank, for the checked one).
interface Inspect : Revisioned Read the study: source, live-edit state, history, knobs.
interface Edit Replace the whole source, guarded by the revision and live-source hash the caller read.
interface Patch Exact, unique, non-overlapping replacements in the live source, guarded like Edit.
interface SetNumbers New values for knobs of the live source, by index, guarded like Edit.
interface Observe : Revisioned Frames of a revision at seconds, optionally against compareTo.
interface Sweep One knob at several values, nothing published.
interface Variant A candidate: a complete source, or edits to the live source.
interface TryVariants Check and draw candidates beside the current revision and hold them, unpublished.
interface Adopt Publish a held candidate by label, guarded like Edit.
interface Restore Make an earlier revision current again, as a new revision.
interface Compare What an edit preserved, outside allowedRegion, in rgb or luma (a study may offer more modes).
interface Preview Optional: the times an accepted edit's preview frames are drawn at.
interface Sized Optional: the frame size asked for; 0 chooses.
interface At Optional: the one time a sweep is drawn at.

StudyVerb

enum class StudyVerb

The standard study verbs, for choosing which a document's registerStudyTools offers.

Constants: INSPECT, EDIT, PATCH, SET_NUMBERS, OBSERVE, COMPARE, SWEEP, TRY_VARIANTS, ADOPT_VARIANT, RESTORE.

LiveStudy

interface LiveStudy

A study a conversation can work on through the standard verbs: the face registerStudyTools wires tools to. A library study adapts its own class to it; a document can implement it directly, or use SourceFramesStudy, which implements all of it from the shared pieces. A verb a study does not offer refuses with a message saying so.

Declaration Summary
fun inspect(q: StudyRequest.Inspect): StudyReport
fun edit(q: StudyRequest.Edit): StudyReport
fun patch(q: StudyRequest.Patch): StudyReport
fun setNumbers(q: StudyRequest.SetNumbers): StudyReport
fun observe(q: StudyRequest.Observe): StudyReport
fun compare(q: StudyRequest.Compare): StudyReport
fun sweep(q: StudyRequest.Sweep): StudyReport
fun tryVariants(q: StudyRequest.TryVariants): StudyReport
fun adoptVariant(q: StudyRequest.Adopt): StudyReport
fun restore(q: StudyRequest.Restore): StudyReport

StudyToolClasses

class StudyToolClasses(val inspect: Class<out StudyRequest.Inspect>? = null, val edit: Class<out StudyRequest.Edit>? = null, val patch: Class<out StudyRequest.Patch>? = null, val setNumbers: Class<out StudyRequest.SetNumbers>? = null, val observe: Class<out StudyRequest.Observe>? = null, val compare: Class<out StudyRequest.Compare>? = null, val sweep: Class<out StudyRequest.Sweep>? = null, val tryVariants: Class<out StudyRequest.TryVariants>? = null, val adoptVariant: Class<out StudyRequest.Adopt>? = null, val restore: Class<out StudyRequest.Restore>? = null)

The request classes a library study's standard tools are declared with; null for a verb it does not offer. Each class's simple name is the tool's name.

Declaration Summary
val inspect: Class<out StudyRequest.Inspect>?
val edit: Class<out StudyRequest.Edit>?
val patch: Class<out StudyRequest.Patch>?
val setNumbers: Class<out StudyRequest.SetNumbers>?
val observe: Class<out StudyRequest.Observe>?
val compare: Class<out StudyRequest.Compare>?
val sweep: Class<out StudyRequest.Sweep>?
val tryVariants: Class<out StudyRequest.TryVariants>?
val adoptVariant: Class<out StudyRequest.Adopt>?
val restore: Class<out StudyRequest.Restore>?

StudyCall

object StudyCall

The requests a document's study tools (Box.registerStudyTools) hand their LiveStudy: the same interfaces a library study's request classes implement, with every optional extra.

Declaration Summary
data class Inspect(override val revision: String = "current") : StudyRequest.Inspect
data class Edit(override val expectedRevision: String, override val expectedSourceHash: String, override val source: String, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Edit, StudyRequest.Preview
data class Patch(override val expectedRevision: String, override val expectedSourceHash: String, override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Patch, StudyRequest.Preview
data class Number(override val knob: Int, override val value: Double) : NumberEdit One knob to set, by index, and its value.
data class SetNumbers(override val expectedRevision: String, override val expectedSourceHash: String, override val numbers: List<Number>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.SetNumbers, StudyRequest.Preview
data class Observe(override val revision: String = "current", override val seconds: List<Double> = emptyList(), override val compareTo: String = "", override val size: Int = 0) : StudyRequest.Observe, StudyRequest.Sized
data class Compare(override val beforeRevision: String, override val afterRevision: String = "current", override val seconds: List<Double> = emptyList(), override val mode: String = "rgb", override val allowedRegion: List<Double> = emptyList(), override val size: Int = 0) : StudyRequest.Compare, StudyRequest.Sized
data class Sweep(override val revision: String = "current", override val knob: Int, override val values: List<Double>, override val seconds: Double = 0.0, override val size: Int = 0) : StudyRequest.Sweep, StudyRequest.Revisioned, StudyRequest.At, StudyRequest.Sized
data class Variant(override val label: String, override val source: String = "", override val edits: List<StudySourceEdit> = emptyList(), override val note: String = "") : StudyRequest.Variant
data class TryVariants(override val variants: List<Variant>, override val seconds: List<Double> = emptyList(), override val size: Int = 0) : StudyRequest.TryVariants, StudyRequest.Sized
data class Adopt(override val expectedRevision: String, override val expectedSourceHash: String, override val label: String, override val description: String = "") : StudyRequest.Adopt
data class Restore(override val expectedRevision: String, override val expectedSourceHash: String, override val revision: String, override val description: String = "") : StudyRequest.Restore

QualifiedSource.kt

Qualified sources: changing a rule without breaking what already works

A rule (a placement recipe, a fallback, a solver strategy) is a source that other builds consume. A QualifiedSource keeps three things beside it and lets the rule change only when all three agree:

  • the rule, a SourceStudy over a box property;
  • the fixtures, a SourceStudy of retained real cases, each an exact input and the outcome the rule gave it (QualifiedFixture);
  • a replay stage, a StudyBuilds runner that runs a candidate rule on every fixture and on a QualifiedControl, an input no correct rule can pass, which must fail.

A change starts as a candidate: a frozen record of the base it was made against, its source, the fixtures, the dependencies and the checks (QualifiedCandidate), named by the hash of all of them. The replay compares each fixture's new outcome with the recorded one: the same, changed, or held (the rule or its checks refused it). A candidate is admitted, and apply publishes it through SourceStudy.edit, only when its replay is current, covers every fixture with the same outcome, the control failed as it must, and nothing it depends on has changed.

The checks that judge a candidate are out of its reach (FrozenRegion): files it may not edit, text after a marker that must stay byte for byte, or a separate frozen copy. Only a person changes them, and only a person changes a fixture's recorded outcome: proposeRebaseline draws each changed outcome with an Approve button, and the press records who approved it and when. No tool approves.

Box.qualifiedSource makes one in a box and registers the agent tools <name>.inspect, .candidate, .test, .apply, .retainFixture and .proposeRebaseline.

src/jvmMain/kotlin/afield/study/source/QualifiedSource.kt — read the source

Used in: Change a rule without breaking what already works

Top-level functions

Declaration Summary
fun Box.qualifiedSource(name: String, seed: String, control: QualifiedControl, frozen: List<FrozenRegion>, replayVersion: String, fixtures: Map<String, QualifiedFixture> = emptyMap(), label: String = name, extension: String = "kt", dependencies: () -> Map<String, String> = { emptyMap() }, retain: ((QualifiedRetainSource) -> QualifiedFixture)? = null, replay: (QualifiedReplay) -> (String) -> Any?): QualifiedSource Keep the rule in this box's property <name>Rule (seeded from seed the first time) under a QualifiedSource, with its fixtures in <name>Fixtures, and register the agent tools <name>.inspect, .candidate, .test, .apply, .retainFixture and .proposeRebaseline. The replay card shows in the output channel name and an approval view, while one waits, in <name>-approval.

QualifiedFixture

data class QualifiedFixture(val input: String, val outcome: Any?)

A retained case: the exact input text and the outcome the rule gave it, which must survive a JSON round trip.

QualifiedControl

data class QualifiedControl(val name: String, val input: String, val messageContains: String? = null)

An input no correct rule can pass, replayed with every candidate. Its replay must fail (the rule or its checks throw), with messageContains in the message when given. A control that passes means the checks are not running, so no candidate is admitted.

FrozenRegion

sealed class FrozenRegion

Where a qualified source keeps its checks out of a candidate's reach. A candidate breaking any of its regions is refused before it is saved. What the regions freeze is recorded in the candidate as QualifiedCandidate.checks and handed to the replay as QualifiedReplay.checks.

Declaration Summary
class Files(val editable: Set<String>) : FrozenRegion() The rule is a JSON object of file name to text, and a candidate may change or add only the editable files.
class After(val marker: String) : FrozenRegion() Everything after marker, which must occur exactly once in the rule, stays byte for byte as it is.
class Baseline(val current: () -> Map<String, String>) : FrozenRegion() A separate copy of the checks, named file to text, frozen when the source is first made. current says what the checks should be now (a kit's files, say); when it differs from the copy, the card and inspect say so, and the copy changes only by QualifiedSource.upgradeChecks.

QualifiedReplay

class QualifiedReplay internal constructor(val source: String, val checks: Map<String, String>, val context: StudyBuildContext)

What a replay works with: the candidate's source, the frozen checks it is judged by, and the build context (files, progress, runProcess). The replay function returns a runner for one input.

QualifiedRetainSource

class QualifiedRetainSource internal constructor(val runner: String, val record: StudyBuildRecord, private val read: (String) -> File)

A finished build attempt a fixture is retained from: its record, and its files by path through file.

Declaration Summary
fun file(path: String): File A file of the attempt; a recorded result file is checked against its hash.

QualifiedVerdict

enum class QualifiedVerdict

How a fixture came out in a replay.

Constants: SAME, CHANGED, HELD.

QualifiedCaseResult

data class QualifiedCaseResult(val fixture: String, val verdict: QualifiedVerdict, val outcome: Any?, val recorded: Any?, val problem: String?)

One fixture in a replay: its verdict, the new outcome, the recorded one, and the refusal when verdict is HELD.

QualifiedControlResult

data class QualifiedControlResult(val name: String, val failed: Boolean, val problem: String?, val passed: Boolean)

The control in a replay: whether it failed, the message it failed with, and whether that counts as passed.

QualifiedReplayReport

data class QualifiedReplayReport(val candidate: String, val sourceHash: String, val fixturesRevision: String, val cases: List<QualifiedCaseResult>, val control: QualifiedControlResult, val passed: Boolean)

A replay's verdict, kept as replay.json in its attempt.

Declaration Summary
val cases: List<QualifiedCaseResult>, val control: QualifiedControlResult, val passed: Boolean)

QualifiedCase

data class QualifiedCase(val input: String, val outcome: Any?, val retained: Map<String, Any?> = emptyMap(), val approvals: List<QualifiedApproval> = emptyList())

A fixture as the corpus holds it, with where it was retained from and every approved change to its outcome.

Declaration Summary
val approvals: List<QualifiedApproval>

QualifiedApproval

data class QualifiedApproval(val proposal: String, val candidate: String, val replay: String, val beforeHash: String, val afterHash: String, val approvedBy: String, val approvedVia: String, val approvedAt: String)

A person's approval of one fixture's new outcome.

Declaration Summary
val afterHash: String, val approvedBy: String, val approvedVia: String, val approvedAt: String)

QualifiedCandidate

data class QualifiedCandidate(val namespace: String, val baseRevision: String, val baseSourceHash: String, val source: String, val sourceHash: String, val description: String, val parent: String?, val fixturesRevision: String, val fixtures: Map<String, QualifiedCase>, val dependencies: Map<String, String>, val checks: Map<String, String>, val id: String = "")

A frozen candidate. id is the hash of every other field, so a candidate also verifies itself: the base it was made against, its source, the fixture corpus, the dependencies and the checks it is judged by.

Declaration Summary
val source: String, val sourceHash: String, val description: String, val parent: String?
val fixturesRevision: String, val fixtures: Map<String, QualifiedCase>
val dependencies: Map<String, String>, val checks: Map<String, String>, val id: String

QualifiedChange

data class QualifiedChange(val path: String, val before: Any?, val after: Any?)

One changed value in a fixture's outcome, at a JSON path.

QualifiedProposalEntry

data class QualifiedProposalEntry(val fixture: String, val beforeHash: String, val afterHash: String, val before: Any?, val after: Any?, val changes: List<QualifiedChange>, val truncated: Boolean)

One fixture in a rebaseline proposal: the recorded and proposed outcomes, their hashes and the changes between them.

Declaration Summary
val before: Any?, val after: Any?, val changes: List<QualifiedChange>, val truncated: Boolean)

QualifiedProposal

data class QualifiedProposal(val namespace: String, val candidate: String, val replay: String, val fixturesRevision: String, val entries: List<QualifiedProposalEntry>, val id: String = "")

Proposed new outcomes from a failed replay, waiting for a person. id is the hash of the rest.

Declaration Summary
val entries: List<QualifiedProposalEntry>, val id: String

QualifiedSource

class QualifiedSource(val name: String, val label: String, val directory: File, readRule: () -> String, replaceRule: (String, String) -> Boolean, readFixtures: () -> String, replaceFixtures: (String, String) -> Boolean, val control: QualifiedControl, val frozen: List<FrozenRegion>, val replayVersion: String, private val seedFixtures: Map<String, QualifiedFixture> = emptyMap(), private val dependencies: () -> Map<String, String> = { emptyMap() }, private val retain: ((QualifiedRetainSource) -> QualifiedFixture)? = null, private val runners: (String) -> StudyBuilds? = { null }, ruleExtension: String = "kt", maxSourceLength: Int = 200000, builds: (List<StudyBuildStage>, () -> Map<String, String>) -> StudyBuilds, private val show: ((channel: String, html: String) -> Unit)? = null, private val onMain: (() -> Unit) -> Unit = { it() }, private val replay: (QualifiedReplay) -> (String) -> Any?) : AutoCloseable

A rule with its fixtures, checks and replay. Made by Box.qualifiedSource; construct directly only to supply the storage yourself (tests do). All changes go through the methods here; rule and fixtures refuse an edit made any other way.

Declaration Summary
val name: String
val label: String
val directory: File
val control: QualifiedControl
val frozen: List<FrozenRegion>
val replayVersion: String
val rule: SourceStudy<String> The rule's checked history. Edits not made by apply or upgradeChecks are refused.
val fixtures: SourceStudy<String> The fixture corpus's checked history. Edits not made by retainFixture, an approval or the seed are refused.
val checksCopy: SourceStudy<String>? The frozen copy of the checks, when frozen has a FrozenRegion.Baseline.
val replays: StudyBuilds The replay runner, with its one stage replay.
fun source(): String The rule as last applied: what builds that use the rule should read.
fun corpus(): Map<String, QualifiedCase> The fixture corpus as last checked.
fun currentDependencies(): Map<String, String> Everything the replay depends on beyond the candidate: dependencies, the replay version and the control.
fun currentChecks(): Map<String, String> The checks a candidate is judged by now: the frozen parts of the applied rule, and the frozen copy.
fun checksNotice(): String? Why the checks need a person, or null: the live rule's frozen parts differ from the applied ones (an edit outside apply), or a FrozenRegion.Baseline's copy differs from what its current says.
fun inspect(includeSource: Boolean = false): Map<String, Any?> What an agent reads first: the rule, the fixtures, the checks, the latest replay and anything waiting for a person.
fun candidate(expectedRevision: String, expectedSourceHash: String, description: String, edits: List<QualifiedEdit> = emptyList(), replacement: String? = null, addFiles: Map<String, String> = emptyMap(), fromCandidate: String? = null): QualifiedCandidate Make a candidate against the live rule, read at expectedRevision with expectedSourceHash. Give either edits (each before must occur exactly once; with FrozenRegion.Files, file names the file), a whole replacement source, or neither with fromCandidate to copy that candidate's source. addFiles adds new editable files. Nothing runs and nothing is published; the candidate is saved and its id returned.
fun loadCandidate(id: String): QualifiedCandidate A saved candidate, checked against its id.
fun test(candidateId: String): StudyBuildRecord Queue the replay of a candidate on every fixture and the control, and return the attempt at once.
fun reportOf(job: StudyBuildRecord): QualifiedReplayReport? The report of a replay attempt, checked against its recorded hash; null when it has none.
fun replayOf(candidateId: String): StudyBuildRecord? The latest replay attempt of candidateId, or null.
fun admissionProblem(candidateId: String): String? Why candidateId cannot be applied now, or null when it can.
fun apply(candidateId: String, expectedRevision: String, expectedSourceHash: String): SourceRevision<String> Publish an admitted candidate as the next revision of the rule, through SourceStudy.edit with the revision and live-source hash the caller read. Refused, with nothing changed, unless admissionProblem is null.
fun retainFixture(runner: String, jobId: String, fixture: String, expectedRevision: String, expectedSourceHash: String): Map<String, Any?> Retain a finished attempt jobId of the build runner runner in this document as fixture fixture, with the input and outcome the retain adapter reads from it. Guarded by the corpus revision and hash the caller read. Retaining the same case again changes nothing; a different case under an existing name is refused.
fun proposeRebaseline(jobId: String): QualifiedProposal Propose the new outcomes of a failed replay jobId whose only failures are changed outcomes. Shows each fixture's recorded and proposed outcome with an Approve button. Nothing changes until a person presses one; this never approves.
fun proposal(id: String): QualifiedProposal A saved proposal, checked against its id.
fun pendingProposals(): List<QualifiedProposal> Proposals with a fixture still waiting for a person.
fun upgradeChecks(by: String, reason: String): Map<String, Any?> Make the checks current: accept the live rule's edited frozen parts as a new revision, and upgrade a FrozenRegion.Baseline's copy to what its current says. For a person (the card's button) or a migration in box code; refused inside an agent tool call. Every candidate made before is then stale.
fun refresh() Draw the replay card in channel name and, while a proposal waits, the approval view in <name>-approval.
override fun close()

QualifiedEdit

data class QualifiedEdit(val before: String, val after: String, val file: String? = null)

One exact replacement in a candidate; file names the file when the rule is made of files.

SourceFramesStudy.kt

A complete study for any text that draws, so a document can offer an agent the standard study verbs without writing a study class.

SourceFramesStudy keeps revisions of the source, draws frames through a StudyFrames, and answers every LiveStudy verb. Box.sourceFramesStudy builds one for a box property, studyFrames turns a single-frame drawing function into a StudyFrames, and StudyKnobs finds and rewrites the plainly written numbers in a source.

src/jvmMain/kotlin/afield/study/source/SourceFramesStudy.kt — read the source

Used in: Studies

Top-level functions

Declaration Summary
fun Box.sourceFramesStudy(property: String, frames: StudyFrames<String>, extension: String = "txt", directory: File = File(BoxStudies.directory(this), property), knobs: (String) -> List<StudyKnob> = StudyKnobs::plainNumbers, withNumbers: (String, Map<Int, Double>) -> String = StudyKnobs::withPlainNumbers, check: (String) -> Unit = {}, aspect: Double = 1.0, defaultSeconds: List<Double> = listOf(0.0)): SourceFramesStudy A SourceFramesStudy of this box's property, its revisions in directory (studies/<box id>/<property> in the box's document by default). Publishing writes the property back, compare-and-swap, the way a library study does.
fun <P> studyFrames(description: String, fast: Boolean = true, draw: (input: P, width: Int, height: Int, seconds: Double) -> CpuRaster): StudyFrames<P> A StudyFrames from one function that draws a frame: draw gets the input, width, height and time.

SourceFramesStudy

class SourceFramesStudy(val directory: File, extension: String, readSource: () -> String, replaceSource: (expected: String, replacement: String) -> Boolean, val frames: StudyFrames<String>, private val knobs: (String) -> List<StudyKnob> = StudyKnobs::plainNumbers, private val withNumbers: (String, Map<Int, Double>) -> String = StudyKnobs::withPlainNumbers, private val check: (String) -> Unit = {}, private val title: String = "Study", private val aspect: Double = 1.0, private val defaultSeconds: List<Double> = listOf(0.0)) : LiveStudy

A study of any text that draws, built entirely from the shared pieces — SourceStudy for revisions, StudyFrames for pictures, StudyEvidence for measurements and sheets, StudyCompare and StudyVariants for comparing and speculating — so a document can have a study as capable as the library's without writing one. Hand it to Box.registerStudyTools.

Declaration Summary
val directory: File
val frames: StudyFrames<String>
val revisions: SourceStudy<Unit> The revision history this study publishes through.
override fun inspect(q: StudyRequest.Inspect): StudyReport
override fun edit(q: StudyRequest.Edit): StudyReport
override fun patch(q: StudyRequest.Patch): StudyReport
override fun setNumbers(q: StudyRequest.SetNumbers): StudyReport
override fun restore(q: StudyRequest.Restore): StudyReport
override fun observe(q: StudyRequest.Observe): StudyReport
override fun compare(q: StudyRequest.Compare): StudyReport
override fun sweep(q: StudyRequest.Sweep): StudyReport
override fun tryVariants(q: StudyRequest.TryVariants): StudyReport
override fun adoptVariant(q: StudyRequest.Adopt): StudyReport
fun heldVariants(): List<Triple<String, String, String>> The held candidates: label, note, source.

StudyKnobs

object StudyKnobs

Knobs for text whose numbers are written plainly — the default for a document's SourceFramesStudy.

Declaration Summary
fun plainNumbers(source: String): List<StudyKnob> Every plain number in source, in order, as a knob whose operand is the word after it (or number).
fun withPlainNumbers(source: String, numbers: Map<Int, Double>): String source with the numbers at numbers' indices replaced, keeping an integer an integer when the value is whole.

SourceStudy.kt

The language-neutral half of a live-code study: the part of "talking a function into existence" that does not care what the language is.

A study holds one editable source — the text in a box that a person and a model are both working on — and a durable, append-only history of the versions of it that passed a check. Every change arrives as a transaction: the caller names the revision it read and a hash of the live source it read, the candidate is checked by the language adapter, and only then is it written into the box, with a compare-and-swap against the text that is there now. So a model preparing an edit while a person types cannot overwrite the person, a candidate that fails its check changes nothing, and every accepted version can be read back, compared and restored.

What the language adds is one function, SourceStudy's admit: compile and check a candidate, returning a value (whatever the language's tools need — a composition, a compiled shader report) or throwing StudyRejection with a message a person or a model can act on. The StackSL, RectSL, stack.auto and function studies are all adapters over it. FunctionStudy keeps the file layout it used while it carried its own private copy of the protocol, so a revision directory written by that older code still reads.

On disk

directory/<id>.<extension> is a revision's source and directory/<id>.json its metadata (parent, description, time, and whatever facts the adapter chose to keep). directory/HEAD names the checked revision the box last held. An id is r<n>-<first 12 hex of SHA-256(source)>, so an id also verifies its own file: a revision whose source was edited on disk refuses to load.

This is trusted local code, not a sandbox: admit runs whatever the adapter runs. Cancellation (cancel) stops a pending publication, not a computation that never checks for it.

src/jvmMain/kotlin/afield/study/source/SourceStudy.kt — read the source

Used in: Studies

StudyRejection

class StudyRejection(message: String, val facts: Map<String, Any?> = emptyMap()) : IllegalArgumentException(message)

A candidate the language adapter refused. message is written for whoever proposed the candidate; facts carry the evidence.

StudyAdmission

data class StudyAdmission<T>(val value: T, val facts: Map<String, Any?> = emptyMap())

An accepted candidate: the compiled value the tools use, and facts worth saving with the revision.

SourceRevision

data class SourceRevision<T>(val id: String, val parent: String?, val description: String, val source: String, val value: T, val facts: Map<String, Any?> = emptyMap())

One checked, immutable version of the source.

Declaration Summary
val id: String
val parent: String?
val description: String
val source: String
val value: T
val facts: Map<String, Any?>

SourceRevisionSummary

data class SourceRevisionSummary(val id: String, val parent: String?, val description: String, val created: String?)

A revision without its source or value, for history listings.

SourceStudy

class SourceStudy<T>(val directory: File, val extension: String, private val readSource: () -> String, private val replaceSource: (expected: String, replacement: String) -> Boolean, private val admit: (source: String, cancelled: () -> Boolean) -> StudyAdmission<T>, val maxSourceLength: Int = 40000, private val load: ((source: String) -> StudyAdmission<T>)? = null)

A durable, transactional history of one editable source, checked by a language adapter.

Declaration Summary
val directory: File
val extension: String
val maxSourceLength: Int
fun liveSource(): String The live text, as the box holds it now.
fun currentId(): String The id of the checked revision the box holds, or EMPTY.
fun cancel(): String Invalidate every pending publication. The working revision is kept.
fun isCancelled(epoch: Long): Boolean Whether an operation started at epoch has since been cancelled — for long reads outside a transaction.
fun epoch(): Long The cancellation epoch to pass to isCancelled.
fun resume(): String Pick up where a previous session left off, if HEAD names a revision whose source still verifies.
fun revision(id: String = "current"): SourceRevision<T> A revision by id, or the active one for "current" (or blank). Loaded from disk and re-admitted on first use.
fun savedSource(id: String): String Read an exact saved source for history inspection without compiling or re-admitting it.
fun forkSaved(id: String, name: String): String Create a named saved child without admission, activation, live source changes or moving HEAD.
fun history(): List<SourceRevisionSummary> The active revision's ancestry, oldest first.
fun allRevisions(): List<SourceRevisionSummary> Every revision on disk, in the order they were made — including ones a restore left off the active line.
fun liveState(): Map<String, Any?> The live source against the checked revision: its hash, whether they differ, and the live text when they do.
fun check(source: String): StudyAdmission<T> Check source with the adapter without publishing anything. This is what speculation uses: the same admission a real edit gets, with no transaction around it.
fun edit(base: String, sourceHash: String, source: String, description: String): SourceRevision<T> Replace the live source with source as a new checked revision.
fun patch(base: String, sourceHash: String, edits: List<StudySourceEdit>, description: String): SourceRevision<T> edit with exact replacements applied to the live source — see applyEdits.
fun restore(base: String, sourceHash: String, id: String, description: String = ""): SourceRevision<T> Make an earlier revision's source current again, as a new revision whose parent is the active one — history is only ever appended to, so the versions in between stay reachable.

BoxStudies

object BoxStudies

Where a study of one box keeps its revisions: studies/<box id> in the box's document.

Declaration Summary
fun documentOf(box: afield.document.graph.Box): afield.app.server.SessionInformation? The open document box belongs to, or null for a box in no open document.
fun show(box: afield.document.graph.Box, channel: String, html: String) Put html in box's output channel and open it, from anywhere. box.display goes through the output proxies an execution sets up, so from library code acting on a box that has not run in this session it sends nothing; this sends the output message itself.
fun rendererAbove(box: afield.document.graph.Box): afield.graphics.vulkan.Renderer? The running renderer a live program draws in: the nearest box, starting at box and walking up its parents, that holds an open renderer — the same population prop<Renderer>(renderer) searches.
fun editorLine(box: afield.document.graph.Box, property: String): Int? The editor's cursor line in box's property, while a page on its document has the box selected with that property in the editor; else null. The page reports its selection (afield.app.server.documentSelection) and the editor's view state (<property>_editorState), each within a tenth of a second.
fun reveal(box: afield.document.graph.Box, property: String, line: Int, column: Int = -1, endColumn: Int = -1) Select box with property in the editor of every page open on its document, and put the cursor on line — selecting columns column up to endColumn (1-based, end exclusive) and focusing the editor when a column is given, so what is typed next replaces them. A study card uses it to take the person from a control to the number it turns; a RectSL window, from a rectangle to its line.
fun personAt(line: Int?, source: String): Map<String, Any?>? What an Inspect says about where the person is looking: the line their editor cursor is on in source, and its text — or null when the box is not open in their editor. "This one" in a request usually means it; in a RectSL scene, clicking a rectangle in the window puts the cursor on its line.
fun lineOfViewState(viewState: String?): Int? The cursor's line in a Monaco view state (editor.saveViewState() as JSON), or null.
fun directory(box: afield.document.graph.Box): File studies/<box id> in box's document.
fun replacer(box: afield.document.graph.Box, property: String): (String, String) -> Boolean A replace-source function for a study of box's property: on the main thread, only if the property still holds what the edit was based on, and marking the sidecar for saving.

StudyFrames.kt

The renderer interface every study draws through: a program and a list of times in, one CpuRaster per time out.

StudyFrames is implemented by StackSL's and RectSL's rasterizers and by studyFrames for a document's own drawing function; renderAll collects its frames into a list.

src/jvmMain/kotlin/afield/study/source/StudyFrames.kt — read the source

Top-level functions

Declaration Summary
fun <P> StudyFrames<P>.renderAll(input: P, width: Int, height: Int, seconds: List<Double>): List<CpuRaster> StudyFrames.render, collected.

StudyFrames

interface StudyFrames<in P>

Where a study's frames come from: P — a program, a compiled scene — drawn at a size and a list of times, each frame handed back as a CpuRaster of the program's raw output.

Declaration Summary
val description: String Which renderer produced the last frames, and why — repeated in every tool result.
val fast: Boolean Whether a frame is cheap right now — whether defaults can afford large sizes and many frames.
fun render(input: P, width: Int, height: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit) input at width by height, once per time in seconds, handed to each in order.

StudyKeys.kt

Keys for a study card: a snapshot of every number in a program, saved by name, which the card can apply or blend towards. They are the person's way of keeping "this look" and "that look" and moving between them before choosing a revision.

A key is a KeyframeScope key like any other, saved with KeyframeScope.saveKey, so each has its .versions history and the keyframe browser can read the folder. Its values are the knobs by index (k0, k1, …) and, under shape, the shape of the program it came from: that program's text with every number blanked out, the language, and what reads each number. So a key applies to the program it was captured from, and to that program with any of its numbers changed, but not to one whose text has changed around them, where the same index may be a different number.

Keys live in keys/ in the study's directory.

src/jvmMain/kotlin/afield/study/source/StudyKeys.kt — read the source

sourceBlock.kt

A block of code that is also its own source: call it like a function, and it carries the Kotlin text it was written as.

val answer = sourceBlock { 6 * 7 }
println(answer())          // 42
println(answer.source)     // 6 * 7

A macro: before the box compiles, sourceBlock { … } is rewritten to a SourceBackedBlock holding both the live lambda and the text of its body. Code that saves a block (a keyframe track, a code-blend plan) saves that text, and a block loaded back from it compiles the text again, in the box it came from, the first time it is called. So a saved block works before (or without) the box that wrote it running again.

A block made by this call carries its lambda and is safe to call at once. A block loaded back from saved text is not safe to call from inside its own box's running code: compiling it runs that box's code again, which calls the block again. Call it from another box, a frame loop or a control callback; SourceBackedBlockSelfBoxReentry stops the loop and says so if you do.

Should the macro not run (code that reaches this function some other way), the result still works, with an empty SourceBackedBlock.source.

src/jvmMain/kotlin/afield/study/source/sourceBlock.kt — read the source

Top-level functions

Declaration Summary
fun <T> sourceBlock(block: () -> T): SourceBackedBlock<T> A block carrying block and no source, which is what sourceBlock { … } compiles to when the macro has not rewritten it.

KnobNames.kt

Short names for the numbers of a program, like wave drift speed or star brightness, for the sliders of a study card, written by Field's fast model from what the compiler already knows.

A card can always say what reads a number: StackSL's compiler records what took it (mix amount, x of palette a), RectSL's tokens the word that reads it (->wall width). That is exact, but a program whose numbers mostly scale and offset reads as a column of multiplies and adds. So the card shows that at first, and asks the fast model once, for all of the program's numbers at a time, to say in a few words what turning each one changes, given the program with its comments and each number's line, spelling and operand. The compiler's label stays on the slider's tooltip.

Names are kept by the program's shape: its text with every number blanked out, the language, and what reads each number. Turning a number, or keeping one, never asks again; the names are stored in the study's directory, beside its revisions, so a document carries them. Nothing is asked for a box in no open document (a test's box), nor for a document an agent opened unless AFIELD_AI_KNOB_NAMES=1; 0 turns asking off, and names already stored are still shown.

src/jvmMain/kotlin/afield/study/ui/KnobNames.kt — read the source

StudyKnob.kt

The language-neutral view of the numbers in a study's source: StudyKnob describes one typed number, and setNumbers applies a batch of new values, keyed by knob index, as a new SourceRevision.

Each study language finds its own knobs and maps them to StudyKnob; slider naming (KnobNames) and saved card keys (StudyKeys) work from that shared shape. setNumbers refuses an empty request, a stale revision or source hash, and any value the language's rewriter rejects, each as a StudyRejection.

src/jvmMain/kotlin/afield/study/ui/StudyKnob.kt — read the source

Top-level functions

Declaration Summary
fun List<NumberEdit>.numbers(): Map<Int, Double> NumberEdits as knob index to value, the form a language's rewriter takes.
fun <T> SourceStudy<T>.setNumbers(expectedRevision: String, expectedSourceHash: String, numbers: Map<Int, Double>, description: String, withNumbers: (source: String, numbers: Map<Int, Double>) -> String): SourceRevision<T> Publish numbers (knob index to value) written into the live source by withNumbers — the language's own rewriter, which keeps each number's spelling — as a new revision, with the usual guards: the knob indices refer to the live source the caller read, so a stale revision or hash is refused, and a value a word cannot hold is refused with the rewriter's reason.

StudyKnob

data class StudyKnob(val index: Int, val line: Int, val typed: String, val operand: String, val value: Double = 0.0)

One number a person typed in a study's source — a knob — as everything language-neutral sees it: the fast model that names sliders (KnobNames), the keys a card saves (StudyKeys), a study's setNumbers tool.

Declaration Summary
val index: Int Index among the program's knobs, in source order — the number a tool call names.
val line: Int 1-based line of the number.
val typed: String The number as typed.
val operand: String What reads it, in the language's own words.
val value: Double Its value now.

NumberEdit

interface NumberEdit

One knob to set, by index, and the value to write there — what every study's setNumbers request carries a list of. Each study's request class keeps its own element type (its JSON schema, with whatever the property descriptions say, is what a model has been shown), and implements this.

Declaration Summary
val knob: Int
val value: Double

StudyReportView.kt

Renders a StudyReport as an HTML card, so display("main", report.h) shows a study's revision, message and pictures in a box's output.

StudyReportView.html builds the card and draws each picture at its own PNG width, capped at StudyReportView.MAX_WIDTH; registerStudyReportView installs it as the .h value printer when a new root starts.

src/jvmMain/kotlin/afield/study/ui/StudyReportView.kt — read the source

Top-level functions

Declaration Summary
fun registerStudyReportView() Registers StudyReportView as .h for every StudyReport.

StudyReportView

object StudyReportView

report.h: a StudyReport as a card — its revision, its message if it has one, and its pictures at their own pixel size (at most MAX_WIDTH CSS px), so a box shows what the model was shown with display("main", report.h) rather than an <img> it builds itself.

Declaration Summary
const val MAX_WIDTH Widest a picture is drawn, in CSS px.
fun pngWidth(dataUrl: String): Int? The width a data:image/png;base64, URL's image is, read from its IHDR, or null if it is not one.
fun html(report: StudyReport): String The card: revision · message, then each picture.

AgentJvmMainLauncher.kt

src/jvmMain/kotlin/afield/tools/automation/AgentJvmMainLauncher.kt — read the source

AgentJvmMainLauncher

object AgentJvmMainLauncher

Declaration Summary
fun main(args: Array<String>)

AgentWorktreeManager.kt

Creates, lists, merges and deletes git worktrees that agents work in, one branch per proposal.

AgentWorktreeManager.createProposal adds a worktree on a new agent/<task>/<id> branch from the current HEAD and records it as JSON in a .metadata folder under the worktree root. That root is afield.agentWorktreeRoot, AFIELD_AGENT_WORKTREE_ROOT, or <checkout>-agent-worktrees beside the checkout. mergeProposal refuses to merge into a checkout with uncommitted changes.

src/jvmMain/kotlin/afield/tools/automation/AgentWorktreeManager.kt — read the source

AgentWorktreeManager

object AgentWorktreeManager

Git worktree proposals for agents: create, list, update, merge, finalize and delete.

Declaration Summary
fun listProposals(projectRoot: File = File(".").canonicalFile): List<AgentWorktreeProposal>
fun createProposal(projectRoot: File, taskName: String, label: String = taskName): AgentWorktreeProposal
fun updateProposal(projectRoot: File, id: String, status: String, message: String = "", reportPath: String = "", dryRunPatchPath: String = ""): AgentWorktreeProposal?
fun deleteProposal(projectRoot: File, id: String): AgentWorktreeActionResponse
fun mergeProposal(projectRoot: File, id: String): AgentWorktreeActionResponse
fun finalizeProposalChanges(projectRoot: File, id: String): AgentWorktreeActionResponse
fun proposal(projectRoot: File, id: String): AgentWorktreeProposal?
fun worktreeRoot(projectRoot: File): Path

MacroReplayCode.kt

The code a replayed execute / launch op runs, and where in the box it sits.

A macro replays against the document as it is now, not as it was when the macro was recorded:

  • A whole-box launch (type: launch, code or all) runs the box's current text. Any code: or recordedCode: on the op is a reference to what ran at recording time; a recording made before the box's code was edited still launches the edited code.
  • A fragment (type: fragment, the default for execute, execute-fragment and launch-fragment) with an explicit code: runs that code: an authored transcript uses it to run a snippet that is not in the box. A recorded fragment carries recordedCode: instead and is found again in the box's current text — the same text where it still occurs (nearest its recorded line), otherwise the same lines of the current text.
  • codeSource: recorded runs the op's own code: / recordedCode: as written, for a macro that has to replay exactly what ran.
  • An op with no code at all runs the box's current text.

note says when what runs differs from what was recorded, for the replay log.

src/jvmMain/kotlin/afield/tools/automation/MacroReplayCode.kt — read the source

operation_transcript.kt

Records the operations a person or agent performs on a document and replays them as named macros.

OperationTranscript logs edits, moves, executions, launches and stops as they happen (recordEdit, recordMove, recordExecuteFragment, recordLaunch, recordStop), and stores macros as YAML files in a macros/ folder at document, project or workspace scope (listMacros, writeMacro, runMacro). The backdoor's macro write and macro run commands use this; the YAML format and examples are in dev_docs/agents/OPERATION_TRANSCRIPT.md.

src/jvmMain/kotlin/afield/tools/automation/operation_transcript.kt — read the source

OperationTranscript

object OperationTranscript

Declaration Summary
data class MacroInfo(val name: String, val path: String, val operationCount: Int)
data class MacroResult(val success: Boolean, val message: String, val name: String = "", val path: String = "", val operationCount: Int = 0, val macros: List<String> = emptyList(), val content: String = "", val recording: Boolean = false, val draft: String = "", val selection: List<String> = emptyList(), val documentMacros: List<String> = emptyList(), val projectMacros: List<String> = emptyList(), val workspaceMacros: List<String> = emptyList(), val projectName: String = "", val scope: String = "document", val direction: String = "", val applied: Int = 0, val skipped: Int = 0, val errors: List<String> = emptyList(), val entries: List<TranscriptEntry> = emptyList(), val offset: Int = 0, val limit: Int = 0, val total: Int = 0, val hasMore: Boolean = false, val prompt: String = "", val output: List<MacroOutputLine> = emptyList(), val setupVerified: Boolean = true)
data class MacroRunResult(val success: Boolean, val message: String, val name: String, val direction: String, val targetBoxes: List<String>, val applied: Int, val skipped: Int, val errors: List<String> = emptyList(), val output: List<MacroOutputLine> = emptyList(), val setupVerified: Boolean = true)
data class MacroReplayOptions(val asCopy: Boolean = false, val offsetX: Double = 0.0, val offsetY: Double = 0.0, val step: Int = -1) How a run places what it replays. asCopy gives the boxes the macro creates fresh uids, so a second run makes a second set; offsetX/offsetY shift their frames; step replays one operation (0-based) of the macro and nothing else, which is how the panel steps through one.
fun markStart(label: String = "", selection: List<String> = emptyList()): MacroResult
fun markEndAndSave(name: String, label: String = "", scope: String = "document", projectName: String = ""): MacroResult
fun listMacros(documentPath: String = currentDocumentPath().orEmpty(), scope: String = "document", projectName: String = ""): MacroResult
fun readMacro(documentPath: String, name: String, scope: String = "document", projectName: String = ""): MacroResult
fun writeMacro(documentPath: String, name: String, contents: String, scope: String = "document", projectName: String = ""): MacroResult
fun promoteMacro(documentPath: String, name: String, scope: String = "document", projectName: String = ""): MacroResult Promote a macro to a broader scope so more documents can share it. The destination is the project when this document belongs to one, otherwise the workspace. The source is copied verbatim (a backup of any existing destination copy is kept), leaving the original in place.
fun renameMacro(documentPath: String, name: String, newName: String, scope: String = "document", projectName: String = ""): MacroResult Rename a saved macro within its scope. The YAML's own name: line follows the file name, so the panel and the palette never disagree about what the macro is called. Refuses to replace an existing macro of the new name.
fun deleteMacro(documentPath: String, name: String, scope: String = "document", projectName: String = ""): MacroResult Delete a saved macro. The file is moved to the scope's .deleted/ folder with a timestamp, not erased, so a deletion from the panel can be recovered by hand; the macro lists only read the scope folder itself.
fun saveTranscriptSelectionAsMacro(documentPath: String, name: String, indices: List<Int>, selection: List<String> = emptyList(), scope: String = "document", projectName: String = ""): MacroResult
fun macroDirectory(documentPath: String, scope: String = "document", projectName: String = ""): MacroResult
fun runMacro(documentPath: String, name: String, targetBoxes: List<String> = emptyList(), direction: String = "do", strict: Boolean = false, scope: String = "document", projectName: String = "", initializeCreatedBoxes: Boolean = true, replay: MacroReplayOptions = MacroReplayOptions()): MacroRunResult
fun macroState(documentPath: String = currentDocumentPath().orEmpty(), scope: String = "document", projectName: String = ""): MacroResult
fun transcriptPage(documentPath: String, offset: Int = 0, limit: Int = 80, includeWaits: Boolean = true): MacroResult
fun undoTranscriptSelection(documentPath: String, indices: List<Int>, strict: Boolean = true): MacroResult
fun aiUndoPrompt(documentPath: String, indices: List<Int>, selection: List<String> = emptyList()): MacroResult
fun recordEdit(box: Box, property: String, from: String?, to: String?, origin: String = "")
fun turnBoundary(documentPath: String): Int Mark an AI turn's start or end: flush the pending edit and forbid merging into anything recorded so far. Returns the cursor (the number of entries), which the session tree keeps as the turn's op.from/op.to.
fun countOrigins(documentPath: String, origins: Set<String>): Int How many recorded edits were made under one of origins.
fun undoOrigins(documentPath: String, origins: Set<String>): MacroResult Undo every recorded edit made under one of origins (AI turns being rewound), newest first, strictly: an edit whose property has changed since is skipped and reported, never overwritten.
fun recordMove(box: Box, from: Rect, to: Rect)
fun recordExecuteFragment(box: Box, execute: Execute)
fun recordLaunch(box: Box, execute: Execute, strand: String = execute.strand)
fun recordStop(box: Box)
fun recordCreate(box: Box) A box the person just made on the canvas. The frontend creates a box by sending its name and then its frame (createBox in Main.kt), so the box's first property message is where the server learns of it; the handlers call this once the value is applied. The entry carries the box's name and frame, and the frame / first-value edits that follow are folded into it (see foldIntoPreviousCreate), so a recording reads create {box, name, frame, properties} rather than an edit from nothing. Without it a recording replayed into a fresh document edited boxes that did not exist (audit, 2026-10-20: applied=2 skipped=5, no boxes).
fun recordDelete(box: Box) A box the person deleted (the -delete-box action). Replays as a tombstone; undoes as a restore.
fun recordConnect(parent: Box, child: Box, connected: Boolean) A connection the person made or removed (the -connect / -disconnect actions, the g key).
fun checkpoint(documentPath: String): Boolean

transcript.kt

Keeps a per-document log of what ran in each box, for activity summaries and the transcript view.

FlightRecorder appends an entry for every execute, launch and stop, and for changes to tracked box properties, then reports per-box execution and launch counts (activitySummaryForDocument, stamped with the current Field launch id), per-line activity (lineActivityForDocument) and an HTML rendering of the log (htmlForDocument). Older logs are kept as transcript_backup_*.edn files in the document's backups/ folder.

src/jvmMain/kotlin/afield/tools/automation/transcript.kt — read the source

FlightRecorder

object FlightRecorder

Declaration Summary
data class BoxActivitySummary(val uid: String, val name: String, val executions: Int, val launches: Int)
data class ActivitySummary(val fieldLaunchId: String, val fieldLaunchStartedAt: Long, val executions: Int, val launches: Int, val perBox: List<BoxActivitySummary>)
fun recordExecuteEntrypoint(entrypoint: String, box: Box, execute: Execute)
fun recordExecuteSymbolEntrypoint(entrypoint: String, box: Box, executeSymbol: ExecuteSymbol)
fun recordLaunchEntrypoint(entrypoint: String, box: Box, execute: Execute, strand: String = execute.strand)
fun recordStopEntrypoint(entrypoint: String, box: Box)
fun recordTrackedPropertyChange(box: Box, property: String, oldValue: Any?, newValue: Any?)
fun checkpointOnMetadataChange(documentPath: String): Boolean
fun entriesForDocument(documentPath: String): List<LinkedHashMap<String, Any?>>
fun activitySummaryForDocument(documentPath: String, untilMillis: Long = Long.MAX_VALUE): ActivitySummary
fun lineActivityForDocument(documentPath: String, uid: String, property: String, propertyText: String, maxEntries: Int = 4000): TranscriptLineActivityResult
fun entriesFor(box: Box): List<LinkedHashMap<String, Any?>>
fun payloadForDocument(documentPath: String): LinkedHashMap<String, Any?>
fun payloadFor(box: Box): LinkedHashMap<String, Any?>
fun htmlForDocument(documentPath: String): String
fun htmlFor(box: Box): String
fun recordAiProvisionalAccepted(box: Box, property: String, acceptedValue: String, archiveMarkdown: String?)

AgentDemoDriftGuard.kt

Checks that each committed, authored document agrees with the loose source files beside it: every document under dev_docs/agents/demos/, and every document directly under AUTHORED_ROOTS that carries inline copies.

A #afield/file-inline property has two copies: the fallback embedded in metadata.edn, and the loose file beside it. Field loads the loose file, unless metadata.edn is the newer of the two and says something else (SavingContext.sidecarOrNewerInline takes that as a hand edit of the inline copy). So a stale inline copy is a trap: an edited sidecar runs fine until someone touches metadata.edn for any other reason, and from then on the old code loads instead. On 2026-10-31 that put the Khronos Box.gltf lesson back into canonical_graphics, whose inline copies were stale for 77 sidecars. This guard compares the two byte-for-byte. --write refreshes the inline fallbacks through the EDN codec in SavingContext.

--write rewrites metadata.edn for every document it visits, and re-encoding is not always byte-identical to what is committed — a hand-edited artifact can carry \uXXXX escapes that the codec re-emits as the literal character. That turns "add one demo" into a diff against unrelated documents, which is an invitation to a merge conflict between two branches that each added one. --only <document>[,<document>…] narrows the run to the documents named, by directory name or by path from the checkout root, so adding a demo touches that demo alone.

src/jvmMain/kotlin/afield/tools/docs/AgentDemoDriftGuard.kt — read the source

AgentDemoDriftGuard

object AgentDemoDriftGuard

Compares each authored document's #afield/file-inline fallbacks with the loose files beside them.

Declaration Summary
fun main(args: Array<String>)

AgentTranscriptIndexGuard.kt

Fails the build when public/docs/agent_transcripts.md stops being a true index of dev_docs/agents/transcripts/.

The failure this exists to stop

The page calls itself "the illustrated index" of the transcript corpus and is the public front door to Field's agent-automation story. The generated half of the same system cannot drift: AgentTranscriptMaterializer and checkAgentTranscripts guard the committed starting-point bundle byte-for-byte. The hand-curated page beside it has no such guard of its own, and goes stale the moment anybody adds a transcript without remembering the page exists — a page that claims to list every transcript, listing most of them.

The page is not generated, because its entries quote the number a box prints and say why that number is the interesting one — 0.985 forwards, 0.246 backwards on a parabola; 473 diagonal breaks against 0. None of that is in the yaml, which carries op verbs, box ids, rects and Kotlin source; a generator over it could emit a name and a box count, which would replace the writing with a table. So the trade here is KDocAttachmentGuard's, not StdlibReferenceGenerator's: keep the prose hand-written and make its incompleteness a build failure, so a new transcript cannot ship without somebody writing a sentence about it.

The five things it checks

  1. Completeness — every *.yaml in the corpus has exactly one entry. An entry is a ### heading whose whole text is the transcript's name, ### transcript_name, at column zero. ### on that page is therefore reserved for entries: a sub-heading inside an entry has to be #### or deeper, and a ### naming anything else is read as an entry for a transcript that does not exist. A heading per entry is also what makes a hundred-odd scenes something a reader can link to and scroll through one at a time.
  2. No orphans — every entry names a transcript that is still in the corpus, so deleting or renaming a yaml turns the stale paragraph red instead of leaving it to be read as current.
  3. The CI claim, per entry — each ## section declares how CI verifies the scenes under it, with an invisible marker line beneath the heading:
    <!-- transcript-index: verified-by = execute -->
    The value is one or more of execute, compile, kotlinjs, naming DemoTranscriptExecSmokeTest.EXECUTE, DemoTranscriptExecSmokeTest.COMPILE_ONLY and DemoTranscriptExecSmokeTest.KOTLINJS — which is where classification is decided, and stays the single source of truth. An entry whose transcript is not in one of its section's declared sets is a failure, which is what makes the section prose ("these build and run headless", "each one builds and compiles in CI") an accurate statement about every entry beneath it rather than about most of them. The marker is an HTML comment, so it renders as nothing.
  4. Open ↗ links that exist — a transcript is offered as an openable starting point exactly when the materializer ships it, i.e. when it is not in AgentTranscriptMaterializer.EXCLUDE. So an entry must carry an assets/agent_transcripts/<name>/metadata.edn link when its transcript is materialized and must not when it is not, and the committed bundle directory must really be there. This is the check that stops the page advertising a document the welcome screen cannot open.
  5. No counts in the CI paragraph — the paragraph marked
    <!-- transcript-index: totals -->
    must not quote a number. A paragraph that quoted the corpus size and each set's size would put every branch that adds a transcript into the same sentence, and any two of them into a conflict there. The counts live in DemoTranscriptExecSmokeTest, which prints them; the prose names the three depths without them.

One section is not about transcripts: the ## section marked <!-- transcript-index: bundled --> lists the saved documents the welcome screen offers as they are (BUNDLED_DOCUMENT_STARTING_POINTS). Its entries are held to that map in both directions instead of to the corpus, each document must be committed, and checks 3 and 4 do not apply.

Running it

./gradlew checkAgentTranscriptIndex          # also runs as part of check
Reads five kinds of text file and nothing else: no Field server, no runloop, no boxes, no GPU. Its heavyweight sibling checkAgentTranscripts builds real boxes and costs ~25 s; a completeness check needs none of that. See dev_docs/agent_transcript_index.md for how to add an entry.

src/jvmMain/kotlin/afield/tools/docs/AgentTranscriptIndexGuard.kt — read the source

AgentTranscriptIndexGuard

object AgentTranscriptIndexGuard

Checks that public/docs/agent_transcripts.md lists every transcript, and only real ones.

Declaration Summary
fun main(args: Array<String>)

AgentTranscriptLayoutGuard.kt

Keeps OperationTranscript and demo boxes visible and compact, while allowing larger boards that have a recorded reason.

A Field box frame is the labelled canvas rectangle, not its Monaco editor or output viewport. Ordinary boxes therefore start near the positive canvas origin at roughly 300 x 100; see dev_docs/agents/OPERATION_TRANSCRIPT.md. A larger visual or capture card is allowed only when its file#box key has a concrete reason in dev_docs/agents/transcript_layout_exceptions.yaml.

The corpus this ceiling is stated over

Both halves: every create op under dev_docs/agents/transcripts, and every box in every metadata.edn under dev_docs/agents/demos. AuditResult reports the two populations separately and main prints both, so "no violations" can never be read off a corpus of zero.

A demo's transcript and its document must agree

A demo directory can hold a build.yaml OperationTranscript beside its metadata.edn, and the transcript is what rebuilds the document. Those transcripts are a third population, parsed like the corpus, and each create must name a box the document carries at the geometry it carries. Agreement is what keeps a replay from writing roomy authoring frames back over a compacted document, and it is also why those frames are not held to the ceiling a second time: an agreeing frame is the document's frame, which is audited (and grandfathered) once, under its metadata.edn key.

The ceiling is 620 x 170 (MAX_ORDINARY_WIDTH, MAX_ORDINARY_HEIGHT). It moves together with the ordinary box size, so a change to the house box size keeps the ratio between an ordinary box and the ceiling the same.

Run with ./gradlew checkAgentTranscriptLayouts (also part of check and docsGates).

src/jvmMain/kotlin/afield/tools/docs/AgentTranscriptLayoutGuard.kt — read the source

AgentTranscriptLayoutGuard

object AgentTranscriptLayoutGuard

Audits transcript and demo box frames against the ordinary size ceiling and its exceptions.

Declaration Summary
const val MAX_ORDINARY_WIDTH
const val MAX_ORDINARY_HEIGHT
fun main(args: Array<String>)
data class AuditResult(val transcripts: Int, val demos: Int, val frames: Int, val largeFrames: Int, val problems: List<String>, val demoTranscripts: Int = 0) The population, stated as two numbers rather than one.
fun audit(repo: File): AuditResult

BoxOutputChannelGuard.kt

Catches the figure that is written and then immediately erased.

display(channel, …) replaces that channel's contents rather than appending to them, so two figures emitted to one channel from the same box leave only the second. Nothing fails, nothing warns, and the box still looks like it worked — the last figure renders, so the loss is only visible if you knew to expect two. Five of the eight boxes in typography_worked_example were in this state, as was afield.audio.synth.showFaustPattern, whose own KDoc promised "a player, a waveform" while the waveform overwrote the player on every call.

The rule: two figures in one box means two channels. This guard reports any box that emits to one channel twice.

What counts as emitting

display(…) itself, plus every fun Box.<name>(… channel: String = "…" …) helper found in src/jvmMain/kotlin — the table is read from the source rather than hardcoded, so a new show* helper is covered the day it is written. A helper whose overloads disagree about the default is skipped rather than guessed at (logs, inspect).

Why only top-level calls

Only calls at brace depth 0 of the box's code are counted. Replacing a channel repeatedly is the correct way to animate — Box.showImg's KDoc calls one display per frame "the path an animation wants" — so an animate { } body, a for loop or any other nested emitter is treated as refresh, not loss.

Two emitters at depth 0 are a loss only when that channel is not animated at all. The establish-animate-settle shape writes the same channel three times: once before the loop to put the first state on screen, once per iteration inside it, and once after it so the settled state is not up to one iteration stale. Its two depth-0 writes are the ends of one animation, not two figures, and the first one's pixels very much do reach a screen — in confluence_grid there is a Thread.sleep(300) immediately after it for exactly that reason. So a channel that also receives a nested emission in the same box is treated as animated and its depth-0 writes are not reported.

The cost of that narrowness is a known blind spot: for (…) { showImg(…) } followed by a top-level showMeshGrid(…) on the same channel is a real collision this will not report, because depth 0 sees only one emitter. The animated-channel rule widens that same blind spot rather than guessing which loops terminate. Both are the same trade: this guard reports a channel only when nothing in the box could have been refreshing it.

Populations

Three: the .yaml box code under dev_docs/agents/transcripts, the _code_.kt box code under dev_docs/agents/demos, and the library helpers themselves — a collision inside a fun Box.… is worse than one in a transcript, since it ships to every caller.

Run with ./gradlew checkBoxOutputChannels (also part of check and docsGates).

src/jvmMain/kotlin/afield/tools/docs/BoxOutputChannelGuard.kt — read the source

BoxOutputChannelGuard

object BoxOutputChannelGuard

Reports boxes whose code emits two figures to one output channel at the top level.

Declaration Summary
data class AuditResult(val boxes: Int, val emissions: Int, val problems: List<String>)
fun main(args: Array<String>)
fun audit(repo: File): AuditResult
fun helperChannelDefaults(librarySource: File): Map<String, String?> helper name -> default channel, for every fun Box.… that takes a channel: String = "…".
fun helpersTakingChannelFirst(librarySource: File): Set<String> Helpers whose every overload takes channel as its first parameter, so a leading string literal names the channel: orca("lead", …) writes "lead", not Box.orca's default "orca".

DocsSearch.kt

Lexical search over the documentation sources (the Markdown pages under public/docs/, outside assets/), for Open Anything. The semantic index finds pages by meaning; this finds them by the text a reader remembers — a page title, a heading, or an identifier from an example such as once.renderer, which an embedding search ranks poorly.

Each page is split into units — its title, each heading, each fenced code block, and each prose paragraph — and a query matches a unit when every whitespace-separated term occurs in it, case insensitively. Hits link into the built site (/field-docs/<page>/#<heading anchor>), so the index works from the Markdown alone and does not need public/field4/ to have been built.

The index is rebuilt when any page's size or modification time changes.

src/jvmMain/kotlin/afield/tools/docs/DocsSearch.kt — read the source

DocsSearch

object DocsSearch

The docs lexical index and its query entry points; see the file overview.

Declaration Summary
data class Hit(val kind: String, val page: String, val pageTitle: String, val heading: String = "", val url: String, val line: Int, val snippet: String, val language: String = "", val excerpt: String = "", val score: Double)
data class Response(val query: String, val hits: List<Hit>, val totalMatches: Int, val truncated: Boolean)
fun docsRoot(): File? -Dafield.docs.dir, else public/docs found by walking up from the working directory.
fun search(rawQuery: String, limit: Int = 40, maxPerPage: Int = 3, root: File? = docsRoot()): Response

ElectronCaptureBrowser.kt

This checkout's Field Electron app, driven by the server's Playwright over CDP.

The capture browser is normally a plain Chromium the server launches (ensureSharedPlaywrightBrowser in Server.kt), which is right for everything that renders in any browser. Some output only exists inside the Electron app — the RTT view (afield.media.stream.ElectronRTTView) draws frames that Electron's sharedTexture delivers, and Chromium has no such API — so a capture of it has to come from Electron itself. Playwright's Java API cannot launch Electron, but it can attach to any Chromium over the DevTools protocol, and Electron takes --remote-debugging-port.

launch starts Electron with APP_URL set to the document, so its first tab is the Field page (loaded with field-preload.js, the preload the RTT view needs), and fieldPage finds that tab among the window's targets (the tab strip is a separate page). Requested with playwright-open --electron on the backdoor. macOS and Linux (native Wayland for the RTT view).

src/jvmMain/kotlin/afield/tools/docs/ElectronCaptureBrowser.kt — read the source

ElectronCaptureBrowser

class ElectronCaptureBrowser private constructor(private val process: Process, val browser: Browser, private val cdpPort: Int) : AutoCloseable

A running Electron app attached over CDP, for captures that only exist inside Electron.

Declaration Summary
val browser: Browser
val alive: Boolean
fun fieldPage(serverPort: Int, timeoutMs: Long = 30_000): Page The Field tab: the page on the server's origin that is not the tab strip.
override fun close()

ForthDocsSupport.kt

src/jvmMain/kotlin/afield/tools/docs/ForthDocsSupport.kt — read the source

Top-level functions

Declaration Summary
fun Box.renderForthDocsSnippet(source: String)

KDocAttachmentGuard.kt

A check over the repository's .kt files under src/ (not src/personal/) for KDoc blocks that would publish against the wrong declaration.

It reports a block whose next line the extractor cannot parse as a declaration (the block slides onto a later symbol) and two blocks stacked with nothing between them. It runs as the smoke test docs.kdoc-attachment.guard.

src/jvmMain/kotlin/afield/tools/docs/KDocAttachmentGuard.kt — read the source

KDocAttachmentGuard

object KDocAttachmentGuard

Fails the build for a KDoc comment that does not attach to the declaration its author meant.

Declaration Summary
enum class Kind Which way a KDoc block fails to reach its declaration.
data class Finding(val path: String, val line: Int, val kind: Kind, val detail: String) One mis-attached KDoc block.
fun main(args: Array<String>)

KotlinSourcePartition.kt

Cuts a Kotlin source file into an ordered tree of line spans that tiles the file exactly.

Why a partition, and not another parse

afield.runtime.completion.KotlinSourceIndex answers "which declarations are in this file, and on what line does each start?" — which is all the completion panel, the semantic index and StdlibReferenceGenerator have ever needed. Rendering a source file as a page needs a strictly stronger fact: every line has to land in exactly one span, in order, with no gaps and no overlaps. A page that lifts KDoc into prose and puts the bodies in code fences numbered from their real source lines is only truthful if the spans it was cut from account for the whole file. Miss three lines and the fence below them is numbered wrong — silently, on a published page, with nothing to notice it.

So the contract here is a tiling, and verify is the thing that proves it:

  • Tile — the top-level spans start at line 1, each begins where the previous one ended, and the last ends on the file's last line.
  • Nest — a child span lies inside its parent and after its previous sibling. Children do not tile their parent: the lines between them (a class's signature, its closing brace, the blank line between two members) are the parent's own text, and reconstruct emits them from the gaps.
  • Reconstruct — walking the tree and emitting each span's lines returns the original file, line for line. This is the assertion that actually has teeth; the two above are what make it debuggable when it fails.
  • Balance — the code text of a Kind.DECLARATION span has balanced (), [] and {}. A span that stops in the middle of a body still tiles and still reconstructs, so this is the one check that can see a mis-cut rather than a lost line.

Precision is measured, not assumed

The end of a declaration is easy when it has a brace body and awkward when it does not — an expression body can run over as many lines as it likes, and Kotlin's continuation rules are genuinely ambiguous without a real parser. The scanner does not guess: anything the scanner cannot attribute to a declaration becomes Kind.FILLER, which is a span like any other and keeps the tiling total. So a heuristic that gives up early costs precision (lines in FILLER) and never correctness (lines lost, or attributed to the wrong declaration). SourcePartitionGuard fails the build on the invariants and prints the FILLER census as a number, so the imprecision is something we can watch shrink instead of something we hope about.

What it does not do

Bodies are not descended into: a fun is one span, whatever is inside it. Only container declarations — class, interface, object, enum class, companion object — recurse, because only their contents are declarations a page would want headers for. A local class inside a function body is therefore invisible here.

Enum entries are not recognised as declarations and land in FILLER; see the census.

src/jvmMain/kotlin/afield/tools/docs/KotlinSourcePartition.kt — read the source

KotlinSourcePartition

object KotlinSourcePartition

Cuts Kotlin source into a verified tiling of declaration, KDoc and filler spans; see the file overview.

Declaration Summary
enum class Kind What a span of lines is, for a renderer deciding between prose and a code fence.
data class Span(val kind: Kind, val startLine: Int, val endLine: Int, val declarationLine: Int = 0, val declKind: String = "", val name: String = "", val bodyOpenLine: Int = 0, val bodyCloseLine: Int = 0, val children: List<Span> = emptyList()) One span of lines, 1-based and inclusive at both ends.
data class Partition(val lines: List<String>, val code: List<String>, val spans: List<Span>, val unterminated: List<Int>) A partitioned file: the original lines, the same lines with comments and string contents blanked out (code, which is what every syntactic decision here is made on), and the span tree.
data class Violation(val rule: String, val line: Int, val detail: String) One broken invariant. rule is the contract name, line where to look.
fun partition(text: String): Partition Partitions text. Never throws on malformed input; malformation shows up in verify.
fun reconstruct(partition: Partition): List<String> Walks the span tree and emits the lines it claims, in order — the inverse of partition.
fun verify(partition: Partition): List<Violation> Every invariant, checked. An empty list is the only acceptable result for repository source.
fun bracketBalance(code: List<String>, fromLine: Int, toLine: Int): Int Net () + [] + {} over fromLine..toLine of already-masked code.

MorphoDocsSupport.kt

Lets a documentation page hold MorphoHDL itself, rather than Kotlin that mentions it.

A fence written as data-lang=morpho shows its body as MorphoHDL, and public/docs_hooks.py wraps that body in a call into this file before the docs runner evaluates it, the same way it already wraps a nano or a forth fence. What the reader sees under the fence is that program compiled, grown out in full with its layout relaxing between expansions, and drawn.

The snippet is handled as a morpho box would handle it, down to using MorphoExecution's own directive parsing, so a program on the page and the same text pasted into a box behave alike. That includes the refusal: without a # main line there is no design to build and no width to build it at, so the snippet fails rather than guessing. A snippet that defines no cells of its own is given morpho's standard library, which is what makes a one-line program publishable. morpho_examples.md is the page that uses this.

src/jvmMain/kotlin/afield/tools/docs/MorphoDocsSupport.kt — read the source

Top-level functions

Declaration Summary
fun Box.renderMorphoDocsSnippet(source: String) Render a MorphoHDL snippet the way a morpho box would, for a data-lang=morpho docs fence.

ShaderBindingDocs.kt

Completion documentation for shader bindings, taken from the comments a box's preamble carries.

A trailing comment on a GLSL push-constant or structure member is copied onto the generated Kotlin accessor in the preamble; ShaderBindingDocs reads it back so completing that accessor shows the note.

src/jvmMain/kotlin/afield/tools/docs/ShaderBindingDocs.kt — read the source

ShaderBindingDocs

object ShaderBindingDocs

Documentation for the shader bindings a box's preamble declares, read back out of the preamble itself.

Declaration Summary
class Lookup internal constructor(private val byReceiver: Map<String, String>, private val byName: Map<String, String>) A preamble's binding comments, resolved once and then asked about each candidate row.
fun indexFor(box: Box): Lookup The bindings documented for a box, from its inherited preamble — the same text completion resolves against, so the lookup and the candidate list agree about what is in scope.
fun forPreamble(preamble: String): Lookup Parse a preamble text directly — for tests, and for any caller that already holds the text.

SourcePageGenerator.kt

Renders Kotlin source files as docs pages: KDoc as prose, declarations as headings, and the bodies in code fences numbered from the lines they really occupy.

What this is for

StdlibReferenceGenerator answers "what is already in the box?" — a symbol per row, a summary per symbol. It is a summary, and a reader who wants the next question answered ("what does it actually do?") has to leave the site. Meanwhile the published pages cite source constantly: path:line citations throughout the corpus, which are otherwise dead text. This is the destination those two facts imply — the place a citation can point at, and the place a reference row can lead.

Fences carry their real line numbers

A page that lifts KDoc out of the source and puts the rest in fences is only trustworthy if the fences still say where their code lives. So every fence carries linenums="N" set to its real first line, and verifyPage checks the published artifact rather than the intent: it parses the fences back out of the emitted markdown and asserts that each one is verbatim the source lines its linenums claims. Nothing else about this generator matters if that fails, which is why it runs over every page on every invocation rather than in a test somewhere.

The cut comes from KotlinSourcePartition, whose tiling invariant is what makes the fences able to account for the file in the first place; SourcePartitionGuard holds that end.

Why the pages are not committed

Every other generated artifact in this repo is committed and drift-checked, and this one is not, for a reason specific to what it contains: a source mirror changes whenever any line of its source changes. Committing it would double every diff in the repository — edit a comment in Functions.kt and a 10,000-line generated page changes with it — and make every source commit require a regeneration step to keep a guard green. The reference pages are safe to commit precisely because they summarise: they move when an API moves, not when a line does.

So these are built into the docs tree by tools/build_zensical_docs.py and never tracked. The cost is that a docs build without a compiled Kotlin tree has no source pages, which is handled where it shows: rewrite_source_citations in public/docs_hooks.py links a citation only when its page was generated, so the pages degrade to plain chips rather than to dead links.

Running it

./gradlew generateSourcePages                     # into build/source-pages
./gradlew checkSourcePages                        # verify only, no output kept

src/jvmMain/kotlin/afield/tools/docs/SourcePageGenerator.kt — read the source

SourcePageGenerator

object SourcePageGenerator

Generates and verifies the source-mirror docs pages; see the file overview.

Declaration Summary
const val SOURCE_REL Where the pages live inside the docs tree, and therefore what their URLs look like.
data class Page(val path: String, val source: String, val markdown: String, val lineCount: Int, val declarationCount: Int, val declarationLines: List<Int>, val citedBy: List<Pair<String, String>>) One rendered page, and everything needed to check and index it.
fun main(args: Array<String>)
fun pagePathFor(relativeSource: String): String The docs-relative page path for a source file.
fun verifyPage(repo: File, page: Page): List<String> Reads the fences back out of page's markdown and checks each against the source it claims.
fun docsUrlFor(relativeSource: String, line: Int? = null): String The docs URL for a source file, and optionally a line in it — the contract other code links by.
const val ANCHOR_INDEX Where the citation linker looks for the basename → page/anchor map.

SourcePartitionGuard.kt

Proves that every Kotlin file in the repository can be cut into spans losslessly, and measures how much of it the cutter actually understands.

The failure this exists to stop

A source page that lifts KDoc into prose, turns declarations into headers and numbers the code fences from their real source lines is correct only if the partition it was rendered from accounts for the whole file. Lose three lines and every fence below them is numbered wrong — on a published page, in a way that looks completely normal, and that a reader can only catch by opening the source they came here to avoid opening. The wrong page is well-formed, so review does not catch it either.

So the partition is checked against the file it came from, over the whole repository, before any page exists. KotlinSourcePartition.verify holds the invariants; this walks the corpus, adds the two checks that need more than one file's worth of context, and reports.

What it checks

  • The invariants — tile, nest, reconstruct, balance, terminated. See KotlinSourcePartition.
  • Agreement — every declaration KotlinSourceIndex.parseTopLevelDefinitions finds is the declarationLine of some DECLARATION span. Two independent recognisers, one corpus: a disagreement means one of them is wrong about real source, and it does not much matter which, because the reference pages and the completion panel are built on the index while the source pages would be built on this. They have to see the same declarations or the two halves of the site disagree.
  • The unattributed census, as a ratchet — every line the partition could not attribute to a declaration, a comment or a header lands in FILLER. That is a correctness-preserving fallback: the tiling still holds, no line is lost. It is not free, though: a FILLER line is a line a page can only show as anonymous code, so the FILLER lines that carry code are counted and pinned, and the pin falls whenever the scanner learns a shape and rises only with a reason. Blank lines land in FILLER too and are not counted: there is nothing for a page to lose.
  • Locals reported as API — a second ratchet counts the declarations KotlinSourceIndex reports from inside a function body, each of which the reference pages and the completion panel would otherwise publish as though it were public.

Running it

./gradlew checkSourcePartition                        # also part of check and docsGates
./gradlew checkSourcePartition --args=--verbose       # …and the per-file and per-shape breakdowns
Needs no Field server, no GPU and no network: it reads source text. Sibling of KDocAttachmentGuard, which guards the other half of the same pipeline: that a KDoc block reaches the declaration its author meant.

src/jvmMain/kotlin/afield/tools/docs/SourcePartitionGuard.kt — read the source

SourcePartitionGuard

object SourcePartitionGuard

Checks every Kotlin file in the repository against the span partition a source page renders.

Declaration Summary
fun main(args: Array<String>)

StdlibReferenceGenerator.kt

Emits Field's codebase reference — the browsable answer to "what is already in the box?" — from the KDoc that lives next to the code.

Why this exists, and why it is not Dokka

Dokka was removed in 20e64da5, because the durable facts belong in KDoc beside the code and a generated wall of one-page-per-symbol HTML is not documentation anyone reads. Field already has two interactive ways into that KDoc — the completion panel's recursive code browser (public/docs/completion_doc_browser.md) and the semantic index — but both need you to already know the name you are looking for. Nothing answered the browsing question. This generator does, with ordinary Zensical markdown inside the existing docs site: a page per completion category, a section per source file, the file's own KDoc as the orientation prose, and a table per type.

One parser, not two

Declarations come from KotlinSourceIndex.parseTopLevelDefinitions — the same scan that feeds the in-app completion doc browser — so a symbol listed here is by construction a symbol the doc panel can resolve. Only the signature text is re-read from source here (the index stores names and lines, not signatures); see signatureAt.

What it will not invent

  • Every summary is the first paragraph of a real KDoc comment; a symbol with no KDoc is listed with an empty summary rather than a guessed one.
  • A KDoc reference in square brackets is linkified only when the target is a type this reference actually emits — the same "links only appear when they resolve" rule the completion panel follows. Everything else degrades to plain inline code.
  • The "see also" links from a reference section to a worked-example page are evidence-based: a page is linked only when it literally mentions MIN_EVIDENCE_HITS or more distinct public symbol names from that source file (see evidenceLinks). No hand-maintained topic map to rot.

File overview rule

The first KDoc block in a file is treated as the file's overview when it ends before the first declaration and is separated from that declaration by at least one blank or comment line — and it is then not also shown as that declaration's own summary. A block written flush against its declaration stays that declaration's own KDoc, of which only the first paragraph is published; the file's "what it is" cell on the index then has nothing to fill it with.

Running it

Regenerate the committed pages (needs no Field server, no GPU, no network):

./gradlew generateCodebaseReference
Run with no output directory and it emits to a temp dir and then drift-checks the committed pages byte-for-byte instead — which is what the docs.codebase-reference.generate FIELD_TEST does, so an API or KDoc change that nobody regenerated fails the build. This mirrors afield.examples.AgentTranscriptMaterializer and the tools/linalg-generator --check mode that guards public/docs/linear_algebra_api.md.

The pages are committed rather than built on the fly: the Zensical docs build must not gain a JVM step, and a generated reference is only trustworthy if an API change shows up as a reviewable diff.

src/jvmMain/kotlin/afield/tools/docs/StdlibReferenceGenerator.kt — read the source

StdlibReferenceGenerator

object StdlibReferenceGenerator

Emits public/docs/reference/ from the KDoc in src/, or drift-checks the committed pages.

Declaration Summary
fun main(args: Array<String>)

canvasFlightRecording.kt

Filming a canvas flight to MP4, one frame at a time.

recordCanvasFlight drives the flight's clock in the capture page (the server's Playwright page, or the Electron window after playwright-open --electron): for each frame it sets the time (window.fieldCanvasFlight.seek, CanvasFlightClient in the frontend), waits until the browser has painted that frame, captures it over CDP and pipes the JPEG to ffmpeg. Nothing runs on the wall clock, so a frame that takes a second to capture is the same frame as one that takes ten milliseconds, and a loaded machine films the same movie as an idle one. Live screen capture would drop frames instead.

The page is sized for the film first (width by height pixels at pixelRatio, through CDP device metrics) and put back afterwards. The report says how many frames were captured and encoded (counted again by ffprobe), and for every stop the frame that reaches it and the camera in that frame.

src/jvmMain/kotlin/afield/tools/docs/canvasFlightRecording.kt — read the source

Top-level functions

Declaration Summary
fun documentBookmarks(info: SessionInformation): BookmarkData This document's bookmarks, as the root box's bookmarks property holds them.
fun canvasFlightProblem(info: SessionInformation, spec: CanvasFlightSpec): String? The first problem with flying spec in info's document, or null: a bad value or a bookmark it lacks.
suspend fun recordCanvasFlight(info: SessionInformation, request: AgentCanvasFlight): AgentCanvasFlightResponse Films request's flight in info's document to request.path. Runs on the Playwright thread; opens the capture page on the document if it is not there already.

doc.kt

Exports a Field document to documentation pages, and the capture helpers that export uses.

exportToDocumentation walks the boxes under a root that carry documentation metadata and writes Markdown pages with their captured assets into a docs directory. splitKotlinIntoChunks and CommentCommandRewriter cut box code into the pieces a page shows, and format expands a box's text into page Markdown.

The rest of the file is capture plumbing: processPng for still images, Window.raiseAndScreenshot, Window.raiseAndRecord and captureGlfwWindow for native renderer windows, and an ffmpeg layer (FFMPEG, runProcessLogged, detectCropBox, encodeWithCropAndMargin) for recordings.

src/jvmMain/kotlin/afield/tools/docs/doc.kt — read the source

Top-level functions

Declaration Summary
fun coerceToDouble(orDefault: Any?): Double
suspend fun exportToDocumentation(root: Box = thisSessionInfo.get().root, baseDirectory: String, subDirectory: String, docName: String) the idea here is that we have two kinds of ways of 'exporting' a Field + Workspace based on metadata associated with the boxes, one for humans and one for AI (incrementally resyncable)
suspend fun format(inside: Box, s: String, baseDir: String, allocateFilename: () -> String): String
fun splitKotlinIntoChunks(src: String): List<Chunk>
fun processPng(inputPath: String, bottomCropPx: Int = 100, borderMarginPx: Int = 20, topCropPx: Int = 0, leftCropPx: Int = 0, rightCropPx: Int = 0)
fun captureRectToPng(rect: Rectangle, out: File)
fun activateThisProcessMac(processName: String)
suspend fun Window.raiseAndScreenshot(waitFrames: Int, outPath: String, captureTimeoutMs: Long = 15_000L, preferDirectCapture: Boolean = true)
suspend fun Window.recordDirect(durationSec: Double, outPath: String, fps: Int = 20, width: Int = 1280, inFlight: Int = 3) Films this window for durationSec seconds into the MP4 at outPath, by direct swapchain copies like raiseAndScreenshot: no screen recording, no window raise, so nothing touches AppKit off its thread. At most inFlight copies are outstanding at once, since each holds a staging buffer the size of the swapchain and a frame serves every pending request; a copy is requested at most every 1/fps s. The frames are encoded at the rate actually reached, so the video plays in real time, scaled to width.
suspend fun Window.raiseAndRecord(waitFrames: Int, outPath: String)
suspend fun captureGlfwWindow(window: Long, callback: suspend (Int, Int, Int, Int) -> Unit)
suspend fun runProcessLogged(args: List<String>, onStdoutLine: ((String) -> Unit)? = null, onStderrLine: ((String) -> Unit)? = null, charset: Charset = Charsets.UTF_8): ProcResult
suspend fun recordProcessReturnMp4WithScreencaptureAsync(x: Int, y: Int, w: Int, h: Int, out: Path, durationSec: Int = 5, margin: Int = 20, displayIndex: Int = 1, showClicks: Boolean = false, captureAudio: Boolean = false, scope: CoroutineScope = CoroutineScope(Dispatchers.IO)): Path
fun detectCropBox(input: Path, probeSeconds: Int = 4, limit: Int = 5, round: Int = 16): CropBox
fun encodeWithCropAndMargin(input: Path, output: Path, crop: CropBox, margin: Int = 20, crf: Int = 20, preset: String = "veryfast", includeAudio: Boolean = false)

Top-level values

Declaration Summary
val FFMPEG: String by lazy Path of the ffmpeg executable: an AFIELD_FFMPEG-style environment override, a standard install location, PATH, or bare ffmpeg.

CommentCommandRewriter

object CommentCommandRewriter

Declaration Summary
data class Options(val skipInsideFencedCodeBlocks: Boolean = true)
suspend fun apply(chunks: List<Chunk>, options: Options = Options(), replacer: suspend (command: String, parameter: String) -> String): List<Chunk>
suspend fun Regex.replaceSuspend(input: CharSequence, transform: suspend (MatchResult) -> CharSequence): String

ChunkType

enum class ChunkType

Constants: CODE, COMMENT.

CommentStyle

enum class CommentStyle

Constants: LINE, BLOCK.

Chunk

data class Chunk(val type: ChunkType, val text: String, val indentUnits: Int? = null, val style: CommentStyle? = null)

Declaration Summary
val type: ChunkType, val text: String, val indentUnits: Int?
val style: CommentStyle?

CropBox

data class CropBox(val w: Int, val h: Int, val x: Int, val y: Int)

ProcResult

data class ProcResult(val exitCode: Int, val out: String, val err: String)

KDocExtractor.kt

Reads the KDoc comments out of a Kotlin source file and attaches each one to the declaration below it.

KDocExtractor.extractKDocs returns one KDocComment per block, with its line span and the name and kind of the declaration it documents. The completion doc panel, KotlinSourceIndex and the generated reference pages all read summaries through it. A block attaches to the first following line that parseDeclaration recognises; KDocAttachmentGuard reports blocks that would attach to the wrong declaration.

src/jvmMain/kotlin/afield/tools/docs/index/KDocExtractor.kt — read the source

KDocComment

data class KDocComment(val text: String, val startLine: Int, val endLine: Int, val declarationLine: Int, val declarationName: String?, val declarationType: String?)

Data class representing a KDoc comment found in source code

Declaration Summary
val text: String
val startLine: Int
val endLine: Int
val declarationLine: Int
val declarationName: String?
val declarationType: String?

KDocExtractor

object KDocExtractor

Utility object for extracting KDoc comments from Kotlin source files

Declaration Summary
fun extractKDocs(sourceText: String): List<KDocComment> Extract all KDoc comments from a Kotlin source file

pageFilm.kt

Filming any web page frame by frame: the page filmer behind film-page on the backdoor.

The canvas flight (recordCanvasFlight) drives Field's own canvas. Some things worth filming are other pages: a chat lens exported to a standalone HTML file, a three.js view on its own page. filmPage opens the page in a capture context of its own (so the shared Field page is untouched), runs a setup script, then steps a frame script through the film's clock, waiting for each frame to paint before it captures it and pipes it to ffmpeg, exactly as the flight does. Each script's answer is JSON text ("null" for none): evaluateWithin waits for a truthy answer, and an undefined would run it again.

src/jvmMain/kotlin/afield/tools/docs/pageFilm.kt — read the source

Top-level functions

Declaration Summary
suspend fun filmPage(request: AgentPageFilm): AgentPageFilmResponse Films request's page to its path. Runs on the Playwright thread.

screenshot.kt

Screenshot and video capture of Field's browser UI, used by the backdoor and the docs runner.

Captures go through the server's shared Playwright page. captureBoxScreenshot and captureWindowScreenshot answer the agent capture requests for one box's output or the whole window, openEditorForCapture opens a box's editor first so inline visualizers are on screen, and captureBoxVideo and captureWindowVideo record frames over CDP and encode them to MP4 with ffmpeg. Before each capture a preparation script waits for the page to settle, with every wait bounded so that a page that never paints fails with a message naming what was pending. trimUniformEdgePixels crops uniform borders from the result.

DocsUiDriver is the object a docs drive script uses to act on the page during a recording, and DocsUiRecorder runs the recording beside it. Box.screenshot is the box-code entry point.

src/jvmMain/kotlin/afield/tools/docs/screenshot.kt — read the source

Top-level functions

Declaration Summary
fun trimUniformEdgePixels(path: Path): UniformEdgeTrimResult
fun performPlaywrightInput(info: SessionInformation, request: AgentPlaywrightInputRequest): AgentPlaywrightInputResponse
suspend fun Box.screenshot(fn: String, padding: Double = 12.0, panIntoView: Boolean = true, hideMonaco: Boolean = true, openOutputs: Boolean = false): Boolean Geometry of the whole box, for captures whose content is the editor rather than a panel.
suspend fun captureWindowScreenshot(info: SessionInformation, request: AgentWindowScreenshot): AgentScreenshotResponse
suspend fun openEditorForCapture(info: SessionInformation, request: AgentOpenEditorRequest): AgentOpenEditorResponse
suspend fun captureBoxScreenshot(info: SessionInformation, request: AgentBoxScreenshot): AgentScreenshotResponse
suspend fun captureWindowVideo(info: SessionInformation, request: AgentWindowVideo): AgentVideoResponse Record a window-level video (full page or editor-only clip) using CDP Page.startScreencast, then encode the JPEG frames to an H.264 MP4 via ffmpeg.
suspend fun captureBoxVideo(info: SessionInformation, request: AgentBoxVideo): AgentVideoResponse Record a box-level video (clipped to the box region).

UniformEdgeTrimResult

data class UniformEdgeTrimResult(val trimmed: Boolean, val left: Int = 0, val top: Int = 0, val right: Int = 0, val bottom: Int = 0)

Declaration Summary
val trimmed: Boolean
val left: Int
val top: Int
val right: Int
val bottom: Int

DocsUiRecorder

class DocsUiRecorder(private val page: Page, private val clip: Rect?, private val fps: Int, private val clipAtEnd: (() -> Rect?)? = null)

Interactive recorder. Unlike recordScreencastToMp4 this exposes explicit start/stop so a scripted sequence of UI actions can run while recording.

Declaration Summary
fun start()
fun markScriptStart() The drive script has begun to run. Recording starts before the fragment is compiled, so everything before this is the page sitting still while Kotlin compiles (2–4 s for the survival-guide intro); stopAndEncode keeps only half a second of it as a lead-in.
suspend fun stopAndEncode(outputPath: Path): Int Stop the loop, encode collected frames to outputPath, return frame count.
val frameCount: Int
var discarded: Boolean True once the drive's script has filmed its own video (DocsUiDriver.filmCanvasFlight): nothing to encode.
suspend fun discard() Stop the loop and drop its frames: the drive's video is being written another way.

DocsUiDriver

class DocsUiDriver internal constructor(private val pageProvider: () -> Page)

The single object a drive script touches. Every method marshals onto the Playwright dispatcher and blocks the calling (main runloop) thread for the result, so scripts never touch the thread-bound Page directly. wait sleeps on the calling thread, leaving the Playwright thread free to keep grabbing recorder frames.

Declaration Summary
fun scriptStarted() Called by the drive op's prelude, the script's first statement.
fun filmCanvasFlight(path: String, stops: List<String>, legSeconds: Double = 6.0, holdSeconds: Double = 2.0, orbitDegreesPerSecond: Double = 8.0, fps: Int = 30, width: Int? = null, height: Int? = null, pixelRatio: Double = 2.0): afield.app.protocol.AgentCanvasFlightResponse Films a canvas flight through stops to path (the drive's video, vid in the script) with the recorder box code's canvasFlight(record = ...) uses, frame by frame, instead of recording the drive in real time. width and height null keep the window's size; the call returns once the film is written, and fails if the film came out short or missed a stop.
fun press(key: String)
fun key(key: String)
fun down(key: String)
fun up(key: String)
fun type(text: String)
fun click(uid: String)
fun clickSelector(selector: String)
fun hover(uid: String)
fun hoverSelector(selector: String)
fun drag(uid: String, dx: Double, dy: Double, steps: Int = 12, hold: Boolean = false)
fun mouseUp()
fun mouseDown() Press the real mouse where it is now; with moveMouse and mouseUp, a drag that starts anywhere (a box edge, to resize).
fun moveMouse(x: Double, y: Double, steps: Int = 1) Move the real mouse to (x, y) in CSS px, in steps steps, as a hand would (canvas hover state, n's drop point).
fun clickAt(x: Double, y: Double, vararg modifiers: String) Click the real mouse at (x, y) in CSS px, with the modifiers ("Alt", "Shift", ...) held.
fun doubleClickAt(x: Double, y: Double) Double-click the real mouse at (x, y) in CSS px: adding a key in the keyframe editor, say.
fun wheel(x: Double, y: Double, dx: Double, dy: Double, vararg modifiers: String) Scroll the real wheel by (dx, dy) at (x, y) in CSS px, with the modifiers held: "Control" for a pinch-style zoom, say.
fun rightClickAt(x: Double, y: Double) Right-click the real mouse at (x, y) in CSS px, to open a context menu.
fun typeSlowly(text: String, delayMs: Long = 70L) Type text one key at a time, delayMs apart, so a recording shows the typing. Each key is its own trip to the Playwright thread and the wait between keys is pause, so the recorder (on that same thread) keeps grabbing frames and the runloop keeps turning; one long keyboard.type(text, delay) would hold the Playwright thread and freeze the video.
fun eval(script: String): JsonElement?
var keypressDisplayTouched: Boolean True once keypressDisplay has switched the display on; the drive op switches it off again after the script, because the capture page outlives one fence and the next capture would otherwise show its keys too.
fun keypressDisplay(on: Boolean = true) Switch Field's keypress display (the palette's Toggle Keypress Display, keycast.js) on or off, without its toast, so a recording shows each key and click as a caption over the canvas.
fun editorHeight(px: Int) Make the editor pane px CSS pixels tall, as dragging its edge would, and lay the editor out again. Screenshot prep sizes the pane for a window capture (840–1200 px), and the note beside the cursor sits at the pane's foot, so a short program's figure is mostly empty pane between the code and its note; a pane sized to the figure keeps them together. It lasts until the next screenshot prep re-sizes the pane, so take the still inside the script — a clip-selector fence's ui.shotClip does — rather than with the op's own capture, which re-preps.
fun openEditor(uid: String, property: String = "code") Open (and pan to) a box's code editor, reusing the screenshot editor-prep path.
fun shot(path: String) Screenshot the whole page to path now (script-timed — use for transient states).
val clipFailures: MutableList<String> Selectors a shotClip could not find, in call order. The drive op reads this after the script and fails the capture, because a thrown error inside a drive fragment does not by itself stop the REPL from running the next statement.
fun shotClip(path: String, selector: String, padding: Double = 8.0) Screenshot, clipped to a CSS selector's bounds, to path now. Use for overlays that close on blur (command palette, popups, AI panels) — capture while still open.
fun waitForSelector(selector: String, timeoutMs: Double = 15000.0) Block until selector is visible (or timeoutMs elapses), so a capture waits for an async panel to populate instead of guessing a fixed pause. E.g. wait for text=Creates a mutable after Control+Space so the autocomplete docs have rendered. Swallows the timeout — the caller captures whatever state exists.
fun pause(ms: Double)+2 overloads · also Long, Int

util_docs.kt

src/jvmMain/kotlin/afield/tools/docs/util_docs.kt — read the source

Doc

annotation class Doc(val value: String)

EffectiveParameter.kt

A bounded scalar parameter that keeps the value it was asked for apart from the value it uses.

EffectiveParameter stores the requested value as written and reports EffectiveParameter.effective as that value coerced into minimum..maximum, with EffectiveParameter.clamped saying whether the two differ. EffectiveParameter.snapshot reads both at once into a ParameterValue. Nothing in src/jvmMain constructs one yet; EffectiveParameterTest covers the behaviour.

src/jvmMain/kotlin/afield/ui/controls/EffectiveParameter.kt — read the source

EffectiveParameter

class EffectiveParameter(val name: String, initial: Double, val minimum: Double, val maximum: Double, val units: String = "", val description: String = "")

A named finite scalar with explicit requested/effective values and units.

Declaration Summary
val name: String
val minimum: Double
val maximum: Double
val units: String
val description: String
var requested: Double
val effective: Double
val clamped: Boolean
fun snapshot(): ParameterValue

ParameterValue

data class ParameterValue(val name: String, val requested: Double, val effective: Double, val minimum: Double, val maximum: Double, val units: String, val description: String)

Declaration Summary
val name: String, val requested: Double, val effective: Double
val minimum: Double, val maximum: Double, val units: String, val description: String

LastRunNotes.kt

The record of what each line of a box produced on its last run, read back as line notes for Forth, Clojure and Python.

src/jvmMain/kotlin/afield/ui/output/LastRunNotes.kt — read the source

Top-level functions

Declaration Summary
fun installLastRunLineNotes() Line notes for every box whose runtime records its runs (LastRunNotes) — Forth, Clojure and Python boxes — on the code property they keep their programs in. A box nothing recorded for is left to the other providers.

LastRunNotes

object LastRunNotes

What each line of a box produced the last time it ran, kept so the editor can show it as a line note (LineNotes) without running anything.

Declaration Summary
class Produced(val firstLine: Int, val lastLine: Int, val caption: String, val png: ByteArray? = null, val value: List<LineNotes.Segment> = emptyList(), val picture: (() -> ByteArray?)? = null, val imageScale: Int = 1, val besidePicture: List<LineNotes.Segment>? = null) What one step of a run produced: its lines firstLine..lastLine (1-based, counted in the text that ran), a sentence about it, and a picture when the language draws one.
fun record(box: Box, property: String, code: String, lineOffset: Int, produced: List<Produced>, stoppedAt: Int? = null, stopReason: String? = null, notReached: Collection<Int> = emptyList(), at: Long = System.currentTimeMillis()) Record a run of code, which sits in box's property starting after box line lineOffset (0-based, as Execute.line counts; 0 for a whole-box run). produced is what its steps came to, stoppedAt the run-relative line it failed on (with stopReason) or null, and notReached run-relative lines the runtime knows it never got to. A run replaces what an earlier run recorded for any of the lines it covered, and leaves the rest — a fragment run updates only the fragment's lines.
fun forget(box: Box, property: String = "code") Forget everything recorded for box's property.
fun note(box: Box, property: String, source: String, line: Int, now: Long = System.currentTimeMillis()): LineNotes.Note? The note for line (1-based) of source, the text of box's property as the editor holds it now, from the last run's record; null when the record says nothing about that line. now is the clock the note's age is read from.
fun ago(millis: Long): String "just now", "12 s ago", "4 min ago", "3 h ago", "2 days ago".
fun oneLine(text: String, limit: Int = 120): String text on one line, cut to limit characters.

LineNotePictures.kt

Java2D drawings of a stack or a relation, sized for a line note.

src/jvmMain/kotlin/afield/ui/output/LineNotePictures.kt — read the source

LineNotePictures

object LineNotePictures

The small pictures line notes (LineNotes) draw for languages whose values are not images: a stack, drawn as cells with its top uppermost, and a relation, drawn as a table. Drawn in Java2D on an off-screen image, so they need no window or GPU.

Declaration Summary
const val SHOWN How many cells of a stack, or rows of a table, a picture shows before it says how many more there are.
const val SCALE Device pixels per CSS pixel: the pictures are drawn at twice their size, so a Retina display shows them sharp rather than upscaled. A note carrying one says so (LineNotes.Note.imageScale).
fun stack(values: List<String>, depth: Int = values.size): ByteArray A stack of values, bottom first as a stack is usually listed; the top is drawn uppermost and outlined. An empty stack is drawn as an empty tray. values may be only the top of a stack depth deep, and the picture says how many more are below.
fun table(columns: List<String>, rows: List<List<String>>, total: Int = rows.size): ByteArray A table of rows under columns, showing at most SHOWN rows and saying how many of total it left out.

LineNoteTools.kt

The AI tool that lets a chat read a program's line notes: ReadLineNotes asks for the notes on some lines of the focused box's property, and readLineNotes answers with text and pictures. Reading a note never runs the program.

src/jvmMain/kotlin/afield/ui/output/LineNoteTools.kt — read the source

Top-level functions

Declaration Summary
fun installLineNoteTools() Offer ReadLineNotes to a conversation focused on a program some language gives notes for — not a plain Kotlin box's code, which only has them inside an embedded StackSL or RectSL string and would carry the schema on every turn for nothing.
fun readLineNotes(box: afield.document.graph.Box, property: String, lines: List<Int>): ReadLineNotesResult ReadLineNotes for box's property as it holds it now: the note for each of lines (at most 12).

ReadLineNotes

data class ReadLineNotes(@JsonPropertyDescription("1-based line numbers of the focused property's current text; at most 12.") val lines: List<Int>)

The AI4 tool that reads a program's line notes (LineNotes) — what the editor shows beside the cursor.

Declaration Summary
val lines: List<Int>

LineNoteAnswer

data class LineNoteAnswer(val line: Int, val text: String, val caption: String?, val hasPicture: Boolean)

One line's note, as ReadLineNotes answers it.

ReadLineNotesResult

data class ReadLineNotesResult(val box: String, val property: String, val notes: List<LineNoteAnswer>, @com.fasterxml.jackson.annotation.JsonIgnore override val toolImages: List<ToolImage> = emptyList()) : CarriesToolImages, SummarisesForToolLog

ReadLineNotes's answer: a note per line asked about (null caption: nothing to say), pictures as images.

Declaration Summary
val box: String
val property: String
val notes: List<LineNoteAnswer>
override val toolImages: List<ToolImage>
override fun toolLogSummary(): String

LineNotes.kt

Line notes: the sentence, and sometimes a picture, that the editor shows for one line of a box's program.

Languages register a LineNotes.Provider for the property their programs live in; the editor asks through a line-note request.

src/jvmMain/kotlin/afield/ui/output/LineNotes.kt — read the source

LineNotes

object LineNotes

What a box's program shows at one of its lines — a sentence, and a picture when there is one — offered wherever the editor looks at a line: at the top of the completion popup opened on it, and in the notes beside the cursor.

Declaration Summary
class Note(val caption: String, val png: ByteArray? = null, val volatile: Boolean = false, val meta: String = "", val value: List<Segment> = emptyList(), val imageScale: Int = 1) A line's note: caption a sentence about it, png a small picture of what it evaluates to, or null. volatile says the note can change while the text does not — a last run's value, which the next run replaces, or its age — so the editor asks again rather than keeping it against the text.
class Segment(val text: String, val strong: Boolean = false) A run of a note's Note.value: strong for a result — a number, a stack value, an answer count.
fun plain(text: String) A plain run of a note's value.
fun strong(text: String) A result in a note's value, which the editor sets in bold.
fun interface Provider Describes line (1-based) of source, the text of box's property as the editor holds it; null when it has nothing to say.
fun register(property: String, applies: (Box) -> Boolean = { true }, provider: Provider) Offer notes for property of the boxes applies accepts. Registrations that accept a box are asked newest first, and the first note wins; a provider with nothing to say returns null and the next is asked. Providers are called off the main thread, one at a time, and may take a moment — a picture is usually rendered — so they should cache.
fun offersFor(box: Box, property: String): Boolean Whether any provider takes box's property — whether asking for its notes can say anything at all.
fun note(box: Box, property: String, source: String, line: Int): Note? The note for line of box's property, holding source; null when no provider takes the box or none has anything to say.

OutputRequests.kt

JSON request bindings that let a box's output iframe, or Kotlin code, call a handler on that box over Field's WebSocket.

src/jvmMain/kotlin/afield/ui/output/OutputRequests.kt — read the source

Top-level functions

Declaration Summary
fun Box.outputRequests(name: String, handler: suspend (JsonElement) -> JsonElement): OutputRequestBinding Bind an output iframe's JSON requests to this box, using Field's existing WebSocket server. Import /output_requests.js in the iframe and call outputRequests(binding.id).request(data). Kotlin tools call the same binding's suspend OutputRequestBinding.request directly.
fun Box.outputRequests(name: String): OutputRequestBinding Look up a live named request binding on this exact box in this document.
fun installOutputRequestHandler()

OutputRequestBinding

class OutputRequestBinding internal constructor(private val owner: Box, val name: String, dispatcher: CoroutineContext, private val handler: suspend (JsonElement) -> JsonElement) : AutoCloseable

One box's asynchronous, serialized JSON callback, shared by browser tools and Kotlin callers.

Declaration Summary
val id: String Opaque, run-specific browser capability; never persists a JVM callback or opens a server.
suspend fun request(data: JsonElement = JsonNull): JsonElement Await the same serialized callback used by this binding's browser clients, without HTTP.
override fun close()

stdlib_output.kt

HTML-safe syntax colouring for a value printed into a box output.

highlightSyntax escapes a string and wraps its numbers, punctuation and brackets in spans, so the result reads as code rather than as prose. It is purely lexical and language-agnostic: it finds floats, integers, single punctuation characters and brackets, and knows nothing of keywords, strings or comments. Everything else is escaped, so the result is always safe to drop into HTML. String.h and String.ss are the same call, shorter.

One of the stdlib files, which the intro preamble star-imports into every box, so everything here can be called unqualified, with Box as the implicit receiver.

src/jvmMain/kotlin/afield/ui/output/stdlib_output.kt — read the source

Top-level functions

Declaration Summary
fun highlightSyntax(input: String): String HTML-escapes a string and wraps its numbers, punctuation and brackets in <span class='ss-…'>, so a value printed into a box output reads as code rather than as prose.

Top-level values

Declaration Summary
val String.h: String This string highlightSyntax-ed — HTML-escaped with numbers and punctuation span-wrapped.
val String.ss: String Alias for String.h: this string highlightSyntax-ed, ready to print into a box output.

GeometryReviewSurface.kt

A persistent annotation inbox for a custom canvas, the same one a Three.js viewer has: Box.geometryReviewSurface binds it to a box and returns a GeometryReviewSurface.

src/jvmMain/kotlin/afield/ui/visualization/GeometryReviewSurface.kt — read the source

Top-level functions

Declaration Summary
fun Box.geometryReviewSurface(name: String, revision: String, outputBox: Box = this): GeometryReviewSurface Give a custom canvas the same persistent annotation inbox as a Three.js viewer.

GeometryReviewSurface

class GeometryReviewSurface internal constructor(internal val owner: Box, val name: String, val uid: String, internal val outputBox: Box, private val onOwner: (() -> JsonObject) -> JsonObject) : AutoCloseable

A custom canvas's review binding; writes dispatch to its Field document's owner thread.

Declaration Summary
fun updateRevision(revision: String) Invalidate the current selection when the authored geometry changes; retain saved notes.
fun reviews(includeResolved: Boolean = false): JsonObject Read notes without acknowledging them or changing the human selection.
fun receive(record: JsonObject, saveNote: Boolean = true): JsonObject Receive a revision-labelled selection or save a note with its optional context/crop evidence.
fun updateNote(request: JsonObject): JsonObject Append a reply or explicitly resolve/reopen, with the current revision and note version.
override fun close()