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
A test verdict for one exact registered source revision.
ToolCallEvidence
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
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
Description of one tool supplied by a pool entry. The source remains ordinary box Kotlin.
ToolPoolEntry
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
A bounded, server-side record of what each box has said: printed text, error diagnostics, display(...) channels, and execution verdicts.
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
The local history of code Field compiles and runs, stored in SQLite and summarised for the code-use panels and agent tools.
CodeUseSnippetRow
CodeUseLaunchRow
CodeUseFailureRow
CodeUseUnresolvedRow
| Declaration | Summary |
|---|---|
val id: Long |
|
val createdAtEpochMs: Long |
|
val snippetId: String |
|
val text: String |
|
val line: Int |
|
val column: Int |
|
val reason: String |
CodeUseSymbolRow
CodeUseSourceTarget
| Declaration | Summary |
|---|---|
val symbolName: String |
|
val kind: String |
|
val sourceFile: String |
|
val line: Int |
|
val confidence: String |
CodeUseSnippetUseRow
CodeUseSourceReferenceRow
| Declaration | Summary |
|---|---|
val symbolName: String |
|
val kind: String |
|
val sourceFile: String |
|
val line: Int |
|
val score: Int |
|
val confidence: String |
|
val evidence: String |
CodeUseHotSourceSymbolRow
CodeUseEarlySourceSymbolRow
CodeUseRelatedSourceRow
CodeUseSemanticAnalyzeRequest
CodeUseSemanticUseRow
CodeUseSemanticUnresolvedRow
| Declaration | Summary |
|---|---|
val text: String |
|
val line: Int |
|
val column: Int |
|
val reason: String |
CodeUseSemanticAnalyzeResult
CodeUseTelemetryService
object CodeUseTelemetryService
Asynchronous, best-effort history of code compiled and run by Field.
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
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
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
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
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.
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
The JDI probe helper: a separate JVM that attaches to Field over JDWP and samples a variable at a source line.
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
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
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
| Declaration | Summary |
|---|---|
val requestId: String |
ProbeVariableRequest
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
| Declaration | Summary |
|---|---|
override val requestId: String |
|
val error: String |
|
val detail: String? |
CompleteResponse
| 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
| 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
| Declaration | Summary |
|---|---|
val symbols: List<ScopeSymbolPayload> |
|
val omittedCount: Int |
SampleResponse
| 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
| 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
A timestamped append-only log of document save and reload events, written to ~/.afield/logs/save_reload.log.
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
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
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
| 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
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
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
One request body for every kit route; each reads the fields its own action needs.
KitLibraryResponse
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
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
| 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
Top-level values
SessionInformation
| 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
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
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
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
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
Wrapper: keep all inspector logic inside this method. If null, treat as null.
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
| 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
| Declaration | Summary |
|---|---|
override fun heading(): String |
|
override fun html(): String |
|
override fun version(): Long |
Heap
| 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
| Declaration | Summary |
|---|---|
override fun heading(): String |
|
override fun html(): String |
AdapterFn
typealias AdapterFn = (Any, Browsable?, String?) -> Browsable?
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
| 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 checkoutdocsImprovementCheckoutnames. 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
| 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
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.
DocsAnnotation
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
One request from the page. Only the fields its action uses are read, and all of them are checked.
DocsPassJob
| Declaration | Summary |
|---|---|
val name: String |
|
val running: Boolean |
|
val startedAtEpochMs: Long |
|
val finishedAtEpochMs: Long |
|
val exitCode: Int? |
|
val logTail: String |
|
val logFile: String |
DocsPassStatusResponse
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
| 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
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
Top-level values
| Declaration | Summary |
|---|---|
val knownDebugInspectors |
TypeDef
ExtractResult
| Declaration | Summary |
|---|---|
val data: Any |
|
val typeDefs: List<TypeDef>, val setters: MutableMap<String, MutableList<(Any?) -> Unit>> |
DebugSocketBridge
| 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
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.
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
| 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
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
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
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:
- personal really is off
-cp(otherwise the layer is decorative: the classes would resolve from the app loader anyway), PersonalLayer.wrapis idempotent (Server.main installs the layer as the context classloader, so KI2'swrap(contextClassLoader)must not stack a second layer on top of the first),- 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
| 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:
scanForStaleClassesis a shallow, best-effort look at what an engine is holding, so the common case — a box with a personal object in a top-levelval— gets caught and reset up front. It is incomplete; its own docs list what it does not look at.staleLayerDiagnosiscatches 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
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
~/.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
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
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
"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
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
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
The namespace behind Box.why; its members are the val Why.<name> by Whying() lines prop writes.
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
| 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
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
Reads and writes a document's history_metrics.edn: activity per save, edit statistics per version, and screenshot references.
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
Reads, compares, searches, and restores the saved versions of one document directory.
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
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>
Vector3fSerializer
object Vector3fSerializer : KSerializer<Vector3f>
JSON serializer for Field's mutable Vector3f, encoded as [x, y, z] floats.
Vector4fSerializer
object Vector4fSerializer : KSerializer<Vector4f>
QuaternionfSerializer
object QuaternionfSerializer : KSerializer<Quaternionf>
DualQuaternionfSerializer
object DualQuaternionfSerializer : KSerializer<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
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
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
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
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
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
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
Compatibility for an existing project launcher. Its original workspace and runtime remain authoritative.
| Declaration | Summary |
|---|---|
val environment: Map<String, String> |
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
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
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.
KitInstanceInspection
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
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
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
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
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
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
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
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
The phase Welcome is showing: idle, building, restarting or failed, with its log path.
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
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
The ordinary Field document and its project's shared parent/resources/preferences.
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
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
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
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
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
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.
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
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
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
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
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
ProjectEntry
| Declaration | Summary |
|---|---|
val name: String |
|
val documents: List<String> |
|
val sizeBytes: Long |
|
val lastModifiedEpochMs: Long |
|
val lastModifiedAgoMs: Long |
|
val kind: String |
RunningDocumentEntry
| Declaration | Summary |
|---|---|
val documentName: String |
|
val sessionCount: Int |
|
val ephemeral: Boolean |
|
val openUrl: String |
GcPauseStats
RunloopTimingStats
RunloopSchedulingStatus
RuntimeAotCacheStatus
| Declaration | Summary |
|---|---|
val state: String |
|
val summary: String |
|
val cachePath: String |
|
val autoGenerate: Boolean |
|
val attempted: Boolean |
|
val detail: String |
RuntimeInfo
CompletionDocDraftSummary
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.
AiToolUseLogSummary
AgentWorktreeProposal
AgentWorktreeActionRequest
data class AgentWorktreeActionRequest(val id: String)
| Declaration | Summary |
|---|---|
val id: String |
AgentWorktreeActionResponse
| Declaration | Summary |
|---|---|
val success: Boolean |
|
val proposal: AgentWorktreeProposal? |
|
val message: String |
AiToolUseImprovementStatus
| 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
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
MavenDependencyEntry
| Declaration | Summary |
|---|---|
val coordinate: String |
|
val resolved: Boolean |
|
val jarCount: Int |
|
val latestVersion: String |
|
val status: String |
|
val resolvedAtEpochMs: Long |
MavenClasspathStatus
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
| Declaration | Summary |
|---|---|
val upgraded: List<String> |
|
val status: MavenClasspathStatus |
MavenSearchResult
| Declaration | Summary |
|---|---|
val groupId: String |
|
val artifactId: String |
|
val latestVersion: String |
|
val packaging: String |
|
val versionCount: Int |
|
val timestampEpochMs: Long |
|
val coordinate: String |
MavenSearchResponse
| Declaration | Summary |
|---|---|
val query: String |
|
val results: List<MavenSearchResult> |
|
val message: String |
FieldPreferences
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
| 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
| Declaration | Summary |
|---|---|
val flagshipModel: String |
|
val fastModel: String |
|
val fallbackModel: String |
AiSettingsStatus
AiSettingsUpdateRequest
AiSettingsUpdateResponse
LocalAiConnectionRequest
LocalAiConnectionStatus
| Declaration | Summary |
|---|---|
val connected: Boolean |
|
val models: List<String> |
|
val modelAvailable: Boolean |
|
val message: String |
WorkspaceSwitchResponse
| 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
| 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
| 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
| 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
| Declaration | Summary |
|---|---|
val boxName: String |
|
val property: String |
|
val line: Int |
|
val snippet: String |
WorkspaceSearchResult
WorkspaceSearchResponse
| Declaration | Summary |
|---|---|
val query: String |
|
val documents: List<WorkspaceSearchResult> |
|
val totalDocuments: Int |
|
val totalMatches: Int |
|
val truncated: Boolean |
WelcomeDirCandidate
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
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
TopologyWatcher
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
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
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
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
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
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
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
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
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.
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
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
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
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
| 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
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
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
ReturnValueHandler
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
| 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
DebugProbeSegment
| Declaration | Summary |
|---|---|
val rawText: String |
|
val expandedText: String |
|
val localLine: Int |
|
val startLine: Int |
|
val endLine: Int |
KotlinExecution
class KotlinExecution : BoxExecution
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
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
@DataSchemainterface, 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
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@DependsOnjars 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
Classfor every box. The check is on class identity, so a shadowed name cannot produce a declaration that compiles and then throwsClassCastExceptionwhere 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
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, soList<Double>reaches the consuming box asList<Double>rather than asAny?. - 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
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
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
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
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
Top-level values
| Declaration | Summary |
|---|---|
val kotlinCompletionBlacklistPatterns: List<String> |
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
Code compiled against one KotlinScriptEngine REPL state and reusable within that state.
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
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)
CompletionTokenBounds
data class CompletionTokenBounds(val start: Int, val end: Int)
KotlinContext
Mutable bookkeeping for one Kotlin scripting context.
KotlinDebugScriptSnapshot
| 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)
KotlinFunctionInfo
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
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
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:
- 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.
- It checks that the engine's bootstrap gate (
KotlinScriptEngine.armBootstrapGateandKotlinScriptEngine.bootstrap) holds an ordinary exec until the gate is released. - It checks that a concurrent exec waits for the bootstrap to finish.
- It checks that a compile without evaluation (
KotlinScriptEngine.compileSnippet, thecheckBoxCodeCompilespath) 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
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:
- A named function that touches the implicit
Boxreceiver, such asfun foo(a: Double) = a + _qqq. The fork stores the receivers in the snippet class's implicit-receiver fields, as the.ktsscript lowering does. Cases A, B, and case L, which checks that a later run bindsfoo(12.0)to the live declaration. - 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. - A delegated property whose provider declares
provideDelegate, such asvar x by keyCamera(). Cases N, Q. - A destructuring declaration with a
_entry, such asval (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. - 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. - A class or object that reaches the box receiver, such as
class C { fun g() { println("hi") } }-- a bareprintlnordisplayis a member of the box. The REPL lowering makes no class capturing, so the read had no snippetthisand 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:
- 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.sizegave 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 (KotlinInterface2checks). - 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 * 2stayed 10 afterval 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
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,copyand destructuring of adata classwork 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 passesExportedTypeNames, so it crosses between boxes with its real type where a box-declared class can only ever cross asAny?. This is the join between the two halves of the work.testFreshEngineGetsFullFidelity— named arguments,copy, destructuring and the data-classtoStringall 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
| 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
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
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
mainbefore the swap (sha12869851…): XFAIL,withCALL still in the IR,got: null; - 5052517 (
1e6f4c6…): XFAIL, same IR, samegot: null; - face220 (
be020c9…): passes;demo()returnshi-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
| 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(ora.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 competingC. 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 evidencek2ImportEvidencecollects: K2 resolved every reference in the box and none of them lives ina.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
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
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
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
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
LanguageSpec
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
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
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
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
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
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).
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
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
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
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
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
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
| 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
RuntimeIdentity.verify found code other than the pinned code; differences has one line per class or library.
RuntimeIdentity
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
Work performed by SimulationClock.advance; discarded elapsed time is reported explicitly.
SimulationClock
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
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
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
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
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
The kind of syntax a metavariable is allowed to bind. Used for early rejection + readability.
Expr
Any expression (KtExpression). The common case.
Ident
A simple name reference (KtNameReferenceExpression).
Stmt
A statement-position element. Treated like Expr in v1.
TypeRef
A type reference (KtTypeReference).
Vararg
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
| Declaration | Summary |
|---|---|
val functionName: String |
|
val signature: String |
|
val prompt: String |
|
val body: String |
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
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
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
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
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
| 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
A build's execution state. Currency against the design is reported separately by StudyStageState.
Constants: QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED, OBSOLETE, INTERRUPTED.
StudyBuildProblem
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
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
What StudyBuilds.prune kept and deleted in the runner at directory, and the bytes the deleted attempts held.
StudyBuildRecord
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.
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
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
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
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.
StudyBuildGate
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
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
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.
StudyBuildContext
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
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
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
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
A difference between two frames, restricted to a region or its complement.
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
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
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
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
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
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.
StudyToolClasses
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.
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.
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
SourceStudyover a box property; - the fixtures, a
SourceStudyof retained real cases, each an exact input and the outcome the rule gave it (QualifiedFixture); - a replay stage, a
StudyBuildsrunner that runs a candidate rule on every fixture and on aQualifiedControl, 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
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
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
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
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
How a fixture came out in a replay.
Constants: SAME, CHANGED, HELD.
QualifiedCaseResult
One fixture in a replay: its verdict, the new outcome, the recorded one, and the refusal when verdict is HELD.
QualifiedControlResult
The control in a replay: whether it failed, the message it failed with, and whether that counts as passed.
QualifiedReplayReport
A replay's verdict, kept as replay.json in its attempt.
| Declaration | Summary |
|---|---|
val cases: List<QualifiedCaseResult>, val control: QualifiedControlResult, val passed: Boolean) |
QualifiedCase
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
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
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.
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
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
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
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
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
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
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
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
A revision without its source or value, for history listings.
SourceStudy
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
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
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
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.
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
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
| 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
Git worktree proposals for agents: create, list, update, merge, finalize and delete.
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,codeorall) runs the box's current text. Anycode:orrecordedCode: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 forexecute,execute-fragmentandlaunch-fragment) with an explicitcode:runs that code: an authored transcript uses it to run a snippet that is not in the box. A recorded fragment carriesrecordedCode: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: recordedruns the op's owncode:/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
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
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
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
- Completeness — every
*.yamlin 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. - 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.
- The CI claim, per entry — each
##section declares how CI verifies the scenes under it, with an invisible marker line beneath the heading:
The value is one or more of<!-- transcript-index: verified-by = execute -->execute,compile,kotlinjs, namingDemoTranscriptExecSmokeTest.EXECUTE,DemoTranscriptExecSmokeTest.COMPILE_ONLYandDemoTranscriptExecSmokeTest.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. - 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 anassets/agent_transcripts/<name>/metadata.ednlink 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. - No counts in the CI paragraph — the paragraph marked
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<!-- transcript-index: totals -->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
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
The docs lexical index and its query entry points; see the file overview.
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
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
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
reconstructemits 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.DECLARATIONspan 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
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
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
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.parseTopLevelDefinitionsfinds is thedeclarationLineof someDECLARATIONspan. 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: aFILLERline is a line a page can only show as anonymous code, so theFILLERlines 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 inFILLERtoo and are not counted: there is nothing for a page to lose. - Locals reported as API — a second ratchet counts the declarations
KotlinSourceIndexreports 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
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_HITSor more distinct public symbol names from that source file (seeevidenceLinks). 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
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
ChunkType
Constants: CODE, COMMENT.
CommentStyle
Constants: LINE, BLOCK.
Chunk
| 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 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
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
UniformEdgeTrimResult
| Declaration | Summary |
|---|---|
val trimmed: Boolean |
|
val left: Int |
|
val top: Int |
|
val right: Int |
|
val bottom: Int |
DocsUiRecorder
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
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
| 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
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
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
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
One line's note, as ReadLineNotes answers it.
ReadLineNotesResult
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
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
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
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() |