KVG Generative Extensions (AI-emitted UI)
kvg specs/kvg/generative-extensions.kmd
Contract for emitting KVG from an LLM ("generative UI" — the Koder-native answer to prompt→prototype/wireframe/slide). Defines the constrained generative subset of KVG-Core an AI may emit, the design-system theming contract (token(role) + host-injected palette), the pre-render validation gate, the safety bounds, and the KVG×A2UI boundary (visual vs interaction). Phase-1 deliverable of ai-RFC-001; backed by the Phase-2 pilot (Gate-1 evidence in that RFC). Normative for any pipeline that asks a model to emit KVG.
When this spec applies
All triggers
- Pedir a um LLM para emitir/gerar KVG a partir de prompt ou arquivo
- Construir pipeline de generative UI / artifact panel que renderiza KVG de IA
- Compor KVG (visual) com A2UI (interação) num produto de design Koder
- Injetar tokens de design (color-roles) em documento KVG gerado
Specification body
KVG Generative Extensions — AI-emitted UI
Version: 0.1 — Proposal
Status: Phase-1 doc (ai-RFC-001), GO for Phase 2 (owner-ratified 2026-06-20)
Reference impl: engines/lang/kvg (parser/validator/renderer), services/ai/canvas (A2UI)
This spec does not add a new KVG profile. Generative UI emits KVG-Core as-is (
specs/kvg/format.kmd). What it adds is a contract: which subset a model may emit, how design tokens compose, how it is validated before render, and where KVG stops and A2UI starts. Reuse-first: no new substrate.
1. Why a subset
A model emits the smallest declarative surface that covers the target artifacts
(cards, charts, flowcharts, wireframes, slides, diagrams). The full normative
format.kmd is ~20k tokens; the emittable subset is ~700 tokens (Phase-1
go/no-go gate: schema < 3k → PASS). Keeping the model's surface small is what
makes one-shot emission reliable (Phase-2 pilot: 10/10 valid + correct, Opus arm).
2. The generative subset (canonical AI-facing schema)
This block is the normative prompt schema — the text a generative pipeline
puts in front of the model. It MUST be kept in sync with format.kmd; it is a
strict subset (every construct below is valid KVG-Core).
# KVG generative subset — emit valid KVG-Core, output ONLY the document.
## Coordinate system (CRITICAL)
Y-UP: y increases UPWARD; origin (0,0) = bottom-left of the viewport.
"Top of canvas" = HIGH y. Text baseline sits at its y.
## Skeleton
kvg-version "0.1.0"
profile Core
viewport 0 0 <W> <H>
title "<short title>"
<palette? (host-injected — see theming)>
scene { <nodes...> }
## Shapes (inside scene { } or a group g #id { })
rect <x> <y> <w> <h> [rx=<r>] fill=<paint> [stroke=<paint> stroke-width=<n>]
circle <cx> <cy> r=<r> fill=<paint>
ellipse <cx> <cy> rx=<rx> ry=<ry> fill=<paint>
line <x1> <y1> <x2> <y2> stroke=<paint> stroke-width=<n>
text <x> <y> "<content>" size=<px> fill=<paint> [anchor="start|middle|end"]
polygon points=[[x,y],...] fill=<paint>
polyline points=[[x,y],...] stroke=<paint> stroke-width=<n>
path d="M x y L x y C ... Z" fill=<paint> [stroke=<paint>]
## Paint = a color OR a design token OR a gradient ref
"#RRGGBB" | "#RRGGBBAA" solid color
token(<role>) design-system role (see theming) — PREFERRED
#<gradient-id> reference a top-level gradient
## Gradients (top level, before scene)
gradient #<id> linear from=[x0,y0] to=[x1,y1] { stop 0.0 "#.." stop 1.0 "#.." }
gradient #<id> radial center=[cx,cy] radius=<r> { stop 0.0 "#.." stop 1.0 "#.." }
## Rules
Header (kvg-version, profile, viewport) is MANDATORY. IDs optional, prefix #.
Comments: // line. One node per line. Positional args first, then name=value.
No scripts/eval/network — purely declarative shapes.
R1 — A generative pipeline MUST present this subset (or a superset strictly within KVG-Core) as the model's schema. It MUST NOT instruct the model to emit Motion/Solid/Interact constructs unless the renderer in the loop declares those capabilities.
3. Theming — token(role) + host-injected palette (H3)
KVG-Core already carries a palette-token mechanism (token(role), KVG-066,
format.kmd §8). Generative UI reuses it for design-system consistency — no
schema extension (reuse-first).
- R2 — The model SHOULD emit colors as
token(<role>)using the Koder color-role vocabulary (specs/themes/color-roles.kmd:bg,surface,surface-variant,text,text-muted,accent,accent-on,border,error,success,warning,info, …), falling back to raw hex only for decorative, role-less fills. - R3 — The host (not the model) MUST inject a
palette { role <name> <hex> … }block built from the active theme before render. The model never hardcodes the theme. - R4 — Re-theming a generated artifact = swap the injected
paletteblock and re-render; thesceneis unchanged. Light↔dark is a palette swap. - R4b — Typography follows the same host-injection pattern as the palette: the
host SHOULD inject a default embedded font as
asset font #id { data "base64:…" }(self-hosted, format.kmd §4.1) and the model emitstext … font=#idto opt into real TTF/OTF typography. Without an injected font, text falls back to the built-in bitmap face (fine for wireframes, weak for polished slides). The model never references a host file path (src=) — embedded only, to keep the document self-contained.
T1 (theming) — A document using
token(role)renders with the light palette and, with only the palette block swapped to dark, renders the identical geometry in dark colors. (Verified in the Phase-2 pilot:h3-token-cardlight vs dark.)
3b. Charts — host-injected data-driven kdef library
Charts are the one place the model would otherwise do error-prone geometry arithmetic by hand (the Phase-2 DeepSeek arm mis-computed bar baselines). Instead, the host injects a small library of parametric chart kdefs (KVG §11.1; kvg#122 mechanism, kvg#123 library) and the model emits only the high-level instance with the data — the kdef computes max/normalize/layout in a tested primitive.
- R4c — For charts, the model SHOULD emit a high-level instance (
barchart data=[…],linechart data=[…]) rather than hand-laid shapes. The host MUST inject the matchingkdefdefinitions (canonical set inengines/lang/kvg/library/charts.kvg) before render, the same way it injects the palette/font. The model never writes theexpandbody.
Canonical bar + line kdefs (Y-up; bars grow up from base):
kdef #barchart extends=group params=[data, h, fill, base, bw, gap] {
expand """
let n = len(data)
let maxv = max(data)
each i in range(n)
let bh = data[i] / maxv * h
emit_node(type: "rect", x: 20 + i * (bw + gap), y: base, w: bw, h: bh, fill: fill)
end
"""
}
kdef #linechart extends=group params=[data, h, fill, base, step] {
expand """
let n = len(data)
let maxv = max(data)
each i in range(n - 1)
emit_node(type: "line",
x1: 20 + i * step, y1: base + data[i] / maxv * h,
x2: 20 + (i + 1) * step, y2: base + data[i + 1] / maxv * h,
stroke: fill, stroke-width: 3)
end
"""
}
Instance: barchart data=[1,5,3,8,2] h=150 fill=token(accent) base=30 bw=40 gap=15.
Pie (kvg#124) — cumulative-angle layout via a running acc accumulator (kgen now
has mutable reassignment) and a path d string built by number→string concat:
kdef #piechart extends=group params=[data, cx, cy, r, colors] {
expand """
let n = len(data)
let total = 0
each v in data
total = total + v
end
let acc = 0
each i in range(n)
let frac = data[i] / total
let a0 = acc * 2 * PI
let a1 = (acc + frac) * 2 * PI
let large = 0
if frac > 0.5 then
large = 1
end
let d = "M " + cx + " " + cy + " L " + (cx + r * cos(a0)) + " " + (cy + r * sin(a0))
+ " A " + r + " " + r + " 0 " + large + " 1 " + (cx + r * cos(a1)) + " " + (cy + r * sin(a1)) + " Z"
emit_node(type: "path", d: d, fill: colors[i])
acc = acc + frac
end
"""
}
Instance: piechart data=[40,35,25] cx=180 cy=110 r=90 colors=["#3B82F6","#10B981","#F59E0B"].
4. Pre-render validation gate (H2)
- R5 — Every AI-emitted KVG document MUST pass
kvg validate(engines/lang/kvgparser + spec checks) before it is rendered or shown.validateis the structural safety net: missing header, undefinedtoken/gradient/#ref, profile mismatch, kdef cycles, resource-limit breaches. - R6 — A document that fails validation MUST be repaired (re-prompt with the
diagnostic) — never rendered partially. Diagnostics carry
code+ line:col for machine-assisted repair.
Hardened by kvg#119 (2026-06-20): the parser previously dropped lexer errors (
_ = p.advance()), so an unterminated/truncated string parsed to corrupted nodes instead of failing. Now every lexer error propagates → truncated LLM output fails validation loudly. Regression #990.
5. Safety bounds (Phase-4)
KVG is declarative — no <script>, no eval, no network/file IO by construction.
On top of that:
- R7 — Rendered widgets MUST NOT fetch remote resources. No
imagenode with a remotesrc; assets are embedded or omitted. (Self-hosted-first.) - R8 — Resource limits enforced at parse time (already in
parser, kvg#113): max node count and nesting depth bound; documents exceeding them are rejected, not truncated. - R9 — Text content is treated as data (the bitmap/TTF text path does no markup interpretation). URLs, if ever surfaced by a host action, are host-allowlisted — never emitted as live links by KVG itself (interaction belongs to A2UI, §6).
- R10 — Every AI-generated document SHOULD be logged (source prompt + emitted
KVG + validation result) for forensics, per
policies/observability-first.kmd.
6. Boundary — KVG (visual) × A2UI (interaction)
ai-RFC-001 and canvas-RFC-001 (A2UI, Accepted) are orthogonal and
composable, not competitors. The "Koder Design" product = KVG ⊕ A2UI.
| KVG | A2UI (canvas) | |
|---|---|---|
| Owns | static visual artifact (wireframe, slide, diagram, chart, illustration) | interaction + control layout + session state (forms, buttons, inputs) |
| Emits | declarative scene graph | typed widget tree + actions |
| Action model | none (visual only) | agent round-trip / client:open_url (allowlisted) |
- R11 — A generative pipeline MUST NOT bolt interaction onto KVG (no AI-emitted
on_clickon KVG nodes — resolves ai-RFC-001 open-question #3). Interaction is A2UI's responsibility. - R12 — Composition: A2UI embeds a KVG document as a visual node (e.g. an
Image/custom widget rendered from KVG) when an interactive surface needs a rich visual; KVG stands alone for pure-visual artifacts. The seam is one-directional: A2UI → contains → KVG, never the reverse.
Prototype target (ai-RFC-001 Phase 3): the Kortex/Kanvas artifact panel renders KVG for visual artifacts and A2UI for interactive ones, with the panel choosing by artifact kind. Pending canvas (
services/ai/canvas) maturing past bootstrap.
7. Empirical basis
This spec is backed by the ai-RFC-001 Phase-1+2 pilot (that RFC, "Experimental results"): generative subset ~700 tok (< 3k gate); Opus one-shot over 10 canonical prompts = 10/10 valid + 10/10 render + visually correct incl. pie-chart path-arcs in Y-up; H3 token-role theming proven (light/dark swap); H2 hardened (kvg#119).
8. Open items (Phase-2 remainder)
- Cross-model arm — partially closed: a DeepSeek run (4 prompts) emitted
3/4 valid+correct one-shot from this subset, the 4th gated by
kvg validate(a dropped header). GPT-5/Gemini arms remain open (no keys provisioned); the AI gateway (services/ai/gateway) has the routing when they are. The wider the model spread, the more the §4 validation gate (not the model) carries safety. - Typography — DONE (kvg#120 + #121): self-hosted embedded fonts render
(
asset font #id { data "base64:…" }→ real TTF/OTF glyphs), host injects a default font per R4b, andanchor="middle"/"end"centers using the real TTF advance (#121). Remaining: WOFF2 payloads need decompression (TTF/OTF parse directly). - Chart helpers — DONE (bar + line + pie). kvg#122 mechanism; kvg#123 kgen
range()/indexing/len()/min/max/floor+ hyphenated emit keys + the bar/line library; kvg#124 mutable reassignment (acc = acc + x) + number→string concat (str()/+) → the pie kdef. Host-injected library:library/charts.kvg. - Promotion — when Phase-2/3 complete, fold the stable parts into the backing
spec
specs/ai-ui/generative-ui.kmdand bump this to normative.
References
meta/docs/stack/rfcs/ai-RFC-001-generative-ui-via-kvg.kmdmeta/docs/stack/rfcs/canvas-RFC-001-a2ui.mdmeta/docs/stack/specs/kvg/format.kmdmeta/docs/stack/specs/themes/color-roles.kmdengines/lang/kvg/parserservices/ai/canvas