ReferenceGenerated codeWhat `jui build` writes into your repository, what it promises about that code, and every knob that changes it. Generated views are size-bounded on purpose — the bounds exist because a deep SwiftUI view exhausts a real device's stack, not because long files are ugly.7 min read
What the build writes`jui build` reads the layouts you author and writes per-platform code next to your app. Two rules keep the boundary clear: anything the build writes is regenerated from scratch on the next run, and anything you author is never touched. The `@generated` sentinel marks the first group so a human, a reviewer, or `jui lint-generated` can tell them apart without knowing the pipeline.Generated view files are the exception that proves the rule: they carry no sentinel because they hold hand-edit markers inside instead, so they get their own walk in the linter — that is the `+N generated views for the size gate` line in its output.
what the build writes
Artifact | Written by | Hand-editable | Carries @generated--------------------------------+------------------+---------------+-------------------Layout JSON (layouts_directory) | you | yes | noLayout JSON (platform copy) | jui build | no | yes (_generated)*GeneratedView.swift / .kt | sjui / kjui | no | no (size-gated)View TSX (web) | rjui | no | yesViewModel / Repository stubs | jui g project | yes, once | header onlyDTO + Domain scaffolds | jui build (api) | Domain only | DTO yesEvery generated function is size-boundedThe generators split a screen into section functions and keep each one inside two bounds. The split is deterministic: names are assigned at emission in document order, so regenerating the same layout twice is byte-identical.• Depth is the hard bound: at most 5 levels of brace nesting per function, measured on braces with string literals and comments stripped. Braces are the metric because ViewBuilder blocks are what nest the generic type — indentation over-reads, since multi-line call arguments are indented far past their structural level.• Lines are a soft target: 250 per function. The bounder splits toward it whenever a safe cut exists, and never contorts the output to reach it. A function that satisfies the depth bound but overruns the line count is reported, not rejected.• A function with no safe cut becomes a declared waiver, printed by name. Cutting is scope-aware: a cut under a binding scope is allowed only when every name the segment references has a known type, and it is never made inside an action or modifier closure. Silently shipping the over-long function is the one behaviour that is not allowed — that is exactly how an 881-line body survived the previous splitter.
Why iOS erases each boundary to `AnyView`Splitting a SwiftUI view into `some View` helpers does not reduce anything the runtime cares about. An opaque result type is transparent to type-metadata instantiation: the screen's composed type keeps its full depth across any number of function boundaries, and the runtime decodes that metadata on a 1 MB device stack. An already-extracted 881-line section still crashed a real device, while the simulator's 8 MB stack hid it. Only value-level erasure — `AnyView`, a non-generic struct — truncates the recursion, so every section boundary is erased.Placement is structural: every boundary, unconditionally, never conditional on a measurement. If `AnyView` appeared only when a function crossed a threshold, a one-line edit could flip it on an unrelated sibling and silently change that view's transition and diffing identity between two builds. The rest of the Swift output — dual SwiftUI + UIKit emission, Dynamic mode — is on /platforms/swift.
Android has no equivalent mechanismThe same extractor bounds Compose output, but the numbers are calibrated for the iOS mechanism and Compose has no counterpart to it. The ceiling that actually exists on Android is the method-size limit — 65,536 bytes of bytecode in a class file, 65,535 code units after dexing — which is what the Compose extractor is written against, and exceeding it is a hard compile error — so a build that succeeded has already proved the point. The measured worst case in a real app was 12.6 % of the cap, smaller than a hand-written ViewModel method in the same project.So oversized Compose functions are not a build warning: a warning that can never be actioned is a warning that trains people to ignore the build. `jui lint-generated` still lists them. If you are debugging the extractor itself, `KJUI_SECTION_WAIVER_WARNINGS=1` puts the warnings back for that run. /platforms/kotlin covers the rest of the Compose output.
`jui lint-generated`The audit command for the generated tree. It is deliberately not part of `jui build` — it answers CI's questions, not the inner loop's. Two independent signals share the command and get distinct exit codes, so one can gate CI without the other holding the gate permanently red.`--fail-on` narrows which signal sets the code; the other is still printed, just not gating. `--verbose` lists every checked file instead of only the failures, and `--fix` prints the regeneration command when a marker is missing (it never edits files itself). Every other subcommand is catalogued on /reference/cli-commands.
jui lint-generated
$ jui lint-generated --fail-on size-ratchetChecked 1217 generated files (+58 generated views for the size gate). OK: 1217 Missing header: 0 Oversized fns: 3 (depth > 5 or lines > 250) Exit codes 0 clean 1 could not run (no jui.config.json) 2 size-ratchet regression — a function grew past the bounds and is not in .jui-size-waivers 3 generated-artifact health — a file lost its @generated markerThe waiver baseline is a ratchet`--update-size-baseline` writes the current set of oversized functions to `.jui-size-waivers`; commit that file. From then on, entries may leave freely — the tools improved — but a new entry means today's build shipped something oversized that yesterday's build bounded, which is a genuine regression signal even though the standing waivers are not.Identity is path plus function name. Depth and line numbers wobble with every content edit and deliberately do not participate, so ordinary copy changes never trip the ratchet. A project without the file gets the same listing purely as information — the gate is opt-in.
.jui-size-waivers
# jui lint-generated size-waiver baseline (ratchet).# One entry per accepted oversized generated function.app/src/main/java/.../ChatGeneratedView.kt Section25app/src/main/java/.../FormGeneratedView.kt Section0_0_1What you can configure, and what you cannotThe knobs live in three places, and it is worth knowing which is which before you go looking in the wrong one.• `jui.config.json` — `build.normalizeLayouts` (default true) decides whether distributed layouts are L1-canonicalized before the platform builders see them. `lint.exclude_dir_names` and `lint.exclude_files` narrow what `jui lint-generated` walks, which is how you keep a vendored or legacy tree out of the audit. `lint.max_depth` and `lint.max_lines` declare the bounds the linter checks against, so a project that wants to gate on a stricter number gets it on every run — CI and local alike — instead of depending on each invocation repeating the same flag.• Command-line flags — `--max-depth` and `--max-lines` override whatever the config declares, and `--fail-on any|size-ratchet|health` picks the gating signal. The order is flag, then `jui.config.json`, then the built-in 5 and 250. The two size flags carry no number of their own by default, precisely so that typing the built-in number explicitly still wins over a config asking for something stricter — a per-invocation override suits a nightly job that gates harder than the inner loop.• Environment — `KJUI_SECTION_WAIVER_WARNINGS=1` restores the Android size warnings for one run. `JSONUI_CLI_DIR` / `JSONUI_MCP_DIR` / `JSONUI_CLI_PATH` relocate the toolchain itself.• Stricter only — the bounds the generators themselves cut against are constants in sjui and kjui, not settings. Raising them in a project would mean shipping a screen that the runtime cannot decode on a real device, so the number that matters is the one the tools were calibrated to. A `lint.max_depth` or `lint.max_lines` above the built-in is therefore refused: the linter warns, uses the built-in, and points you at `.jui-size-waivers`. Standing slack belongs there, recorded function by function where a new entry is visible, rather than folded into a number that hides every overrun at once.
jui.config.json
{ "build": { "normalizeLayouts": true }, "lint": { "exclude_dir_names": ["Legacy", "Sandbox"], "exclude_files": ["web/src/generated/useColorMode.ts"], "max_depth": 4, "max_lines": 200 }}Warnings you may see, and where they come from`jui build` prints its diagnostics; it does not fail on them. Measured on jsonui-cli 1.8.36 — an unknown attribute, a missing required attribute, an unknown component type and a localize finding are each reported, and the build still exits 0. It does count them — the run prints `Validation warnings found: 3` — but nothing reads that number back, and no path fails on one. It exits non-zero on the failures that stop the run instead — no `jui.config.json`, a layout that is not valid JSON, a responsive-variant contract violation, an `@canonical` mark that does not resolve. Running clean is a rule in the agent rulebook installed under `.claude/jsonui-rules/`, and it binds a workflow rather than this process's exit code. To fail on localize findings, run `jui lint-strings` directly — that command exits 2. Since jsonui-cli 1.8.38 a binding whose contents are not an expression is warned about and emitted as literal text rather than interpolated: measured, `@{ bad name }` now raises one warning and the generated file parses, where before it raised nothing and produced code that did not. `sjui build` is a different command with a different answer: its canonical binding-rule errors fail the build whatever the flags, and its other validation errors fail it under `--strict`. Everything measured above is `jui build` on the web path, the only one this site can run. This page said the opposite until 2026-09-04, and upstream's own comment carried the same wording: a reader took it for a mechanism here.An unknown-attribute warning is worth a second look before you delete the attribute: it also fires when a key is canonical but has not been registered in the attribute SSoT yet, which is a tooling gap rather than a mistake in your layout.
build diagnostics
What you see | Severity | Where to fix it----------------------------------------------+----------+------------------------------binding-double-default, binding-two-way- | error | the binding expression complex, binding-negation-context, ... | |business logic in a binding (ternary, call) | warning | move it to the ViewModelUnknown attribute 'x' for component type 'y' | warning | the attribute, or the SSoTcolor not in colors.json; emitting as-is | warning | use a palette tokenData property 'x' is defined but never used | warning | drop it, or bind itoversized generated function (iOS) | warning | split the layout, or waivemissing @generated marker | lint | regenerate (jui build)Next
CLI command referenceEvery jui / sjui / kjui / rjui subcommand with its flags and exit codes./reference/cli-commands
SwiftJsonUIWhat the Swift generator emits, dual SwiftUI + UIKit output, and Dynamic mode./platforms/swift
KotlinJsonUICompose-first output, the XML freeze, and the Android production caveats./platforms/kotlin