KVG Generative Extensions (AI-emitted UI)

proposal-v0.1

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.

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 (parservalidatorrenderer), 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 MotionSolidInteract 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 palette block and

    re-render; the scene is 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 emits text … font=#id to 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-card light 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 maxnormalizelayout 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 matching kdef definitions (canonical set in engineslangkvglibrarycharts.kvg) before render, the same way it injects the palette/font. The model never writes the expand` body.

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/kvg

    parser + spec checks) before it is rendered or shown. validate is the structural safety net: missing header, undefined tokengradient#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 image node with a

    remote src; 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_click on 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 = 1010 valid + 1010 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)

  1. 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.

  2. 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, and anchor="middle"/"end" centers using the real TTF advance (#121). Remaining: WOFF2 payloads need decompression (TTF/OTF parse directly).

  3. Chart helpers — DONE (bar + line + pie). kvg#122 mechanism; kvg#123 kgen

    range()indexinglen()minmax/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: `librarycharts.kvg`.

  4. Promotion — when Phase-2/3 complete, fold the stable parts into the backing

    spec specs/ai-ui/generative-ui.kmd and bump this to normative.