AgentTranscriptLayoutGuard.kt
src/jvmMain/kotlin/afield/tools/docs/AgentTranscriptLayoutGuard.kt · 401 lines
package afield.tools.docs
import org.yaml.snakeyaml.Yaml
import java.io.File
import kotlin.system.exitProcess
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).
object AgentTranscriptLayoutGuard
Audits transcript and demo box frames against the ordinary size ceiling and its exceptions.
const val MAX_ORDINARY_WIDTH = 620.0
const val MAX_ORDINARY_HEIGHT = 170.0
private const val CORPUS_REL = "dev_docs/agents/transcripts"
private const val DEMOS_REL = "dev_docs/agents/demos"
private const val EXCEPTIONS_REL = "dev_docs/agents/transcript_layout_exceptions.yaml"
private val CREATE_VERBS = setOf("create", "new-box", "box")
private data class Frame(
val key: String,
val source: String,
val x: Double,
val y: Double,
val width: Double,
val height: Double,
) {
val isLarge: Boolean
get() = width > MAX_ORDINARY_WIDTH || height > MAX_ORDINARY_HEIGHT
}
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
val repo = repoRoot()
val result = audit(repo)
if (result.problems.isNotEmpty()) {
System.err.println("AgentTranscriptLayoutGuard FAILED with ${result.problems.size} problem(s):")
result.problems.forEach { System.err.println(" - $it") }
System.err.println(
"\nUse compact positive create.frame geometry for ordinary boxes. If a large frame is part of " +
"the document or capture composition, add a specific file#box reason to $EXCEPTIONS_REL."
)
exitProcess(1)
}
println(
"AgentTranscriptLayoutGuard ok — population: ${result.transcripts} transcript(s) under $CORPUS_REL " +
"+ ${result.demos} demo document(s) and ${result.demoTranscripts} demo transcript(s) under " +
"$DEMOS_REL, ${result.frames} create frame(s), " +
"${result.largeFrames} reviewed large frame(s)"
)
}
data class AuditResult
The population, stated as two numbers rather than one.
transcripts and demos are separate fields because the ceiling in the failure message names
every box in the corpus, and a single total cannot say that half of it went to zero. The
demo half was outside this guard until 2026-09-05 and had drifted entirely over the ceiling
while the guard reported success; a count that cannot distinguish "no demos" from "no demo
violations" would let that happen again silently.
data class AuditResult(
val transcripts: Int,
val demos: Int,
val frames: Int,
val largeFrames: Int,
val problems: List<String>,
/** Demo `build.yaml` transcripts, the population [checkDemoReplayAgreement] holds to its document. */
val demoTranscripts: Int = 0,
)
fun audit(repo: File): AuditResult
body — 134 lines, from line 101
fun audit(repo: File): AuditResult {
val corpus = File(repo, CORPUS_REL)
val demosDir = File(repo, DEMOS_REL)
val problems = mutableListOf<String>()
if (!corpus.isDirectory) {
return AuditResult(0, 0, 0, 0, listOf("no transcript corpus at ${corpus.absolutePath}"))
}
// Half a corpus is not a smaller corpus, it is a different guard. The ceiling this reports
// is stated over "every shipped box", so a demos directory that has moved or been renamed
// must stop the run rather than shrink it.
if (!demosDir.isDirectory) {
return AuditResult(0, 0, 0, 0, listOf("no demo corpus at ${demosDir.absolutePath}"))
}
val files = corpus.walkTopDown()
.filter { it.isFile && it.extension.lowercase() in setOf("yaml", "yml") }
.sortedBy { it.relativeTo(corpus).invariantSeparatorsPath }
.toList()
val demoDocuments = demosDir.listFiles()
?.filter { it.isDirectory && File(it, "metadata.edn").isFile }
?.sortedBy { it.name }
.orEmpty()
val demoFrames = demoDocuments.flatMap { parseDemoFrames(it, problems) }
val demoTranscripts = demosDir.listFiles()
?.filter { it.isDirectory }
?.mapNotNull { dir -> File(dir, "build.yaml").takeIf { it.isFile } }
?.sortedBy { it.parentFile.name }
.orEmpty()
val demoTranscriptFrames = demoTranscripts.flatMap {
parseFrames("${it.parentFile.name}/${it.name}", it, problems)
}
checkDemoReplayAgreement(demoFrames, demoTranscriptFrames, problems)
val frames = files.flatMap { parseFrames(it.relativeTo(corpus).invariantSeparatorsPath, it, problems) } +
demoFrames + demoTranscriptFrames
val byKey = frames.groupBy { it.key }
byKey.filterValues { it.size > 1 }.forEach { (key, duplicates) ->
problems.add("$key identifies ${duplicates.size} create frames; box ids must be unique within a transcript")
}
for (frame in frames) {
if (!frame.x.isFinite() || !frame.y.isFinite() || !frame.width.isFinite() || !frame.height.isFinite()) {
problems.add("${frame.source}: frame values must be finite")
continue
}
if (frame.x < 0.0 || frame.y < 0.0) {
problems.add(
"${frame.source}: negative position (${frame.x}, ${frame.y}); ordinary transcripts begin in " +
"the positive canvas near (40, 40)"
)
}
if (frame.width <= 0.0 || frame.height <= 0.0) {
problems.add("${frame.source}: frame dimensions must be positive, got ${frame.width} x ${frame.height}")
}
}
val exceptions = parseExceptions(File(repo, EXCEPTIONS_REL), problems)
// Demo transcript frames are left out here on purpose: one that agrees with its document is
// that document's frame, already held to the ceiling under its `metadata.edn` key, and one that
// disagrees has already been reported by checkDemoReplayAgreement.
val demoTranscriptKeys = demoTranscriptFrames.mapTo(HashSet()) { it.key }
val large = frames.filter { it.isLarge && it.key !in demoTranscriptKeys }
for (frame in large) {
if (frame.key !in exceptions) {
problems.add(
"${frame.source}: ${frame.width} x ${frame.height} exceeds the ordinary " +
"${MAX_ORDINARY_WIDTH.toInt()} x ${MAX_ORDINARY_HEIGHT.toInt()} ceiling; compact it or add " +
"a reviewed `${frame.key}` reason to $EXCEPTIONS_REL"
)
}
}
for ((key, reason) in exceptions) {
val matched = byKey[key]
when {
reason.length < 20 -> problems.add("$EXCEPTIONS_REL: `$key` needs a specific reason of at least 20 characters")
matched == null -> problems.add("$EXCEPTIONS_REL: `$key` names no create frame")
matched.none { it.isLarge } -> problems.add("$EXCEPTIONS_REL: `$key` is no longer large; remove its stale exception")
}
}
return AuditResult(
files.size, demoDocuments.size, frames.size, large.size, problems.sorted(), demoTranscripts.size,
)
}
private fun parseFrames(relative: String, file: File, problems: MutableList<String>): List<Frame> {
val loaded = runCatching { Yaml().load<Any?>(file.readText()) }.getOrElse { error ->
problems.add("$relative: invalid YAML: ${error.message}")
return emptyList()
}
val operations = when (loaded) {
is List<*> -> loaded
is Map<*, *> -> (loaded["operations"] as? List<*>) ?: (loaded["ops"] as? List<*>) ?: emptyList<Any?>()
else -> emptyList()
}
val result = mutableListOf<Frame>()
for ((index, rawOperation) in operations.withIndex()) {
val operation = rawOperation as? Map<*, *> ?: continue
val verb = operation.keys.firstOrNull()?.toString()?.lowercase() ?: continue
if (verb !in CREATE_VERBS) continue
val payload = operation.values.firstOrNull() as? Map<*, *>
if (payload == null) {
problems.add("$relative operation ${index + 1}: $verb payload is not a map")
continue
}
val box = (payload["box"] ?: payload["uid"] ?: payload["id"] ?: payload["target"])
?.toString()?.takeIf { it.isNotBlank() }
if (box == null) {
problems.add("$relative operation ${index + 1}: $verb has no box id")
continue
}
val source = "$relative#$box"
val rawFrame = (payload["frame"] ?: payload["rect"]) as? Map<*, *>
if (rawFrame == null) {
problems.add("$source: create has no frame map; an omitted frame is an invisible Rect(0,0,0,0)")
continue
}
val values = listOf("x", "y", "w", "h").associateWith { field ->
val value = rawFrame[field]
if (value !is Number) problems.add("$source: frame.$field must be numeric, got ${value ?: "missing"}")
(value as? Number)?.toDouble() ?: Double.NaN
}
result.add(
Frame(
key = source,
source = source,
x = values.getValue("x"),
y = values.getValue("y"),
width = values.getValue("w"),
height = values.getValue("h"),
)
)
}
return result
}
A demo's build.yaml must create each box at the geometry its metadata.edn carries.
The two files are one artifact seen twice: the transcript rebuilds the document. Auditing only
the document let a transcript keep the roomy authoring boilerplate indefinitely and pass every
gate, until somebody replayed it and wrote 700 x 470 back over a box compacted to 86 tall.
Requiring agreement makes a replay idempotent, and catches the drift in the commit that
introduces it rather than at the replay. A box the transcript creates and the document lacks is
reported too; the reverse is allowed, since a document can carry boxes added by hand after the
transcript ran.
source — 59 lines, from line 247
private fun checkDemoReplayAgreement(
documentFrames: List<Frame>,
transcriptFrames: List<Frame>,
problems: MutableList<String>,
) {
val documents = documentFrames.associateBy { it.key }
for (frame in transcriptFrames) {
val demo = frame.key.substringBefore('/')
val box = frame.key.substringAfterLast('#')
val document = documents["$demo/metadata.edn#$box"]
if (document == null) {
problems.add(
"${frame.source}: creates a box '$box' that $demo/metadata.edn does not carry; a demo's " +
"transcript and the document it rebuilds must describe the same boxes"
)
continue
}
if (!sameFrame(frame, document)) {
problems.add(
"${frame.source}: create frame ${describe(frame)} does not match ${describe(document)} in " +
"$demo/metadata.edn; replaying this transcript would write its geometry over the " +
"committed document. Copy the document's frame into create.frame."
)
}
}
}
private const val FRAME_EPSILON = 0.01
private fun sameFrame(a: Frame, b: Frame): Boolean =
listOf(a.x - b.x, a.y - b.y, a.width - b.width, a.height - b.height)
.all { it.isFinite() && kotlin.math.abs(it) < FRAME_EPSILON }
private fun describe(frame: Frame): String = "${frame.width} x ${frame.height} at (${frame.x}, ${frame.y})"
private fun parseExceptions(file: File, problems: MutableList<String>): Map<String, String> {
if (!file.isFile) {
problems.add("no layout-exception registry at ${file.absolutePath}")
return emptyMap()
}
val loaded = runCatching { Yaml().load<Any?>(file.readText()) }.getOrElse { error ->
problems.add("$EXCEPTIONS_REL: invalid YAML: ${error.message}")
return emptyMap()
}
val root = loaded as? Map<*, *>
val raw = root?.get("largeFrames") as? Map<*, *>
if (raw == null) {
problems.add("$EXCEPTIONS_REL: expected a `largeFrames` map")
return emptyMap()
}
val result = linkedMapOf<String, String>()
raw.forEach { (rawKey, rawReason) ->
val key = rawKey?.toString()?.trim().orEmpty()
val reason = rawReason?.toString()?.trim().orEmpty()
if (key.isEmpty()) problems.add("$EXCEPTIONS_REL: empty largeFrames key")
else result[key] = reason
}
return result
}
Frames from one loadable demo document under dev_docs/agents/demos.
These carry the same boxes as a transcript — often literally the same box, since a demo and
its transcript are generated from one set of *_code_.kt files — but they were outside this
guard's population until 2026-09-05, and every one of them had drifted over the ceiling as a
result: 24 of 26 boxes, up to 760 x 600. The ceiling was never the missing part; the
population was. A metadata.edn is EDN rather than YAML, so the frames are read with a regex
rather than through a parser.
The scan is per box, not one spanning regex, and that is the whole point of this function.
The original form matched "id" "…" (.*?) #afield/rect [x y w h] across the document, so a
box with no frame silently absorbed the next box's rectangle and hid that box from the
guard entirely — one omission, two boxes unchecked, no message. parseFrames rejects exactly
that omission on the YAML side ("an omitted frame is an invisible Rect(0,0,0,0)"); the EDN
side swallowed it. So the document is cut into one segment per "id", each segment must
carry exactly one "frame" #afield/rect, and a segment that does not is a problem rather
than a gap. Re-measured 2026-09-20: identical attribution to the spanning form over all 20
demos, 72 frames, so the change adds a failure mode and removes none.
That corpus is 21 demos and 77 demo frames as of 2026-09-21, when marf_demo landed. The
sentence above is deliberately not edited to match: what it claims is that the two parsers
agreed over the corpus they were compared on, and retyping the numbers would assert a
comparison nobody re-ran. main prints the live population on every run, which is the
number to believe. To recount the demo half on its own:
grep -roh '"frame" #afield.rect' dev_docs/agents/demos --include=metadata.edn | wc -l
Two traps in that one line, both hit while writing it. --include=metadata.edn is
load-bearing: without it the document.md mirrors are counted too and the answer is 120.
And it is grep -oh … | wc -l, not grep -c, because -c counts matching lines and a
metadata.edn is one enormous line — that spelling answers 64. The glob you would reach for
first cannot be written in this KDoc at all: an adjacent * and / close the comment and
swallow the rest of the file. dev_docs/GUARD_CORPORA.md is the argument for why a dated
measurement in prose has to say that it is one.
root is the document's implicit container box and legitimately has no frame; it is the only
id exempted, and it is exempted by name so that a box called anything else cannot slip
through as one.
source — 52 lines, from line 349
private fun parseDemoFrames(dir: File, problems: MutableList<String>): List<Frame> {
val edn = File(dir, "metadata.edn")
if (!edn.isFile) return emptyList()
val text = edn.readText()
val relative = "${dir.name}/metadata.edn"
// A box entry is `{"id" "…" "properties" {…}}`. Requiring the `"properties"` key keeps nested
// maps that also carry an `"id"` (a box's persisted `_controlGroupValues`, one per slider
// or checkbox) from being read as frameless boxes.
val idRe = Regex("""\"id\"\s+\"([^\"]+)\"\s+\"properties\"""")
val frameRe = Regex("""\"frame\"\s+#afield/rect\s+\[([^\]]+)\]""")
val ids = idRe.findAll(text).toList()
val result = mutableListOf<Frame>()
for ((index, id) in ids.withIndex()) {
val box = id.groupValues[1]
val end = if (index + 1 < ids.size) ids[index + 1].range.first else text.length
val segment = text.substring(id.range.last + 1, end)
val rects = frameRe.findAll(segment).toList()
if (rects.isEmpty()) {
if (box != "root") {
problems.add(
"$relative: box '$box' has no `\"frame\" #afield/rect`; a frameless box is an " +
"invisible Rect(0,0,0,0), and it used to make the next box invisible to this guard too"
)
}
continue
}
if (rects.size > 1) {
problems.add("$relative: box '$box' declares ${rects.size} frames; exactly one is expected")
continue
}
val raw = rects.single().groupValues[1]
val parts = raw.trim().split(Regex("\\s+")).mapNotNull { it.toDoubleOrNull() }
if (parts.size != 4) {
problems.add("$relative: box '$box' has an unreadable frame '$raw'")
continue
}
result.add(Frame("$relative#$box", "$relative box '$box'", parts[0], parts[1], parts[2], parts[3]))
}
return result
}
private fun repoRoot(): File {
var candidate: File? = File(System.getProperty("user.dir")).absoluteFile
while (candidate != null) {
if (File(candidate, "public/docs").isDirectory && File(candidate, "src/jvmMain/kotlin").isDirectory) {
return candidate
}
candidate = candidate.parentFile
}
return File(System.getProperty("user.dir")).absoluteFile
}
}