// packages/ui-gen/src/harness/check/runtime-render/prepare-mockup.ts
//
// Schema-first mockup props for runtime render evaluation.
//
// Priority:
// 1. Existing fixture props (commit/contract `props` field) — highest fidelity
// 2. PropEntry.example
// 3. PropEntry.default
// 4. Deterministic schema synthesis from JSON Schema
// (default → example → enum[0] → typed placeholder)
// 5. LLM fallback (deferred — v2)
// 6. Empty fallback ({}) if all else fails
//
// Output is validated against the contract's `properties` shape — if a field
// is required and ends up missing, we surface that as a synthesis warning
// (the render check will likely fail downstream, but with a clearer signal).
import type {
DataContract,
JsonObject,
JsonSchema,
JsonValue,
PropEntry,
PropsSpec,
} from "@ggui-ai/protocol";
export interface MockupPropsResult {
/** The synthesized props object — pass directly into render(). */
readonly props: JsonObject;
/** How each field was sourced — useful for debugging eval failures. */
readonly source: Readonly>;
/** Synthesis warnings (missing required fields, unsupported schemas, etc.). */
readonly warnings: readonly string[];
}
export type MockupSource =
| "fixture"
| "entry-example"
| "entry-default"
| "schema-default"
| "schema-example"
| "schema-enum"
| "schema-synth"
| "empty";
// ─────────────────────────────────────────────────────────────────────────────
// Public entry point
// ─────────────────────────────────────────────────────────────────────────────
export interface PrepareMockupInput {
readonly contract: DataContract | undefined;
/**
* Pre-supplied fixture props (e.g., from a benchmark commit's `props` field).
* Wins over schema synthesis when keys match.
*/
readonly fixtureProps?: JsonObject;
}
/**
* Synthesize a JsonObject of props matching the contract.propsSpec shape.
*
* Deterministic — no LLM call. Returns even if the contract is missing
* (in which case `props` is just the fixtureProps or `{}`).
*/
export function prepareMockupProps(input: PrepareMockupInput): MockupPropsResult {
const { contract, fixtureProps } = input;
const propsSpec = contract?.propsSpec as PropsSpec | undefined;
const props: JsonObject = {};
const source: Record = {};
const warnings: string[] = [];
if (!propsSpec || !propsSpec.properties) {
// No props contract — mirror the fixture if present, else empty.
if (fixtureProps) {
Object.assign(props, fixtureProps);
for (const k of Object.keys(fixtureProps)) source[k] = "fixture";
}
return { props, source, warnings };
}
for (const [key, entry] of Object.entries(propsSpec.properties)) {
// Priority 1: fixture wins
if (fixtureProps && Object.prototype.hasOwnProperty.call(fixtureProps, key)) {
props[key] = fixtureProps[key];
source[key] = "fixture";
continue;
}
const synth = synthesizePropValue(entry, key, warnings);
if (synth.kind === "ok") {
props[key] = synth.value;
source[key] = synth.source;
} else if (entry.required) {
warnings.push(`Required prop '${key}' could not be synthesized (${synth.reason})`);
}
// Optional props that fail synthesis are simply omitted.
}
return { props, source, warnings };
}
// ─────────────────────────────────────────────────────────────────────────────
// Per-prop synthesis
// ─────────────────────────────────────────────────────────────────────────────
type SynthOk = { kind: "ok"; value: JsonValue; source: MockupSource };
type SynthFail = { kind: "fail"; reason: string };
function synthesizePropValue(
entry: PropEntry,
fieldName: string,
warnings: string[],
): SynthOk | SynthFail {
// PropEntry-level wins over schema-level
if (entry.example !== undefined) {
return { kind: "ok", value: entry.example, source: "entry-example" };
}
if (entry.default !== undefined) {
return { kind: "ok", value: entry.default, source: "entry-default" };
}
return synthesizeFromSchema(entry.schema, fieldName, warnings, 0);
}
const MAX_DEPTH = 6;
function synthesizeFromSchema(
schema: JsonSchema | undefined,
hint: string,
warnings: string[],
depth: number,
): SynthOk | SynthFail {
if (!schema) return { kind: "fail", reason: "no schema" };
if (depth > MAX_DEPTH) return { kind: "fail", reason: "schema too deep" };
// Schema-level overrides
if (schema.default !== undefined) {
return { kind: "ok", value: schema.default, source: "schema-default" };
}
if (schema.example !== undefined) {
return { kind: "ok", value: schema.example, source: "schema-example" };
}
if (schema.enum && schema.enum.length > 0) {
return { kind: "ok", value: schema.enum[0]!, source: "schema-enum" };
}
// Union schemas — try first branch only
if (schema.oneOf && schema.oneOf.length > 0) {
return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1);
}
if (schema.anyOf && schema.anyOf.length > 0) {
return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1);
}
// Draft-07 type array (`['string','null']` — the canonical nullable
// form since draft-2026-08-19): synthesize from the first non-null
// member ('null' only when it is the sole member). The depth guard
// above bounds the recursion.
if (Array.isArray(schema.type)) {
const primary = schema.type.find((t) => t !== "null") ?? "null";
return synthesizeFromSchema(
{ ...schema, type: primary },
hint,
warnings,
depth + 1,
);
}
switch (schema.type) {
case "string": {
// Format-aware placeholder
switch (schema.format) {
case "date":
case "date-time":
return { kind: "ok", value: "2026-04-13T12:00:00Z", source: "schema-synth" };
case "email":
return { kind: "ok", value: "user@example.com", source: "schema-synth" };
case "uri":
case "url":
return { kind: "ok", value: "https://example.com", source: "schema-synth" };
case "uuid":
return { kind: "ok", value: "00000000-0000-0000-0000-000000000000", source: "schema-synth" };
default: {
// Use field name as a hint so it appears in DOM and prop-coverage check sees it.
// Capitalize first letter for natural-looking placeholder.
const cap = hint.charAt(0).toUpperCase() + hint.slice(1);
return { kind: "ok", value: `Sample ${cap}`, source: "schema-synth" };
}
}
}
case "integer":
case "number": {
const min = typeof schema.minimum === "number" ? schema.minimum : 1;
const max = typeof schema.maximum === "number" ? schema.maximum : 100;
const value = Math.min(max, Math.max(min, 42));
return { kind: "ok", value, source: "schema-synth" };
}
case "boolean":
return { kind: "ok", value: true, source: "schema-synth" };
case "null":
return { kind: "ok", value: null, source: "schema-synth" };
case "array": {
if (!schema.items) {
return { kind: "ok", value: [], source: "schema-synth" };
}
// Synthesize 2 items so list-rendering checks (length > 0) pass.
const items: JsonValue[] = [];
for (let i = 0; i < 2; i++) {
const itemHint = `${hint}Item${i + 1}`;
const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1);
if (itemSynth.kind === "ok") {
// Inject an `id` for list-key purposes if the item is an object lacking one.
if (
itemSynth.value !== null &&
typeof itemSynth.value === "object" &&
!Array.isArray(itemSynth.value) &&
!("id" in itemSynth.value)
) {
(itemSynth.value as JsonObject).id = `${hint}-${i + 1}`;
}
items.push(itemSynth.value);
}
}
return { kind: "ok", value: items, source: "schema-synth" };
}
case "object": {
const obj: JsonObject = {};
const propsMap = schema.properties ?? {};
const required = new Set(schema.required ?? []);
for (const [k, sub] of Object.entries(propsMap)) {
const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1);
if (subSynth.kind === "ok") {
obj[k] = subSynth.value;
} else if (required.has(k)) {
warnings.push(
`Required object field '${hint}.${k}' could not be synthesized (${subSynth.reason})`,
);
}
}
return { kind: "ok", value: obj, source: "schema-synth" };
}
default:
return { kind: "fail", reason: `unsupported schema type: ${schema.type ?? "unknown"}` };
}
}