@ggui-ai/ui-gen 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/base.d.ts +7 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +17 -3
- package/dist/adapters/generation-dispatch.js +1078 -3464
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +60 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +1346 -3557
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +10 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-BEkzEtNz.d.ts} +2 -2
- package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/index.d.ts +38 -6
- package/dist/evaluation/index.js +670 -3374
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +10 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +587 -30
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +7 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +15 -2
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +11 -2
- package/dist/harness/runtime.js +3 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +2 -2
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +1346 -3557
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
- package/dist/patch.d.ts +1 -1
- package/dist/providers/index.js +5 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
- package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +16 -12
- package/src/adapters/base.ts +8 -0
- package/src/adapters/claude/claude-code-login.ts +128 -0
- package/src/adapters/claude/sdk.ts +9 -1
- package/src/adapters/generation-dispatch.ts +19 -2
- package/src/adapters/openai/tokens.ts +18 -0
- package/src/adapters/provider-router.ts +28 -0
- package/src/boilerplate/generate.ts +19 -4
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +49 -10
- package/src/evaluation/design-identity.ts +16 -5
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +58 -0
- package/src/evaluation/visual-evaluator.ts +240 -30
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +26 -5
- package/src/harness/claude-code-login-query.ts +529 -0
- package/src/harness/coding/assemble-result.ts +18 -2
- package/src/harness/coding/generate-task-runner.ts +75 -36
- package/src/harness/coding/run-coding-turn.ts +9 -0
- package/src/harness/coding/run-eval-round.ts +338 -38
- package/src/harness/enforced-coding.ts +20 -0
- package/src/harness/llm-router.ts +390 -55
- package/src/harness/llm-trace-sink.ts +7 -1
- package/src/harness/result-types.ts +14 -0
- package/src/harness/runtime.ts +6 -0
- package/src/index.ts +2 -1
- package/src/providers/openai.ts +31 -19
- package/src/validation/primitives.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/boilerplate/json-schema-ts.ts","../../src/check/contract-validation.ts","../../src/check/type-checker.ts","../../src/check/run-tier0.ts","../../src/contract-context.ts"],"names":["ts","listContractGadgets","HOOK_NAME_RE"],"mappings":"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","file":"prompts.js","sourcesContent":["// packages/ui-gen/src/boilerplate/json-schema-ts.ts\n//\n// JSON Schema → TypeScript type string converter.\n//\n// Pure, self-contained, no env reads, no framework deps. Exposed\n// publicly on `@ggui-ai/ui-gen/boilerplate` because it's the primitive\n// that turns a contract's JSON Schema into the `interface Props { ... }`\n// block the boilerplate generator writes.\n\nimport type { JsonSchema } from '@ggui-ai/protocol';\n\n/**\n * Convert a JsonSchema to a TypeScript type string — recursive.\n *\n * Handles: string, number, integer, boolean, null, array (+ tuples), object,\n * enum, oneOf/anyOf (unions), nullable, const, additionalProperties.\n */\nexport function jsonSchemaTypeToTs(schema: JsonSchema): string {\n // ── Union types (oneOf / anyOf) ──\n const unionMembers = schema.oneOf ?? schema.anyOf;\n if (unionMembers?.length) {\n const types = [...new Set(unionMembers.map(s => jsonSchemaTypeToTs(s)))];\n return types.length === 1 ? types[0] : types.join(' | ');\n }\n\n // ── Const (literal type) ──\n if (schema.const !== undefined) {\n return typeof schema.const === 'string' ? `'${schema.const}'` : String(schema.const);\n }\n\n // ── Enum (union of literals) ──\n if (schema.enum?.length) {\n return schema.enum\n .map(v => typeof v === 'string' ? `'${v}'` : String(v))\n .join(' | ');\n }\n\n // ── Draft-07 type array (`['string','null']` — the canonical\n // nullable form since draft-2026-08-19): members' TS union. Each\n // member re-enters with the single type so object/array members\n // keep their properties/items; `nullable` is folded in here (not in\n // the recursion) so it can't double-append. ──\n if (Array.isArray(schema.type)) {\n const { nullable: _nullable, ...rest } = schema;\n const members = [...new Set(schema.type)].map((member) =>\n jsonSchemaTypeToTs({ ...rest, type: member }),\n );\n const unique = [...new Set(members)];\n if (schema.nullable && !unique.includes('null')) unique.push('null');\n return unique.join(' | ');\n }\n\n let result: string;\n\n switch (schema.type) {\n case 'string':\n result = 'string';\n break;\n case 'number':\n case 'integer':\n result = 'number';\n break;\n case 'boolean':\n result = 'boolean';\n break;\n case 'null':\n return 'null';\n\n case 'array': {\n if (schema.items) {\n const itemType = jsonSchemaTypeToTs(schema.items);\n // Use Array<T> for complex object types, T[] for simple.\n // `items.type` may be a type SET — treat any set containing\n // 'object' like the single-string form.\n const itemsDeclareObject = Array.isArray(schema.items.type)\n ? schema.items.type.includes('object')\n : schema.items.type === 'object';\n result = (itemsDeclareObject && schema.items.properties)\n ? `Array<${itemType}>`\n : (itemType.includes('|') ? `(${itemType})[]` : `${itemType}[]`);\n } else {\n result = 'unknown[]';\n }\n break;\n }\n\n case 'object': {\n if (schema.properties) {\n const required = schema.required ?? [];\n const fields = Object.entries(schema.properties)\n .map(([key, prop]) => {\n const opt = !required.includes(key);\n return `${key}${opt ? '?' : ''}: ${jsonSchemaTypeToTs(prop)}`;\n })\n .join('; ');\n result = `{ ${fields} }`;\n } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {\n result = `Record<string, ${jsonSchemaTypeToTs(schema.additionalProperties)}>`;\n } else {\n result = 'Record<string, unknown>';\n }\n break;\n }\n\n default:\n result = 'unknown';\n }\n\n // Apply nullable (OpenAPI 3.0 shorthand)\n if (schema.nullable && result !== 'unknown') {\n return `${result} | null`;\n }\n\n return result;\n}\n","// packages/ui-gen/src/check/contract-validation.ts\n//\n// Shared validation utilities for data-contract conformance —\n// deterministic tier-0 gate for `PropsSpec` / `StreamSpec` / `ActionSpec`\n// agreement between the LLM-emitted componentCode and the contract the\n// agent handed to `ggui_render`.\n//\n// Used by:\n// (1) Self-check soft gate in the coding agent\n// (`coding-agent/self-check.ts`) — surfaces mismatches as\n// structured violations for the LLM retry loop.\n// (2) Tier-0 hard gate in the evaluation pipeline — errors (severity\n// `error`) are blocking; warnings surface as advisory.\n// (3) Adapter tool-family wiring for multi-SDK generation runs.\n// (4) Benchmarks + LLM prompt construction — `jsonSchemaTypeToTs` +\n// `propsSpecToTypeScript` + `inferPropsSpecFromSampleData` are\n// pure helpers reused outside the check path. Re-exported here so\n// those callers don't need a separate import for the subpath.\n//\n// Deps are pure: `@ggui-ai/protocol` (public types) + `typescript`\n// (compiler API) + the in-package `jsonSchemaTypeToTs` primitive from\n// `../boilerplate/json-schema-ts.js`.\n\nimport type {\n PropsSpec,\n StreamSpec,\n ActionSpec,\n DataContract,\n JsonSchema,\n JsonValue,\n PropEntry,\n JsonObject,\n} from \"@ggui-ai/protocol\";\nimport ts from \"typescript\";\n\n// Re-export the pure JSON-Schema → TypeScript-type converter from the\n// in-package `boilerplate/` primitive, so consumers of the tier-0 check\n// surface (self-check, tiers, benchmarks, prompts, adapters) import a\n// single symbol from a single subpath.\nexport { jsonSchemaTypeToTs } from \"../boilerplate/json-schema-ts.js\";\nimport { jsonSchemaTypeToTs } from \"../boilerplate/json-schema-ts.js\";\n\n// =============================================================================\n// Contract Issue\n// =============================================================================\n\nexport interface ContractIssue {\n severity: \"error\" | \"warning\";\n field: string;\n message: string;\n fix: string;\n}\n\n// =============================================================================\n// Props Interface Extraction (AST-based)\n// =============================================================================\n\ninterface ExtractedProp {\n name: string;\n type: string;\n optional: boolean;\n}\n\n/**\n * Extract field names and types from a TypeScript Props interface in source code.\n * Uses the TypeScript compiler API — handles nested types, generics, unions correctly.\n */\nexport function extractPropsInterface(code: string): ExtractedProp[] | null {\n const sf = ts.createSourceFile(\"component.tsx\", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const props: ExtractedProp[] = [];\n\n function visitMembers(members: ts.NodeArray<ts.TypeElement>): void {\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.name) {\n const name = member.name.getText(sf);\n const type = member.type ? member.type.getText(sf) : \"unknown\";\n const optional = !!member.questionToken;\n props.push({ name, type, optional });\n }\n }\n }\n\n function visit(node: ts.Node): void {\n // interface Props { ... }\n if (ts.isInterfaceDeclaration(node) && node.name.text === \"Props\") {\n visitMembers(node.members);\n return;\n }\n // type Props = { ... }\n if (ts.isTypeAliasDeclaration(node) && node.name.text === \"Props\") {\n if (ts.isTypeLiteralNode(node.type)) {\n visitMembers(node.type.members);\n }\n return;\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sf);\n return props.length > 0 ? props : null;\n}\n\n// =============================================================================\n// Contract Validation\n// =============================================================================\n\n/**\n * Compare extracted Props interface against a PropsSpec contract.\n * Returns issues: errors for missing required fields, warnings for type mismatches.\n */\nexport function validatePropsAgainstSchema(\n code: string,\n spec: PropsSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n const extracted = extractPropsInterface(code);\n\n if (!extracted) {\n // Can't extract — not necessarily an error (might use inline types)\n return [];\n }\n\n const extractedMap = new Map(extracted.map((p) => [p.name, p]));\n\n for (const [propName, entry] of Object.entries(spec.properties)) {\n const extractedProp = extractedMap.get(propName);\n\n if (!extractedProp) {\n if (entry.required) {\n issues.push({\n severity: \"error\",\n field: propName,\n message: `Props interface is missing required field '${propName}' from the data contract`,\n fix: `Add \\`${propName}${entry.required ? \"\" : \"?\"}: ${jsonSchemaTypeToTs(entry.schema)}\\` to your Props interface`,\n });\n } else {\n issues.push({\n severity: \"warning\",\n field: propName,\n message: `Props interface is missing optional field '${propName}' from the data contract`,\n fix: `Consider adding \\`${propName}?: ${jsonSchemaTypeToTs(entry.schema)}\\` to your Props interface`,\n });\n }\n continue;\n }\n\n // Basic type compatibility check (only if we have type info)\n if (extractedProp.type !== \"unknown\") {\n const expectedTsType = jsonSchemaTypeToTs(entry.schema);\n if (!isTypeCompatible(extractedProp.type, expectedTsType, entry.schema)) {\n issues.push({\n severity: \"warning\",\n field: propName,\n message: `Field '${propName}' has type '${extractedProp.type}' but contract expects '${expectedTsType}'`,\n fix: `Change \\`${propName}\\` type to \\`${expectedTsType}\\``,\n });\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Check if a TypeScript type string is compatible with a JSON Schema type.\n * Loose comparison — we only check the base type, not full structural equality.\n */\nfunction isTypeCompatible(tsType: string, expectedTsType: string, schema: JsonSchema): boolean {\n const normalized = tsType.replace(/\\s/g, \"\").toLowerCase();\n\n switch (schema.type) {\n case \"string\":\n return normalized.includes(\"string\");\n case \"number\":\n case \"integer\":\n return normalized.includes(\"number\");\n case \"boolean\":\n return normalized.includes(\"boolean\");\n case \"array\":\n return normalized.includes(\"[]\") || normalized.includes(\"array\");\n case \"object\":\n return ![\"string\", \"number\", \"boolean\"].some((t) => normalized === t);\n default:\n return true;\n }\n}\n\n// =============================================================================\n// JSON Schema → TypeScript Conversion (for LLM prompts)\n// =============================================================================\n\n// `jsonSchemaTypeToTs` lives in `@ggui-ai/ui-gen/boilerplate` and is\n// re-exported at the top of this file.\n\n/**\n * Convert a full PropsSpec to a TypeScript interface string for LLM prompt injection.\n * Includes required/optional markers and descriptions as comments.\n */\nexport function propsSpecToTypeScript(spec: PropsSpec, indent = 2): string {\n const pad = \" \".repeat(indent);\n const lines: string[] = [];\n\n for (const [propName, entry] of Object.entries(spec.properties)) {\n if (entry.description) {\n lines.push(`${pad}/** ${entry.description} */`);\n }\n const optional = !entry.required;\n const tsType = jsonSchemaTypeToTs(entry.schema);\n const defaultStr = entry.default !== undefined ? ` // default: ${JSON.stringify(entry.default)}` : \"\";\n lines.push(`${pad}${propName}${optional ? \"?\" : \"\"}: ${tsType};${defaultStr}`);\n // Include example as a comment so the LLM sees the exact data shape\n if (entry.example !== undefined) {\n const exampleStr = JSON.stringify(entry.example, null, 2)\n .split(\"\\n\")\n .map((l, i) => (i === 0 ? `${pad}// Example: ${l}` : `${pad}// ${l}`))\n .join(\"\\n\");\n lines.push(exampleStr);\n }\n }\n\n return `{\\n${lines.join(\"\\n\")}\\n}`;\n}\n\n// =============================================================================\n// StreamSpec Validation\n// =============================================================================\n\n/**\n * Validate that the generated source code properly handles stream events.\n * Requires useStream() wire hooks — legacy DOM event patterns are no longer accepted.\n */\nexport function validateStreamSpecConformance(\n code: string,\n spec: StreamSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n const usesWireStream = code.includes(\"useStream\");\n\n if (!usesWireStream) {\n issues.push({\n severity: \"error\",\n field: \"__streamSpec__\",\n message: \"Component does not handle stream events — use useStream() from the boilerplate wire hooks\",\n fix: \"Use the useStream('eventName') hook from the boilerplate to receive real-time data from the agent\",\n });\n return issues;\n }\n\n // Check each declared channel is referenced in the code. `spec` is a\n // flat `Record<channelName, StreamChannelEntry>` — iterate directly.\n for (const [channelName, entry] of Object.entries(spec)) {\n if (!code.includes(`'${channelName}'`) && !code.includes(`\"${channelName}\"`)) {\n issues.push({\n severity: \"warning\",\n field: channelName,\n message: `StreamSpec declares channel '${channelName}' but component doesn't reference it`,\n fix: `Add useStream('${channelName}') to receive ${entry.description || channelName} events`,\n });\n }\n }\n\n return issues;\n}\n\n// =============================================================================\n// ActionSpec Validation\n// =============================================================================\n\n/**\n * Validate that the generated source code wires the declared actions.\n * Checks that action IDs are referenced via useAction() wire hooks, string literals, or prop callbacks.\n */\nexport function validateActionSpecConformance(\n code: string,\n spec: ActionSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n // `spec` is a flat `Record<actionId, ActionEntry>` — iterate directly.\n for (const [actionId, entry] of Object.entries(spec)) {\n // Check the action ID or its label is referenced in the code\n const idReferenced = code.includes(`'${actionId}'`) || code.includes(`\"${actionId}\"`);\n const labelReferenced = code.includes(entry.label);\n const callbackName = `on${actionId.charAt(0).toUpperCase()}${actionId.slice(1)}`;\n const callbackReferenced = code.includes(callbackName);\n\n if (!idReferenced && !labelReferenced && !callbackReferenced) {\n issues.push({\n severity: \"error\",\n field: actionId,\n message: `ActionSpec declares action '${actionId}' (\"${entry.label}\") but component doesn't wire it`,\n fix: `Use the useAction('${actionId}') hook from the boilerplate and wire it to a button or form submit`,\n });\n }\n }\n\n return issues;\n}\n\n// =============================================================================\n// Unified Contract Validation\n// =============================================================================\n\n/**\n * Validate all contract (props, stream, actions) against the source code.\n * Returns combined issues from all three validations.\n */\nexport function validateAllContracts(\n code: string,\n contract: DataContract,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n if (contract.propsSpec) {\n issues.push(...validatePropsAgainstSchema(code, contract.propsSpec));\n }\n\n if (contract.streamSpec) {\n issues.push(...validateStreamSpecConformance(code, contract.streamSpec));\n }\n\n if (contract.actionSpec) {\n issues.push(...validateActionSpecConformance(code, contract.actionSpec));\n }\n\n return issues;\n}\n\n// =============================================================================\n// Sample Data → PropsSpec Inference (backward compat)\n// =============================================================================\n\n/**\n * Infer a PropsSpec from sample data.\n * Convenience utility for migration and for agents that pass sample data instead of schemas.\n * All fields are marked as required (can't distinguish required/optional from data alone).\n */\nexport function inferPropsSpecFromSampleData(data: JsonObject): PropsSpec {\n const properties: Record<string, PropEntry> = {};\n\n for (const [key, value] of Object.entries(data)) {\n properties[key] = {\n schema: inferJsonSchema(value),\n required: true,\n example: value as JsonValue,\n };\n }\n\n return { properties };\n}\n\nfunction inferJsonSchema(value: unknown): JsonSchema {\n if (value === null || value === undefined) return { type: \"null\" };\n if (typeof value === \"string\") return { type: \"string\" };\n if (typeof value === \"number\") return { type: \"number\" };\n if (typeof value === \"boolean\") return { type: \"boolean\" };\n\n if (Array.isArray(value)) {\n if (value.length === 0) return { type: \"array\" };\n return {\n type: \"array\",\n items: inferJsonSchema(value[0]),\n };\n }\n\n if (typeof value === \"object\") {\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const [k, v] of Object.entries(value as JsonObject)) {\n properties[k] = inferJsonSchema(v);\n required.push(k);\n }\n return { type: \"object\", properties, required };\n }\n\n return { type: \"string\" };\n}\n","// packages/ui-gen/src/check/type-checker.ts\n//\n// TypeScript type-checker for LLM-generated UI components. Uses the\n// TypeScript compiler API with a virtual filesystem to type-check TSX\n// code against React types and `@ggui-ai/design` primitives, giving\n// the LLM familiar TS error feedback without writing anything to disk.\n//\n// The checker builds a virtual filesystem seeded with `typescript/lib`,\n// `@types/react`, `@ggui-ai/design/dist`, and `@ggui-ai/wire/dist`\n// `.d.ts` content, then drives `ts.createProgram` against a synthetic\n// `Component.tsx`. Exposed on `@ggui-ai/ui-gen/check` so\n// `createUiGenerator` runs the same tier-0 gate as the hosted\n// generation path.\n//\n// Path-resolution note: the three workspace-rooted lookups —\n// `.pnpm/@types+react@*`, `packages/design/dist`, and\n// `packages/wire/dist` — are anchored relative to this file. tsup\n// bundles `src/check/index.ts` (splitting: false) into a flat\n// `dist/check/index.js`, so the src and dist locations sit at the same\n// depth and both resolve correctly. `typescript/lib/typescript.js`\n// keeps using `createRequire(import.meta.url).resolve` —\n// position-independent, so no change.\n//\n// Used by:\n// (1) Tier-0 `type_check` — blocking errors (see BLOCKING_CODES)\n// map to PRODUCTION_FAILED; non-blocking TS diagnostics surface\n// as warnings so the LLM can iterate on non-crash-risk findings\n// without stalling the harness.\n// (2) The `self_check` tool — called from generator SDKs (Anthropic /\n// OpenAI / Google) to give the LLM TS feedback mid-generation.\n// (3) The tier-0 orchestrator `runTier0Checks` — runs in parallel\n// with the wire-preservation + lint checks under the same tier\n// budget.\n//\n// Design notes that motivate non-obvious choices:\n// - Classic React JSX mode (`ts.JsxEmit.React`). The synthetic\n// prefix (see SYNTHETIC_PREFIX) supplies `import React from\n// 'react'` for the classic JSX factory PLUS a self-contained\n// global `JSX` namespace. `@types/react` v19 removed the global\n// `JSX` namespace that classic mode resolves intrinsic elements +\n// the `key`/`ref` carve-out through; without the shim, `<div>`\n// degrades to `any` and every typed component falsely rejects the\n// intrinsic `key` prop. Automatic mode was tried but its\n// `react/jsx-runtime` resolution does not survive the VFS.\n// - `strict: false` + `strictNullChecks: true`: we want the runtime\n// crash classes (`undefined.foo()`, `null.bar`) to surface as\n// blocking, but not the optional-chaining / exhaustive-check\n// noise that full `strict` would emit on LLM code.\n// - `types: []`: prevents the VFS from auto-pulling every\n// `@types/*` package that happens to be hoisted — only react and\n// the design/wire dist types should be reachable, matching the\n// forbidden-import policy enforced by `react-linter` elsewhere.\n// - TS2307 (\"Cannot find module\") is deliberately NOT blocking —\n// Lambda bundles code without type declarations, so the VFS can't\n// see every package that may exist at runtime. Forbidden imports\n// are caught by `runSelfChecks` regex instead.\n\nimport ts from 'typescript';\nimport fs from 'fs';\nimport path from 'path';\nimport { createRequire } from 'module';\nimport { describeAllowedImports } from '../validation/allowed-imports.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TypeCheckDiagnostic {\n code: number;\n line: number;\n message: string;\n fix: string;\n}\n\nexport interface TypeCheckResult {\n errors: TypeCheckDiagnostic[]; // Blocking\n warnings: TypeCheckDiagnostic[]; // Non-blocking\n}\n\n// ---------------------------------------------------------------------------\n// Blocking error codes — these would break at runtime\n// ---------------------------------------------------------------------------\n\nconst BLOCKING_CODES = new Set([\n 2304, // Cannot find name\n // 2307 (Cannot find module) is NOT blocking — Lambda bundles code without\n // type declarations, so the VFS can't resolve react/@ggui-ai/design.\n // Forbidden imports are caught by runSelfChecks regex instead.\n 2305, // Module has no exported member\n 2322, // Type not assignable\n 2339, // Property does not exist on type\n 2741, // Missing required property\n 2769, // No overload matches this call\n 17004, // Cannot use JSX unless '--jsx' flag\n 18047, // 'X' is possibly 'null' — causes runtime crash\n 18048, // 'X' is possibly 'undefined' — causes runtime crash\n]);\n\n// ---------------------------------------------------------------------------\n// Fix suggestions per error code\n// ---------------------------------------------------------------------------\n\nfunction generateFix(code: number, message?: string, sourceLine?: string): string {\n // Source-line hint: when the failing line is short and self-contained,\n // appending it gives the LLM a precise target. Skip for very long lines\n // (>140 chars — usually multi-prop JSX where the line itself is the\n // diagnostic) since they bloat the violation envelope without helping.\n const sourceHint =\n sourceLine && sourceLine.length > 0 && sourceLine.length <= 140\n ? ` Offending line: \\`${sourceLine}\\``\n : '';\n\n // Detect event handler on non-interactive primitive\n if ((code === 2322 || code === 2339) && message && /onClick|onDoubleClick|onMouseEnter|onMouseLeave|onPress/.test(message)) {\n return `This structural primitive has no event handlers of its own — add the trait as a PROP: as={Clickable} (then onClick works), imported from @ggui-ai/design. Do NOT wrap it in <Clickable>…</Clickable>; as is a prop, not a wrapper element.${sourceHint}`;\n }\n\n // Detect underscore-prefix-on-prop trap: `Property '_X' does not exist`.\n // LLM saw an unused-var warning on `X` and tried to silence it by renaming\n // the destructure to `_X`, breaking prop access. Steer back.\n if (code === 2339 && message && /Property '_[a-zA-Z]/.test(message)) {\n return `You renamed a prop with a leading underscore to silence \\`no-unused-vars\\`, but the prop on \\`Props\\` doesn't have that prefix. Restore the original name; better, don't destructure props you won't render — access them via \\`props.fieldName\\` only when needed.${sourceHint}`;\n }\n\n // Detect \"Cannot find name\" for destructured props\n if (code === 2304 && message && !message.includes('module') && !message.includes('import')) {\n return `This name is not defined in scope. Either you destructured props (use \\`props.fieldName\\` directly instead) OR you removed a helper declaration in this patch but kept a JSX/expression reference to it. Read your full patch and either restore the declaration or remove the reference.${sourceHint}`;\n }\n\n // Detect \"unknown is not assignable to ReactNode\"\n if ((code === 2322 || code === 2769) && message && /unknown.*ReactNode|ReactNode.*unknown/.test(message)) {\n return `This value has type 'unknown' and cannot be rendered in JSX. Cast it: String(value) or add a type annotation.${sourceHint}`;\n }\n\n switch (code) {\n case 2307:\n return `Only these imports are allowed: ${describeAllowedImports()}${sourceHint}`;\n case 2322:\n case 2769:\n return `Type mismatch on this expression. Check the offending line below — the prop name in JSX is what TypeScript is rejecting; the message tells you the expected type.${sourceHint}`;\n case 2339:\n return `This prop doesn't exist on this component — check the available props on the component's interface (visible at the top of the file, or in the design-system reference).${sourceHint}`;\n case 2305:\n return `This name is not defined. Check your imports and variable declarations.${sourceHint}`;\n case 2741:\n return `A required prop is missing. Check the component's Props interface.${sourceHint}`;\n case 18047:\n case 18048:\n return `This value might be null/undefined. Every dereference on the same nullable still needs \\`?.\\` — \\`x?.foo && x?.bar\\`, NOT \\`x?.foo && x.bar\\`. Or hoist: \\`const v = x; if (!v) return null;\\` then access \\`v.foo\\`/\\`v.bar\\` unguarded.${sourceHint}`;\n default:\n return `Review the TypeScript error and fix the type issue.${sourceHint}`;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Virtual Filesystem — lazy singleton\n// ---------------------------------------------------------------------------\n\ninterface VfsEntry {\n content: string;\n sourceFile: ts.SourceFile;\n}\n\nlet vfsCache: Map<string, VfsEntry> | null = null;\n\nfunction parseAndStore(\n vfs: Map<string, VfsEntry>,\n virtualPath: string,\n content: string,\n): void {\n const sourceFile = ts.createSourceFile(\n virtualPath,\n content,\n ts.ScriptTarget.ES2020,\n true,\n virtualPath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,\n );\n vfs.set(virtualPath, { content, sourceFile });\n}\n\n/**\n * Recursively walk a directory and return all file paths matching a filter.\n */\nfunction walkDir(dir: string, filter: (f: string) => boolean): string[] {\n const results: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(full, filter));\n } else if (filter(full)) {\n results.push(full);\n }\n }\n return results;\n}\n\n/**\n * Resolve `@types/react`'s root directory through Node's module\n * resolution. Position-independent — walks the consumer's node_modules\n * chain. Works in monorepo (where the type package is hoisted under\n * `<root>/node_modules/.pnpm/@types+react@*`) and in standalone\n * `packages/`-only installs (e.g. the projected public OSS repo where\n * the `packages/` prefix is stripped, so a hardcoded `../../../..`\n * walk lands in the wrong place).\n */\nfunction findReactTypesDir(): string | null {\n try {\n // `@types/react` doesn't expose `package.json` as a public subpath,\n // so resolve via the always-present `index.d.ts`.\n const indexDts = createRequire(import.meta.url).resolve('@types/react/index.d.ts');\n return path.dirname(indexDts);\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve a workspace package's `dist/` directory through Node's\n * module resolution. Like `findReactTypesDir`, this works in both\n * monorepo (where the package is hoisted/symlinked) and standalone\n * installs — no relative-path math required.\n */\nfunction findPackageDistDir(pkg: string): string | null {\n try {\n const pkgJson = createRequire(import.meta.url).resolve(`${pkg}/package.json`);\n const dist = path.join(path.dirname(pkgJson), 'dist');\n return fs.existsSync(dist) ? dist : null;\n } catch {\n return null;\n }\n}\n\nasync function loadVfs(): Promise<Map<string, VfsEntry>> {\n if (vfsCache) return vfsCache;\n\n const vfs = new Map<string, VfsEntry>();\n\n // ---- 1. TypeScript lib files ----\n // `createRequire(import.meta.url).resolve('typescript/lib/typescript.js')`\n // is position-independent — it walks the consumer's node_modules chain,\n // so this lookup works identically from core/, packages/ui-gen/, and any\n // future host package.\n const require_ = createRequire(import.meta.url);\n const tsDir = path.dirname(require_.resolve('typescript/lib/typescript.js'));\n const libFiles = fs.readdirSync(tsDir).filter((f) => /^lib\\..*\\.d\\.ts$/.test(f));\n for (const file of libFiles) {\n const content = fs.readFileSync(path.join(tsDir, file), 'utf-8');\n parseAndStore(vfs, file, content);\n }\n\n // ---- 2. React types ----\n // Resolved through `createRequire(import.meta.url).resolve('@types/\n // react/index.d.ts')` — position-independent. Works the same in\n // monorepo (hoisted to root) and in standalone `packages/`-only\n // installs (the projected public OSS repo strips the `packages/`\n // prefix, so a hardcoded `../../../..` walk would miss).\n const reactDir = findReactTypesDir();\n if (reactDir) {\n const reactDts = fs.readdirSync(reactDir).filter((f) => f.endsWith('.d.ts'));\n for (const file of reactDts) {\n const content = fs.readFileSync(path.join(reactDir, file), 'utf-8');\n parseAndStore(vfs, `node_modules/@types/react/${file}`, content);\n }\n }\n\n // ---- 3. Design system types ----\n const designDist = findPackageDistDir('@ggui-ai/design');\n if (designDist) {\n const dtsFiles = walkDir(designDist, (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'));\n for (const file of dtsFiles) {\n const rel = path.relative(designDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/design/${rel}`, content);\n }\n }\n\n // ---- 4. Wire module types ----\n const wireDist = findPackageDistDir('@ggui-ai/wire');\n if (wireDist) {\n const dtsFiles = walkDir(wireDist, (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'));\n for (const file of dtsFiles) {\n const rel = path.relative(wireDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/wire/${rel}`, content);\n }\n }\n\n // ---- 5. Gadgets types ----\n // Generated component code direct-imports standard-library gadget\n // hooks — `import { useGeolocation } from '@ggui-ai/gadgets'`.\n // Loading the `@ggui-ai/gadgets` package's shipped `.d.ts` into the\n // VFS makes those bare-named imports resolve against the real hook\n // declarations (named option/return types preserved). The standard\n // gadget hooks are plain named exports the resolver finds directly.\n const clientLibsDist = findPackageDistDir('@ggui-ai/gadgets');\n if (clientLibsDist) {\n const dtsFiles = walkDir(\n clientLibsDist,\n (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'),\n );\n for (const file of dtsFiles) {\n const rel = path.relative(clientLibsDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/gadgets/${rel}`, content);\n }\n }\n\n vfsCache = vfs;\n return vfs;\n}\n\n// ---------------------------------------------------------------------------\n// Module resolution helper\n// ---------------------------------------------------------------------------\n\n/**\n * Try to resolve a relative import within the VFS.\n * Given `containingFile` (e.g. `node_modules/@ggui-ai/design/primitives/index.d.ts`)\n * and `importPath` (e.g. `./Card` or, as every published declaration says\n * since 0.15.0, `./Card.js`), try:\n * - <resolved>.d.ts\n * - <resolved>/index.d.ts\n * - <resolved>.ts\n * A `.js` / `.mjs` / `.cjs` extension on the specifier is the ESM\n * convention for \"the module that compiles to this file\" — TypeScript's\n * NodeNext maps `./Card.js` to `./Card.d.ts`, and so does this: the\n * extension is stripped before the candidates are tried. Without that, a\n * `.js`-suffixed re-export resolved to nothing, the imported names typed\n * as `any`, and the checker reported zero errors on code it must reject\n * (ggui 0.15.0 publish-gate, type-checker.test.ts).\n */\nfunction resolveRelativeInVfs(\n vfs: Map<string, VfsEntry>,\n containingFile: string,\n importPath: string,\n): string | undefined {\n // Strip leading '/' from containingFile — TS may prepend getCurrentDirectory()\n const normalized = containingFile.startsWith('/') ? containingFile.slice(1) : containingFile;\n const dir = path.posix.dirname(normalized);\n // `./x.js` (`.mjs`, `.cjs`) names the declaration `./x.d.ts` — strip it.\n const specifier = importPath.replace(/\\.(?:m|c)?js$/, '');\n const resolved = path.posix.normalize(`${dir}/${specifier}`);\n\n // Try .d.ts extension\n const withDts = `${resolved}.d.ts`;\n if (vfs.has(withDts)) return withDts;\n\n // Try /index.d.ts\n const withIndexDts = `${resolved}/index.d.ts`;\n if (vfs.has(withIndexDts)) return withIndexDts;\n\n // Try .ts extension\n const withTs = `${resolved}.ts`;\n if (vfs.has(withTs)) return withTs;\n\n // Might already be the exact path\n if (vfs.has(resolved)) return resolved;\n\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// CompilerHost factory\n// ---------------------------------------------------------------------------\n\nconst COMPILER_OPTIONS: ts.CompilerOptions = {\n target: ts.ScriptTarget.ES2020,\n module: ts.ModuleKind.ESNext,\n // Classic React JSX mode. The synthetic prefix (SYNTHETIC_PREFIX)\n // supplies `import React from 'react'` plus a `declare global` JSX\n // namespace shim: `@types/react` v19 removed the *global* `JSX`\n // namespace (it lives at `React.JSX` now), which classic mode needs\n // for `JSX.IntrinsicElements` and the `key`/`ref` carve-out. Without\n // the shim, `<div>` degrades to `any` and every typed component\n // falsely rejects the intrinsic `key` prop.\n jsx: ts.JsxEmit.React,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n strict: false,\n strictNullChecks: true, // Catch undefined.foo() errors that cause runtime crashes\n noEmit: true,\n skipLibCheck: true,\n noImplicitAny: true,\n types: [],\n esModuleInterop: true,\n};\n\n/**\n * Normalize a path that TS's internal module resolution might generate.\n * Since getCurrentDirectory() returns '/', TS will probe paths like\n * '/node_modules/@types/react/jsx-runtime.d.ts'. We strip the leading\n * '/' to match our VFS keys.\n */\nfunction normalizeVfsPath(f: string): string | null {\n if (f.startsWith('/')) {\n return f.slice(1);\n }\n return null;\n}\n\n/**\n * Core module resolution logic shared between resolveModuleNameLiterals\n * (for user imports) and resolveModuleNames (for TS-internal imports\n * like jsxImportSource).\n */\nfunction resolveModuleName(\n vfs: Map<string, VfsEntry>,\n name: string,\n containingFile: string,\n): ts.ResolvedModuleWithFailedLookupLocations {\n // React core\n if (name === 'react') {\n return resolved('node_modules/@types/react/index.d.ts');\n }\n if (name === 'react/jsx-runtime') {\n return resolved('node_modules/@types/react/jsx-runtime.d.ts');\n }\n if (name === 'react/jsx-dev-runtime') {\n const p = 'node_modules/@types/react/jsx-dev-runtime.d.ts';\n if (vfs.has(p)) return resolved(p);\n return resolved('node_modules/@types/react/jsx-runtime.d.ts');\n }\n\n // @ggui-ai/design sub-paths\n const designPrefix = '@ggui-ai/design/';\n if (name.startsWith(designPrefix)) {\n const subpath = name.slice(designPrefix.length);\n const indexPath = `node_modules/@ggui-ai/design/${subpath}/index.d.ts`;\n if (vfs.has(indexPath)) return resolved(indexPath);\n const directPath = `node_modules/@ggui-ai/design/${subpath}.d.ts`;\n if (vfs.has(directPath)) return resolved(directPath);\n }\n\n // @ggui-ai/design root\n if (name === '@ggui-ai/design') {\n return resolved('node_modules/@ggui-ai/design/index.d.ts');\n }\n\n // @ggui-ai/wire\n if (name === '@ggui-ai/wire') {\n return resolved('node_modules/@ggui-ai/wire/index.d.ts');\n }\n\n // @ggui-ai/gadgets — the STDLIB gadget package. Generated\n // code direct-imports the seven STDLIB hooks (`useGeolocation`, …)\n // as plain named exports from this specifier. The shipped `.d.ts`\n // (loaded into the VFS above) carries their real declarations.\n if (name === '@ggui-ai/gadgets') {\n return resolved('node_modules/@ggui-ai/gadgets/index.d.ts');\n }\n\n // Relative imports — resolve within VFS\n if (name.startsWith('./') || name.startsWith('../')) {\n const resolvedPath = resolveRelativeInVfs(vfs, containingFile, name);\n if (resolvedPath) return resolved(resolvedPath);\n }\n\n // Third-party gadget wrapper packages. The render handler fetches each\n // non-stdlib gadget's `.d.ts` and `typecheck` overlays it at\n // `node_modules/<package>/index.d.ts`. A generated direct import\n // `import { useX } from '<package>'` resolves through this\n // bare-specifier branch against the overlaid `.d.ts` — named\n // option/return types preserved instead of collapsing to `any`.\n // Generic VFS lookup — no per-package allow-list.\n const bareIndex = `node_modules/${name}/index.d.ts`;\n if (vfs.has(bareIndex)) return resolved(bareIndex);\n\n // Unknown module\n return { resolvedModule: undefined };\n}\n\nfunction createVfsHost(\n vfs: Map<string, VfsEntry>,\n componentCode: string,\n): ts.CompilerHost {\n // Create fresh source file for the user component\n const componentFile = ts.createSourceFile(\n 'Component.tsx',\n componentCode,\n ts.ScriptTarget.ES2020,\n true,\n ts.ScriptKind.TSX,\n );\n\n const host: ts.CompilerHost = {\n getSourceFile(fileName: string) {\n if (fileName === 'Component.tsx') return componentFile;\n const entry = vfs.get(fileName);\n if (entry) return entry.sourceFile;\n // Try normalized path (strip leading '/')\n const normalized = normalizeVfsPath(fileName);\n if (normalized) return vfs.get(normalized)?.sourceFile;\n return undefined;\n },\n getDefaultLibFileName() {\n return 'lib.es2020.full.d.ts';\n },\n writeFile() {\n // no-op\n },\n getCurrentDirectory() {\n return '/';\n },\n getCanonicalFileName(f: string) {\n return f;\n },\n useCaseSensitiveFileNames() {\n return true;\n },\n getNewLine() {\n return '\\n';\n },\n fileExists(f: string) {\n if (f === 'Component.tsx' || vfs.has(f)) return true;\n // Normalize bare-specifier lookups that TS's internal resolver\n // turns into relative-looking paths from getCurrentDirectory '/'.\n const normalized = normalizeVfsPath(f);\n if (normalized && vfs.has(normalized)) return true;\n return false;\n },\n readFile(f: string) {\n if (f === 'Component.tsx') return componentCode;\n const direct = vfs.get(f);\n if (direct) return direct.content;\n const normalized = normalizeVfsPath(f);\n if (normalized) return vfs.get(normalized)?.content;\n return undefined;\n },\n resolveModuleNameLiterals(\n moduleLiterals: readonly ts.StringLiteralLike[],\n containingFile: string,\n ): readonly ts.ResolvedModuleWithFailedLookupLocations[] {\n return moduleLiterals.map((literal) =>\n resolveModuleName(vfs, literal.text, containingFile),\n );\n },\n };\n\n return host;\n}\n\nfunction resolved(resolvedFileName: string): ts.ResolvedModuleWithFailedLookupLocations {\n return {\n resolvedModule: {\n resolvedFileName,\n isExternalLibraryImport: true,\n extension: ts.Extension.Dts,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport async function typecheck(\n code: string,\n /**\n * A `package -> .d.ts content` map for third-party gadget wrappers.\n * The render handler parallel-fetches each non-stdlib gadget's `.d.ts`\n * (via `GadgetDescriptor.typesUrl` + SRI) and threads the result\n * here. Each entry is overlaid into the per-call VFS at\n * `node_modules/<package>/index.d.ts`. A generated direct import\n * `import { useX } from '<package>'` resolves through the\n * bare-specifier branch in `resolveModuleName` directly against this\n * overlaid entry — named option/return types preserved, so a\n * wrong-typed hook call surfaces a blocking TS error instead of\n * collapsing to `any`.\n *\n * Stdlib gadgets (`package: '@ggui-ai/gadgets'`) need no entry —\n * `@ggui-ai/gadgets` ships its `.d.ts` into the VFS unconditionally.\n * Omit for standard-library-only callers.\n */\n dtsMap?: Readonly<Record<string, string>>,\n): Promise<TypeCheckResult> {\n const vfs = await loadVfs();\n\n // Overlay each third-party gadget wrapper's `.d.ts` per-call without\n // mutating the cached VFS. The bare-specifier branch in\n // `resolveModuleName` resolves a generated `import { useX } from\n // '<package>'` against the overlaid entry, so the LLM sandbox sees\n // the wrapper's strict hook/component types (named option/return\n // shapes) rather than `any`. A third-party wrapper with no `dtsMap`\n // entry collapses to `any` (TS2307 is non-blocking) — degraded UX,\n // not a generation blocker; the fix is to thread `gadgetTypes`.\n // Stdlib (`@ggui-ai/gadgets`) ships its `.d.ts` unconditionally.\n const effectiveVfs = (() => {\n const dtsEntries = dtsMap !== undefined ? Object.entries(dtsMap) : [];\n if (dtsEntries.length === 0) {\n return vfs;\n }\n const overlay = new Map(vfs);\n for (const [pkg, content] of dtsEntries) {\n parseAndStore(overlay, `node_modules/${pkg}/index.d.ts`, content);\n }\n return overlay;\n })();\n\n // Prepend a synthetic prefix for classic JSX mode:\n // - `import React from 'react'` — the classic-mode JSX factory\n // (`React.createElement`) needs the React namespace in scope.\n // - a `declare global` JSX shim — `@types/react` v19 dropped the\n // GLOBAL `JSX` namespace (it is `React.JSX` now); classic mode\n // resolves intrinsic elements + the `key`/`ref` carve-out through\n // the global `JSX`, so without this `<div>` degrades to `any` and\n // every typed component falsely rejects the intrinsic `key` prop.\n // Mirrors `react/jsx-runtime`'s own JSX namespace, hoisted global.\n // All prefix lines are skipped from diagnostics via `lineOffset`.\n // `@types/react` v19 removed the GLOBAL `JSX` namespace (it lives at\n // `React.JSX` now — un-exported, not reliably reachable through the\n // VFS). Classic JSX mode resolves intrinsic elements + the `key`\n // carve-out through the global `JSX`, so declare a self-contained\n // one. Intrinsic elements are permissive (`[elem]: any`): the\n // type-checker's value is on the design components — which keep\n // their real prop types regardless of this namespace — and on\n // runtime-crash detection, not on deep typing of raw HTML (which the\n // generation prompt discourages anyway).\n const globalJsxShim =\n 'declare global { namespace JSX {' +\n ' type ElementType = string | ((props: any) => any) | (new (props: any) => any);' +\n ' interface Element { type: any; props: any; key: string | number | null; }' +\n ' interface ElementClass { render(): any; }' +\n ' interface ElementAttributesProperty { props: object; }' +\n ' interface ElementChildrenAttribute { children: object; }' +\n ' interface IntrinsicAttributes { key?: string | number | bigint | null; }' +\n ' interface IntrinsicClassAttributes<T> { ref?: any; }' +\n ' interface IntrinsicElements { [elem: string]: any; }' +\n ' } }\\n';\n const SYNTHETIC_PREFIX = \"import React from 'react';\\n\" + globalJsxShim;\n const prefixedCode = SYNTHETIC_PREFIX + code;\n const lineOffset = SYNTHETIC_PREFIX.split('\\n').length - 1;\n\n const host = createVfsHost(effectiveVfs, prefixedCode);\n\n const program = ts.createProgram(['Component.tsx'], COMPILER_OPTIONS, host);\n const diagnostics = ts.getPreEmitDiagnostics(program);\n\n const errors: TypeCheckDiagnostic[] = [];\n const warnings: TypeCheckDiagnostic[] = [];\n\n for (const diag of diagnostics) {\n // Only report diagnostics from the user's component file\n if (diag.file && diag.file.fileName !== 'Component.tsx') continue;\n // Global diagnostics (no file) — skip as they come from config\n if (!diag.file) continue;\n // Skip diagnostics on the synthetic import prefix line\n const diagLine = ts.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line;\n if (diagLine < lineOffset) continue;\n\n const rawLine = diag.file\n ? ts.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line + 1\n : 0;\n // Subtract the synthetic import line to get the user's original line number\n const line = Math.max(1, rawLine - lineOffset);\n const message = ts.flattenDiagnosticMessageText(diag.messageText, '\\n');\n const code_ = diag.code;\n\n // Extract the offending source line for the fix hint — without it\n // \"Type 'string' is not assignable to type 'number'\" is unactionable\n // because the LLM doesn't know which prop on which primitive. With\n // the line (\"<Icon size=\\\"md\\\" ...>\"), the LLM can target precisely.\n const sourceLines = diag.file.text.split('\\n');\n const sourceLine =\n rawLine >= 1 && rawLine <= sourceLines.length\n ? sourceLines[rawLine - 1]?.trim() ?? ''\n : '';\n\n const entry: TypeCheckDiagnostic = {\n code: code_,\n line,\n message,\n fix: generateFix(code_, message, sourceLine),\n };\n\n if (BLOCKING_CODES.has(code_)) {\n errors.push(entry);\n } else {\n warnings.push(entry);\n }\n }\n\n return { errors, warnings };\n}\n","// packages/ui-gen/src/check/run-tier0.ts\n//\n// Tier-0 deterministic CHECK orchestrator — the programmatic half of\n// the three-tier evaluation system:\n// Tier 0 — programmatic (deterministic, no LLM) ← this file\n// Tier 1 — LLM critical checks (functionality, crash)\n// Tier 2 — LLM quality checks (interactivity, accessibility, layout, etc.)\n//\n// Composes the deterministic helpers (`extract-wire-calls`,\n// `react-linter`, `contract-validation`, `type-checker`) from the\n// `@ggui-ai/ui-gen/check` subpath. The purely-typed evaluation surface\n// (`EvalTier` / `EvalOutcome` / `priorityForIssue` / …) is imported\n// directly from `@ggui-ai/ui-gen/evaluation` — its canonical home.\n\nimport ts from 'typescript';\nimport type { DataContract } from '@ggui-ai/protocol';\nimport { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';\nimport { consumedTokenManifest } from '@ggui-ai/design/themes';\nimport { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';\nimport { validateAllContracts, type ContractIssue } from './contract-validation.js';\nimport { typecheck } from './type-checker.js';\nimport { lintReactHooks, type ReactLintDiagnostic } from './react-linter.js';\nimport { isAllowedImport, describeAllowedImports } from '../validation/allowed-imports.js';\nimport {\n checkWireImports,\n checkWirePreservation,\n type WireKind,\n} from './extract-wire-calls.js';\nimport {\n priorityForIssue,\n type EvalCategory,\n type EvalIssue,\n type EvalResult,\n} from '../evaluation/types-public.js';\n\n/**\n * Strip line + block comments from source, leaving all other text in\n * place. Uses the TS scanner so a `//` or block-comment sequence inside\n * a string / template / regex literal is NOT mistaken for a comment.\n * Comment characters are replaced with spaces (newlines kept) so byte\n * offsets and line numbers are unchanged.\n *\n * Used by regex-based CHECK rules that must not be satisfied by\n * commented-out code (e.g. `gadget_preservation`).\n */\nfunction stripComments(code: string): string {\n const scanner = ts.createScanner(\n ts.ScriptTarget.Latest,\n /* skipTrivia */ false,\n ts.LanguageVariant.JSX,\n code,\n );\n let out = '';\n for (\n let token = scanner.scan();\n token !== ts.SyntaxKind.EndOfFileToken;\n token = scanner.scan()\n ) {\n const text = scanner.getTokenText();\n out +=\n token === ts.SyntaxKind.SingleLineCommentTrivia ||\n token === ts.SyntaxKind.MultiLineCommentTrivia\n ? text.replace(/[^\\n]/g, ' ')\n : text;\n }\n return out;\n}\n\n/**\n * AST-precise detector for the unambiguous nested-interactive\n * double-wire pattern. See the call-site comment in `runTier0Checks` +\n * the `double-wired-action:certain` block for the rationale.\n *\n * The detector accepts a JSX element as the outer host when:\n * - tagName ∈ {Card, Box, Stack, Row}\n * - has `as={Clickable | Pressable}` (`Hoverable` doesn't fire click)\n * - has `onClick` (or `onPress`) whose handler calls one of the\n * known `useAction` bindings.\n *\n * Then it walks descendants of that outer element and matches an inner\n * interactive primitive when:\n * - tagName ∈ {Button, Checkbox, Input, Toggle, Slider, RadioGroup,\n * Select, TextArea, Link}\n * - has any of {onClick, onChange, onPress, onSelect} whose handler\n * calls the SAME useAction binding as the outer.\n *\n * Patterns it INTENTIONALLY does not catch:\n * - helper-function indirection (`const fire = () => toggle({id})`\n * in both handlers) — the AST sees two distinct callees, not the\n * same binding. The runtime dedup in `useAction` still catches the\n * symptom; the static-time catch is a teaching aid, not a safety\n * net.\n * - cross-component prop drilling — same reason.\n * - imperative DOM addEventListener — out of scope for JSX walking.\n */\nfunction detectCertainDoubleWiredActions(\n sourceCode: string,\n actionBindings: readonly string[],\n /**\n * `constrained` keeps the design-package tag sets above. `free`\n * retargets the SAME invariant (\"one useAction binding → exactly one\n * gesture surface\") at raw elements: the outer host is ANY JSX element\n * whose onClick/onPress calls a binding (`<div onClick>`, `<li>`,\n * `<tr>`, `<button>`, `<a>`, `[role=button]`, a design primitive…),\n * and the inner match is ANY descendant whose handler attribute calls\n * the same binding. Tag names stop mattering because in free mode a\n * raw element is a first-class gesture surface.\n */\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): EvalIssue[] {\n const issues: EvalIssue[] = [];\n if (actionBindings.length === 0) return issues;\n const bindings = new Set(actionBindings);\n const anyElementIsInteractive = designMode === 'free';\n const TRAIT_HOST_TAGS = new Set(['Card', 'Box', 'Stack', 'Row']);\n const TRAITS_THAT_FIRE = new Set(['Clickable', 'Pressable']);\n const INTERACTIVE_DESCENDANTS = new Set([\n 'Button',\n 'Checkbox',\n 'Input',\n 'Toggle',\n 'Slider',\n 'RadioGroup',\n 'Select',\n 'TextArea',\n 'Link',\n ]);\n const HANDLER_ATTRS = ['onClick', 'onChange', 'onPress', 'onSelect'] as const;\n\n const sf = ts.createSourceFile(\n 'source.tsx',\n sourceCode,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n ts.ScriptKind.TSX,\n );\n\n function tagNameText(\n node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n ): string | undefined {\n return ts.isIdentifier(node.tagName) ? node.tagName.text : undefined;\n }\n\n function attrExpression(\n node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attrName: string,\n ): ts.Expression | undefined {\n for (const attr of node.attributes.properties) {\n if (\n ts.isJsxAttribute(attr) &&\n ts.isIdentifier(attr.name) &&\n attr.name.text === attrName &&\n attr.initializer !== undefined &&\n ts.isJsxExpression(attr.initializer)\n ) {\n return attr.initializer.expression;\n }\n }\n return undefined;\n }\n\n function isTraitHostWithFiringAs(node: ts.JsxOpeningElement): boolean {\n const tag = tagNameText(node);\n if (tag === undefined) return false;\n // Free mode: every element with a handler is a gesture surface —\n // the callee check in `visit` decides whether it fires a binding.\n if (anyElementIsInteractive) return true;\n if (!TRAIT_HOST_TAGS.has(tag)) return false;\n const asExpr = attrExpression(node, 'as');\n return (\n asExpr !== undefined &&\n ts.isIdentifier(asExpr) &&\n TRAITS_THAT_FIRE.has(asExpr.text)\n );\n }\n\n // Best-effort callee extraction from event-handler expression shapes\n // the LLM actually emits. Forms covered:\n // {handler} → 'handler'\n // {() => callee(arg)} → 'callee'\n // {(e) => callee(e)} → 'callee'\n // {() => { callee(arg); }} → 'callee'\n // {function () { callee(arg); }} → 'callee'\n // Forms intentionally not covered: chained calls, conditional\n // expressions, calls inside if-branches. Those are rare in\n // LLM-generated JSX and would expand the FP surface.\n function extractCalleeName(\n expr: ts.Expression | undefined,\n ): string | undefined {\n if (expr === undefined) return undefined;\n if (ts.isIdentifier(expr)) return expr.text;\n if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {\n const body = expr.body;\n if (ts.isCallExpression(body) && ts.isIdentifier(body.expression)) {\n return body.expression.text;\n }\n if (ts.isBlock(body)) {\n for (const stmt of body.statements) {\n if (\n ts.isExpressionStatement(stmt) &&\n ts.isCallExpression(stmt.expression) &&\n ts.isIdentifier(stmt.expression.expression)\n ) {\n return stmt.expression.expression.text;\n }\n }\n }\n }\n return undefined;\n }\n\n function findMatchingInteractiveDescendant(\n outer: ts.JsxElement,\n expectedBinding: string,\n ): { tag: string; line: number } | undefined {\n let found: { tag: string; line: number } | undefined;\n function walk(node: ts.Node): void {\n if (found !== undefined) return;\n const opening = ts.isJsxElement(node)\n ? node.openingElement\n : ts.isJsxSelfClosingElement(node)\n ? node\n : undefined;\n if (opening !== undefined) {\n const tag = tagNameText(opening);\n if (\n tag !== undefined &&\n (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))\n ) {\n for (const attrName of HANDLER_ATTRS) {\n const callee = extractCalleeName(attrExpression(opening, attrName));\n if (callee === expectedBinding) {\n found = {\n tag,\n line:\n sf.getLineAndCharacterOfPosition(opening.getStart()).line + 1,\n };\n return;\n }\n }\n }\n }\n ts.forEachChild(node, walk);\n }\n // Walk only the outer element's children, not the outer itself.\n ts.forEachChild(outer, walk);\n return found;\n }\n\n function visit(node: ts.Node): void {\n if (ts.isJsxElement(node) && isTraitHostWithFiringAs(node.openingElement)) {\n const outerCallee =\n extractCalleeName(attrExpression(node.openingElement, 'onClick')) ??\n extractCalleeName(attrExpression(node.openingElement, 'onPress'));\n if (outerCallee !== undefined && bindings.has(outerCallee)) {\n const match = findMatchingInteractiveDescendant(node, outerCallee);\n if (match !== undefined) {\n const outerTag = tagNameText(node.openingElement);\n const outerLine =\n sf.getLineAndCharacterOfPosition(node.openingElement.getStart())\n .line + 1;\n const outerLabel = anyElementIsInteractive\n ? `<${outerTag}>`\n : `<${outerTag} as={...}>`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'interactivity',\n subcategory: 'double-wired-action:certain',\n severity: 'critical',\n description:\n `Nested-interactive double-wire: outer ${outerLabel} at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. ` +\n `One user click on the inner control fires its handler AND bubbles to the outer handler — '${outerCallee}' dispatches TWICE, the action runs back-to-back, and a toggle-style action silently reverts the user's change.`,\n fix: anyElementIsInteractive\n ? `Pick ONE gesture surface for '${outerCallee}': either remove the onClick from the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR drop the inner <${match.tag}>'s handler and let the outer <${outerTag}> own it. Never wire both to the same useAction binding — stopPropagation is not a fix.`\n : `Pick ONE surface for the gesture: either drop \\`as={...}\\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,\n line: outerLine,\n });\n }\n }\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sf);\n return issues;\n}\n\n/**\n * The consumed-token manifest as a closed set (ggui#613 residual 3 —\n * the generation-time closure leg of the #598 record). Every theme\n * MUST define exactly these names (the coverage gate); a generated\n * component may therefore consume ONLY these names. Anything else\n * is a reference no theme is obligated to define, and under the s4\n * fallback ban it paints nothing.\n */\nconst MANIFEST_TOKENS: ReadonlySet<string> = new Set(consumedTokenManifest);\nconst TOKEN_REF_RE = /var\\(\\s*(--ggui-[a-zA-Z0-9-]+)/g;\n\n/** Map wire kind to the matching `@ggui-ai/wire` hook name. */\nconst HOOK_NAME_FOR: Readonly<Record<WireKind, string>> = {\n action: 'useAction',\n stream: 'useStream',\n context: 'useGguiContext',\n};\n\n/**\n * Map wire kind to the contract field the agent must declare on the\n * `contract` input to `ggui_render` (post-Phase-2 flat handshake input)\n * to make the `useX('<name>')` call legal.\n *\n * Drives the remediation message on the `wire_undeclared` fail.\n * Telling the LLM \"declare it on contextSpec.<slotName>\" with the\n * JSON-Schema starter is concretely actionable; \"declare a contract\"\n * alone is not.\n */\nconst CONTRACT_FIELD_FOR: Readonly<Record<WireKind, string>> = {\n action: 'actionSpec',\n stream: 'streamSpec',\n context: 'contextSpec',\n};\n\n// =============================================================================\n// Tier 0: Programmatic Hard Checks\n// =============================================================================\n\n/**\n * Whether comment-free `source` imports the named gadget `exportName`\n * from `pkg` — `import { …, exportName, … } from '<pkg>'` (the source\n * name, left of any `as`). Kind-agnostic — hook and component exports\n * are both direct-imported the same way. The `gadget_preservation`\n * anchor: generated code direct-imports each gadget export, and the\n * import is what the iframe rewriter binds to the per-package runtime\n * shim.\n */\nfunction isGadgetExportImported(\n source: string,\n pkg: string,\n exportName: string,\n): boolean {\n const esc = pkg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const re = new RegExp(\n `import\\\\s*(?:[A-Za-z_$][\\\\w$]*\\\\s*,\\\\s*)?\\\\{([^}]*)\\\\}\\\\s*from\\\\s*['\"]${esc}['\"]`,\n 'g',\n );\n let m: RegExpExecArray | null;\n while ((m = re.exec(source)) !== null) {\n for (const raw of m[1].split(',')) {\n if (raw.trim().split(/\\s+as\\s+/)[0]?.trim() === exportName) return true;\n }\n }\n return false;\n}\n\n/**\n * AST-precise detector for the compiles-but-renders-nothing class: the\n * default-exported component's body never returns a tree.\n *\n * The boilerplate HANDS the model the wrapper — `base.tsx.tmpl` and\n * `base-free.tsx.tmpl` both emit `return (` … `);` around `{{LAYOUT}}`.\n * A model rewriting the root block can delete those two lines with it,\n * leaving the JSX as a bare expression statement. That is legal TSX: it\n * parses, esbuild compiles it, `typecheck()` passes it, and the\n * component renders a blank frame. Seen once in twenty takes of the\n * ggui#1113 A/B (`xs-chat-card`, run 2 — the judge spent a screenshot\n * and an LLM call to report \"completely blank\").\n *\n * `compile/default-export` already guards the scaffold's\n * `export default function` line and `types/props-interface` its\n * `interface Props` line. The `return (` was the third scaffold line\n * with nothing watching it.\n *\n * Conservative by construction — it reports ONLY when it positively\n * identified a block-bodied default-export component:\n * - a concise arrow body (`export default () => <X/>`) returns by\n * construction → silent\n * - `export default memo(Component)` or any other expression it\n * cannot resolve to a function → silent\n * - a `return` inside a nested helper is not the component's own (the\n * walk stops at every nested function boundary), but ONE own-level\n * `return <value>` anywhere — an early `return null` included —\n * satisfies it.\n *\n * Returns `null` when the component returns (or could not be\n * identified), else the 1-based line of the first bare JSX expression\n * statement in its body — the line the fix belongs on — or `undefined`\n * when the body has no JSX statement to point at either.\n */\nfunction detectComponentNeverReturns(\n sourceCode: string,\n): { jsxStatementLine: number | undefined } | null {\n const sf = ts.createSourceFile(\n 'source.tsx',\n sourceCode,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n ts.ScriptKind.TSX,\n );\n\n const isFunctionLike = (n: ts.Node): boolean =>\n ts.isFunctionDeclaration(n) ||\n ts.isFunctionExpression(n) ||\n ts.isArrowFunction(n) ||\n ts.isMethodDeclaration(n) ||\n ts.isClassDeclaration(n) ||\n ts.isClassExpression(n);\n\n /** The top-level declaration `name` binds to, when it is a function. */\n function resolveTopLevel(\n name: string,\n ): ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | undefined {\n for (const st of sf.statements) {\n if (ts.isFunctionDeclaration(st) && st.name?.text === name) return st;\n if (ts.isVariableStatement(st)) {\n for (const decl of st.declarationList.declarations) {\n if (\n ts.isIdentifier(decl.name) &&\n decl.name.text === name &&\n decl.initializer !== undefined &&\n (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))\n ) {\n return decl.initializer;\n }\n }\n }\n }\n return undefined;\n }\n\n let component:\n | ts.FunctionDeclaration\n | ts.FunctionExpression\n | ts.ArrowFunction\n | undefined;\n for (const st of sf.statements) {\n if (\n ts.isFunctionDeclaration(st) &&\n st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)\n ) {\n component = st;\n break;\n }\n if (ts.isExportAssignment(st) && st.isExportEquals !== true) {\n const expr = st.expression;\n if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) component = expr;\n else if (ts.isIdentifier(expr)) component = resolveTopLevel(expr.text);\n break;\n }\n }\n // No default export at all, or one this detector cannot resolve to a\n // function — `compile/default-export` owns the first case and the\n // second is not ours to guess at.\n if (component === undefined) return null;\n\n const body = component.body;\n // A concise arrow body IS the returned value.\n if (body === undefined || !ts.isBlock(body)) return null;\n\n let returnsValue = false;\n let jsxStatementLine: number | undefined;\n const visit = (node: ts.Node): void => {\n if (returnsValue) return;\n if (ts.isReturnStatement(node)) {\n if (node.expression !== undefined) returnsValue = true;\n return;\n }\n if (\n jsxStatementLine === undefined &&\n ts.isExpressionStatement(node) &&\n (ts.isJsxElement(node.expression) ||\n ts.isJsxFragment(node.expression) ||\n ts.isJsxSelfClosingElement(node.expression))\n ) {\n jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;\n }\n node.forEachChild((child) => {\n // A helper component's own `return` is not this component's.\n if (!isFunctionLike(child)) visit(child);\n });\n };\n for (const st of body.statements) if (!isFunctionLike(st)) visit(st);\n\n return returnsValue ? null : { jsxStatementLine };\n}\n\n/**\n * Run all tier 0 (deterministic, no-LLM) checks against component source.\n *\n * Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].\n * Also runs contract validation when contract are provided.\n */\nexport async function runTier0Checks(\n sourceCode: string,\n contract?: DataContract,\n buildErrors?: string[],\n /**\n * A `package -> .d.ts content` map for third-party gadget wrappers.\n * Threaded verbatim into `typecheck()` so the in-memory TS sandbox\n * overlays each wrapper `.d.ts` at `node_modules/<package>/index.d.ts`\n * — a generated direct import `import { useX } from '<package>'`\n * resolves against the real declaration (strict option/return\n * narrowing). Standard-library-only callers omit it.\n */\n gadgetTypes?: Readonly<Record<string, string>>,\n /**\n * Which triad produced the source. `constrained` (default) runs every\n * leg exactly as before. `free` gates OFF the design-VOCABULARY legs —\n * `raw-spacing`, `raw-pixels`, `numeric-spacing-prop`, the\n * `stack-row-*` prop checks and `clickable-wrapper` — because raw\n * elements, inline CSS and literal spacing are first-class there. It\n * KEEPS every contract leg: `hex-color`, `hardcoded-color-fn`,\n * `named-color`, `token-fallback`, `off-manifest-token`,\n * `asset-color-pair` (color stays on the closed token manifest),\n * imports, security, Props / default-export, wire_* and gadget checks,\n * and the double-wired-action detector (retargeted at raw elements).\n */\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): Promise<EvalIssue[]> {\n const issues: EvalIssue[] = [];\n const lines = sourceCode.split('\\n');\n // Design-vocabulary legs fire only when the prompt taught the\n // vocabulary — see `designMode` above.\n const enforceDesignVocabulary = designMode === 'constrained';\n\n // ── Compile check ─────────────────────────────────────────\n //\n // Fail only when the caller ATTEMPTED compile and captured errors.\n // Absent `buildErrors` means the caller hasn't run esbuild at all\n // (e.g. OSS `createUiGenerator` which is sometimes wrapped by\n // `withBrowserCompile` and sometimes not); degrade gracefully —\n // downstream checks (security, forbidden imports, wire-import\n // presence, react-linter, Props-interface, default-export) still\n // fire on sourceCode alone.\n if (buildErrors && buildErrors.length > 0) {\n const errorDetail = buildErrors\n .map((e) => {\n // Extract key info from esbuild error (e.g., line number, error type)\n const match = e.match(/<stdin>:(\\d+):\\d+: ERROR: (.+)/);\n return match ? `Line ${match[1]}: ${match[2]}` : e.slice(0, 200);\n })\n .join('\\n');\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'build-error',\n severity: 'critical',\n description: `Component failed to compile:\\n${errorDetail}`,\n fix: 'Fix the JSX/TypeScript syntax errors listed above',\n });\n }\n\n // ── assetColor allow-list ─────────────────────────────────\n //\n // T-4: Box gains a typed brand-color escape pair —\n // <Box assetColor=\"#635BFF\" assetSemantic=\"stripe-brand-purple\">\n // The static hex / rgba checks below allow a hex/rgba/hsl whose\n // `JSXAttribute` parent is `assetColor`, but only when a sibling\n // `assetSemantic` attribute carries a non-empty string value on the\n // same JSX opening tag. Both pieces are required: an `assetColor`\n // alone, or `assetSemantic=\"\"`, fails.\n //\n // Implementation: collect the (1-based) line numbers of every line\n // whose JSX opening tag legitimately satisfies this pair. The\n // hex/colorFn/named-color checks below skip emitting a failure\n // when the matching line is in this allow-list.\n const assetEscapedLines = new Set<number>();\n // Match each JSX opening tag (allowing multiline attribute lists).\n // The regex captures the tag's attribute span; we then check it for\n // the required `assetColor` + non-empty `assetSemantic` pair, and\n // record every source line that the tag spans.\n const jsxOpenRegex = /<([A-Z][A-Za-z0-9]*)\\b([^>]*)>/gs;\n for (const match of sourceCode.matchAll(jsxOpenRegex)) {\n const tagAttrs = match[2];\n const assetColorAttr = tagAttrs.match(/\\bassetColor\\s*=\\s*(?:[\"']([^\"']*)[\"']|\\{[^}]*\\})/);\n if (!assetColorAttr) continue;\n const assetSemanticAttr = tagAttrs.match(/\\bassetSemantic\\s*=\\s*[\"']([^\"']*)[\"']/);\n const startLine = sourceCode.slice(0, match.index ?? 0).split('\\n').length;\n const tagLineCount = match[0].split('\\n').length;\n // Both attributes must be present, AND assetSemantic must be a\n // non-empty string literal. A `{expr}` value for assetSemantic is\n // treated as not-statically-verifiable; the escape is opt-in\n // documentation, not a runtime expression. Failing pairs emit a\n // dedicated `tokens:asset-color-pair` failure AND do NOT enter\n // the allow-list — so the embedded hex still fires its own\n // `tokens:hex-color` failure (two issues, one per axis).\n if (!assetSemanticAttr || assetSemanticAttr[1].length === 0) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'asset-color-pair',\n severity: 'critical',\n description:\n 'Box `assetColor` set without a non-empty `assetSemantic` — the typed brand-color escape requires both. ' +\n '`assetSemantic` is a human-readable label that documents why this color bypasses the theme.',\n fix:\n 'Pair the `assetColor` with a non-empty `assetSemantic` literal, e.g. ' +\n '`<Box assetColor=\"#635BFF\" assetSemantic=\"stripe-brand-purple\">`. Reach for `surface=\"...\"` first ' +\n 'whenever the color SHOULD track the operator\\'s theme.',\n line: startLine,\n });\n continue;\n }\n for (let l = 0; l < tagLineCount; l++) {\n assetEscapedLines.add(startLine + l);\n }\n }\n\n // Gadget packages the contract declares are import-\n // allowlisted. Generated code direct-imports gadget exports from\n // their package. `clientCapabilities.gadgets` is\n // package-keyed, so the map's own keys ARE the permitted import\n // sources, alongside the STDLIB `@ggui-ai/gadgets`.\n const allowedGadgetPackages = new Set<string>(\n Object.keys(contract?.clientCapabilities?.gadgets ?? {}),\n );\n\n // ── Line-by-line checks ───────────────────────────────────\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNum = i + 1;\n\n // Security: eval()\n if (/\\beval\\s*\\(/.test(line)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'security',\n subcategory: 'eval',\n severity: 'critical',\n description: 'eval() is forbidden',\n fix: 'Remove eval() call entirely',\n line: lineNum,\n });\n }\n\n // Security: fetch()\n if (/\\bfetch\\s*\\(/.test(line)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'security',\n subcategory: 'fetch',\n severity: 'critical',\n description: 'fetch() is forbidden — use props for data',\n fix: 'Remove fetch() and pass data via props',\n line: lineNum,\n });\n }\n\n // Forbidden imports (skip commented-out imports)\n const trimmedLine = line.trim();\n if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*')) continue;\n const importMatch = trimmedLine.match(/import\\s+.*from\\s+['\"]([^'\"]+)['\"]/);\n if (importMatch) {\n const pkg = importMatch[1];\n // Allowlist mirrors `validation/component-detailed.ts` ALLOWED_IMPORTS —\n // these two must stay in sync. Generated code direct-imports\n // gadget exports, so every gadget package the contract declares\n // (`clientCapabilities.gadgets[*].package`) is allowlisted alongside\n // the STDLIB `@ggui-ai/gadgets`. Rejecting a declared gadget package\n // would contradict the `gadget_preservation` check (which REQUIRES\n // the direct import).\n if (!isAllowedImport(pkg, allowedGadgetPackages)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: 'disallowed-package',\n severity: 'critical',\n description: `Import from \"${pkg}\" is not allowed`,\n fix: `Only import from: ${describeAllowedImports()}`,\n line: lineNum,\n });\n }\n }\n\n // Hardcoded hex colors (not inside CSS variable fallbacks).\n //\n // Empirical (warn → fail rationale): when the\n // operator's prompt evoked a strong aesthetic (\"garden / living\n // ecosystem\"), the LLM emitted hex literals (`#B8FF3A` lime,\n // `#0a0a0a` near-black) inline on `color`/`background` style\n // overrides — completely overriding the operator's selected theme.\n // The warn-level rule was advisory and got ignored. As a `fail`\n // it forces the LLM to remediate before passing self-check.\n //\n // Generation contract this enforces (per the contract bar): the\n // LLM describes structure + intent; the theme + primitive variants\n // describe visuals. Hardcoded hex is a contract violation. The\n // existing `var(--ggui-*, #fallback)` exception keeps legit token\n // patterns flowing untouched. Asset-fixed brand colors get a\n // typed escape on Box (`assetColor` + `assetSemantic`) — see\n // T-4 follow-up.\n // Literal fallback inside ANY token reference (ggui#598 slice 4\n // origin-kill, generalized by ggui#613 residual 4 from color to\n // every token): the runtime injects every token — colors, spacing,\n // typography, radius, shadows — so a literal fallback can only\n // ever paint default-brand pixels under a failed injection, which\n // must be visible (unset) rather than silently default. The old\n // hex-only exemption plus a remediation that suggested\n // `var(--x, ${hex})` actively taught the model to launder invented\n // values INTO fallbacks (adversarial-cycle find). A token-to-token\n // fallback (`var(--a, var(--b))`) is not a literal — it stays on\n // the manifest check alone.\n const literalFallbackMatch = line.match(\n /var\\(\\s*(--ggui-[a-zA-Z0-9-]+)\\s*,\\s*([^)]+)\\)/,\n );\n const literalFallback =\n literalFallbackMatch !== null &&\n !literalFallbackMatch[2]!.trim().startsWith('var(')\n ? { token: literalFallbackMatch[1]!, literal: literalFallbackMatch[2]!.trim() }\n : null;\n if (literalFallback !== null && !assetEscapedLines.has(lineNum)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'token-fallback',\n severity: 'critical',\n description: `Literal fallback \"${literalFallback.literal}\" inside a token reference — the runtime injects every token, and the fallback paints the wrong value exactly when the theme matters most.`,\n fix: `Drop the fallback: write var(${literalFallback.token}) bare. If the token might not exist, that is a vocabulary problem to fix by choosing a token from the design-system docs, never by a literal.`,\n line: lineNum,\n });\n }\n\n const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\\b/);\n if (hexMatch && !assetEscapedLines.has(lineNum)) {\n // A hex that lives inside a token fallback is already reported\n // above as `token-fallback` — don't double-report it as\n // `hex-color`.\n if (literalFallback !== null && literalFallback.literal.includes(hexMatch[0])) {\n // reported above\n } else if (!line.includes('// fallback')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'hex-color',\n severity: 'critical',\n description: `Hardcoded color \"${hexMatch[0]}\" breaks theme switching — use design tokens.`,\n fix: `Replace with a primitive variant (Button variant=\"primary\"|Badge variant=\"success\"|...) OR a bare token reference like var(--ggui-color-primary-500) — never a literal fallback. Hardcoded colors mean the operator's theme has no effect on this surface.`,\n line: lineNum,\n });\n }\n }\n\n // Hardcoded rgba()/hsl() color functions — same reasoning as above.\n // Treated as `fail` for symmetry — a hardcoded `rgba(184, 255, 58, 1)`\n // is functionally identical to `#B8FF3A` from a theme-override\n // standpoint.\n const colorFnMatch = line.match(/\\b(rgba?|hsla?)\\s*\\(/);\n if (\n colorFnMatch &&\n !line.includes('var(--ggui-') &&\n !line.includes('// fallback') &&\n !line.includes('$value') &&\n !assetEscapedLines.has(lineNum)\n ) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'hardcoded-color-fn',\n severity: 'critical',\n description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching — use design tokens.`,\n fix: 'Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.',\n line: lineNum,\n });\n }\n\n // Off-manifest token references (ggui#613 residual 3): a\n // `var(--ggui-*)` whose name is outside the consumed-token\n // manifest. The manifest is the closed vocabulary the prompt\n // teaches AND the set every theme must define — so a name outside\n // it is invented, and with literal fallbacks banned it renders\n // UNSET. Fail, like the other token-contract violations; one issue\n // per distinct name per line.\n const offManifest = new Set<string>();\n for (const m of line.matchAll(TOKEN_REF_RE)) {\n const name = m[1]!;\n if (!MANIFEST_TOKENS.has(name)) offManifest.add(name);\n }\n for (const name of offManifest) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'off-manifest-token',\n severity: 'critical',\n description: `Token \"${name}\" is not in the design system's token manifest — no theme defines it, so it renders unset.`,\n fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,\n line: lineNum,\n });\n }\n\n // Raw CSS length on a spacing / radius prop — bypasses the design\n // scale. Same reasoning as the hardcoded-color checks above: a\n // literal `gap=\"8px\"` defeats the theme's spacing scale exactly as\n // `#fff` defeats its colors. Only the STRING-literal form fails;\n // the numeric escape (`gap={12}`) is untouched, so a genuine\n // off-scale pixel value still has a path.\n const rawSpacingMatch = enforceDesignVocabulary\n ? line.match(\n /\\b(gap|padding|paddingX|paddingY|margin|radius)\\s*=\\s*[\"'][\\d.]+(?:px|rem|em)[\"']/,\n )\n : null;\n if (rawSpacingMatch) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'raw-spacing',\n severity: 'critical',\n description: `\\`${rawSpacingMatch[1]}\\` uses a raw CSS length — bypasses the design spacing/radius scale.`,\n fix: `Use a scale name: ${rawSpacingMatch[1]}=\"xs|sm|md|lg|xl\" (spacing also has none|2xl; radius none|sm|md|lg|xl). For an exact off-scale pixel value pass a number — ${rawSpacingMatch[1]}={12}.`,\n line: lineNum,\n });\n }\n\n // CSS named-color literals on color/background sinks. Closes the\n // residual escape after the hex/rgb checks: `style={{ color:\n // 'lime' }}` or `style={{ background: 'royalblue' }}`. The hex\n // check doesn't match these because they're keyword identifiers,\n // not `#`-prefixed hex. CSS-spec named colors are a finite set;\n // we match the obvious ones the LLM reaches for.\n //\n // Allowed keywords (no fail): `inherit`, `currentColor`,\n // `transparent`, `unset`, `initial`, `revert`, `none`. These have\n // legitimate semantic uses (inherit for nested rendering, none\n // for explicit empty fill).\n //\n // Out-of-scope by design: token-name strings like `color:\n // 'primary'` — those would render as invalid CSS and a separate\n // self-check (the existing prop-value validator) catches them.\n const namedColorMatch = line.match(\n /(?:^|[^a-zA-Z-])(?:color|background|backgroundColor|borderColor)\\s*:\\s*['\"]([a-z][a-zA-Z]+)['\"]/,\n );\n if (namedColorMatch) {\n const named = namedColorMatch[1].toLowerCase();\n const allowed = new Set([\n 'inherit', 'currentcolor', 'transparent',\n 'unset', 'initial', 'revert', 'none',\n ]);\n // Heuristic for the named-color lexicon. Covers every CSS-spec\n // color the LLM has empirically reached for; new keywords can\n // be added as they surface in benchmarks. Token-name strings\n // (`'primary'`) bypass this check because they're not in the\n // lexicon — the prop-value validator catches them as invalid\n // enum values.\n const namedCssColors = new Set([\n 'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink',\n 'cyan', 'magenta', 'lime', 'teal', 'navy', 'maroon', 'olive',\n 'aqua', 'fuchsia', 'silver', 'gray', 'grey', 'white', 'black',\n 'brown', 'gold', 'indigo', 'violet', 'crimson', 'tomato',\n 'coral', 'salmon', 'orchid', 'plum', 'tan', 'khaki', 'beige',\n 'ivory', 'lavender', 'mint', 'turquoise', 'azure',\n 'royalblue', 'darkblue', 'lightblue', 'skyblue', 'steelblue',\n 'darkred', 'lightgreen', 'darkgreen', 'forestgreen',\n 'darkorange', 'lightgray', 'lightgrey', 'darkgray', 'darkgrey',\n 'hotpink', 'deeppink', 'lightpink', 'lightyellow',\n ]);\n if (!allowed.has(named) && namedCssColors.has(named)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'named-color',\n severity: 'critical',\n description: `Hardcoded CSS named color \"${named}\" breaks theme switching — use design tokens.`,\n fix: `Replace with a typed slot (Text tone=\"muted\" / Box surface=\"accent\" / Badge variant=\"success\") OR a semantic token: var(--ggui-color-onSunken), var(--ggui-color-primary-500), etc. The keyword \"${named}\" maps to a fixed RGB; the operator's theme has no effect on it.`,\n line: lineNum,\n });\n }\n }\n\n // Raw pixel values in spacing CSS properties\n const pxMatch = enforceDesignVocabulary\n ? line.match(/(?:padding|margin|gap|borderRadius)\\s*:\\s*['\"]?\\d+px/)\n : null;\n if (pxMatch && !line.includes('var(--ggui-')) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'tokens',\n subcategory: 'raw-pixels',\n description: 'Raw pixel value in spacing — must use design tokens',\n fix: 'Replace with a bare var(--ggui-spacing-*) token — no literal fallback (the runtime injects every token) — or the scale name on the primitive prop (gap=\"md\", padding=\"lg\")',\n line: lineNum,\n });\n }\n\n // Numeric spacing props on components (e.g., padding={24}, gap={8})\n // These bypass the design system — should use var(--ggui-spacing-*)\n const numericSpacingMatch = enforceDesignVocabulary\n ? line.match(/\\b(padding|paddingX|paddingY|gap|margin)\\s*=\\s*\\{?\\s*(\\d+)\\s*\\}?/)\n : null;\n if (numericSpacingMatch && !line.includes('var(--ggui-')) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'tokens',\n subcategory: 'numeric-spacing-prop',\n description: `Numeric spacing prop ${numericSpacingMatch[1]}={${numericSpacingMatch[2]}} — use design tokens instead`,\n fix: `Replace with ${numericSpacingMatch[1]}=\"var(--ggui-spacing-*)\"`,\n line: lineNum,\n });\n }\n }\n\n // ── Clickable trait check — onClick on a structural primitive needs as={Clickable} ──\n // Only the four trait-host primitives (typed `WithTrait<…>`) accept\n // `as={Clickable}`. Text/Heading/Badge/Avatar/Image are NOT hosts, so an\n // onClick on them is a plain type error the type-checker catches — don't\n // steer those toward `as={Clickable}` (it won't compile on them).\n const clickablePattern = /<(?:Card|Box|Stack|Row)\\s[^>]*onClick/;\n for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {\n if (clickablePattern.test(lines[i]) && !lines[i].includes('as={Clickable}') && !lines[i].includes('as={Pressable}')) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: 'clickable-wrapper',\n description: `onClick on a structural primitive without as={Clickable} — the bare primitive has no click or keyboard handling`,\n fix: `Add as={Clickable} to the element, e.g., <Card as={Clickable} onClick={handler}>`,\n line: i + 1,\n });\n }\n }\n\n // ── Double-wired action check (warn — broad regex) ──────────\n // A useAction callback dispatched from 2+ call sites fires the action\n // more than once per gesture when an interactive element nests inside\n // another (the inner gesture bubbles to the outer handler). Captured\n // in scenario-07: a `Card as={Clickable} onClick` row containing a\n // `Checkbox onChange`, both calling the same action → double-toggle.\n //\n // Binding extraction. The constrained regex is the pre-`designMode`\n // one, kept verbatim: it matches `useAction(` only, so a binding\n // declared with the generic the boilerplate itself emits —\n // `useAction<ActionXPayload>('x')` — is NOT collected there (a\n // pre-existing gap, left in place because the constrained gate must\n // stay byte-identical for the A/B; collapse the two regexes once that\n // pin is lifted). `free` mode accepts the optional generic so the\n // one-gesture-surface invariant actually fires on boilerplate-shaped\n // hook lines.\n const bindingRe =\n designMode === 'free'\n ? /(?:const|let)\\s+(\\w+)\\s*=\\s*useAction\\s*(?:<[^>]*>)?\\s*\\(/g\n : /(?:const|let)\\s+(\\w+)\\s*=\\s*useAction\\s*\\(/g;\n const actionBindings = [...sourceCode.matchAll(bindingRe)]\n .map((m) => m[1])\n .filter((name): name is string => typeof name === 'string');\n for (const binding of actionBindings) {\n const callSites = (\n sourceCode.match(new RegExp(`\\\\b${binding}\\\\s*\\\\(`, 'g')) ?? []\n ).length;\n if (callSites >= 2) {\n // Kept at WARN — too many legit patterns (confirm/cancel, branched\n // dispatch, sibling buttons in a list) match this generic shape. The\n // structural correctness backstop lives at runtime in `useAction`'s\n // task-scoped dedup; this check is a quality nudge for the LLM, not a\n // load-bearing safety gate. See `docs/principles/no-silent-block.md`\n // — three-pattern FP test fails for this detector → severity = warn.\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'interactivity',\n subcategory: 'double-wired-action',\n description: `Action '${binding}' is dispatched from ${callSites} call sites — if an interactive element nests inside another, one gesture fires the action twice (the inner gesture bubbles to the outer handler).`,\n fix: `Wire each useAction callback to exactly ONE interactive surface; never nest interactive elements (e.g. a Checkbox inside a Card as={Clickable}).`,\n });\n }\n }\n\n // ── Double-wired action check (fail — narrow AST) ────────────\n // Precise detector for the unambiguous shape: an outer trait-host JSX\n // element (Card/Box/Stack/Row with `as={Clickable|Pressable}`) wired\n // via onClick/onPress to useAction binding X, with a descendant\n // interactive primitive (Checkbox/Button/Input/Toggle/Slider/\n // RadioGroup/Select/TextArea/Link) wired via on* to the SAME X.\n //\n // Near-zero FP: the only patterns matching `<Card as={Clickable}\n // onClick={X}> ... <Checkbox onChange={X}>` are bugs. Three-pattern\n // FP test passes → severity earns 'fail' per no-silent-block\n // principle Rule 1. The runtime dedup in @ggui-ai/wire's useAction\n // catches the SYMPTOM regardless of shape; this check catches the\n // CAUSE one turn earlier and prevents shipping the broken a11y nest.\n if (actionBindings.length > 0) {\n issues.push(\n ...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode),\n );\n }\n\n // ── Connection suppression (ggui#670 Phase 2) ────────────────\n // `useRender().isConnected` is a LIVE read of the relay latch, and\n // the ATTEMPT is the runtime's self-heal sensor (#440/#443\n // attempt-always): a dispatch fired while the latch stands is how the\n // runtime discovers the host can relay again. Binding `disabled` to\n // the connection state strips the handler and blinds that sensor —\n // the control stays dead after recovery. The runtime owns the dead\n // zone's presentation (ggui#670 Phase 3: standing notice, then a\n // per-gesture cue) — generated code never suppresses on it. Direct\n // binding only, and only when the source reads\n // `useRender` at all — a same-named `props.isConnected` (a device-\n // status card) is not the connection state. Whole-source scan so a\n // multi-line attribute is caught; the issue reports the attribute's\n // line. The lookbehind keeps `aria-disabled={…}` (the idiom) out.\n if (/\\buseRender\\s*\\(/.test(sourceCode)) {\n const suppressionRe = /(?<![\\w-])disabled\\s*=\\s*\\{([^}]*)\\}/gs;\n for (const m of sourceCode.matchAll(suppressionRe)) {\n const expr = m[1]!;\n if (!/\\bisConnected\\b/.test(expr)) continue;\n const attrLine = sourceCode.slice(0, m.index ?? 0).split('\\n').length;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'interactivity',\n subcategory: 'connection-suppression',\n severity: 'critical',\n description:\n `\\`disabled\\` is bound to the connection state (\\`${expr.trim().replace(/\\s+/g, ' ')}\\`) — that strips the click handler, and the click is how the runtime detects that the host can relay actions again. The control stays dead after recovery.`,\n fix:\n 'Leave the control enabled — never bind `disabled` to the connection state. The runtime presents the dead zone itself (a standing notice, then a per-gesture cue) and the attempt is how it detects recovery; a suppressed control stays dead after the host recovers.',\n line: attrLine,\n });\n }\n }\n\n // ── Optional props accessed without guard ──────────────────\n // Check if Props interface has optional fields that are accessed without ?. or ?? or &&\n const propsInterfaceMatch = sourceCode.match(/interface Props\\s*\\{([^}]+)\\}/);\n if (propsInterfaceMatch) {\n const propsBody = propsInterfaceMatch[1];\n const optionalProps = [...propsBody.matchAll(/(\\w+)\\?:/g)].map(m => m[1]);\n for (const prop of optionalProps) {\n // Check for unguarded access: props.field. (dot access) or props.field[ (bracket access)\n // without being preceded by props.field?. or props.field ?? or props.field &&\n const accessPattern = new RegExp(`props\\\\.${prop}[.\\\\[]`, 'g');\n const guardPattern = new RegExp(`props\\\\.${prop}(\\\\?[.[]|\\\\s*&&|\\\\s*\\\\?\\\\?)`, 'g');\n const accesses = (sourceCode.match(accessPattern) || []).length;\n const guards = (sourceCode.match(guardPattern) || []).length;\n if (accesses > 0 && guards === 0) {\n issues.push({\n tier: 0, result: 'warn', category: 'crash', subcategory: `optional-prop:${prop}`,\n description: `Optional prop 'props.${prop}' accessed without null guard — will crash if undefined`,\n fix: `Use props.${prop}?.field or props.${prop} ?? fallback or {props.${prop} && ...}`,\n });\n }\n }\n }\n\n // ── Common pitfalls (recurring LLM mistakes that cost a turn) ─\n // Scan whole source so multiline opening tags (attrs split across lines) are caught.\n // Each match's line number = the line where the tag opens.\n const ALIGN_VALUES = new Set(['start', 'center', 'end', 'stretch']);\n const JUSTIFY_VALUES = new Set(['start', 'center', 'end', 'between', 'around', 'evenly']);\n const tagRegex = /<(Stack|Row)\\b([^>]*)>/gs;\n for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {\n const tagName = tag[1];\n const attrs = tag[2];\n const tagLine = sourceCode.slice(0, tag.index).split('\\n').length;\n\n if (/\\bpadding=/.test(attrs)) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-padding',\n description: `<${tagName}> does not accept a 'padding' prop`,\n fix: `Wrap the children in <Box padding=\"...\"> or use the parent <Card padding=\"...\">`,\n line: tagLine,\n });\n }\n const alignAttr = attrs.match(/\\balign=[\"']([^\"']+)[\"']/);\n if (alignAttr && !ALIGN_VALUES.has(alignAttr[1])) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-align',\n description: `align=\"${alignAttr[1]}\" is invalid on <${tagName}>`,\n fix: `Use align=\"start\" | \"center\" | \"end\" | \"stretch\" (NOT flex-start/flex-end/space-between)`,\n line: tagLine,\n });\n }\n const justifyAttr = attrs.match(/\\bjustify=[\"']([^\"']+)[\"']/);\n if (justifyAttr && !JUSTIFY_VALUES.has(justifyAttr[1])) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-justify',\n description: `justify=\"${justifyAttr[1]}\" is invalid on <${tagName}>`,\n fix: `Use justify=\"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\"`,\n line: tagLine,\n });\n }\n }\n\n // ── Missing Props interface ───────────────────────────────\n if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'types',\n subcategory: 'props-interface',\n severity: 'critical',\n description: 'No Props interface found — data is likely hardcoded',\n fix: 'Add interface Props { ... } with typed fields and default values in the function signature',\n });\n }\n\n // ── Missing default export ────────────────────────────────\n if (!sourceCode.includes('export default function')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'default-export',\n severity: 'critical',\n description: 'Missing default export function',\n fix: 'Add export default function Component(props: Props) { ... }',\n });\n }\n\n // ── Component never returns its tree ──────────────────────\n // The scaffold hands the model `return (` … `);` around the layout.\n // A root-block rewrite can delete them with it, and bare JSX at\n // statement position compiles, type-checks and renders NOTHING.\n const neverReturns = detectComponentNeverReturns(sourceCode);\n if (neverReturns !== null) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'renders-nothing',\n severity: 'critical',\n description:\n neverReturns.jsxStatementLine === undefined\n ? 'The component never returns its tree — it renders a blank frame'\n : `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,\n fix: 'Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.',\n ...(neverReturns.jsxStatementLine === undefined ? {} : { line: neverReturns.jsxStatementLine }),\n });\n }\n\n // (Controller-View checks removed — generated components use a\n // single-function pattern.)\n\n // ── Contract validation ───────────────────────────────────\n if (contract) {\n try {\n const contractIssues: ContractIssue[] = validateAllContracts(sourceCode, contract);\n for (const ci of contractIssues) {\n issues.push({\n tier: 0,\n result: ci.severity === 'error' ? 'fail' as const : 'warn' as const,\n category: 'contract' as const,\n subcategory: ci.field,\n severity: ci.severity === 'error' ? 'critical' as const : 'major' as const,\n description: ci.message,\n fix: ci.fix,\n });\n }\n } catch {\n // Contract validation may fail on malformed code — non-blocking\n }\n\n // ── Wire hook usage — every contract hook must be used in the component body ──\n //\n // `actionSpec` / `streamSpec` are flat `Record<name, Entry>` maps.\n // See `@ggui-ai/protocol` DataContract.\n const fnBody = sourceCode.slice(sourceCode.indexOf('export default function'));\n if (contract.actionSpec) {\n for (const actionName of Object.keys(contract.actionSpec)) {\n if (!fnBody.includes(actionName)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `action:${actionName}`,\n description: `Action hook '${actionName}' from contract is declared but never used in the component`,\n fix: `Wire ${actionName}() to a Button onClick, form onSubmit, or other user interaction`,\n });\n }\n }\n }\n if (contract.streamSpec) {\n for (const channelName of Object.keys(contract.streamSpec)) {\n if (!fnBody.includes(channelName)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `stream:${channelName}`,\n description: `Stream hook '${channelName}' from contract is declared but never rendered in the component`,\n fix: `Render ${channelName}.latest data in the JSX (with null guard: ${channelName}.latest && ...)`,\n });\n }\n }\n }\n // agentCapabilities.tools is intentionally NOT checked here — it\n // is a catalog the AGENT invokes, not a component hook surface.\n // Cross-refs surface via actionSpec[*].nextStep and\n // streamSpec[*].source.tool, which the action/stream loops above\n // already cover.\n if (contract.clientCapabilities) {\n // Iterate the flattened `GadgetUse[]` — the export name\n // (`use.name`) is the identity the generated code references.\n for (const use of listContractGadgets(contract)) {\n if (!fnBody.includes(use.name)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `clientCapability:${use.name}`,\n description: `Client capability '${use.name}' from contract is declared but the export is never used`,\n fix: HOOK_NAME_RE.test(use.name)\n ? `Import \\`${use.name}\\` from \\`${use.package}\\` and call it inside the component — bind the return value and surface its \\`.value\\` / \\`.status\\` in JSX.`\n : `Import \\`${use.name}\\` from \\`${use.package}\\` and render it as a JSX element (\\`<${use.name} … />\\`).`,\n });\n }\n }\n }\n\n // ── Wire preservation ─────────────────────────────────────────────────\n // The substring checks above (`fnBody.includes(name)`) are permissive —\n // they pass if the name appears ANYWHERE in the function body, including\n // a comment or unrelated identifier. The AST-backed `checkWirePreservation`\n // is the strict seal: it verifies that the boilerplate-emitted\n // `useAction('<name>')` / `useStream('<name>')` / `useGguiContext('<name>')`\n // hook call ACTUALLY exists, keyed on the string-literal first argument.\n //\n // Catches the abandonment/deletion case: LLM's apply_changes drops the\n // hook call entirely, so the wire never fires at runtime. `pnpm\n // typecheck` can't catch this (a missing call is a missing side effect,\n // not a type error). Seal B fires as `fail` — deterministic, retriable\n // feedback.\n //\n // The runtime auto-commit path (`coding-agent/tools.ts::autoCommit`)\n // calls `runTier0Checks`, which is where this rule lives so it\n // fires on the live generation path.\n try {\n const report = checkWirePreservation(sourceCode, contract);\n for (const site of report.missing) {\n const hook = HOOK_NAME_FOR[site.kind];\n const fix =\n site.kind === 'context'\n ? `Restore \\`const [${site.name}, set${site.name.charAt(0).toUpperCase() + site.name.slice(1)}] = ${hook}('${site.name}')\\` ` +\n `at the top of the component body. The boilerplate auto-emits this destructure for every declared ` +\n `contextSpec slot — do not delete it.`\n : `Restore \\`const ${site.name} = ${hook}('${site.name}')\\` at the top of the ` +\n `component body and consume the returned binding (in JSX, a callback, or an effect).`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'contract',\n subcategory: `wire_preservation:${site.kind}:${site.name}`,\n severity: 'critical',\n description:\n `Contract declares ${site.kind} '${site.name}' but no ${hook}('${site.name}') ` +\n `call exists in the component. The boilerplate placed this hook for you — ` +\n `do not delete it.`,\n fix,\n });\n }\n } catch {\n // Malformed code surfaces as a primary error elsewhere — don't mask\n // it with a wire-preservation noise-violation here.\n }\n\n // ── Props field rendering — verify each prop is actually used in the component ──\n if (contract.propsSpec) {\n const propsProperties = contract.propsSpec.properties ?? {};\n for (const propName of Object.keys(propsProperties)) {\n // Check if prop is referenced in the function body (via props.fieldName)\n if (!fnBody.includes(`props.${propName}`) && !fnBody.includes(`{props.${propName}}`)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `prop:${propName}`,\n description: `Props field '${propName}' from contract is never rendered — data is wasted`,\n fix: `Render props.${propName} in the JSX (e.g., <Text>{props.${propName}}</Text>)`,\n });\n }\n }\n }\n }\n\n // ── Wire_undeclared (UNCONDITIONAL) ──────────────────────────────────\n //\n // Every wire-call site in the emitted code MUST correspond to a\n // declared entry on the agent-authored `contract`, regardless of\n // whether the contract is partially or wholly absent. Empty contract\n // → ALL wire calls are \"extra\" → all fail with `wire_undeclared`.\n // Failure mode this guards against: agent describes contextSpec in\n // plain prompt text but doesn't author `contract`; the LLM emits\n // `useGguiContext('foo')` / `useAction('foo')` with no slot\n // registration; the iframe crashes at first paint with\n // `[ggui] useGguiContext('foo'): no Context registered`. The\n // remediation message names the exact contract field to declare on,\n // so the agent's next turn can fix structurally.\n try {\n const contractsForExtraCheck: DataContract = contract ?? {};\n const extras = checkWirePreservation(sourceCode, contractsForExtraCheck).extra;\n for (const site of extras) {\n const hook = HOOK_NAME_FOR[site.kind];\n const field = CONTRACT_FIELD_FOR[site.kind];\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'contract',\n subcategory: `wire_undeclared:${site.kind}:${site.name}`,\n severity: 'critical',\n description:\n `Component calls ${hook}('${site.name}') but the contract does not declare ` +\n `${site.kind} '${site.name}'. Every wire reference in the generated code MUST ` +\n `correspond to a declared entry on the agent-authored contract — the runtime ` +\n `mounts/registers wire surfaces from the contract, not from the code. ` +\n (contract === undefined\n ? 'No contract authored at all — describing the contract in the prompt text is NOT enough; ' +\n 'the agent MUST pass a structured `contract` field on the ggui_render call.'\n : ''),\n fix:\n `Either (a) declare \\`${field}.${site.name}\\` on the \\`contract\\` input so the ` +\n `runtime registers the surface for this UI, or (b) remove the ${hook}('${site.name}') ` +\n `call from the component if it isn't part of the intended wire surface.`,\n });\n }\n } catch {\n // Malformed code surfaces elsewhere; don't mask with noise.\n }\n\n // ── Wire-hook import presence ────────────────────────────────────────\n //\n // `rewriteImports` (packages/design/src/rendering/rewrite-imports.ts) only\n // substitutes `from '@ggui-ai/wire'` specifiers that already exist in the\n // compiled code; a hook call like `useAction('X')` without the paired\n // `import { useAction } from '@ggui-ai/wire'` line leaves no specifier\n // for the shim to attach, so the hook is undeclared at eval time and the\n // browser throws `ReferenceError: useAction is not defined`. This check\n // runs unconditionally (no contract needed) — for every wire hook the\n // component CALLS at least once, the matching named import must be\n // present at top level. Independent of `wire_preservation` which checks\n // that contract-declared hooks are called.\n try {\n const importReport = checkWireImports(sourceCode);\n for (const miss of importReport.missing) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `wire_import_missing:${miss.hook}`,\n severity: 'critical',\n description:\n `Component calls ${miss.hook}(...) but does not import it from ` +\n `'@ggui-ai/wire'. Without the import, rewriteImports has no ` +\n `specifier to attach the data-URL shim to, so the hook is ` +\n `undeclared at browser eval time and the component crashes on ` +\n `mount with \\`ReferenceError: ${miss.hook} is not defined\\`.`,\n fix:\n `Add \\`import { ${miss.hook} } from '@ggui-ai/wire';\\` at the ` +\n `top of the file alongside the other imports.`,\n });\n }\n } catch {\n // Malformed code surfaces as a primary error elsewhere — don't mask it.\n }\n\n // ── No-require pattern for @-scoped packages ─────────────────────────\n //\n // A repeating LLM failure mode: when the boilerplate emits a static\n // `import { useLeafletMap } from '@ggui-samples/gadget-leaflet'`, the\n // model sometimes removes the import and substitutes\n // `const { useLeafletMap } = require('@ggui-samples/gadget-leaflet')`\n // (or `require('@ggui-ai/gadgets')`) inline. That FAILS in a real\n // browser ESM iframe: `require` is not defined, and `rewriteImports`\n // never sees a static `from '<pkg>'` specifier to attach the data-URL\n // shim to.\n //\n // This check rejects any `require('@...')` call expression on an\n // @-scoped package. The fix is always the same — convert to a\n // top-level static `import { … } from '<pkg>'`. Targeted at @-scoped\n // packages because that's the LLM's typical hedge specifier shape\n // (CJS bare `require('react')` etc. don't appear in our generated\n // code path).\n {\n // Match `require('@scope/pkg')` and `require(\"@scope/pkg\")` with a\n // simple regex. The LLM only emits this pattern as call\n // expressions, so the false-positive rate is negligible in\n // practice; an AST walk would be more rigorous but unnecessary.\n const requireRe = /require\\s*\\(\\s*['\"](@[^'\"]+)['\"]\\s*\\)/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = requireRe.exec(sourceCode)) !== null) {\n const pkg = m[1];\n if (pkg === undefined || seen.has(pkg)) continue;\n seen.add(pkg);\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `require_disallowed:${pkg}`,\n severity: 'critical',\n description:\n `Component uses \\`require('${pkg}')\\` — CommonJS require is ` +\n `not available in the iframe's browser ESM runtime, and the ` +\n `import-rewriter only attaches data-URL shims to STATIC import ` +\n `specifiers. The component will fail to mount with ` +\n `\\`ReferenceError: require is not defined\\` at the first call.`,\n fix:\n `Replace with a top-level static \\`import { … } from '${pkg}'\\` ` +\n `at the top of the file. The boilerplate emits this import for ` +\n `you when the contract declares a matching ` +\n `\\`clientCapabilities.gadgets[*]\\` entry — restore the line ` +\n `instead of inlining a require() call.`,\n });\n }\n }\n\n // ── gadget import preservation ───────────────────────────────────────\n //\n // Gadgets are DIRECT-imported. The boilerplate emits, per\n // registered gadget package, one combined import:\n // `import { useLeafletMap } from '@scope/leaflet';`\n // The LLM (correctly) intuits the hook is runtime-resolved and tends\n // to delete the import — but the import IS the resolution anchor: the\n // iframe rewriter rewrites the package specifier to a per-package\n // shim, and without the import the hook is unbound (ReferenceError).\n //\n // This check verifies, for every contract-declared gadget export,\n // that it is still imported from its package. The export NAME\n // appearing alone is not enough — the `import { … }` statement must\n // carry it. Hooks AND components are both direct-imported and\n // both checked here; the fix hint adapts to the export kind (hooks\n // are CALLED, components are RENDERED as JSX).\n {\n // Scan comment-free source — a commented-out import must NOT\n // satisfy the preservation check.\n const gadgetScanSource = stripComments(sourceCode);\n // Iterate the flattened `GadgetUse[]`. Kind is discriminated\n // by the export-name grammar — `use`-prefixed → hook, else component.\n for (const use of contract ? listContractGadgets(contract) : []) {\n const exportName = use.name;\n if (isGadgetExportImported(gadgetScanSource, use.package, exportName)) {\n continue;\n }\n const isComponent = !HOOK_NAME_RE.test(exportName);\n const kind = isComponent ? 'component' : 'hook';\n // Local binding name for the hook worked example (mirrors the\n // boilerplate's derivation: strip `use`, lowercase first char).\n const bindingName =\n exportName.length > 3\n ? exportName.charAt(3).toLowerCase() + exportName.slice(4)\n : exportName;\n const fix = isComponent\n ? `The component \\`${exportName}\\` is REAL and CORRECT — do NOT remove it. ` +\n `Restore the gadget plumbing in 2 steps:\\n` +\n `(1) Keep \\`import { ${exportName} } from '${use.package}';\\` at the top of the file.\\n` +\n `(2) RENDER \\`<${exportName} … />\\` as a JSX element in the tree you return — pass its props from the contract. Do NOT call it like a hook.\\n` +\n `Import \\`${exportName}\\` ONLY from '${use.package}' — that is the package it is registered under. If you remove the import again, this check will fail again — \\`${exportName}\\` is not optional.`\n : `The hook \\`${exportName}\\` is REAL and CORRECT — do NOT remove it. ` +\n `Restore the gadget plumbing in 2 steps:\\n` +\n `(1) Keep \\`import { ${exportName} } from '${use.package}';\\` at the top of the file.\\n` +\n `(2) CALL \\`${exportName}(...)\\` inside the component body and render its return value. Example:\\n` +\n ` \\`const ${bindingName} = ${exportName}({ /* props from contract */ });\\`\\n` +\n ` \\`return <div>{/* render ${bindingName} */}</div>;\\`\\n` +\n `Import \\`${exportName}\\` ONLY from '${use.package}' — that is the package it is registered under. If you remove the import again, this check will fail again — \\`${exportName}\\` is not optional.`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `gadget_preservation:${exportName}`,\n severity: 'critical',\n description:\n `Contract declares \\`clientCapabilities.gadgets['${use.package}']['${exportName}']\\` ` +\n `(a ${kind}) but the ` +\n `component does not import \\`${exportName}\\` from \\`${use.package}\\`. ` +\n `The boilerplate emits \\`import { ${exportName} } from '${use.package}';\\` ` +\n `— keep it. Without the import the iframe runtime cannot resolve the ` +\n `${kind} and the registered gadget is unreachable.`,\n fix,\n });\n }\n }\n\n // ── TypeScript type checking + React hooks linting ──────────\n const [typeResult, reactResult] = await Promise.all([\n typecheck(sourceCode, gadgetTypes).catch((err) => {\n console.warn('[runTier0Checks] TypeChecker failed:', err instanceof Error ? err.message : String(err));\n return null;\n }),\n lintReactHooks(sourceCode).catch((err) => {\n console.warn('[runTier0Checks] React linter failed:', err instanceof Error ? err.message : String(err));\n return [] as ReactLintDiagnostic[];\n }),\n ]);\n\n // TS type errors → fail, warnings → warn\n if (typeResult) {\n for (const error of typeResult.errors) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'types',\n subcategory: `ts${error.code}`,\n description: error.message,\n fix: error.fix,\n line: error.line,\n });\n }\n for (const warning of typeResult.warnings) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'types',\n subcategory: `ts${warning.code}`,\n description: warning.message,\n fix: warning.fix,\n line: warning.line,\n });\n }\n }\n\n // React hooks violations — errors → fail, warnings → warn\n for (const diag of reactResult) {\n issues.push({\n tier: 0,\n result: diag.severity === 'error' ? 'fail' : 'warn',\n category: 'types',\n subcategory: diag.rule,\n description: diag.message,\n fix: diag.fix,\n line: diag.line,\n });\n }\n\n // Populate priority on every tier-0 issue. No current code path reads\n // `.priority` — it is reserved for future eval-policy consumption.\n for (const issue of issues) {\n if (!issue.priority) issue.priority = priorityForIssue(issue.category);\n }\n\n return issues;\n}\n\n/**\n * Run tier 0 checks and return a full EvalResult with pass list.\n */\nexport async function runTier0(\n sourceCode: string,\n contract?: DataContract,\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): Promise<EvalResult> {\n const issues = await runTier0Checks(sourceCode, contract, undefined, undefined, designMode);\n const pass: string[] = [];\n\n // Build pass list from categories that had no issues\n const failedCategories = new Set(issues.map(i => i.category));\n const tier0Categories: EvalCategory[] = ['compile', 'security', 'contract', 'types', 'imports', 'tokens'];\n\n for (const cat of tier0Categories) {\n if (!failedCategories.has(cat)) {\n pass.push(cat);\n }\n }\n\n return { issues, pass };\n}\n\n// Tier-0 deterministic checks are run above. Axis-gated mode checks are\n// dispatched from `evaluation/axis-checks/` — see `runAxisChecks`.\n","/**\n * Contract + rendering-context prompt rendering.\n *\n * Surfaces every contract dimension explicitly to the LLM. A compact\n * bullet list of action / stream names is not enough — without the\n * Props shape, generation fails tier-0 on most cells with \"Props\n * interface missing required field '<name>'\".\n *\n * Renders:\n *\n * - Props contract — required + optional field lists + a real\n * TypeScript interface (compiled via `propsSpecToTypeScript`)\n * - Action contract — id / label / example / nextStep hint\n * - Stream contract — channel descriptions + payload schemas + source\n * - AgentTools — catalog of tools the contract references (via\n * `actionSpec[*].nextStep` and `streamSpec[*].source.tool`)\n * - ClientCapabilities — browser-capability gadgets the UI mounts:\n * hooks it calls (e.g. `useGeolocation` from `@ggui-ai/gadgets`)\n * and components it renders (e.g. `<LeafletMap />`)\n * - Required UI Surfaces — derived list of \"every contract surface\n * must have visible UI\" so the LLM doesn't drop required props or\n * skip rendering data sources\n *\n * Rendering context (device + shell + viewport) is layered alongside —\n * shells (`chat` / `fullscreen` / `partial`) require very different\n * sizing strategies and including the hint inline cuts a class of\n * \"designed for fullscreen, rendered in a chat bubble\" bugs.\n */\nimport { STYLING_PROFILE_PRECEDENCE_NOTE } from './boilerplate/styling-profile.js';\nimport type { RenderingContext as ProtocolRenderingContext } from '@ggui-ai/protocol';\nimport type {\n ActionEntry,\n BlueprintVariance,\n GadgetDescriptor,\n DataContract,\n StreamChannelEntry,\n} from '@ggui-ai/protocol';\nimport { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';\nimport { propsSpecToTypeScript } from './check/index.js';\nimport {\n canvasDisplayModeForShell,\n canvasForViewportWidth,\n type CanvasClass,\n} from './design-mode.js';\n\n/**\n * Rendering context — how and where the component will be displayed.\n * Affects layout strategy, sizing, and interaction patterns. The ONE\n * vocabulary is `@ggui-ai/protocol`'s `renderingContextSchema` (ggui#1000);\n * this is its type, derived — never re-declared here.\n */\nexport type RenderingContext = ProtocolRenderingContext;\n\nconst SHELL_HINTS: Record<string, string> = {\n chat: `**Chat Shell — Inline card in scrolling conversation**\n- Container: width varies (60-80% of viewport), height: auto (natural content height)\n- Chrome: parent provides card border + shadow + rounded corners — do NOT add your own box-shadow or outer border-radius\n- Sizing: target 300-600px natural height. Do NOT use min-height: 100vh. No full-viewport designs.\n- Padding: use var(--ggui-spacing-2) for inner elements, var(--ggui-spacing-4) for sections. Tight.\n- Scrolling: parent scrolls (chat feed) — component should NOT have internal scroll\n- Layout: single column, compact. No sidebar. Stack everything vertically.\n- Width: width: 100% (fills parent bubble). Do NOT set max-width.`,\n\n fullscreen: `**Fullscreen Shell — Takes over entire viewport**\n- Container: the whole viewport, edge to edge — the frame stretches your root to its full height (you never set a viewport height yourself)\n- Chrome: NONE — your component IS the entire UI. You own all visual chrome.\n- Sizing: size to content; to fill a parent that has a height use height: 100% or flex: 1. NEVER min-height: 100vh / 100dvh — the frame already stretches the root, and a viewport height only fights it (tier-0 fail).\n- Padding: component owns ALL padding. Use var(--ggui-spacing-6) or larger for breathing room.\n- Scrolling: component manages its own scroll if content exceeds viewport (overflow-y: auto)\n- Layout: can use multi-column, sidebars, headers/footers. Full creative control.\n- Navigation: swipe left/right between pages is handled by the shell — don't implement it.\n- DO NOT add outer margins or max-width containers. Fill the space.`,\n\n partial: `**Partial Shell — Embedded panel in a larger page**\n- Container: width constrained by parent (could be 400px sidebar or 800px main area)\n- Chrome: parent may or may not have border — add your own Card/shadow if needed\n- Sizing: width: 100% to fill parent. Use max-width if you want to center content.\n- Padding: use standard spacing (var(--ggui-spacing-4) to var(--ggui-spacing-6))\n- Scrolling: can scroll internally (overflow-y: auto) — parent does NOT scroll for you\n- Layout: responsive within the container. Flex-wrap for variable parent widths.\n- Context: other UI around you (nav, sidebar) — don't duplicate page-level chrome.`,\n};\n\nconst DEVICE_HINTS: Record<string, string> = {\n mobile: `**Mobile:** Single column. Touch targets 44px+. Compact padding. Stack all sections vertically.`,\n tablet: `**Tablet:** 1-2 columns adaptive. Medium touch targets. Side-by-side where space allows.`,\n desktop: `**Desktop:** Multi-column. Hover states. Dense layouts. Keyboard shortcuts. Pointer interactions.`,\n spatial: `**Spatial:** Fixed-size floating panel (~600x400). High contrast. Large text. No hover (gaze/hand input).`,\n};\n\n/**\n * Map a host rendering context onto the canvas class the `free` prompt\n * states and the visual evaluator judges at. The class is derived from\n * DISPLAY MODE + width: a `chat` shell is always the inline card\n * whatever the host viewport is (the bubble, not the window, bounds the\n * component); a known viewport width refines the fullscreen / partial\n * shells; otherwise the device category decides.\n */\nexport function canvasForRenderingContext(ctx: RenderingContext): CanvasClass {\n const mode = canvasDisplayModeForShell(ctx.shell);\n if (mode === 'inline') return 'xs-chat-card';\n if (ctx.viewport) return canvasForViewportWidth(mode, ctx.viewport.width);\n switch (ctx.device) {\n case 'mobile':\n return 'mobile-fullscreen-small';\n case 'tablet':\n case 'spatial':\n return 'md';\n case 'desktop':\n return ctx.shell === 'partial' ? 'md' : 'lg';\n }\n}\n\n/** Build rendering-context block to inject into user prompt. */\nexport function buildRenderingContext(ctx: RenderingContext): string {\n const parts = [\n '## Rendering Context',\n `- Device: ${ctx.device}`,\n ctx.viewport ? `- Viewport: ${ctx.viewport.width}×${ctx.viewport.height}px` : '',\n `- Shell: ${ctx.shell}`,\n '',\n DEVICE_HINTS[ctx.device] || '',\n SHELL_HINTS[ctx.shell] || '',\n ].filter(Boolean);\n return parts.join('\\n');\n}\n\n/** Append rendering context to user prompt if present. */\nexport function injectRenderingContext(\n userPrompt: string,\n rendering?: RenderingContext,\n): string {\n if (!rendering) return userPrompt;\n return userPrompt + '\\n\\n' + buildRenderingContext(rendering);\n}\n\n/**\n * Build the contract-context block. Surfaces every dimension of the\n * contract so the LLM has a complete spec rather than the user-prompt\n * narrative alone — the eval scores against the contract regardless of\n * what prose the prompt carries, so contract-first rendering closes\n * the prompt-vs-contract drift class.\n */\nexport function buildContractsContext(\n contract: DataContract,\n /**\n * Operator-registered gadget catalog. When a\n * `clientCapabilities.gadgets[*]` ref is THIN (just `{hook}` —\n * no per-binding `package`), look the hook up here to surface the\n * correct package in user-prompt lines like `via useLeafletMap from\n * @ggui-samples/gadget-leaflet`. Mirrors the same plumb in\n * `harness/prompts.ts:buildContractsContext` — both surfaces are\n * separate copies used by different call sites (this one is for\n * `createUiGenerator`, the other for the benchmark / dispatch path).\n */\n appGadgets?: readonly GadgetDescriptor[],\n): string {\n const parts: string[] = [];\n\n // The LLM direct-imports each hook from its own package\n // (resolved from the per-entry `package` field). The parameter stays\n // on the signature so the TypeScript sandbox augmentation layer can\n // still consume it.\n void appGadgets;\n\n // `intent` is not a contract field; `userPrompt` already carries\n // the agent's narrative as the prompt body, so the contract-context\n // block focuses on the structural shape (props / actions / streams /\n // contexts). There is no categorical `interaction` mode label: the\n // four-spec surface (props / actions / streams / contexts) describes\n // the wire exhaustively — actions drive turns, contexts observe\n // state, and there is no third category.\n\n if (contract.propsSpec) {\n const propsSpec = contract.propsSpec;\n const requiredFields = Object.entries(propsSpec.properties || {})\n .filter(([, e]) => (e as { required?: boolean }).required)\n .map(([k]) => k);\n const optionalFields = Object.entries(propsSpec.properties || {})\n .filter(([, e]) => !(e as { required?: boolean }).required)\n .map(([k]) => k);\n\n const tsInterface = propsSpecToTypeScript(propsSpec);\n const fieldInfo: string[] = [];\n if (requiredFields.length > 0) fieldInfo.push(`Required: ${requiredFields.join(', ')}`);\n if (optionalFields.length > 0) fieldInfo.push(`Optional: ${optionalFields.join(', ')}`);\n\n parts.push(\n `## Props Contract (MUST use these exact prop names and types)\\n\\n` +\n (fieldInfo.length > 0 ? fieldInfo.join('\\n') + '\\n\\n' : '') +\n `\\`\\`\\`typescript\\ninterface Props ${tsInterface}\\n\\`\\`\\``,\n );\n }\n\n if (contract.actionSpec && Object.keys(contract.actionSpec).length > 0) {\n const actions = Object.entries(contract.actionSpec)\n .map(([id, entry]: [string, ActionEntry]) => {\n let line = ` - ${id}: \"${entry.label ?? id}\"${entry.description ? ` — ${entry.description}` : ''}`;\n if (entry.example !== undefined) {\n line += `\\n Example payload: \\`${JSON.stringify(entry.example)}\\``;\n }\n if (entry.nextStep) {\n line += `\\n Next-step hint: agent intends to call \\`${entry.nextStep}\\``;\n }\n // ggui#1108: `confirm` was the one declared action field the contract\n // renderer dropped — every other author field (label, description,\n // example, nextStep) reaches the model and this one did not. It is\n // rendered as INFORMATION, not as a rule: the terminal-actions section\n // deliberately does NOT cite it, because citing it measured ZERO —\n // two wordings, n=4 each, `save` + `confirm: true` came back armed 0/4\n // and 0/4. The field's own doc says \"whether to show confirmation\n // BEFORE triggering\", which is gravity, not one-shot-ness, and the\n // model reads it that way. Emitted only when true, so an absent flag\n // leaves the prompt bytes identical to before this line existed.\n if (entry.confirm === true) {\n line += `\\n Author flag: \\`confirm\\` — the author marked this action as one to confirm with the user before it fires.`;\n }\n return line;\n })\n .join('\\n');\n parts.push(`## Action Contract (wire these callbacks)\\n${actions}`);\n }\n\n if (contract.streamSpec && Object.keys(contract.streamSpec).length > 0) {\n const channels = Object.entries(contract.streamSpec)\n .map(([name, entry]: [string, StreamChannelEntry]) => {\n const bits = [` - ${name}`];\n if (entry.description) bits.push(`— ${entry.description}`);\n if (entry.source?.tool) {\n bits.push(`(source tool: \\`${entry.source.tool}\\`)`);\n }\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## Stream Contract (real-time channels via \\`useStream(name)\\`)\\n${channels}\\n\\n` +\n `Payload schemas:\\n\\`\\`\\`json\\n${JSON.stringify(contract.streamSpec, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n const agentTools = contract.agentCapabilities?.tools;\n if (agentTools && Object.keys(agentTools).length > 0) {\n const tools = Object.entries(agentTools)\n .map(([name, entry]) => {\n const bits = [` - **\\`${name}\\`**`];\n if (entry.toolInfo?.description) bits.push(`— ${entry.toolInfo.description}`);\n if (entry.usage) bits.push(`(usage: ${entry.usage})`);\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## AgentTools Catalog (tools the contract references)\\n${tools}\\n\\n` +\n `These are referenced via \\`actionSpec[*].nextStep\\` (the tool the agent SHOULD call next ` +\n `after the action fires) and \\`streamSpec[*].source.tool\\` (the tool feeding a channel). ` +\n `The agent OWNS the call — your component does not invoke these directly.\\n\\n` +\n `Schemas:\\n\\`\\`\\`json\\n${JSON.stringify(agentTools, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n const clientCapabilities = contract.clientCapabilities?.gadgets;\n const gadgetUses = listContractGadgets(contract);\n if (gadgetUses.length > 0) {\n // The wire `clientCapabilities.gadgets` is a package-keyed\n // two-level map; `listContractGadgets` flattens it to `GadgetUse[]`.\n // Kind is discriminated by the export-name grammar — `use`-prefixed\n // names are hooks (imported + CALLED), PascalCase names are\n // components (imported + RENDERED as JSX). Both are direct-imported.\n const caps = gadgetUses\n .map((use) => {\n const bits = [` - **\\`${use.name}\\`**`];\n bits.push(\n HOOK_NAME_RE.test(use.name)\n ? `via direct-imported hook \\`${use.name}\\` from \\`${use.package}\\` (call it)`\n : `via direct-imported component \\`<${use.name} />\\` from \\`${use.package}\\` (render as JSX)`,\n );\n if (use.description) bits.push(`— ${use.description}`);\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## ClientCapabilities Contract (browser-capability gadgets the UI mounts)\\n${caps}\\n\\n` +\n `Hook entries are imported + called; component entries are imported + rendered as JSX. ` +\n `Capability values reach the agent only when the UI threads them into an actionSpec ` +\n `payload or contextSpec slot.\\n\\n` +\n `Declarations:\\n\\`\\`\\`json\\n${JSON.stringify(clientCapabilities, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n // Required-UI derivation — the architectural fix at the heart of v15.\n // Every contract surface implies a visible UI element; eval scores\n // against the contract regardless of what the user-prompt narrative\n // says. Emitting the implied UI surfaces explicitly makes the contract\n // the single source of truth for \"what must render\".\n const uiRequirements: string[] = [];\n if (contract.propsSpec?.properties) {\n for (const [name, entry] of Object.entries(contract.propsSpec.properties)) {\n // Opt-in required (`=== true`) — the wire gate's reading, ggui#528.\n // An omitted-`required` prop is OPTIONAL: the agent may leave it\n // out of a legitimate render, so the derived instruction must\n // demand a presence-safe component, not unconditional display.\n const required = (entry as { required?: boolean }).required === true;\n if (required) {\n uiRequirements.push(\n `- **Required prop \\`${name}\\`** must appear somewhere in rendered DOM (display the value, or use it to drive a label/aria/key).`,\n );\n } else {\n uiRequirements.push(\n `- **Optional prop \\`${name}\\`** may be ABSENT at runtime — the component must render without it (guard the access or apply its default) and must display it when it IS provided.`,\n );\n }\n }\n }\n if (contract.actionSpec) {\n for (const [name, entry] of Object.entries(contract.actionSpec)) {\n const label = (entry as ActionEntry).label ?? name;\n uiRequirements.push(\n `- **Action \\`${name}\\`** (\"${label}\") needs an interactive control that fires it (Button click, form submit, key press, etc.).`,\n );\n }\n }\n if (contract.streamSpec) {\n for (const name of Object.keys(contract.streamSpec)) {\n uiRequirements.push(\n `- **Stream \\`${name}\\`** must mirror into local state via \\`useEffect\\` and render the resulting state — \\`useStream\\` alone never re-renders the DOM.`,\n );\n }\n }\n // agentTools entries don't imply UI on their own — they're catalog\n // declarations the agent uses, not hooks the component invokes.\n // The implied UI lives on the referencing actionSpec entries\n // (already enumerated above) and streamSpec channels with `source`\n // declarations. No per-tool UI requirement is emitted here.\n for (const use of gadgetUses) {\n // Hook AND component exports are direct-imported; the\n // implied UI differs — a hook is called + its value threaded,\n // a component is rendered as a JSX element. Kind is discriminated\n // by the export-name grammar.\n const exportName = use.name;\n const pkg = use.package;\n uiRequirements.push(\n HOOK_NAME_RE.test(exportName)\n ? `- **ClientCapability \\`${exportName}\\`** — the boilerplate has direct-imported \\`${exportName}\\` from \\`${pkg}\\` above a \\`// DO NOT EDIT\\` banner; keep that import, call \\`${exportName}\\` inside the component, and thread the resulting value into a context slot or action payload if the agent needs to observe it.`\n : `- **ClientCapability \\`${exportName}\\`** — the boilerplate has direct-imported \\`${exportName}\\` from \\`${pkg}\\` above a \\`// DO NOT EDIT\\` banner; keep that import and RENDER \\`<${exportName} … />\\` as a JSX element in the tree you return.`,\n );\n }\n if (uiRequirements.length > 0) {\n parts.push(\n `## Required UI Surfaces (derived from contract — every surface below MUST have visible UI, eval will fail if any is missing)\\n` +\n uiRequirements.join('\\n'),\n );\n }\n\n return parts.join('\\n\\n');\n}\n\n/**\n * Append the contract-context block to a user prompt if a contract is present.\n *\n * `appGadgets` forwards into `buildContractsContext` so thin contract\n * refs (`{hook}` without per-binding `package`) resolve to the\n * registered descriptor's package in the prompt-emitted \"import X from\n * Y\" lines.\n */\nexport function injectContracts(\n userPrompt: string,\n contract?: DataContract,\n appGadgets?: readonly GadgetDescriptor[],\n): string {\n if (!contract) return userPrompt;\n const ctx = buildContractsContext(contract, appGadgets);\n if (!ctx) return userPrompt;\n return userPrompt + '\\n\\n' + ctx;\n}\n\n/**\n * Build the variance-context block. Surfaces the agent's declared\n * styling signals (persona, aesthetic, context, seedPrompt) so cold-gen\n * aligns the produced component with the requested variant. Each field\n * is optional; the block only renders when at least one is present.\n *\n * - `persona` names the USER mental model (e.g. \"data-analyst\",\n * \"mobile-first reader\") — drives copy register + density choices.\n * - `aesthetic` names the VISUAL treatment (e.g. \"glassmorphic\",\n * \"editorial\", \"brutalist\") — drives surface decoration, typographic\n * weight, color usage.\n * - `context` is structured key-value signal alongside the persona —\n * small JSON-safe shape, serialized verbatim into the prompt.\n * - `seedPrompt` is the operator's original natural-language steer that\n * produced this variant in cache storage — useful as a one-line\n * directive for cold-gen.\n *\n * Returns an empty string when no fields are populated so the inject\n * helper can no-op cleanly.\n */\nexport function buildVarianceContext(\n variance: BlueprintVariance,\n options?: { readonly profileDeclared?: boolean },\n): string {\n const lines: string[] = [];\n if (typeof variance.persona === 'string' && variance.persona.length > 0) {\n lines.push(`- **Persona**: ${variance.persona}`);\n }\n if (typeof variance.aesthetic === 'string' && variance.aesthetic.length > 0) {\n lines.push(`- **Aesthetic**: ${variance.aesthetic}`);\n }\n if (typeof variance.seedPrompt === 'string' && variance.seedPrompt.length > 0) {\n lines.push(`- **Seed prompt**: ${variance.seedPrompt}`);\n }\n if (\n variance.context !== undefined &&\n variance.context !== null &&\n typeof variance.context === 'object' &&\n !Array.isArray(variance.context) &&\n Object.keys(variance.context).length > 0\n ) {\n lines.push(`- **Context**: ${JSON.stringify(variance.context)}`);\n }\n if (lines.length === 0) return '';\n return [\n '## Variance (apply to styling, not contract shape)',\n '',\n 'The agent (or operator) declared these variant signals. Honor them in',\n 'the visual treatment + copy register — but the contract shape (props,',\n 'actions, contexts, streams) remains canonical. Variance changes HOW',\n 'the UI looks and reads, not WHAT it does.',\n ...(options?.profileDeclared === true ? [STYLING_PROFILE_PRECEDENCE_NOTE] : []),\n '',\n ...lines,\n ].join('\\n');\n}\n\n/** Append the variance-context block to a user prompt if variance is present. */\nexport function injectVariance(\n userPrompt: string,\n variance?: BlueprintVariance,\n options?: { readonly profileDeclared?: boolean },\n): string {\n if (!variance) return userPrompt;\n const block = buildVarianceContext(variance, options);\n if (!block) return userPrompt;\n return userPrompt + '\\n\\n' + block;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/boilerplate/json-schema-ts.ts","../../src/check/contract-validation.ts","../../src/check/type-checker.ts","../../src/check/run-tier0.ts","../../src/contract-context.ts"],"names":["ts","listContractGadgets","HOOK_NAME_RE"],"mappings":"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","file":"prompts.js","sourcesContent":["// packages/ui-gen/src/boilerplate/json-schema-ts.ts\n//\n// JSON Schema → TypeScript type string converter.\n//\n// Pure, self-contained, no env reads, no framework deps. Exposed\n// publicly on `@ggui-ai/ui-gen/boilerplate` because it's the primitive\n// that turns a contract's JSON Schema into the `interface Props { ... }`\n// block the boilerplate generator writes.\n\nimport type { JsonSchema } from '@ggui-ai/protocol';\n\n/**\n * Convert a JsonSchema to a TypeScript type string — recursive.\n *\n * Handles: string, number, integer, boolean, null, array (+ tuples), object,\n * enum, oneOf/anyOf (unions), nullable, const, additionalProperties.\n */\nexport function jsonSchemaTypeToTs(schema: JsonSchema): string {\n // ── Union types (oneOf / anyOf) ──\n const unionMembers = schema.oneOf ?? schema.anyOf;\n if (unionMembers?.length) {\n const types = [...new Set(unionMembers.map(s => jsonSchemaTypeToTs(s)))];\n return types.length === 1 ? types[0] : types.join(' | ');\n }\n\n // ── Const (literal type) ──\n if (schema.const !== undefined) {\n return typeof schema.const === 'string' ? `'${schema.const}'` : String(schema.const);\n }\n\n // ── Enum (union of literals) ──\n if (schema.enum?.length) {\n return schema.enum\n .map(v => typeof v === 'string' ? `'${v}'` : String(v))\n .join(' | ');\n }\n\n // ── Draft-07 type array (`['string','null']` — the canonical\n // nullable form since draft-2026-08-19): members' TS union. Each\n // member re-enters with the single type so object/array members\n // keep their properties/items; `nullable` is folded in here (not in\n // the recursion) so it can't double-append. ──\n if (Array.isArray(schema.type)) {\n const { nullable: _nullable, ...rest } = schema;\n const members = [...new Set(schema.type)].map((member) =>\n jsonSchemaTypeToTs({ ...rest, type: member }),\n );\n const unique = [...new Set(members)];\n if (schema.nullable && !unique.includes('null')) unique.push('null');\n return unique.join(' | ');\n }\n\n let result: string;\n\n switch (schema.type) {\n case 'string':\n result = 'string';\n break;\n case 'number':\n case 'integer':\n result = 'number';\n break;\n case 'boolean':\n result = 'boolean';\n break;\n case 'null':\n return 'null';\n\n case 'array': {\n if (schema.items) {\n const itemType = jsonSchemaTypeToTs(schema.items);\n // Use Array<T> for complex object types, T[] for simple.\n // `items.type` may be a type SET — treat any set containing\n // 'object' like the single-string form.\n const itemsDeclareObject = Array.isArray(schema.items.type)\n ? schema.items.type.includes('object')\n : schema.items.type === 'object';\n result = (itemsDeclareObject && schema.items.properties)\n ? `Array<${itemType}>`\n : (itemType.includes('|') ? `(${itemType})[]` : `${itemType}[]`);\n } else {\n result = 'unknown[]';\n }\n break;\n }\n\n case 'object': {\n if (schema.properties) {\n const required = schema.required ?? [];\n const fields = Object.entries(schema.properties)\n .map(([key, prop]) => {\n const opt = !required.includes(key);\n return `${key}${opt ? '?' : ''}: ${jsonSchemaTypeToTs(prop)}`;\n })\n .join('; ');\n result = `{ ${fields} }`;\n } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {\n result = `Record<string, ${jsonSchemaTypeToTs(schema.additionalProperties)}>`;\n } else {\n result = 'Record<string, unknown>';\n }\n break;\n }\n\n default:\n result = 'unknown';\n }\n\n // Apply nullable (OpenAPI 3.0 shorthand)\n if (schema.nullable && result !== 'unknown') {\n return `${result} | null`;\n }\n\n return result;\n}\n","// packages/ui-gen/src/check/contract-validation.ts\n//\n// Shared validation utilities for data-contract conformance —\n// deterministic tier-0 gate for `PropsSpec` / `StreamSpec` / `ActionSpec`\n// agreement between the LLM-emitted componentCode and the contract the\n// agent handed to `ggui_render`.\n//\n// Used by:\n// (1) Self-check soft gate in the coding agent\n// (`coding-agent/self-check.ts`) — surfaces mismatches as\n// structured violations for the LLM retry loop.\n// (2) Tier-0 hard gate in the evaluation pipeline — errors (severity\n// `error`) are blocking; warnings surface as advisory.\n// (3) Adapter tool-family wiring for multi-SDK generation runs.\n// (4) Benchmarks + LLM prompt construction — `jsonSchemaTypeToTs` +\n// `propsSpecToTypeScript` + `inferPropsSpecFromSampleData` are\n// pure helpers reused outside the check path. Re-exported here so\n// those callers don't need a separate import for the subpath.\n//\n// Deps are pure: `@ggui-ai/protocol` (public types) + `typescript`\n// (compiler API) + the in-package `jsonSchemaTypeToTs` primitive from\n// `../boilerplate/json-schema-ts.js`.\n\nimport type {\n PropsSpec,\n StreamSpec,\n ActionSpec,\n DataContract,\n JsonSchema,\n JsonValue,\n PropEntry,\n JsonObject,\n} from \"@ggui-ai/protocol\";\nimport ts from \"typescript\";\n\n// Re-export the pure JSON-Schema → TypeScript-type converter from the\n// in-package `boilerplate/` primitive, so consumers of the tier-0 check\n// surface (self-check, tiers, benchmarks, prompts, adapters) import a\n// single symbol from a single subpath.\nexport { jsonSchemaTypeToTs } from \"../boilerplate/json-schema-ts.js\";\nimport { jsonSchemaTypeToTs } from \"../boilerplate/json-schema-ts.js\";\n\n// =============================================================================\n// Contract Issue\n// =============================================================================\n\nexport interface ContractIssue {\n severity: \"error\" | \"warning\";\n field: string;\n message: string;\n fix: string;\n}\n\n// =============================================================================\n// Props Interface Extraction (AST-based)\n// =============================================================================\n\ninterface ExtractedProp {\n name: string;\n type: string;\n optional: boolean;\n}\n\n/**\n * Extract field names and types from a TypeScript Props interface in source code.\n * Uses the TypeScript compiler API — handles nested types, generics, unions correctly.\n */\nexport function extractPropsInterface(code: string): ExtractedProp[] | null {\n const sf = ts.createSourceFile(\"component.tsx\", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const props: ExtractedProp[] = [];\n\n function visitMembers(members: ts.NodeArray<ts.TypeElement>): void {\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.name) {\n const name = member.name.getText(sf);\n const type = member.type ? member.type.getText(sf) : \"unknown\";\n const optional = !!member.questionToken;\n props.push({ name, type, optional });\n }\n }\n }\n\n function visit(node: ts.Node): void {\n // interface Props { ... }\n if (ts.isInterfaceDeclaration(node) && node.name.text === \"Props\") {\n visitMembers(node.members);\n return;\n }\n // type Props = { ... }\n if (ts.isTypeAliasDeclaration(node) && node.name.text === \"Props\") {\n if (ts.isTypeLiteralNode(node.type)) {\n visitMembers(node.type.members);\n }\n return;\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sf);\n return props.length > 0 ? props : null;\n}\n\n// =============================================================================\n// Contract Validation\n// =============================================================================\n\n/**\n * Compare extracted Props interface against a PropsSpec contract.\n * Returns issues: errors for missing required fields, warnings for type mismatches.\n */\nexport function validatePropsAgainstSchema(\n code: string,\n spec: PropsSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n const extracted = extractPropsInterface(code);\n\n if (!extracted) {\n // Can't extract — not necessarily an error (might use inline types)\n return [];\n }\n\n const extractedMap = new Map(extracted.map((p) => [p.name, p]));\n\n for (const [propName, entry] of Object.entries(spec.properties)) {\n const extractedProp = extractedMap.get(propName);\n\n if (!extractedProp) {\n if (entry.required) {\n issues.push({\n severity: \"error\",\n field: propName,\n message: `Props interface is missing required field '${propName}' from the data contract`,\n fix: `Add \\`${propName}${entry.required ? \"\" : \"?\"}: ${jsonSchemaTypeToTs(entry.schema)}\\` to your Props interface`,\n });\n } else {\n issues.push({\n severity: \"warning\",\n field: propName,\n message: `Props interface is missing optional field '${propName}' from the data contract`,\n fix: `Consider adding \\`${propName}?: ${jsonSchemaTypeToTs(entry.schema)}\\` to your Props interface`,\n });\n }\n continue;\n }\n\n // Basic type compatibility check (only if we have type info)\n if (extractedProp.type !== \"unknown\") {\n const expectedTsType = jsonSchemaTypeToTs(entry.schema);\n if (!isTypeCompatible(extractedProp.type, expectedTsType, entry.schema)) {\n issues.push({\n severity: \"warning\",\n field: propName,\n message: `Field '${propName}' has type '${extractedProp.type}' but contract expects '${expectedTsType}'`,\n fix: `Change \\`${propName}\\` type to \\`${expectedTsType}\\``,\n });\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Check if a TypeScript type string is compatible with a JSON Schema type.\n * Loose comparison — we only check the base type, not full structural equality.\n */\nfunction isTypeCompatible(tsType: string, expectedTsType: string, schema: JsonSchema): boolean {\n const normalized = tsType.replace(/\\s/g, \"\").toLowerCase();\n\n switch (schema.type) {\n case \"string\":\n return normalized.includes(\"string\");\n case \"number\":\n case \"integer\":\n return normalized.includes(\"number\");\n case \"boolean\":\n return normalized.includes(\"boolean\");\n case \"array\":\n return normalized.includes(\"[]\") || normalized.includes(\"array\");\n case \"object\":\n return ![\"string\", \"number\", \"boolean\"].some((t) => normalized === t);\n default:\n return true;\n }\n}\n\n// =============================================================================\n// JSON Schema → TypeScript Conversion (for LLM prompts)\n// =============================================================================\n\n// `jsonSchemaTypeToTs` lives in `@ggui-ai/ui-gen/boilerplate` and is\n// re-exported at the top of this file.\n\n/**\n * Convert a full PropsSpec to a TypeScript interface string for LLM prompt injection.\n * Includes required/optional markers and descriptions as comments.\n */\nexport function propsSpecToTypeScript(spec: PropsSpec, indent = 2): string {\n const pad = \" \".repeat(indent);\n const lines: string[] = [];\n\n for (const [propName, entry] of Object.entries(spec.properties)) {\n if (entry.description) {\n lines.push(`${pad}/** ${entry.description} */`);\n }\n const optional = !entry.required;\n const tsType = jsonSchemaTypeToTs(entry.schema);\n const defaultStr = entry.default !== undefined ? ` // default: ${JSON.stringify(entry.default)}` : \"\";\n lines.push(`${pad}${propName}${optional ? \"?\" : \"\"}: ${tsType};${defaultStr}`);\n // Include example as a comment so the LLM sees the exact data shape\n if (entry.example !== undefined) {\n const exampleStr = JSON.stringify(entry.example, null, 2)\n .split(\"\\n\")\n .map((l, i) => (i === 0 ? `${pad}// Example: ${l}` : `${pad}// ${l}`))\n .join(\"\\n\");\n lines.push(exampleStr);\n }\n }\n\n return `{\\n${lines.join(\"\\n\")}\\n}`;\n}\n\n// =============================================================================\n// StreamSpec Validation\n// =============================================================================\n\n/**\n * Validate that the generated source code properly handles stream events.\n * Requires useStream() wire hooks — legacy DOM event patterns are no longer accepted.\n */\nexport function validateStreamSpecConformance(\n code: string,\n spec: StreamSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n const usesWireStream = code.includes(\"useStream\");\n\n if (!usesWireStream) {\n issues.push({\n severity: \"error\",\n field: \"__streamSpec__\",\n message: \"Component does not handle stream events — use useStream() from the boilerplate wire hooks\",\n fix: \"Use the useStream('eventName') hook from the boilerplate to receive real-time data from the agent\",\n });\n return issues;\n }\n\n // Check each declared channel is referenced in the code. `spec` is a\n // flat `Record<channelName, StreamChannelEntry>` — iterate directly.\n for (const [channelName, entry] of Object.entries(spec)) {\n if (!code.includes(`'${channelName}'`) && !code.includes(`\"${channelName}\"`)) {\n issues.push({\n severity: \"warning\",\n field: channelName,\n message: `StreamSpec declares channel '${channelName}' but component doesn't reference it`,\n fix: `Add useStream('${channelName}') to receive ${entry.description || channelName} events`,\n });\n }\n }\n\n return issues;\n}\n\n// =============================================================================\n// ActionSpec Validation\n// =============================================================================\n\n/**\n * Validate that the generated source code wires the declared actions.\n * Checks that action IDs are referenced via useAction() wire hooks, string literals, or prop callbacks.\n */\nexport function validateActionSpecConformance(\n code: string,\n spec: ActionSpec,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n // `spec` is a flat `Record<actionId, ActionEntry>` — iterate directly.\n for (const [actionId, entry] of Object.entries(spec)) {\n // Check the action ID or its label is referenced in the code\n const idReferenced = code.includes(`'${actionId}'`) || code.includes(`\"${actionId}\"`);\n const labelReferenced = code.includes(entry.label);\n const callbackName = `on${actionId.charAt(0).toUpperCase()}${actionId.slice(1)}`;\n const callbackReferenced = code.includes(callbackName);\n\n if (!idReferenced && !labelReferenced && !callbackReferenced) {\n issues.push({\n severity: \"error\",\n field: actionId,\n message: `ActionSpec declares action '${actionId}' (\"${entry.label}\") but component doesn't wire it`,\n fix: `Use the useAction('${actionId}') hook from the boilerplate and wire it to a button or form submit`,\n });\n }\n }\n\n return issues;\n}\n\n// =============================================================================\n// Unified Contract Validation\n// =============================================================================\n\n/**\n * Validate all contract (props, stream, actions) against the source code.\n * Returns combined issues from all three validations.\n */\nexport function validateAllContracts(\n code: string,\n contract: DataContract,\n): ContractIssue[] {\n const issues: ContractIssue[] = [];\n\n if (contract.propsSpec) {\n issues.push(...validatePropsAgainstSchema(code, contract.propsSpec));\n }\n\n if (contract.streamSpec) {\n issues.push(...validateStreamSpecConformance(code, contract.streamSpec));\n }\n\n if (contract.actionSpec) {\n issues.push(...validateActionSpecConformance(code, contract.actionSpec));\n }\n\n return issues;\n}\n\n// =============================================================================\n// Sample Data → PropsSpec Inference (backward compat)\n// =============================================================================\n\n/**\n * Infer a PropsSpec from sample data.\n * Convenience utility for migration and for agents that pass sample data instead of schemas.\n * All fields are marked as required (can't distinguish required/optional from data alone).\n */\nexport function inferPropsSpecFromSampleData(data: JsonObject): PropsSpec {\n const properties: Record<string, PropEntry> = {};\n\n for (const [key, value] of Object.entries(data)) {\n properties[key] = {\n schema: inferJsonSchema(value),\n required: true,\n example: value as JsonValue,\n };\n }\n\n return { properties };\n}\n\nfunction inferJsonSchema(value: unknown): JsonSchema {\n if (value === null || value === undefined) return { type: \"null\" };\n if (typeof value === \"string\") return { type: \"string\" };\n if (typeof value === \"number\") return { type: \"number\" };\n if (typeof value === \"boolean\") return { type: \"boolean\" };\n\n if (Array.isArray(value)) {\n if (value.length === 0) return { type: \"array\" };\n return {\n type: \"array\",\n items: inferJsonSchema(value[0]),\n };\n }\n\n if (typeof value === \"object\") {\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const [k, v] of Object.entries(value as JsonObject)) {\n properties[k] = inferJsonSchema(v);\n required.push(k);\n }\n return { type: \"object\", properties, required };\n }\n\n return { type: \"string\" };\n}\n","// packages/ui-gen/src/check/type-checker.ts\n//\n// TypeScript type-checker for LLM-generated UI components. Uses the\n// TypeScript compiler API with a virtual filesystem to type-check TSX\n// code against React types and `@ggui-ai/design` primitives, giving\n// the LLM familiar TS error feedback without writing anything to disk.\n//\n// The checker builds a virtual filesystem seeded with `typescript/lib`,\n// `@types/react`, `@ggui-ai/design/dist`, and `@ggui-ai/wire/dist`\n// `.d.ts` content, then drives `ts.createProgram` against a synthetic\n// `Component.tsx`. Exposed on `@ggui-ai/ui-gen/check` so\n// `createUiGenerator` runs the same tier-0 gate as the hosted\n// generation path.\n//\n// Path-resolution note: the three workspace-rooted lookups —\n// `.pnpm/@types+react@*`, `packages/design/dist`, and\n// `packages/wire/dist` — are anchored relative to this file. tsup\n// bundles `src/check/index.ts` (splitting: false) into a flat\n// `dist/check/index.js`, so the src and dist locations sit at the same\n// depth and both resolve correctly. `typescript/lib/typescript.js`\n// keeps using `createRequire(import.meta.url).resolve` —\n// position-independent, so no change.\n//\n// Used by:\n// (1) Tier-0 `type_check` — blocking errors (see BLOCKING_CODES)\n// map to PRODUCTION_FAILED; non-blocking TS diagnostics surface\n// as warnings so the LLM can iterate on non-crash-risk findings\n// without stalling the harness.\n// (2) The `self_check` tool — called from generator SDKs (Anthropic /\n// OpenAI / Google) to give the LLM TS feedback mid-generation.\n// (3) The tier-0 orchestrator `runTier0Checks` — runs in parallel\n// with the wire-preservation + lint checks under the same tier\n// budget.\n//\n// Design notes that motivate non-obvious choices:\n// - Classic React JSX mode (`ts.JsxEmit.React`). The synthetic\n// prefix (see SYNTHETIC_PREFIX) supplies `import React from\n// 'react'` for the classic JSX factory PLUS a self-contained\n// global `JSX` namespace. `@types/react` v19 removed the global\n// `JSX` namespace that classic mode resolves intrinsic elements +\n// the `key`/`ref` carve-out through; without the shim, `<div>`\n// degrades to `any` and every typed component falsely rejects the\n// intrinsic `key` prop. Automatic mode was tried but its\n// `react/jsx-runtime` resolution does not survive the VFS.\n// - `strict: false` + `strictNullChecks: true`: we want the runtime\n// crash classes (`undefined.foo()`, `null.bar`) to surface as\n// blocking, but not the optional-chaining / exhaustive-check\n// noise that full `strict` would emit on LLM code.\n// - `types: []`: prevents the VFS from auto-pulling every\n// `@types/*` package that happens to be hoisted — only react and\n// the design/wire dist types should be reachable, matching the\n// forbidden-import policy enforced by `react-linter` elsewhere.\n// - TS2307 (\"Cannot find module\") is deliberately NOT blocking —\n// Lambda bundles code without type declarations, so the VFS can't\n// see every package that may exist at runtime. Forbidden imports\n// are caught by `runSelfChecks` regex instead.\n\nimport ts from 'typescript';\nimport fs from 'fs';\nimport path from 'path';\nimport { createRequire } from 'module';\nimport { describeAllowedImports } from '../validation/allowed-imports.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TypeCheckDiagnostic {\n code: number;\n line: number;\n message: string;\n fix: string;\n}\n\nexport interface TypeCheckResult {\n errors: TypeCheckDiagnostic[]; // Blocking\n warnings: TypeCheckDiagnostic[]; // Non-blocking\n}\n\n// ---------------------------------------------------------------------------\n// Blocking error codes — these would break at runtime\n// ---------------------------------------------------------------------------\n\nconst BLOCKING_CODES = new Set([\n 2304, // Cannot find name\n // 2307 (Cannot find module) is NOT blocking — Lambda bundles code without\n // type declarations, so the VFS can't resolve react/@ggui-ai/design.\n // Forbidden imports are caught by runSelfChecks regex instead.\n 2305, // Module has no exported member\n 2322, // Type not assignable\n 2339, // Property does not exist on type\n 2741, // Missing required property\n 2769, // No overload matches this call\n 17004, // Cannot use JSX unless '--jsx' flag\n 18047, // 'X' is possibly 'null' — causes runtime crash\n 18048, // 'X' is possibly 'undefined' — causes runtime crash\n]);\n\n// ---------------------------------------------------------------------------\n// Fix suggestions per error code\n// ---------------------------------------------------------------------------\n\nfunction generateFix(code: number, message?: string, sourceLine?: string): string {\n // Source-line hint: when the failing line is short and self-contained,\n // appending it gives the LLM a precise target. Skip for very long lines\n // (>140 chars — usually multi-prop JSX where the line itself is the\n // diagnostic) since they bloat the violation envelope without helping.\n const sourceHint =\n sourceLine && sourceLine.length > 0 && sourceLine.length <= 140\n ? ` Offending line: \\`${sourceLine}\\``\n : '';\n\n // Detect event handler on non-interactive primitive\n if ((code === 2322 || code === 2339) && message && /onClick|onDoubleClick|onMouseEnter|onMouseLeave|onPress/.test(message)) {\n return `This structural primitive has no event handlers of its own — add the trait as a PROP: as={Clickable} (then onClick works), imported from @ggui-ai/design. Do NOT wrap it in <Clickable>…</Clickable>; as is a prop, not a wrapper element.${sourceHint}`;\n }\n\n // Detect underscore-prefix-on-prop trap: `Property '_X' does not exist`.\n // LLM saw an unused-var warning on `X` and tried to silence it by renaming\n // the destructure to `_X`, breaking prop access. Steer back.\n if (code === 2339 && message && /Property '_[a-zA-Z]/.test(message)) {\n return `You renamed a prop with a leading underscore to silence \\`no-unused-vars\\`, but the prop on \\`Props\\` doesn't have that prefix. Restore the original name; better, don't destructure props you won't render — access them via \\`props.fieldName\\` only when needed.${sourceHint}`;\n }\n\n // Detect \"Cannot find name\" for destructured props\n if (code === 2304 && message && !message.includes('module') && !message.includes('import')) {\n return `This name is not defined in scope. Either you destructured props (use \\`props.fieldName\\` directly instead) OR you removed a helper declaration in this patch but kept a JSX/expression reference to it. Read your full patch and either restore the declaration or remove the reference.${sourceHint}`;\n }\n\n // Detect \"unknown is not assignable to ReactNode\"\n if ((code === 2322 || code === 2769) && message && /unknown.*ReactNode|ReactNode.*unknown/.test(message)) {\n return `This value has type 'unknown' and cannot be rendered in JSX. Cast it: String(value) or add a type annotation.${sourceHint}`;\n }\n\n switch (code) {\n case 2307:\n return `Only these imports are allowed: ${describeAllowedImports()}${sourceHint}`;\n case 2322:\n case 2769:\n return `Type mismatch on this expression. Check the offending line below — the prop name in JSX is what TypeScript is rejecting; the message tells you the expected type.${sourceHint}`;\n case 2339:\n return `This prop doesn't exist on this component — check the available props on the component's interface (visible at the top of the file, or in the design-system reference).${sourceHint}`;\n case 2305:\n return `This name is not defined. Check your imports and variable declarations.${sourceHint}`;\n case 2741:\n return `A required prop is missing. Check the component's Props interface.${sourceHint}`;\n case 18047:\n case 18048:\n return `This value might be null/undefined. Every dereference on the same nullable still needs \\`?.\\` — \\`x?.foo && x?.bar\\`, NOT \\`x?.foo && x.bar\\`. Or hoist: \\`const v = x; if (!v) return null;\\` then access \\`v.foo\\`/\\`v.bar\\` unguarded.${sourceHint}`;\n default:\n return `Review the TypeScript error and fix the type issue.${sourceHint}`;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Virtual Filesystem — lazy singleton\n// ---------------------------------------------------------------------------\n\ninterface VfsEntry {\n content: string;\n sourceFile: ts.SourceFile;\n}\n\nlet vfsCache: Map<string, VfsEntry> | null = null;\n\nfunction parseAndStore(\n vfs: Map<string, VfsEntry>,\n virtualPath: string,\n content: string,\n): void {\n const sourceFile = ts.createSourceFile(\n virtualPath,\n content,\n ts.ScriptTarget.ES2020,\n true,\n virtualPath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,\n );\n vfs.set(virtualPath, { content, sourceFile });\n}\n\n/**\n * Recursively walk a directory and return all file paths matching a filter.\n */\nfunction walkDir(dir: string, filter: (f: string) => boolean): string[] {\n const results: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(full, filter));\n } else if (filter(full)) {\n results.push(full);\n }\n }\n return results;\n}\n\n/**\n * Resolve `@types/react`'s root directory through Node's module\n * resolution. Position-independent — walks the consumer's node_modules\n * chain. Works in monorepo (where the type package is hoisted under\n * `<root>/node_modules/.pnpm/@types+react@*`) and in standalone\n * `packages/`-only installs (e.g. the projected public OSS repo where\n * the `packages/` prefix is stripped, so a hardcoded `../../../..`\n * walk lands in the wrong place).\n */\nfunction findReactTypesDir(): string | null {\n try {\n // `@types/react` doesn't expose `package.json` as a public subpath,\n // so resolve via the always-present `index.d.ts`.\n const indexDts = createRequire(import.meta.url).resolve('@types/react/index.d.ts');\n return path.dirname(indexDts);\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve a workspace package's `dist/` directory through Node's\n * module resolution. Like `findReactTypesDir`, this works in both\n * monorepo (where the package is hoisted/symlinked) and standalone\n * installs — no relative-path math required.\n */\nfunction findPackageDistDir(pkg: string): string | null {\n try {\n const pkgJson = createRequire(import.meta.url).resolve(`${pkg}/package.json`);\n const dist = path.join(path.dirname(pkgJson), 'dist');\n return fs.existsSync(dist) ? dist : null;\n } catch {\n return null;\n }\n}\n\nasync function loadVfs(): Promise<Map<string, VfsEntry>> {\n if (vfsCache) return vfsCache;\n\n const vfs = new Map<string, VfsEntry>();\n\n // ---- 1. TypeScript lib files ----\n // `createRequire(import.meta.url).resolve('typescript/lib/typescript.js')`\n // is position-independent — it walks the consumer's node_modules chain,\n // so this lookup works identically from core/, packages/ui-gen/, and any\n // future host package.\n const require_ = createRequire(import.meta.url);\n const tsDir = path.dirname(require_.resolve('typescript/lib/typescript.js'));\n const libFiles = fs.readdirSync(tsDir).filter((f) => /^lib\\..*\\.d\\.ts$/.test(f));\n for (const file of libFiles) {\n const content = fs.readFileSync(path.join(tsDir, file), 'utf-8');\n parseAndStore(vfs, file, content);\n }\n\n // ---- 2. React types ----\n // Resolved through `createRequire(import.meta.url).resolve('@types/\n // react/index.d.ts')` — position-independent. Works the same in\n // monorepo (hoisted to root) and in standalone `packages/`-only\n // installs (the projected public OSS repo strips the `packages/`\n // prefix, so a hardcoded `../../../..` walk would miss).\n const reactDir = findReactTypesDir();\n if (reactDir) {\n const reactDts = fs.readdirSync(reactDir).filter((f) => f.endsWith('.d.ts'));\n for (const file of reactDts) {\n const content = fs.readFileSync(path.join(reactDir, file), 'utf-8');\n parseAndStore(vfs, `node_modules/@types/react/${file}`, content);\n }\n }\n\n // ---- 3. Design system types ----\n const designDist = findPackageDistDir('@ggui-ai/design');\n if (designDist) {\n const dtsFiles = walkDir(designDist, (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'));\n for (const file of dtsFiles) {\n const rel = path.relative(designDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/design/${rel}`, content);\n }\n }\n\n // ---- 4. Wire module types ----\n const wireDist = findPackageDistDir('@ggui-ai/wire');\n if (wireDist) {\n const dtsFiles = walkDir(wireDist, (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'));\n for (const file of dtsFiles) {\n const rel = path.relative(wireDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/wire/${rel}`, content);\n }\n }\n\n // ---- 5. Gadgets types ----\n // Generated component code direct-imports standard-library gadget\n // hooks — `import { useGeolocation } from '@ggui-ai/gadgets'`.\n // Loading the `@ggui-ai/gadgets` package's shipped `.d.ts` into the\n // VFS makes those bare-named imports resolve against the real hook\n // declarations (named option/return types preserved). The standard\n // gadget hooks are plain named exports the resolver finds directly.\n const clientLibsDist = findPackageDistDir('@ggui-ai/gadgets');\n if (clientLibsDist) {\n const dtsFiles = walkDir(\n clientLibsDist,\n (f) => f.endsWith('.d.ts') && !f.endsWith('.d.ts.map'),\n );\n for (const file of dtsFiles) {\n const rel = path.relative(clientLibsDist, file);\n const content = fs.readFileSync(file, 'utf-8');\n parseAndStore(vfs, `node_modules/@ggui-ai/gadgets/${rel}`, content);\n }\n }\n\n vfsCache = vfs;\n return vfs;\n}\n\n// ---------------------------------------------------------------------------\n// Module resolution helper\n// ---------------------------------------------------------------------------\n\n/**\n * Try to resolve a relative import within the VFS.\n * Given `containingFile` (e.g. `node_modules/@ggui-ai/design/primitives/index.d.ts`)\n * and `importPath` (e.g. `./Card` or, as every published declaration says\n * since 0.15.0, `./Card.js`), try:\n * - <resolved>.d.ts\n * - <resolved>/index.d.ts\n * - <resolved>.ts\n * A `.js` / `.mjs` / `.cjs` extension on the specifier is the ESM\n * convention for \"the module that compiles to this file\" — TypeScript's\n * NodeNext maps `./Card.js` to `./Card.d.ts`, and so does this: the\n * extension is stripped before the candidates are tried. Without that, a\n * `.js`-suffixed re-export resolved to nothing, the imported names typed\n * as `any`, and the checker reported zero errors on code it must reject\n * (ggui 0.15.0 publish-gate, type-checker.test.ts).\n */\nfunction resolveRelativeInVfs(\n vfs: Map<string, VfsEntry>,\n containingFile: string,\n importPath: string,\n): string | undefined {\n // Strip leading '/' from containingFile — TS may prepend getCurrentDirectory()\n const normalized = containingFile.startsWith('/') ? containingFile.slice(1) : containingFile;\n const dir = path.posix.dirname(normalized);\n // `./x.js` (`.mjs`, `.cjs`) names the declaration `./x.d.ts` — strip it.\n const specifier = importPath.replace(/\\.(?:m|c)?js$/, '');\n const resolved = path.posix.normalize(`${dir}/${specifier}`);\n\n // Try .d.ts extension\n const withDts = `${resolved}.d.ts`;\n if (vfs.has(withDts)) return withDts;\n\n // Try /index.d.ts\n const withIndexDts = `${resolved}/index.d.ts`;\n if (vfs.has(withIndexDts)) return withIndexDts;\n\n // Try .ts extension\n const withTs = `${resolved}.ts`;\n if (vfs.has(withTs)) return withTs;\n\n // Might already be the exact path\n if (vfs.has(resolved)) return resolved;\n\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// CompilerHost factory\n// ---------------------------------------------------------------------------\n\nconst COMPILER_OPTIONS: ts.CompilerOptions = {\n target: ts.ScriptTarget.ES2020,\n module: ts.ModuleKind.ESNext,\n // Classic React JSX mode. The synthetic prefix (SYNTHETIC_PREFIX)\n // supplies `import React from 'react'` plus a `declare global` JSX\n // namespace shim: `@types/react` v19 removed the *global* `JSX`\n // namespace (it lives at `React.JSX` now), which classic mode needs\n // for `JSX.IntrinsicElements` and the `key`/`ref` carve-out. Without\n // the shim, `<div>` degrades to `any` and every typed component\n // falsely rejects the intrinsic `key` prop.\n jsx: ts.JsxEmit.React,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n strict: false,\n strictNullChecks: true, // Catch undefined.foo() errors that cause runtime crashes\n noEmit: true,\n skipLibCheck: true,\n noImplicitAny: true,\n types: [],\n esModuleInterop: true,\n};\n\n/**\n * Normalize a path that TS's internal module resolution might generate.\n * Since getCurrentDirectory() returns '/', TS will probe paths like\n * '/node_modules/@types/react/jsx-runtime.d.ts'. We strip the leading\n * '/' to match our VFS keys.\n */\nfunction normalizeVfsPath(f: string): string | null {\n if (f.startsWith('/')) {\n return f.slice(1);\n }\n return null;\n}\n\n/**\n * Core module resolution logic shared between resolveModuleNameLiterals\n * (for user imports) and resolveModuleNames (for TS-internal imports\n * like jsxImportSource).\n */\nfunction resolveModuleName(\n vfs: Map<string, VfsEntry>,\n name: string,\n containingFile: string,\n): ts.ResolvedModuleWithFailedLookupLocations {\n // React core\n if (name === 'react') {\n return resolved('node_modules/@types/react/index.d.ts');\n }\n if (name === 'react/jsx-runtime') {\n return resolved('node_modules/@types/react/jsx-runtime.d.ts');\n }\n if (name === 'react/jsx-dev-runtime') {\n const p = 'node_modules/@types/react/jsx-dev-runtime.d.ts';\n if (vfs.has(p)) return resolved(p);\n return resolved('node_modules/@types/react/jsx-runtime.d.ts');\n }\n\n // @ggui-ai/design sub-paths\n const designPrefix = '@ggui-ai/design/';\n if (name.startsWith(designPrefix)) {\n const subpath = name.slice(designPrefix.length);\n const indexPath = `node_modules/@ggui-ai/design/${subpath}/index.d.ts`;\n if (vfs.has(indexPath)) return resolved(indexPath);\n const directPath = `node_modules/@ggui-ai/design/${subpath}.d.ts`;\n if (vfs.has(directPath)) return resolved(directPath);\n }\n\n // @ggui-ai/design root\n if (name === '@ggui-ai/design') {\n return resolved('node_modules/@ggui-ai/design/index.d.ts');\n }\n\n // @ggui-ai/wire\n if (name === '@ggui-ai/wire') {\n return resolved('node_modules/@ggui-ai/wire/index.d.ts');\n }\n\n // @ggui-ai/gadgets — the STDLIB gadget package. Generated\n // code direct-imports the seven STDLIB hooks (`useGeolocation`, …)\n // as plain named exports from this specifier. The shipped `.d.ts`\n // (loaded into the VFS above) carries their real declarations.\n if (name === '@ggui-ai/gadgets') {\n return resolved('node_modules/@ggui-ai/gadgets/index.d.ts');\n }\n\n // Relative imports — resolve within VFS\n if (name.startsWith('./') || name.startsWith('../')) {\n const resolvedPath = resolveRelativeInVfs(vfs, containingFile, name);\n if (resolvedPath) return resolved(resolvedPath);\n }\n\n // Third-party gadget wrapper packages. The render handler fetches each\n // non-stdlib gadget's `.d.ts` and `typecheck` overlays it at\n // `node_modules/<package>/index.d.ts`. A generated direct import\n // `import { useX } from '<package>'` resolves through this\n // bare-specifier branch against the overlaid `.d.ts` — named\n // option/return types preserved instead of collapsing to `any`.\n // Generic VFS lookup — no per-package allow-list.\n const bareIndex = `node_modules/${name}/index.d.ts`;\n if (vfs.has(bareIndex)) return resolved(bareIndex);\n\n // Unknown module\n return { resolvedModule: undefined };\n}\n\nfunction createVfsHost(\n vfs: Map<string, VfsEntry>,\n componentCode: string,\n): ts.CompilerHost {\n // Create fresh source file for the user component\n const componentFile = ts.createSourceFile(\n 'Component.tsx',\n componentCode,\n ts.ScriptTarget.ES2020,\n true,\n ts.ScriptKind.TSX,\n );\n\n const host: ts.CompilerHost = {\n getSourceFile(fileName: string) {\n if (fileName === 'Component.tsx') return componentFile;\n const entry = vfs.get(fileName);\n if (entry) return entry.sourceFile;\n // Try normalized path (strip leading '/')\n const normalized = normalizeVfsPath(fileName);\n if (normalized) return vfs.get(normalized)?.sourceFile;\n return undefined;\n },\n getDefaultLibFileName() {\n return 'lib.es2020.full.d.ts';\n },\n writeFile() {\n // no-op\n },\n getCurrentDirectory() {\n return '/';\n },\n getCanonicalFileName(f: string) {\n return f;\n },\n useCaseSensitiveFileNames() {\n return true;\n },\n getNewLine() {\n return '\\n';\n },\n fileExists(f: string) {\n if (f === 'Component.tsx' || vfs.has(f)) return true;\n // Normalize bare-specifier lookups that TS's internal resolver\n // turns into relative-looking paths from getCurrentDirectory '/'.\n const normalized = normalizeVfsPath(f);\n if (normalized && vfs.has(normalized)) return true;\n return false;\n },\n readFile(f: string) {\n if (f === 'Component.tsx') return componentCode;\n const direct = vfs.get(f);\n if (direct) return direct.content;\n const normalized = normalizeVfsPath(f);\n if (normalized) return vfs.get(normalized)?.content;\n return undefined;\n },\n resolveModuleNameLiterals(\n moduleLiterals: readonly ts.StringLiteralLike[],\n containingFile: string,\n ): readonly ts.ResolvedModuleWithFailedLookupLocations[] {\n return moduleLiterals.map((literal) =>\n resolveModuleName(vfs, literal.text, containingFile),\n );\n },\n };\n\n return host;\n}\n\nfunction resolved(resolvedFileName: string): ts.ResolvedModuleWithFailedLookupLocations {\n return {\n resolvedModule: {\n resolvedFileName,\n isExternalLibraryImport: true,\n extension: ts.Extension.Dts,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport async function typecheck(\n code: string,\n /**\n * A `package -> .d.ts content` map for third-party gadget wrappers.\n * The render handler parallel-fetches each non-stdlib gadget's `.d.ts`\n * (via `GadgetDescriptor.typesUrl` + SRI) and threads the result\n * here. Each entry is overlaid into the per-call VFS at\n * `node_modules/<package>/index.d.ts`. A generated direct import\n * `import { useX } from '<package>'` resolves through the\n * bare-specifier branch in `resolveModuleName` directly against this\n * overlaid entry — named option/return types preserved, so a\n * wrong-typed hook call surfaces a blocking TS error instead of\n * collapsing to `any`.\n *\n * Stdlib gadgets (`package: '@ggui-ai/gadgets'`) need no entry —\n * `@ggui-ai/gadgets` ships its `.d.ts` into the VFS unconditionally.\n * Omit for standard-library-only callers.\n */\n dtsMap?: Readonly<Record<string, string>>,\n): Promise<TypeCheckResult> {\n const vfs = await loadVfs();\n\n // Overlay each third-party gadget wrapper's `.d.ts` per-call without\n // mutating the cached VFS. The bare-specifier branch in\n // `resolveModuleName` resolves a generated `import { useX } from\n // '<package>'` against the overlaid entry, so the LLM sandbox sees\n // the wrapper's strict hook/component types (named option/return\n // shapes) rather than `any`. A third-party wrapper with no `dtsMap`\n // entry collapses to `any` (TS2307 is non-blocking) — degraded UX,\n // not a generation blocker; the fix is to thread `gadgetTypes`.\n // Stdlib (`@ggui-ai/gadgets`) ships its `.d.ts` unconditionally.\n const effectiveVfs = (() => {\n const dtsEntries = dtsMap !== undefined ? Object.entries(dtsMap) : [];\n if (dtsEntries.length === 0) {\n return vfs;\n }\n const overlay = new Map(vfs);\n for (const [pkg, content] of dtsEntries) {\n parseAndStore(overlay, `node_modules/${pkg}/index.d.ts`, content);\n }\n return overlay;\n })();\n\n // Prepend a synthetic prefix for classic JSX mode:\n // - `import React from 'react'` — the classic-mode JSX factory\n // (`React.createElement`) needs the React namespace in scope.\n // - a `declare global` JSX shim — `@types/react` v19 dropped the\n // GLOBAL `JSX` namespace (it is `React.JSX` now); classic mode\n // resolves intrinsic elements + the `key`/`ref` carve-out through\n // the global `JSX`, so without this `<div>` degrades to `any` and\n // every typed component falsely rejects the intrinsic `key` prop.\n // Mirrors `react/jsx-runtime`'s own JSX namespace, hoisted global.\n // All prefix lines are skipped from diagnostics via `lineOffset`.\n // `@types/react` v19 removed the GLOBAL `JSX` namespace (it lives at\n // `React.JSX` now — un-exported, not reliably reachable through the\n // VFS). Classic JSX mode resolves intrinsic elements + the `key`\n // carve-out through the global `JSX`, so declare a self-contained\n // one. Intrinsic elements are permissive (`[elem]: any`): the\n // type-checker's value is on the design components — which keep\n // their real prop types regardless of this namespace — and on\n // runtime-crash detection, not on deep typing of raw HTML (which the\n // generation prompt discourages anyway).\n const globalJsxShim =\n 'declare global { namespace JSX {' +\n ' type ElementType = string | ((props: any) => any) | (new (props: any) => any);' +\n ' interface Element { type: any; props: any; key: string | number | null; }' +\n ' interface ElementClass { render(): any; }' +\n ' interface ElementAttributesProperty { props: object; }' +\n ' interface ElementChildrenAttribute { children: object; }' +\n ' interface IntrinsicAttributes { key?: string | number | bigint | null; }' +\n ' interface IntrinsicClassAttributes<T> { ref?: any; }' +\n ' interface IntrinsicElements { [elem: string]: any; }' +\n ' } }\\n';\n const SYNTHETIC_PREFIX = \"import React from 'react';\\n\" + globalJsxShim;\n const prefixedCode = SYNTHETIC_PREFIX + code;\n const lineOffset = SYNTHETIC_PREFIX.split('\\n').length - 1;\n\n const host = createVfsHost(effectiveVfs, prefixedCode);\n\n const program = ts.createProgram(['Component.tsx'], COMPILER_OPTIONS, host);\n const diagnostics = ts.getPreEmitDiagnostics(program);\n\n const errors: TypeCheckDiagnostic[] = [];\n const warnings: TypeCheckDiagnostic[] = [];\n\n for (const diag of diagnostics) {\n // Only report diagnostics from the user's component file\n if (diag.file && diag.file.fileName !== 'Component.tsx') continue;\n // Global diagnostics (no file) — skip as they come from config\n if (!diag.file) continue;\n // Skip diagnostics on the synthetic import prefix line\n const diagLine = ts.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line;\n if (diagLine < lineOffset) continue;\n\n const rawLine = diag.file\n ? ts.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line + 1\n : 0;\n // Subtract the synthetic import line to get the user's original line number\n const line = Math.max(1, rawLine - lineOffset);\n const message = ts.flattenDiagnosticMessageText(diag.messageText, '\\n');\n const code_ = diag.code;\n\n // Extract the offending source line for the fix hint — without it\n // \"Type 'string' is not assignable to type 'number'\" is unactionable\n // because the LLM doesn't know which prop on which primitive. With\n // the line (\"<Icon size=\\\"md\\\" ...>\"), the LLM can target precisely.\n const sourceLines = diag.file.text.split('\\n');\n const sourceLine =\n rawLine >= 1 && rawLine <= sourceLines.length\n ? sourceLines[rawLine - 1]?.trim() ?? ''\n : '';\n\n const entry: TypeCheckDiagnostic = {\n code: code_,\n line,\n message,\n fix: generateFix(code_, message, sourceLine),\n };\n\n if (BLOCKING_CODES.has(code_)) {\n errors.push(entry);\n } else {\n warnings.push(entry);\n }\n }\n\n return { errors, warnings };\n}\n","// packages/ui-gen/src/check/run-tier0.ts\n//\n// Tier-0 deterministic CHECK orchestrator — the programmatic half of\n// the three-tier evaluation system:\n// Tier 0 — programmatic (deterministic, no LLM) ← this file\n// Tier 1 — LLM critical checks (functionality, crash)\n// Tier 2 — LLM quality checks (interactivity, accessibility, layout, etc.)\n//\n// Composes the deterministic helpers (`extract-wire-calls`,\n// `react-linter`, `contract-validation`, `type-checker`) from the\n// `@ggui-ai/ui-gen/check` subpath. The purely-typed evaluation surface\n// (`EvalTier` / `EvalOutcome` / `priorityForIssue` / …) is imported\n// directly from `@ggui-ai/ui-gen/evaluation` — its canonical home.\n\nimport ts from 'typescript';\nimport type { DataContract } from '@ggui-ai/protocol';\nimport { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';\nimport { consumedTokenManifest } from '@ggui-ai/design/themes';\nimport { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';\nimport { validateAllContracts, type ContractIssue } from './contract-validation.js';\nimport { typecheck } from './type-checker.js';\nimport { lintReactHooks, type ReactLintDiagnostic } from './react-linter.js';\nimport { isAllowedImport, describeAllowedImports } from '../validation/allowed-imports.js';\nimport {\n checkWireImports,\n checkWirePreservation,\n type WireKind,\n} from './extract-wire-calls.js';\nimport {\n priorityForIssue,\n type EvalCategory,\n type EvalIssue,\n type EvalResult,\n} from '../evaluation/types-public.js';\n\n/**\n * Strip line + block comments from source, leaving all other text in\n * place. Uses the TS scanner so a `//` or block-comment sequence inside\n * a string / template / regex literal is NOT mistaken for a comment.\n * Comment characters are replaced with spaces (newlines kept) so byte\n * offsets and line numbers are unchanged.\n *\n * Used by regex-based CHECK rules that must not be satisfied by\n * commented-out code (e.g. `gadget_preservation`).\n */\nfunction stripComments(code: string): string {\n const scanner = ts.createScanner(\n ts.ScriptTarget.Latest,\n /* skipTrivia */ false,\n ts.LanguageVariant.JSX,\n code,\n );\n let out = '';\n for (\n let token = scanner.scan();\n token !== ts.SyntaxKind.EndOfFileToken;\n token = scanner.scan()\n ) {\n const text = scanner.getTokenText();\n out +=\n token === ts.SyntaxKind.SingleLineCommentTrivia ||\n token === ts.SyntaxKind.MultiLineCommentTrivia\n ? text.replace(/[^\\n]/g, ' ')\n : text;\n }\n return out;\n}\n\n/**\n * AST-precise detector for the unambiguous nested-interactive\n * double-wire pattern. See the call-site comment in `runTier0Checks` +\n * the `double-wired-action:certain` block for the rationale.\n *\n * The detector accepts a JSX element as the outer host when:\n * - tagName ∈ {Card, Box, Stack, Row}\n * - has `as={Clickable | Pressable}` (`Hoverable` doesn't fire click)\n * - has `onClick` (or `onPress`) whose handler calls one of the\n * known `useAction` bindings.\n *\n * Then it walks descendants of that outer element and matches an inner\n * interactive primitive when:\n * - tagName ∈ {Button, Checkbox, Input, Toggle, Slider, RadioGroup,\n * Select, TextArea, Link}\n * - has any of {onClick, onChange, onPress, onSelect} whose handler\n * calls the SAME useAction binding as the outer.\n *\n * Patterns it INTENTIONALLY does not catch:\n * - helper-function indirection (`const fire = () => toggle({id})`\n * in both handlers) — the AST sees two distinct callees, not the\n * same binding. The runtime dedup in `useAction` still catches the\n * symptom; the static-time catch is a teaching aid, not a safety\n * net.\n * - cross-component prop drilling — same reason.\n * - imperative DOM addEventListener — out of scope for JSX walking.\n */\nfunction detectCertainDoubleWiredActions(\n sourceCode: string,\n actionBindings: readonly string[],\n /**\n * `constrained` keeps the design-package tag sets above. `free`\n * retargets the SAME invariant (\"one useAction binding → exactly one\n * gesture surface\") at raw elements: the outer host is ANY JSX element\n * whose onClick/onPress calls a binding (`<div onClick>`, `<li>`,\n * `<tr>`, `<button>`, `<a>`, `[role=button]`, a design primitive…),\n * and the inner match is ANY descendant whose handler attribute calls\n * the same binding. Tag names stop mattering because in free mode a\n * raw element is a first-class gesture surface.\n */\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): EvalIssue[] {\n const issues: EvalIssue[] = [];\n if (actionBindings.length === 0) return issues;\n const bindings = new Set(actionBindings);\n const anyElementIsInteractive = designMode === 'free';\n const TRAIT_HOST_TAGS = new Set(['Card', 'Box', 'Stack', 'Row']);\n const TRAITS_THAT_FIRE = new Set(['Clickable', 'Pressable']);\n const INTERACTIVE_DESCENDANTS = new Set([\n 'Button',\n 'Checkbox',\n 'Input',\n 'Toggle',\n 'Slider',\n 'RadioGroup',\n 'Select',\n 'TextArea',\n 'Link',\n ]);\n const HANDLER_ATTRS = ['onClick', 'onChange', 'onPress', 'onSelect'] as const;\n\n const sf = ts.createSourceFile(\n 'source.tsx',\n sourceCode,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n ts.ScriptKind.TSX,\n );\n\n function tagNameText(\n node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n ): string | undefined {\n return ts.isIdentifier(node.tagName) ? node.tagName.text : undefined;\n }\n\n function attrExpression(\n node: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attrName: string,\n ): ts.Expression | undefined {\n for (const attr of node.attributes.properties) {\n if (\n ts.isJsxAttribute(attr) &&\n ts.isIdentifier(attr.name) &&\n attr.name.text === attrName &&\n attr.initializer !== undefined &&\n ts.isJsxExpression(attr.initializer)\n ) {\n return attr.initializer.expression;\n }\n }\n return undefined;\n }\n\n function isTraitHostWithFiringAs(node: ts.JsxOpeningElement): boolean {\n const tag = tagNameText(node);\n if (tag === undefined) return false;\n // Free mode: every element with a handler is a gesture surface —\n // the callee check in `visit` decides whether it fires a binding.\n if (anyElementIsInteractive) return true;\n if (!TRAIT_HOST_TAGS.has(tag)) return false;\n const asExpr = attrExpression(node, 'as');\n return (\n asExpr !== undefined &&\n ts.isIdentifier(asExpr) &&\n TRAITS_THAT_FIRE.has(asExpr.text)\n );\n }\n\n // Best-effort callee extraction from event-handler expression shapes\n // the LLM actually emits. Forms covered:\n // {handler} → 'handler'\n // {() => callee(arg)} → 'callee'\n // {(e) => callee(e)} → 'callee'\n // {() => { callee(arg); }} → 'callee'\n // {function () { callee(arg); }} → 'callee'\n // Forms intentionally not covered: chained calls, conditional\n // expressions, calls inside if-branches. Those are rare in\n // LLM-generated JSX and would expand the FP surface.\n function extractCalleeName(\n expr: ts.Expression | undefined,\n ): string | undefined {\n if (expr === undefined) return undefined;\n if (ts.isIdentifier(expr)) return expr.text;\n if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {\n const body = expr.body;\n if (ts.isCallExpression(body) && ts.isIdentifier(body.expression)) {\n return body.expression.text;\n }\n if (ts.isBlock(body)) {\n for (const stmt of body.statements) {\n if (\n ts.isExpressionStatement(stmt) &&\n ts.isCallExpression(stmt.expression) &&\n ts.isIdentifier(stmt.expression.expression)\n ) {\n return stmt.expression.expression.text;\n }\n }\n }\n }\n return undefined;\n }\n\n function findMatchingInteractiveDescendant(\n outer: ts.JsxElement,\n expectedBinding: string,\n ): { tag: string; line: number } | undefined {\n let found: { tag: string; line: number } | undefined;\n function walk(node: ts.Node): void {\n if (found !== undefined) return;\n const opening = ts.isJsxElement(node)\n ? node.openingElement\n : ts.isJsxSelfClosingElement(node)\n ? node\n : undefined;\n if (opening !== undefined) {\n const tag = tagNameText(opening);\n if (\n tag !== undefined &&\n (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))\n ) {\n for (const attrName of HANDLER_ATTRS) {\n const callee = extractCalleeName(attrExpression(opening, attrName));\n if (callee === expectedBinding) {\n found = {\n tag,\n line:\n sf.getLineAndCharacterOfPosition(opening.getStart()).line + 1,\n };\n return;\n }\n }\n }\n }\n ts.forEachChild(node, walk);\n }\n // Walk only the outer element's children, not the outer itself.\n ts.forEachChild(outer, walk);\n return found;\n }\n\n function visit(node: ts.Node): void {\n if (ts.isJsxElement(node) && isTraitHostWithFiringAs(node.openingElement)) {\n const outerCallee =\n extractCalleeName(attrExpression(node.openingElement, 'onClick')) ??\n extractCalleeName(attrExpression(node.openingElement, 'onPress'));\n if (outerCallee !== undefined && bindings.has(outerCallee)) {\n const match = findMatchingInteractiveDescendant(node, outerCallee);\n if (match !== undefined) {\n const outerTag = tagNameText(node.openingElement);\n const outerLine =\n sf.getLineAndCharacterOfPosition(node.openingElement.getStart())\n .line + 1;\n const outerLabel = anyElementIsInteractive\n ? `<${outerTag}>`\n : `<${outerTag} as={...}>`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'interactivity',\n subcategory: 'double-wired-action:certain',\n severity: 'critical',\n description:\n `Nested-interactive double-wire: outer ${outerLabel} at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. ` +\n `One user click on the inner control fires its handler AND bubbles to the outer handler — '${outerCallee}' dispatches TWICE, the action runs back-to-back, and a toggle-style action silently reverts the user's change.`,\n fix: anyElementIsInteractive\n ? `Pick ONE gesture surface for '${outerCallee}': either remove the onClick from the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR drop the inner <${match.tag}>'s handler and let the outer <${outerTag}> own it. Never wire both to the same useAction binding — stopPropagation is not a fix.`\n : `Pick ONE surface for the gesture: either drop \\`as={...}\\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,\n line: outerLine,\n });\n }\n }\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sf);\n return issues;\n}\n\n/**\n * The consumed-token manifest as a closed set (ggui#613 residual 3 —\n * the generation-time closure leg of the #598 record). Every theme\n * MUST define exactly these names (the coverage gate); a generated\n * component may therefore consume ONLY these names. Anything else\n * is a reference no theme is obligated to define, and under the s4\n * fallback ban it paints nothing.\n */\nconst MANIFEST_TOKENS: ReadonlySet<string> = new Set(consumedTokenManifest);\nconst TOKEN_REF_RE = /var\\(\\s*(--ggui-[a-zA-Z0-9-]+)/g;\n\n/** Map wire kind to the matching `@ggui-ai/wire` hook name. */\nconst HOOK_NAME_FOR: Readonly<Record<WireKind, string>> = {\n action: 'useAction',\n stream: 'useStream',\n context: 'useGguiContext',\n};\n\n/**\n * Map wire kind to the contract field the agent must declare on the\n * `contract` input to `ggui_render` (post-Phase-2 flat handshake input)\n * to make the `useX('<name>')` call legal.\n *\n * Drives the remediation message on the `wire_undeclared` fail.\n * Telling the LLM \"declare it on contextSpec.<slotName>\" with the\n * JSON-Schema starter is concretely actionable; \"declare a contract\"\n * alone is not.\n */\nconst CONTRACT_FIELD_FOR: Readonly<Record<WireKind, string>> = {\n action: 'actionSpec',\n stream: 'streamSpec',\n context: 'contextSpec',\n};\n\n// =============================================================================\n// Tier 0: Programmatic Hard Checks\n// =============================================================================\n\n/**\n * Whether comment-free `source` imports the named gadget `exportName`\n * from `pkg` — `import { …, exportName, … } from '<pkg>'` (the source\n * name, left of any `as`). Kind-agnostic — hook and component exports\n * are both direct-imported the same way. The `gadget_preservation`\n * anchor: generated code direct-imports each gadget export, and the\n * import is what the iframe rewriter binds to the per-package runtime\n * shim.\n */\nfunction isGadgetExportImported(\n source: string,\n pkg: string,\n exportName: string,\n): boolean {\n const esc = pkg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const re = new RegExp(\n `import\\\\s*(?:[A-Za-z_$][\\\\w$]*\\\\s*,\\\\s*)?\\\\{([^}]*)\\\\}\\\\s*from\\\\s*['\"]${esc}['\"]`,\n 'g',\n );\n let m: RegExpExecArray | null;\n while ((m = re.exec(source)) !== null) {\n for (const raw of m[1].split(',')) {\n if (raw.trim().split(/\\s+as\\s+/)[0]?.trim() === exportName) return true;\n }\n }\n return false;\n}\n\n/**\n * AST-precise detector for the compiles-but-renders-nothing class: the\n * default-exported component's body never returns a tree.\n *\n * The boilerplate HANDS the model the wrapper — `base.tsx.tmpl` and\n * `base-free.tsx.tmpl` both emit `return (` … `);` around `{{LAYOUT}}`.\n * A model rewriting the root block can delete those two lines with it,\n * leaving the JSX as a bare expression statement. That is legal TSX: it\n * parses, esbuild compiles it, `typecheck()` passes it, and the\n * component renders a blank frame. Seen once in twenty takes of the\n * ggui#1113 A/B (`xs-chat-card`, run 2 — the judge spent a screenshot\n * and an LLM call to report \"completely blank\").\n *\n * `compile/default-export` already guards the scaffold's\n * `export default function` line and `types/props-interface` its\n * `interface Props` line. The `return (` was the third scaffold line\n * with nothing watching it.\n *\n * Conservative by construction — it reports ONLY when it positively\n * identified a block-bodied default-export component:\n * - a concise arrow body (`export default () => <X/>`) returns by\n * construction → silent\n * - `export default memo(Component)` or any other expression it\n * cannot resolve to a function → silent\n * - a `return` inside a nested helper is not the component's own (the\n * walk stops at every nested function boundary), but ONE own-level\n * `return <value>` anywhere — an early `return null` included —\n * satisfies it.\n *\n * Returns `null` when the component returns (or could not be\n * identified), else the 1-based line of the first bare JSX expression\n * statement in its body — the line the fix belongs on — or `undefined`\n * when the body has no JSX statement to point at either.\n */\nfunction detectComponentNeverReturns(\n sourceCode: string,\n): { jsxStatementLine: number | undefined } | null {\n const sf = ts.createSourceFile(\n 'source.tsx',\n sourceCode,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n ts.ScriptKind.TSX,\n );\n\n const isFunctionLike = (n: ts.Node): boolean =>\n ts.isFunctionDeclaration(n) ||\n ts.isFunctionExpression(n) ||\n ts.isArrowFunction(n) ||\n ts.isMethodDeclaration(n) ||\n ts.isClassDeclaration(n) ||\n ts.isClassExpression(n);\n\n /** The top-level declaration `name` binds to, when it is a function. */\n function resolveTopLevel(\n name: string,\n ): ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | undefined {\n for (const st of sf.statements) {\n if (ts.isFunctionDeclaration(st) && st.name?.text === name) return st;\n if (ts.isVariableStatement(st)) {\n for (const decl of st.declarationList.declarations) {\n if (\n ts.isIdentifier(decl.name) &&\n decl.name.text === name &&\n decl.initializer !== undefined &&\n (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))\n ) {\n return decl.initializer;\n }\n }\n }\n }\n return undefined;\n }\n\n let component:\n | ts.FunctionDeclaration\n | ts.FunctionExpression\n | ts.ArrowFunction\n | undefined;\n for (const st of sf.statements) {\n if (\n ts.isFunctionDeclaration(st) &&\n st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)\n ) {\n component = st;\n break;\n }\n if (ts.isExportAssignment(st) && st.isExportEquals !== true) {\n const expr = st.expression;\n if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) component = expr;\n else if (ts.isIdentifier(expr)) component = resolveTopLevel(expr.text);\n break;\n }\n }\n // No default export at all, or one this detector cannot resolve to a\n // function — `compile/default-export` owns the first case and the\n // second is not ours to guess at.\n if (component === undefined) return null;\n\n const body = component.body;\n // A concise arrow body IS the returned value.\n if (body === undefined || !ts.isBlock(body)) return null;\n\n let returnsValue = false;\n let jsxStatementLine: number | undefined;\n const visit = (node: ts.Node): void => {\n if (returnsValue) return;\n if (ts.isReturnStatement(node)) {\n if (node.expression !== undefined) returnsValue = true;\n return;\n }\n if (\n jsxStatementLine === undefined &&\n ts.isExpressionStatement(node) &&\n (ts.isJsxElement(node.expression) ||\n ts.isJsxFragment(node.expression) ||\n ts.isJsxSelfClosingElement(node.expression))\n ) {\n jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;\n }\n node.forEachChild((child) => {\n // A helper component's own `return` is not this component's.\n if (!isFunctionLike(child)) visit(child);\n });\n };\n for (const st of body.statements) if (!isFunctionLike(st)) visit(st);\n\n return returnsValue ? null : { jsxStatementLine };\n}\n\n/**\n * Run all tier 0 (deterministic, no-LLM) checks against component source.\n *\n * Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].\n * Also runs contract validation when contract are provided.\n */\nexport async function runTier0Checks(\n sourceCode: string,\n contract?: DataContract,\n buildErrors?: string[],\n /**\n * A `package -> .d.ts content` map for third-party gadget wrappers.\n * Threaded verbatim into `typecheck()` so the in-memory TS sandbox\n * overlays each wrapper `.d.ts` at `node_modules/<package>/index.d.ts`\n * — a generated direct import `import { useX } from '<package>'`\n * resolves against the real declaration (strict option/return\n * narrowing). Standard-library-only callers omit it.\n */\n gadgetTypes?: Readonly<Record<string, string>>,\n /**\n * Which triad produced the source. `constrained` (default) runs every\n * leg exactly as before. `free` gates OFF the design-VOCABULARY legs —\n * `raw-spacing`, `raw-pixels`, `numeric-spacing-prop`, the\n * `stack-row-*` prop checks and `clickable-wrapper` — because raw\n * elements, inline CSS and literal spacing are first-class there. It\n * KEEPS every contract leg: `hex-color`, `hardcoded-color-fn`,\n * `named-color`, `token-fallback`, `off-manifest-token`,\n * `asset-color-pair` (color stays on the closed token manifest),\n * imports, security, Props / default-export, wire_* and gadget checks,\n * and the double-wired-action detector (retargeted at raw elements).\n */\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): Promise<EvalIssue[]> {\n const issues: EvalIssue[] = [];\n const lines = sourceCode.split('\\n');\n // Design-vocabulary legs fire only when the prompt taught the\n // vocabulary — see `designMode` above.\n const enforceDesignVocabulary = designMode === 'constrained';\n\n // ── Compile check ─────────────────────────────────────────\n //\n // Fail only when the caller ATTEMPTED compile and captured errors.\n // Absent `buildErrors` means the caller hasn't run esbuild at all\n // (e.g. OSS `createUiGenerator` which is sometimes wrapped by\n // `withBrowserCompile` and sometimes not); degrade gracefully —\n // downstream checks (security, forbidden imports, wire-import\n // presence, react-linter, Props-interface, default-export) still\n // fire on sourceCode alone.\n if (buildErrors && buildErrors.length > 0) {\n const errorDetail = buildErrors\n .map((e) => {\n // Extract key info from esbuild error (e.g., line number, error type)\n const match = e.match(/<stdin>:(\\d+):\\d+: ERROR: (.+)/);\n return match ? `Line ${match[1]}: ${match[2]}` : e.slice(0, 200);\n })\n .join('\\n');\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'build-error',\n severity: 'critical',\n description: `Component failed to compile:\\n${errorDetail}`,\n fix: 'Fix the JSX/TypeScript syntax errors listed above',\n });\n }\n\n // ── assetColor allow-list ─────────────────────────────────\n //\n // T-4: Box gains a typed brand-color escape pair —\n // <Box assetColor=\"#635BFF\" assetSemantic=\"stripe-brand-purple\">\n // The static hex / rgba checks below allow a hex/rgba/hsl whose\n // `JSXAttribute` parent is `assetColor`, but only when a sibling\n // `assetSemantic` attribute carries a non-empty string value on the\n // same JSX opening tag. Both pieces are required: an `assetColor`\n // alone, or `assetSemantic=\"\"`, fails.\n //\n // Implementation: collect the (1-based) line numbers of every line\n // whose JSX opening tag legitimately satisfies this pair. The\n // hex/colorFn/named-color checks below skip emitting a failure\n // when the matching line is in this allow-list.\n const assetEscapedLines = new Set<number>();\n // Match each JSX opening tag (allowing multiline attribute lists).\n // The regex captures the tag's attribute span; we then check it for\n // the required `assetColor` + non-empty `assetSemantic` pair, and\n // record every source line that the tag spans.\n const jsxOpenRegex = /<([A-Z][A-Za-z0-9]*)\\b([^>]*)>/gs;\n for (const match of sourceCode.matchAll(jsxOpenRegex)) {\n const tagAttrs = match[2];\n const assetColorAttr = tagAttrs.match(/\\bassetColor\\s*=\\s*(?:[\"']([^\"']*)[\"']|\\{[^}]*\\})/);\n if (!assetColorAttr) continue;\n const assetSemanticAttr = tagAttrs.match(/\\bassetSemantic\\s*=\\s*[\"']([^\"']*)[\"']/);\n const startLine = sourceCode.slice(0, match.index ?? 0).split('\\n').length;\n const tagLineCount = match[0].split('\\n').length;\n // Both attributes must be present, AND assetSemantic must be a\n // non-empty string literal. A `{expr}` value for assetSemantic is\n // treated as not-statically-verifiable; the escape is opt-in\n // documentation, not a runtime expression. Failing pairs emit a\n // dedicated `tokens:asset-color-pair` failure AND do NOT enter\n // the allow-list — so the embedded hex still fires its own\n // `tokens:hex-color` failure (two issues, one per axis).\n if (!assetSemanticAttr || assetSemanticAttr[1].length === 0) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'asset-color-pair',\n severity: 'critical',\n description:\n 'Box `assetColor` set without a non-empty `assetSemantic` — the typed brand-color escape requires both. ' +\n '`assetSemantic` is a human-readable label that documents why this color bypasses the theme.',\n fix:\n 'Pair the `assetColor` with a non-empty `assetSemantic` literal, e.g. ' +\n '`<Box assetColor=\"#635BFF\" assetSemantic=\"stripe-brand-purple\">`. Reach for `surface=\"...\"` first ' +\n 'whenever the color SHOULD track the operator\\'s theme.',\n line: startLine,\n });\n continue;\n }\n for (let l = 0; l < tagLineCount; l++) {\n assetEscapedLines.add(startLine + l);\n }\n }\n\n // Gadget packages the contract declares are import-\n // allowlisted. Generated code direct-imports gadget exports from\n // their package. `clientCapabilities.gadgets` is\n // package-keyed, so the map's own keys ARE the permitted import\n // sources, alongside the STDLIB `@ggui-ai/gadgets`.\n const allowedGadgetPackages = new Set<string>(\n Object.keys(contract?.clientCapabilities?.gadgets ?? {}),\n );\n\n // ── Line-by-line checks ───────────────────────────────────\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNum = i + 1;\n\n // Security: eval()\n if (/\\beval\\s*\\(/.test(line)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'security',\n subcategory: 'eval',\n severity: 'critical',\n description: 'eval() is forbidden',\n fix: 'Remove eval() call entirely',\n line: lineNum,\n });\n }\n\n // Security: fetch()\n if (/\\bfetch\\s*\\(/.test(line)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'security',\n subcategory: 'fetch',\n severity: 'critical',\n description: 'fetch() is forbidden — use props for data',\n fix: 'Remove fetch() and pass data via props',\n line: lineNum,\n });\n }\n\n // Forbidden imports (skip commented-out imports)\n const trimmedLine = line.trim();\n if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*')) continue;\n const importMatch = trimmedLine.match(/import\\s+.*from\\s+['\"]([^'\"]+)['\"]/);\n if (importMatch) {\n const pkg = importMatch[1];\n // Allowlist mirrors `validation/component-detailed.ts` ALLOWED_IMPORTS —\n // these two must stay in sync. Generated code direct-imports\n // gadget exports, so every gadget package the contract declares\n // (`clientCapabilities.gadgets[*].package`) is allowlisted alongside\n // the STDLIB `@ggui-ai/gadgets`. Rejecting a declared gadget package\n // would contradict the `gadget_preservation` check (which REQUIRES\n // the direct import).\n if (!isAllowedImport(pkg, allowedGadgetPackages)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: 'disallowed-package',\n severity: 'critical',\n description: `Import from \"${pkg}\" is not allowed`,\n fix: `Only import from: ${describeAllowedImports()}`,\n line: lineNum,\n });\n }\n }\n\n // Hardcoded hex colors (not inside CSS variable fallbacks).\n //\n // Empirical (warn → fail rationale): when the\n // operator's prompt evoked a strong aesthetic (\"garden / living\n // ecosystem\"), the LLM emitted hex literals (`#B8FF3A` lime,\n // `#0a0a0a` near-black) inline on `color`/`background` style\n // overrides — completely overriding the operator's selected theme.\n // The warn-level rule was advisory and got ignored. As a `fail`\n // it forces the LLM to remediate before passing self-check.\n //\n // Generation contract this enforces (per the contract bar): the\n // LLM describes structure + intent; the theme + primitive variants\n // describe visuals. Hardcoded hex is a contract violation. The\n // existing `var(--ggui-*, #fallback)` exception keeps legit token\n // patterns flowing untouched. Asset-fixed brand colors get a\n // typed escape on Box (`assetColor` + `assetSemantic`) — see\n // T-4 follow-up.\n // Literal fallback inside ANY token reference (ggui#598 slice 4\n // origin-kill, generalized by ggui#613 residual 4 from color to\n // every token): the runtime injects every token — colors, spacing,\n // typography, radius, shadows — so a literal fallback can only\n // ever paint default-brand pixels under a failed injection, which\n // must be visible (unset) rather than silently default. The old\n // hex-only exemption plus a remediation that suggested\n // `var(--x, ${hex})` actively taught the model to launder invented\n // values INTO fallbacks (adversarial-cycle find). A token-to-token\n // fallback (`var(--a, var(--b))`) is not a literal — it stays on\n // the manifest check alone.\n const literalFallbackMatch = line.match(\n /var\\(\\s*(--ggui-[a-zA-Z0-9-]+)\\s*,\\s*([^)]+)\\)/,\n );\n const literalFallback =\n literalFallbackMatch !== null &&\n !literalFallbackMatch[2]!.trim().startsWith('var(')\n ? { token: literalFallbackMatch[1]!, literal: literalFallbackMatch[2]!.trim() }\n : null;\n if (literalFallback !== null && !assetEscapedLines.has(lineNum)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'token-fallback',\n severity: 'critical',\n description: `Literal fallback \"${literalFallback.literal}\" inside a token reference — the runtime injects every token, and the fallback paints the wrong value exactly when the theme matters most.`,\n fix: `Drop the fallback: write var(${literalFallback.token}) bare. If the token might not exist, that is a vocabulary problem to fix by choosing a token from the design-system docs, never by a literal.`,\n line: lineNum,\n });\n }\n\n const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\\b/);\n if (hexMatch && !assetEscapedLines.has(lineNum)) {\n // A hex that lives inside a token fallback is already reported\n // above as `token-fallback` — don't double-report it as\n // `hex-color`.\n if (literalFallback !== null && literalFallback.literal.includes(hexMatch[0])) {\n // reported above\n } else if (!line.includes('// fallback')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'hex-color',\n severity: 'critical',\n description: `Hardcoded color \"${hexMatch[0]}\" breaks theme switching — use design tokens.`,\n fix: `Replace with a primitive variant (Button variant=\"primary\"|Badge variant=\"success\"|...) OR a bare token reference like var(--ggui-color-primary-500) — never a literal fallback. Hardcoded colors mean the operator's theme has no effect on this surface.`,\n line: lineNum,\n });\n }\n }\n\n // Hardcoded rgba()/hsl() color functions — same reasoning as above.\n // Treated as `fail` for symmetry — a hardcoded `rgba(184, 255, 58, 1)`\n // is functionally identical to `#B8FF3A` from a theme-override\n // standpoint.\n const colorFnMatch = line.match(/\\b(rgba?|hsla?)\\s*\\(/);\n if (\n colorFnMatch &&\n !line.includes('var(--ggui-') &&\n !line.includes('// fallback') &&\n !line.includes('$value') &&\n !assetEscapedLines.has(lineNum)\n ) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'hardcoded-color-fn',\n severity: 'critical',\n description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching — use design tokens.`,\n fix: 'Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.',\n line: lineNum,\n });\n }\n\n // Off-manifest token references (ggui#613 residual 3): a\n // `var(--ggui-*)` whose name is outside the consumed-token\n // manifest. The manifest is the closed vocabulary the prompt\n // teaches AND the set every theme must define — so a name outside\n // it is invented, and with literal fallbacks banned it renders\n // UNSET. Fail, like the other token-contract violations; one issue\n // per distinct name per line.\n const offManifest = new Set<string>();\n for (const m of line.matchAll(TOKEN_REF_RE)) {\n const name = m[1]!;\n if (!MANIFEST_TOKENS.has(name)) offManifest.add(name);\n }\n for (const name of offManifest) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'off-manifest-token',\n severity: 'critical',\n description: `Token \"${name}\" is not in the design system's token manifest — no theme defines it, so it renders unset.`,\n fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,\n line: lineNum,\n });\n }\n\n // Raw CSS length on a spacing / radius prop — bypasses the design\n // scale. Same reasoning as the hardcoded-color checks above: a\n // literal `gap=\"8px\"` defeats the theme's spacing scale exactly as\n // `#fff` defeats its colors. Only the STRING-literal form fails;\n // the numeric escape (`gap={12}`) is untouched, so a genuine\n // off-scale pixel value still has a path.\n const rawSpacingMatch = enforceDesignVocabulary\n ? line.match(\n /\\b(gap|padding|paddingX|paddingY|margin|radius)\\s*=\\s*[\"'][\\d.]+(?:px|rem|em)[\"']/,\n )\n : null;\n if (rawSpacingMatch) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'raw-spacing',\n severity: 'critical',\n description: `\\`${rawSpacingMatch[1]}\\` uses a raw CSS length — bypasses the design spacing/radius scale.`,\n fix: `Use a scale name: ${rawSpacingMatch[1]}=\"xs|sm|md|lg|xl\" (spacing also has none|2xl; radius none|sm|md|lg|xl). For an exact off-scale pixel value pass a number — ${rawSpacingMatch[1]}={12}.`,\n line: lineNum,\n });\n }\n\n // CSS named-color literals on color/background sinks. Closes the\n // residual escape after the hex/rgb checks: `style={{ color:\n // 'lime' }}` or `style={{ background: 'royalblue' }}`. The hex\n // check doesn't match these because they're keyword identifiers,\n // not `#`-prefixed hex. CSS-spec named colors are a finite set;\n // we match the obvious ones the LLM reaches for.\n //\n // Allowed keywords (no fail): `inherit`, `currentColor`,\n // `transparent`, `unset`, `initial`, `revert`, `none`. These have\n // legitimate semantic uses (inherit for nested rendering, none\n // for explicit empty fill).\n //\n // Out-of-scope by design: token-name strings like `color:\n // 'primary'` — those would render as invalid CSS and a separate\n // self-check (the existing prop-value validator) catches them.\n const namedColorMatch = line.match(\n /(?:^|[^a-zA-Z-])(?:color|background|backgroundColor|borderColor)\\s*:\\s*['\"]([a-z][a-zA-Z]+)['\"]/,\n );\n if (namedColorMatch) {\n const named = namedColorMatch[1].toLowerCase();\n const allowed = new Set([\n 'inherit', 'currentcolor', 'transparent',\n 'unset', 'initial', 'revert', 'none',\n ]);\n // Heuristic for the named-color lexicon. Covers every CSS-spec\n // color the LLM has empirically reached for; new keywords can\n // be added as they surface in benchmarks. Token-name strings\n // (`'primary'`) bypass this check because they're not in the\n // lexicon — the prop-value validator catches them as invalid\n // enum values.\n const namedCssColors = new Set([\n 'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink',\n 'cyan', 'magenta', 'lime', 'teal', 'navy', 'maroon', 'olive',\n 'aqua', 'fuchsia', 'silver', 'gray', 'grey', 'white', 'black',\n 'brown', 'gold', 'indigo', 'violet', 'crimson', 'tomato',\n 'coral', 'salmon', 'orchid', 'plum', 'tan', 'khaki', 'beige',\n 'ivory', 'lavender', 'mint', 'turquoise', 'azure',\n 'royalblue', 'darkblue', 'lightblue', 'skyblue', 'steelblue',\n 'darkred', 'lightgreen', 'darkgreen', 'forestgreen',\n 'darkorange', 'lightgray', 'lightgrey', 'darkgray', 'darkgrey',\n 'hotpink', 'deeppink', 'lightpink', 'lightyellow',\n ]);\n if (!allowed.has(named) && namedCssColors.has(named)) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'tokens',\n subcategory: 'named-color',\n severity: 'critical',\n description: `Hardcoded CSS named color \"${named}\" breaks theme switching — use design tokens.`,\n fix: `Replace with a typed slot (Text tone=\"muted\" / Box surface=\"accent\" / Badge variant=\"success\") OR a semantic token: var(--ggui-color-onSunken), var(--ggui-color-primary-500), etc. The keyword \"${named}\" maps to a fixed RGB; the operator's theme has no effect on it.`,\n line: lineNum,\n });\n }\n }\n\n // Raw pixel values in spacing CSS properties\n const pxMatch = enforceDesignVocabulary\n ? line.match(/(?:padding|margin|gap|borderRadius)\\s*:\\s*['\"]?\\d+px/)\n : null;\n if (pxMatch && !line.includes('var(--ggui-')) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'tokens',\n subcategory: 'raw-pixels',\n description: 'Raw pixel value in spacing — must use design tokens',\n fix: 'Replace with a bare var(--ggui-spacing-*) token — no literal fallback (the runtime injects every token) — or the scale name on the primitive prop (gap=\"md\", padding=\"lg\")',\n line: lineNum,\n });\n }\n\n // Numeric spacing props on components (e.g., padding={24}, gap={8})\n // These bypass the design system — should use var(--ggui-spacing-*)\n const numericSpacingMatch = enforceDesignVocabulary\n ? line.match(/\\b(padding|paddingX|paddingY|gap|margin)\\s*=\\s*\\{?\\s*(\\d+)\\s*\\}?/)\n : null;\n if (numericSpacingMatch && !line.includes('var(--ggui-')) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'tokens',\n subcategory: 'numeric-spacing-prop',\n description: `Numeric spacing prop ${numericSpacingMatch[1]}={${numericSpacingMatch[2]}} — use design tokens instead`,\n fix: `Replace with ${numericSpacingMatch[1]}=\"var(--ggui-spacing-*)\"`,\n line: lineNum,\n });\n }\n }\n\n // ── Clickable trait check — onClick on a structural primitive needs as={Clickable} ──\n // Only the four trait-host primitives (typed `WithTrait<…>`) accept\n // `as={Clickable}`. Text/Heading/Badge/Avatar/Image are NOT hosts, so an\n // onClick on them is a plain type error the type-checker catches — don't\n // steer those toward `as={Clickable}` (it won't compile on them).\n const clickablePattern = /<(?:Card|Box|Stack|Row)\\s[^>]*onClick/;\n for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {\n if (clickablePattern.test(lines[i]) && !lines[i].includes('as={Clickable}') && !lines[i].includes('as={Pressable}')) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: 'clickable-wrapper',\n description: `onClick on a structural primitive without as={Clickable} — the bare primitive has no click or keyboard handling`,\n fix: `Add as={Clickable} to the element, e.g., <Card as={Clickable} onClick={handler}>`,\n line: i + 1,\n });\n }\n }\n\n // ── Double-wired action check (warn — broad regex) ──────────\n // A useAction callback dispatched from 2+ call sites fires the action\n // more than once per gesture when an interactive element nests inside\n // another (the inner gesture bubbles to the outer handler). Captured\n // in scenario-07: a `Card as={Clickable} onClick` row containing a\n // `Checkbox onChange`, both calling the same action → double-toggle.\n //\n // Binding extraction. The constrained regex is the pre-`designMode`\n // one, kept verbatim: it matches `useAction(` only, so a binding\n // declared with the generic the boilerplate itself emits —\n // `useAction<ActionXPayload>('x')` — is NOT collected there (a\n // pre-existing gap, left in place because the constrained gate must\n // stay byte-identical for the A/B; collapse the two regexes once that\n // pin is lifted). `free` mode accepts the optional generic so the\n // one-gesture-surface invariant actually fires on boilerplate-shaped\n // hook lines.\n const bindingRe =\n designMode === 'free'\n ? /(?:const|let)\\s+(\\w+)\\s*=\\s*useAction\\s*(?:<[^>]*>)?\\s*\\(/g\n : /(?:const|let)\\s+(\\w+)\\s*=\\s*useAction\\s*\\(/g;\n const actionBindings = [...sourceCode.matchAll(bindingRe)]\n .map((m) => m[1])\n .filter((name): name is string => typeof name === 'string');\n for (const binding of actionBindings) {\n const callSites = (\n sourceCode.match(new RegExp(`\\\\b${binding}\\\\s*\\\\(`, 'g')) ?? []\n ).length;\n if (callSites >= 2) {\n // Kept at WARN — too many legit patterns (confirm/cancel, branched\n // dispatch, sibling buttons in a list) match this generic shape. The\n // structural correctness backstop lives at runtime in `useAction`'s\n // task-scoped dedup; this check is a quality nudge for the LLM, not a\n // load-bearing safety gate. See `docs/principles/no-silent-block.md`\n // — three-pattern FP test fails for this detector → severity = warn.\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'interactivity',\n subcategory: 'double-wired-action',\n description: `Action '${binding}' is dispatched from ${callSites} call sites — if an interactive element nests inside another, one gesture fires the action twice (the inner gesture bubbles to the outer handler).`,\n fix: `Wire each useAction callback to exactly ONE interactive surface; never nest interactive elements (e.g. a Checkbox inside a Card as={Clickable}).`,\n });\n }\n }\n\n // ── Double-wired action check (fail — narrow AST) ────────────\n // Precise detector for the unambiguous shape: an outer trait-host JSX\n // element (Card/Box/Stack/Row with `as={Clickable|Pressable}`) wired\n // via onClick/onPress to useAction binding X, with a descendant\n // interactive primitive (Checkbox/Button/Input/Toggle/Slider/\n // RadioGroup/Select/TextArea/Link) wired via on* to the SAME X.\n //\n // Near-zero FP: the only patterns matching `<Card as={Clickable}\n // onClick={X}> ... <Checkbox onChange={X}>` are bugs. Three-pattern\n // FP test passes → severity earns 'fail' per no-silent-block\n // principle Rule 1. The runtime dedup in @ggui-ai/wire's useAction\n // catches the SYMPTOM regardless of shape; this check catches the\n // CAUSE one turn earlier and prevents shipping the broken a11y nest.\n if (actionBindings.length > 0) {\n issues.push(\n ...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode),\n );\n }\n\n // ── Connection suppression (ggui#670 Phase 2) ────────────────\n // `useRender().isConnected` is a LIVE read of the relay latch, and\n // the ATTEMPT is the runtime's self-heal sensor (#440/#443\n // attempt-always): a dispatch fired while the latch stands is how the\n // runtime discovers the host can relay again. Binding `disabled` to\n // the connection state strips the handler and blinds that sensor —\n // the control stays dead after recovery. The runtime owns the dead\n // zone's presentation (ggui#670 Phase 3: standing notice, then a\n // per-gesture cue) — generated code never suppresses on it. Direct\n // binding only, and only when the source reads\n // `useRender` at all — a same-named `props.isConnected` (a device-\n // status card) is not the connection state. Whole-source scan so a\n // multi-line attribute is caught; the issue reports the attribute's\n // line. The lookbehind keeps `aria-disabled={…}` (the idiom) out.\n if (/\\buseRender\\s*\\(/.test(sourceCode)) {\n const suppressionRe = /(?<![\\w-])disabled\\s*=\\s*\\{([^}]*)\\}/gs;\n for (const m of sourceCode.matchAll(suppressionRe)) {\n const expr = m[1]!;\n if (!/\\bisConnected\\b/.test(expr)) continue;\n const attrLine = sourceCode.slice(0, m.index ?? 0).split('\\n').length;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'interactivity',\n subcategory: 'connection-suppression',\n severity: 'critical',\n description:\n `\\`disabled\\` is bound to the connection state (\\`${expr.trim().replace(/\\s+/g, ' ')}\\`) — that strips the click handler, and the click is how the runtime detects that the host can relay actions again. The control stays dead after recovery.`,\n fix:\n 'Leave the control enabled — never bind `disabled` to the connection state. The runtime presents the dead zone itself (a standing notice, then a per-gesture cue) and the attempt is how it detects recovery; a suppressed control stays dead after the host recovers.',\n line: attrLine,\n });\n }\n }\n\n // ── Optional props accessed without guard ──────────────────\n // Check if Props interface has optional fields that are accessed without ?. or ?? or &&\n const propsInterfaceMatch = sourceCode.match(/interface Props\\s*\\{([^}]+)\\}/);\n if (propsInterfaceMatch) {\n const propsBody = propsInterfaceMatch[1];\n const optionalProps = [...propsBody.matchAll(/(\\w+)\\?:/g)].map(m => m[1]);\n for (const prop of optionalProps) {\n // Check for unguarded access: props.field. (dot access) or props.field[ (bracket access)\n // without being preceded by props.field?. or props.field ?? or props.field &&\n const accessPattern = new RegExp(`props\\\\.${prop}[.\\\\[]`, 'g');\n const guardPattern = new RegExp(`props\\\\.${prop}(\\\\?[.[]|\\\\s*&&|\\\\s*\\\\?\\\\?)`, 'g');\n const accesses = (sourceCode.match(accessPattern) || []).length;\n const guards = (sourceCode.match(guardPattern) || []).length;\n if (accesses > 0 && guards === 0) {\n issues.push({\n tier: 0, result: 'warn', category: 'crash', subcategory: `optional-prop:${prop}`,\n description: `Optional prop 'props.${prop}' accessed without null guard — will crash if undefined`,\n fix: `Use props.${prop}?.field or props.${prop} ?? fallback or {props.${prop} && ...}`,\n });\n }\n }\n }\n\n // ── Common pitfalls (recurring LLM mistakes that cost a turn) ─\n // Scan whole source so multiline opening tags (attrs split across lines) are caught.\n // Each match's line number = the line where the tag opens.\n const ALIGN_VALUES = new Set(['start', 'center', 'end', 'stretch']);\n const JUSTIFY_VALUES = new Set(['start', 'center', 'end', 'between', 'around', 'evenly']);\n const tagRegex = /<(Stack|Row)\\b([^>]*)>/gs;\n for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {\n const tagName = tag[1];\n const attrs = tag[2];\n const tagLine = sourceCode.slice(0, tag.index).split('\\n').length;\n\n if (/\\bpadding=/.test(attrs)) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-padding',\n description: `<${tagName}> does not accept a 'padding' prop`,\n fix: `Wrap the children in <Box padding=\"...\"> or use the parent <Card padding=\"...\">`,\n line: tagLine,\n });\n }\n const alignAttr = attrs.match(/\\balign=[\"']([^\"']+)[\"']/);\n if (alignAttr && !ALIGN_VALUES.has(alignAttr[1])) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-align',\n description: `align=\"${alignAttr[1]}\" is invalid on <${tagName}>`,\n fix: `Use align=\"start\" | \"center\" | \"end\" | \"stretch\" (NOT flex-start/flex-end/space-between)`,\n line: tagLine,\n });\n }\n const justifyAttr = attrs.match(/\\bjustify=[\"']([^\"']+)[\"']/);\n if (justifyAttr && !JUSTIFY_VALUES.has(justifyAttr[1])) {\n issues.push({\n tier: 0, result: 'fail', category: 'types', subcategory: 'stack-row-justify',\n description: `justify=\"${justifyAttr[1]}\" is invalid on <${tagName}>`,\n fix: `Use justify=\"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\"`,\n line: tagLine,\n });\n }\n }\n\n // ── Missing Props interface ───────────────────────────────\n if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'types',\n subcategory: 'props-interface',\n severity: 'critical',\n description: 'No Props interface found — data is likely hardcoded',\n fix: 'Add interface Props { ... } with typed fields and default values in the function signature',\n });\n }\n\n // ── Missing default export ────────────────────────────────\n if (!sourceCode.includes('export default function')) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'default-export',\n severity: 'critical',\n description: 'Missing default export function',\n fix: 'Add export default function Component(props: Props) { ... }',\n });\n }\n\n // ── Component never returns its tree ──────────────────────\n // The scaffold hands the model `return (` … `);` around the layout.\n // A root-block rewrite can delete them with it, and bare JSX at\n // statement position compiles, type-checks and renders NOTHING.\n const neverReturns = detectComponentNeverReturns(sourceCode);\n if (neverReturns !== null) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'compile',\n subcategory: 'renders-nothing',\n severity: 'critical',\n description:\n neverReturns.jsxStatementLine === undefined\n ? 'The component never returns its tree — it renders a blank frame'\n : `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,\n fix: 'Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.',\n ...(neverReturns.jsxStatementLine === undefined ? {} : { line: neverReturns.jsxStatementLine }),\n });\n }\n\n // (Controller-View checks removed — generated components use a\n // single-function pattern.)\n\n // ── Contract validation ───────────────────────────────────\n if (contract) {\n try {\n const contractIssues: ContractIssue[] = validateAllContracts(sourceCode, contract);\n for (const ci of contractIssues) {\n issues.push({\n tier: 0,\n result: ci.severity === 'error' ? 'fail' as const : 'warn' as const,\n category: 'contract' as const,\n subcategory: ci.field,\n severity: ci.severity === 'error' ? 'critical' as const : 'major' as const,\n description: ci.message,\n fix: ci.fix,\n });\n }\n } catch {\n // Contract validation may fail on malformed code — non-blocking\n }\n\n // ── Wire hook usage — every contract hook must be used in the component body ──\n //\n // `actionSpec` / `streamSpec` are flat `Record<name, Entry>` maps.\n // See `@ggui-ai/protocol` DataContract.\n const fnBody = sourceCode.slice(sourceCode.indexOf('export default function'));\n if (contract.actionSpec) {\n for (const actionName of Object.keys(contract.actionSpec)) {\n if (!fnBody.includes(actionName)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `action:${actionName}`,\n description: `Action hook '${actionName}' from contract is declared but never used in the component`,\n fix: `Wire ${actionName}() to a Button onClick, form onSubmit, or other user interaction`,\n });\n }\n }\n }\n if (contract.streamSpec) {\n for (const channelName of Object.keys(contract.streamSpec)) {\n if (!fnBody.includes(channelName)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `stream:${channelName}`,\n description: `Stream hook '${channelName}' from contract is declared but never rendered in the component`,\n fix: `Render ${channelName}.latest data in the JSX (with null guard: ${channelName}.latest && ...)`,\n });\n }\n }\n }\n // agentCapabilities.tools is intentionally NOT checked here — it\n // is a catalog the AGENT invokes, not a component hook surface.\n // Cross-refs surface via actionSpec[*].nextStep and\n // streamSpec[*].source.tool, which the action/stream loops above\n // already cover.\n if (contract.clientCapabilities) {\n // Iterate the flattened `GadgetUse[]` — the export name\n // (`use.name`) is the identity the generated code references.\n for (const use of listContractGadgets(contract)) {\n if (!fnBody.includes(use.name)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `clientCapability:${use.name}`,\n description: `Client capability '${use.name}' from contract is declared but the export is never used`,\n fix: HOOK_NAME_RE.test(use.name)\n ? `Import \\`${use.name}\\` from \\`${use.package}\\` and call it inside the component — bind the return value and surface its \\`.value\\` / \\`.status\\` in JSX.`\n : `Import \\`${use.name}\\` from \\`${use.package}\\` and render it as a JSX element (\\`<${use.name} … />\\`).`,\n });\n }\n }\n }\n\n // ── Wire preservation ─────────────────────────────────────────────────\n // The substring checks above (`fnBody.includes(name)`) are permissive —\n // they pass if the name appears ANYWHERE in the function body, including\n // a comment or unrelated identifier. The AST-backed `checkWirePreservation`\n // is the strict seal: it verifies that the boilerplate-emitted\n // `useAction('<name>')` / `useStream('<name>')` / `useGguiContext('<name>')`\n // hook call ACTUALLY exists, keyed on the string-literal first argument.\n //\n // Catches the abandonment/deletion case: LLM's apply_changes drops the\n // hook call entirely, so the wire never fires at runtime. `pnpm\n // typecheck` can't catch this (a missing call is a missing side effect,\n // not a type error). Seal B fires as `fail` — deterministic, retriable\n // feedback.\n //\n // The runtime auto-commit path (`coding-agent/tools.ts::autoCommit`)\n // calls `runTier0Checks`, which is where this rule lives so it\n // fires on the live generation path.\n try {\n const report = checkWirePreservation(sourceCode, contract);\n for (const site of report.missing) {\n const hook = HOOK_NAME_FOR[site.kind];\n const fix =\n site.kind === 'context'\n ? `Restore \\`const [${site.name}, set${site.name.charAt(0).toUpperCase() + site.name.slice(1)}] = ${hook}('${site.name}')\\` ` +\n `at the top of the component body. The boilerplate auto-emits this destructure for every declared ` +\n `contextSpec slot — do not delete it.`\n : `Restore \\`const ${site.name} = ${hook}('${site.name}')\\` at the top of the ` +\n `component body and consume the returned binding (in JSX, a callback, or an effect).`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'contract',\n subcategory: `wire_preservation:${site.kind}:${site.name}`,\n severity: 'critical',\n description:\n `Contract declares ${site.kind} '${site.name}' but no ${hook}('${site.name}') ` +\n `call exists in the component. The boilerplate placed this hook for you — ` +\n `do not delete it.`,\n fix,\n });\n }\n } catch {\n // Malformed code surfaces as a primary error elsewhere — don't mask\n // it with a wire-preservation noise-violation here.\n }\n\n // ── Props field rendering — verify each prop is actually used in the component ──\n if (contract.propsSpec) {\n const propsProperties = contract.propsSpec.properties ?? {};\n for (const propName of Object.keys(propsProperties)) {\n // Check if prop is referenced in the function body (via props.fieldName)\n if (!fnBody.includes(`props.${propName}`) && !fnBody.includes(`{props.${propName}}`)) {\n issues.push({\n tier: 0, result: 'warn', category: 'contract', subcategory: `prop:${propName}`,\n description: `Props field '${propName}' from contract is never rendered — data is wasted`,\n fix: `Render props.${propName} in the JSX (e.g., <Text>{props.${propName}}</Text>)`,\n });\n }\n }\n }\n }\n\n // ── Wire_undeclared (UNCONDITIONAL) ──────────────────────────────────\n //\n // Every wire-call site in the emitted code MUST correspond to a\n // declared entry on the agent-authored `contract`, regardless of\n // whether the contract is partially or wholly absent. Empty contract\n // → ALL wire calls are \"extra\" → all fail with `wire_undeclared`.\n // Failure mode this guards against: agent describes contextSpec in\n // plain prompt text but doesn't author `contract`; the LLM emits\n // `useGguiContext('foo')` / `useAction('foo')` with no slot\n // registration; the iframe crashes at first paint with\n // `[ggui] useGguiContext('foo'): no Context registered`. The\n // remediation message names the exact contract field to declare on,\n // so the agent's next turn can fix structurally.\n try {\n const contractsForExtraCheck: DataContract = contract ?? {};\n const extras = checkWirePreservation(sourceCode, contractsForExtraCheck).extra;\n for (const site of extras) {\n const hook = HOOK_NAME_FOR[site.kind];\n const field = CONTRACT_FIELD_FOR[site.kind];\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'contract',\n subcategory: `wire_undeclared:${site.kind}:${site.name}`,\n severity: 'critical',\n description:\n `Component calls ${hook}('${site.name}') but the contract does not declare ` +\n `${site.kind} '${site.name}'. Every wire reference in the generated code MUST ` +\n `correspond to a declared entry on the agent-authored contract — the runtime ` +\n `mounts/registers wire surfaces from the contract, not from the code. ` +\n (contract === undefined\n ? 'No contract authored at all — describing the contract in the prompt text is NOT enough; ' +\n 'the agent MUST pass a structured `contract` field on the ggui_render call.'\n : ''),\n fix:\n `Either (a) declare \\`${field}.${site.name}\\` on the \\`contract\\` input so the ` +\n `runtime registers the surface for this UI, or (b) remove the ${hook}('${site.name}') ` +\n `call from the component if it isn't part of the intended wire surface.`,\n });\n }\n } catch {\n // Malformed code surfaces elsewhere; don't mask with noise.\n }\n\n // ── Wire-hook import presence ────────────────────────────────────────\n //\n // `rewriteImports` (packages/design/src/rendering/rewrite-imports.ts) only\n // substitutes `from '@ggui-ai/wire'` specifiers that already exist in the\n // compiled code; a hook call like `useAction('X')` without the paired\n // `import { useAction } from '@ggui-ai/wire'` line leaves no specifier\n // for the shim to attach, so the hook is undeclared at eval time and the\n // browser throws `ReferenceError: useAction is not defined`. This check\n // runs unconditionally (no contract needed) — for every wire hook the\n // component CALLS at least once, the matching named import must be\n // present at top level. Independent of `wire_preservation` which checks\n // that contract-declared hooks are called.\n try {\n const importReport = checkWireImports(sourceCode);\n for (const miss of importReport.missing) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `wire_import_missing:${miss.hook}`,\n severity: 'critical',\n description:\n `Component calls ${miss.hook}(...) but does not import it from ` +\n `'@ggui-ai/wire'. Without the import, rewriteImports has no ` +\n `specifier to attach the data-URL shim to, so the hook is ` +\n `undeclared at browser eval time and the component crashes on ` +\n `mount with \\`ReferenceError: ${miss.hook} is not defined\\`.`,\n fix:\n `Add \\`import { ${miss.hook} } from '@ggui-ai/wire';\\` at the ` +\n `top of the file alongside the other imports.`,\n });\n }\n } catch {\n // Malformed code surfaces as a primary error elsewhere — don't mask it.\n }\n\n // ── No-require pattern for @-scoped packages ─────────────────────────\n //\n // A repeating LLM failure mode: when the boilerplate emits a static\n // `import { useLeafletMap } from '@ggui-samples/gadget-leaflet'`, the\n // model sometimes removes the import and substitutes\n // `const { useLeafletMap } = require('@ggui-samples/gadget-leaflet')`\n // (or `require('@ggui-ai/gadgets')`) inline. That FAILS in a real\n // browser ESM iframe: `require` is not defined, and `rewriteImports`\n // never sees a static `from '<pkg>'` specifier to attach the data-URL\n // shim to.\n //\n // This check rejects any `require('@...')` call expression on an\n // @-scoped package. The fix is always the same — convert to a\n // top-level static `import { … } from '<pkg>'`. Targeted at @-scoped\n // packages because that's the LLM's typical hedge specifier shape\n // (CJS bare `require('react')` etc. don't appear in our generated\n // code path).\n {\n // Match `require('@scope/pkg')` and `require(\"@scope/pkg\")` with a\n // simple regex. The LLM only emits this pattern as call\n // expressions, so the false-positive rate is negligible in\n // practice; an AST walk would be more rigorous but unnecessary.\n const requireRe = /require\\s*\\(\\s*['\"](@[^'\"]+)['\"]\\s*\\)/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = requireRe.exec(sourceCode)) !== null) {\n const pkg = m[1];\n if (pkg === undefined || seen.has(pkg)) continue;\n seen.add(pkg);\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `require_disallowed:${pkg}`,\n severity: 'critical',\n description:\n `Component uses \\`require('${pkg}')\\` — CommonJS require is ` +\n `not available in the iframe's browser ESM runtime, and the ` +\n `import-rewriter only attaches data-URL shims to STATIC import ` +\n `specifiers. The component will fail to mount with ` +\n `\\`ReferenceError: require is not defined\\` at the first call.`,\n fix:\n `Replace with a top-level static \\`import { … } from '${pkg}'\\` ` +\n `at the top of the file. The boilerplate emits this import for ` +\n `you when the contract declares a matching ` +\n `\\`clientCapabilities.gadgets[*]\\` entry — restore the line ` +\n `instead of inlining a require() call.`,\n });\n }\n }\n\n // ── gadget import preservation ───────────────────────────────────────\n //\n // Gadgets are DIRECT-imported. The boilerplate emits, per\n // registered gadget package, one combined import:\n // `import { useLeafletMap } from '@scope/leaflet';`\n // The LLM (correctly) intuits the hook is runtime-resolved and tends\n // to delete the import — but the import IS the resolution anchor: the\n // iframe rewriter rewrites the package specifier to a per-package\n // shim, and without the import the hook is unbound (ReferenceError).\n //\n // This check verifies, for every contract-declared gadget export,\n // that it is still imported from its package. The export NAME\n // appearing alone is not enough — the `import { … }` statement must\n // carry it. Hooks AND components are both direct-imported and\n // both checked here; the fix hint adapts to the export kind (hooks\n // are CALLED, components are RENDERED as JSX).\n {\n // Scan comment-free source — a commented-out import must NOT\n // satisfy the preservation check.\n const gadgetScanSource = stripComments(sourceCode);\n // Iterate the flattened `GadgetUse[]`. Kind is discriminated\n // by the export-name grammar — `use`-prefixed → hook, else component.\n for (const use of contract ? listContractGadgets(contract) : []) {\n const exportName = use.name;\n if (isGadgetExportImported(gadgetScanSource, use.package, exportName)) {\n continue;\n }\n const isComponent = !HOOK_NAME_RE.test(exportName);\n const kind = isComponent ? 'component' : 'hook';\n // Local binding name for the hook worked example (mirrors the\n // boilerplate's derivation: strip `use`, lowercase first char).\n const bindingName =\n exportName.length > 3\n ? exportName.charAt(3).toLowerCase() + exportName.slice(4)\n : exportName;\n const fix = isComponent\n ? `The component \\`${exportName}\\` is REAL and CORRECT — do NOT remove it. ` +\n `Restore the gadget plumbing in 2 steps:\\n` +\n `(1) Keep \\`import { ${exportName} } from '${use.package}';\\` at the top of the file.\\n` +\n `(2) RENDER \\`<${exportName} … />\\` as a JSX element in the tree you return — pass its props from the contract. Do NOT call it like a hook.\\n` +\n `Import \\`${exportName}\\` ONLY from '${use.package}' — that is the package it is registered under. If you remove the import again, this check will fail again — \\`${exportName}\\` is not optional.`\n : `The hook \\`${exportName}\\` is REAL and CORRECT — do NOT remove it. ` +\n `Restore the gadget plumbing in 2 steps:\\n` +\n `(1) Keep \\`import { ${exportName} } from '${use.package}';\\` at the top of the file.\\n` +\n `(2) CALL \\`${exportName}(...)\\` inside the component body and render its return value. Example:\\n` +\n ` \\`const ${bindingName} = ${exportName}({ /* props from contract */ });\\`\\n` +\n ` \\`return <div>{/* render ${bindingName} */}</div>;\\`\\n` +\n `Import \\`${exportName}\\` ONLY from '${use.package}' — that is the package it is registered under. If you remove the import again, this check will fail again — \\`${exportName}\\` is not optional.`;\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'imports',\n subcategory: `gadget_preservation:${exportName}`,\n severity: 'critical',\n description:\n `Contract declares \\`clientCapabilities.gadgets['${use.package}']['${exportName}']\\` ` +\n `(a ${kind}) but the ` +\n `component does not import \\`${exportName}\\` from \\`${use.package}\\`. ` +\n `The boilerplate emits \\`import { ${exportName} } from '${use.package}';\\` ` +\n `— keep it. Without the import the iframe runtime cannot resolve the ` +\n `${kind} and the registered gadget is unreachable.`,\n fix,\n });\n }\n }\n\n // ── TypeScript type checking + React hooks linting ──────────\n const [typeResult, reactResult] = await Promise.all([\n typecheck(sourceCode, gadgetTypes).catch((err) => {\n console.warn('[runTier0Checks] TypeChecker failed:', err instanceof Error ? err.message : String(err));\n return null;\n }),\n lintReactHooks(sourceCode).catch((err) => {\n console.warn('[runTier0Checks] React linter failed:', err instanceof Error ? err.message : String(err));\n return [] as ReactLintDiagnostic[];\n }),\n ]);\n\n // TS type errors → fail, warnings → warn\n if (typeResult) {\n for (const error of typeResult.errors) {\n issues.push({\n tier: 0,\n result: 'fail',\n category: 'types',\n subcategory: `ts${error.code}`,\n description: error.message,\n fix: error.fix,\n line: error.line,\n });\n }\n for (const warning of typeResult.warnings) {\n issues.push({\n tier: 0,\n result: 'warn',\n category: 'types',\n subcategory: `ts${warning.code}`,\n description: warning.message,\n fix: warning.fix,\n line: warning.line,\n });\n }\n }\n\n // React hooks violations — errors → fail, warnings → warn\n for (const diag of reactResult) {\n issues.push({\n tier: 0,\n result: diag.severity === 'error' ? 'fail' : 'warn',\n category: 'types',\n subcategory: diag.rule,\n description: diag.message,\n fix: diag.fix,\n line: diag.line,\n });\n }\n\n // Populate priority on every tier-0 issue. No current code path reads\n // `.priority` — it is reserved for future eval-policy consumption.\n for (const issue of issues) {\n if (!issue.priority) issue.priority = priorityForIssue(issue.category);\n }\n\n return issues;\n}\n\n/**\n * Run tier 0 checks and return a full EvalResult with pass list.\n */\nexport async function runTier0(\n sourceCode: string,\n contract?: DataContract,\n designMode: DesignMode = DEFAULT_DESIGN_MODE,\n): Promise<EvalResult> {\n const issues = await runTier0Checks(sourceCode, contract, undefined, undefined, designMode);\n const pass: string[] = [];\n\n // Build pass list from categories that had no issues\n const failedCategories = new Set(issues.map(i => i.category));\n const tier0Categories: EvalCategory[] = ['compile', 'security', 'contract', 'types', 'imports', 'tokens'];\n\n for (const cat of tier0Categories) {\n if (!failedCategories.has(cat)) {\n pass.push(cat);\n }\n }\n\n return { issues, pass };\n}\n\n// Tier-0 deterministic checks are run above. Axis-gated mode checks are\n// dispatched from `evaluation/axis-checks/` — see `runAxisChecks`.\n","/**\n * Contract + rendering-context prompt rendering.\n *\n * Surfaces every contract dimension explicitly to the LLM. A compact\n * bullet list of action / stream names is not enough — without the\n * Props shape, generation fails tier-0 on most cells with \"Props\n * interface missing required field '<name>'\".\n *\n * Renders:\n *\n * - Props contract — required + optional field lists + a real\n * TypeScript interface (compiled via `propsSpecToTypeScript`)\n * - Action contract — id / label / example / nextStep hint\n * - Stream contract — channel descriptions + payload schemas + source\n * - AgentTools — catalog of tools the contract references (via\n * `actionSpec[*].nextStep` and `streamSpec[*].source.tool`)\n * - ClientCapabilities — browser-capability gadgets the UI mounts:\n * hooks it calls (e.g. `useGeolocation` from `@ggui-ai/gadgets`)\n * and components it renders (e.g. `<LeafletMap />`)\n * - Required UI Surfaces — derived list of \"every contract surface\n * must have visible UI\" so the LLM doesn't drop required props or\n * skip rendering data sources\n *\n * Rendering context (device + shell + viewport) is layered alongside —\n * shells (`chat` / `fullscreen` / `partial`) require very different\n * sizing strategies and including the hint inline cuts a class of\n * \"designed for fullscreen, rendered in a chat bubble\" bugs.\n */\nimport { STYLING_PROFILE_PRECEDENCE_NOTE } from './boilerplate/styling-profile.js';\nimport type { RenderingContext as ProtocolRenderingContext } from '@ggui-ai/protocol';\nimport type {\n ActionEntry,\n BlueprintVariance,\n GadgetDescriptor,\n DataContract,\n StreamChannelEntry,\n} from '@ggui-ai/protocol';\nimport { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';\nimport { propsSpecToTypeScript } from './check/index.js';\nimport {\n canvasDisplayModeForShell,\n canvasForViewportWidth,\n type CanvasClass,\n} from './design-mode.js';\n\n/**\n * Rendering context — how and where the component will be displayed.\n * Affects layout strategy, sizing, and interaction patterns. The ONE\n * vocabulary is `@ggui-ai/protocol`'s `renderingContextSchema` (ggui#1000);\n * this is its type, derived — never re-declared here.\n */\nexport type RenderingContext = ProtocolRenderingContext;\n\nconst SHELL_HINTS: Record<string, string> = {\n chat: `**Chat Shell — Inline card in scrolling conversation**\n- Container: width varies (60-80% of viewport), height: auto (natural content height)\n- Chrome: parent provides card border + shadow + rounded corners — do NOT add your own box-shadow or outer border-radius\n- Sizing: target 300-600px natural height. Do NOT use min-height: 100vh. No full-viewport designs.\n- Padding: use var(--ggui-spacing-2) for inner elements, var(--ggui-spacing-4) for sections. Tight.\n- Scrolling: parent scrolls (chat feed) — component should NOT have internal scroll\n- Layout: single column, compact. No sidebar. Stack everything vertically.\n- Width: the root is width: 100% (fills the bubble) — never cap the ROOT. A prose column inside it carries the reading cap (<Container maxWidth=\"sm\">), which is inert at bubble width.`,\n\n fullscreen: `**Fullscreen Shell — Takes over entire viewport**\n- Container: the whole viewport, edge to edge — the frame stretches your root to its full height (you never set a viewport height yourself)\n- Chrome: NONE — your component IS the entire UI. You own all visual chrome.\n- Sizing: size to content; to fill a parent that has a height use height: 100% or flex: 1. NEVER min-height: 100vh / 100dvh — the frame already stretches the root, and a viewport height only fights it (tier-0 fail).\n- Padding: component owns ALL padding. Use var(--ggui-spacing-6) or larger for breathing room.\n- Scrolling: component manages its own scroll if content exceeds viewport (overflow-y: auto)\n- Layout: can use multi-column, sidebars, headers/footers. Full creative control.\n- Navigation: swipe left/right between pages is handled by the shell — don't implement it.\n- DO NOT add outer margins or max-width containers. Fill the space.`,\n\n partial: `**Partial Shell — Embedded panel in a larger page**\n- Container: width constrained by parent (could be 400px sidebar or 800px main area)\n- Chrome: parent may or may not have border — add your own Card/shadow if needed\n- Sizing: width: 100% to fill parent. Use max-width if you want to center content.\n- Padding: use standard spacing (var(--ggui-spacing-4) to var(--ggui-spacing-6))\n- Scrolling: can scroll internally (overflow-y: auto) — parent does NOT scroll for you\n- Layout: responsive within the container. Flex-wrap for variable parent widths.\n- Context: other UI around you (nav, sidebar) — don't duplicate page-level chrome.`,\n};\n\nconst DEVICE_HINTS: Record<string, string> = {\n mobile: `**Mobile:** Single column. Touch targets 44px+. Compact padding. Stack all sections vertically.`,\n tablet: `**Tablet:** 1-2 columns adaptive. Medium touch targets. Side-by-side where space allows.`,\n desktop: `**Desktop:** Multi-column. Hover states. Dense layouts. Keyboard shortcuts. Pointer interactions.`,\n spatial: `**Spatial:** Fixed-size floating panel (~600x400). High contrast. Large text. No hover (gaze/hand input).`,\n};\n\n/**\n * Map a host rendering context onto the canvas class the `free` prompt\n * states and the visual evaluator judges at. The class is derived from\n * DISPLAY MODE + width: a `chat` shell is always the inline card\n * whatever the host viewport is (the bubble, not the window, bounds the\n * component); a known viewport width refines the fullscreen / partial\n * shells; otherwise the device category decides.\n */\nexport function canvasForRenderingContext(ctx: RenderingContext): CanvasClass {\n const mode = canvasDisplayModeForShell(ctx.shell);\n if (mode === 'inline') return 'xs-chat-card';\n if (ctx.viewport) return canvasForViewportWidth(mode, ctx.viewport.width);\n switch (ctx.device) {\n case 'mobile':\n return 'mobile-fullscreen-small';\n case 'tablet':\n case 'spatial':\n return 'md';\n case 'desktop':\n return ctx.shell === 'partial' ? 'md' : 'lg';\n }\n}\n\n/** Build rendering-context block to inject into user prompt. */\nexport function buildRenderingContext(ctx: RenderingContext): string {\n const parts = [\n '## Rendering Context',\n `- Device: ${ctx.device}`,\n ctx.viewport ? `- Viewport: ${ctx.viewport.width}×${ctx.viewport.height}px` : '',\n `- Shell: ${ctx.shell}`,\n '',\n DEVICE_HINTS[ctx.device] || '',\n SHELL_HINTS[ctx.shell] || '',\n ].filter(Boolean);\n return parts.join('\\n');\n}\n\n/** Append rendering context to user prompt if present. */\nexport function injectRenderingContext(\n userPrompt: string,\n rendering?: RenderingContext,\n): string {\n if (!rendering) return userPrompt;\n return userPrompt + '\\n\\n' + buildRenderingContext(rendering);\n}\n\n/**\n * Build the contract-context block. Surfaces every dimension of the\n * contract so the LLM has a complete spec rather than the user-prompt\n * narrative alone — the eval scores against the contract regardless of\n * what prose the prompt carries, so contract-first rendering closes\n * the prompt-vs-contract drift class.\n */\nexport function buildContractsContext(\n contract: DataContract,\n /**\n * Operator-registered gadget catalog. When a\n * `clientCapabilities.gadgets[*]` ref is THIN (just `{hook}` —\n * no per-binding `package`), look the hook up here to surface the\n * correct package in user-prompt lines like `via useLeafletMap from\n * @ggui-samples/gadget-leaflet`. Mirrors the same plumb in\n * `harness/prompts.ts:buildContractsContext` — both surfaces are\n * separate copies used by different call sites (this one is for\n * `createUiGenerator`, the other for the benchmark / dispatch path).\n */\n appGadgets?: readonly GadgetDescriptor[],\n): string {\n const parts: string[] = [];\n\n // The LLM direct-imports each hook from its own package\n // (resolved from the per-entry `package` field). The parameter stays\n // on the signature so the TypeScript sandbox augmentation layer can\n // still consume it.\n void appGadgets;\n\n // `intent` is not a contract field; `userPrompt` already carries\n // the agent's narrative as the prompt body, so the contract-context\n // block focuses on the structural shape (props / actions / streams /\n // contexts). There is no categorical `interaction` mode label: the\n // four-spec surface (props / actions / streams / contexts) describes\n // the wire exhaustively — actions drive turns, contexts observe\n // state, and there is no third category.\n\n if (contract.propsSpec) {\n const propsSpec = contract.propsSpec;\n const requiredFields = Object.entries(propsSpec.properties || {})\n .filter(([, e]) => (e as { required?: boolean }).required)\n .map(([k]) => k);\n const optionalFields = Object.entries(propsSpec.properties || {})\n .filter(([, e]) => !(e as { required?: boolean }).required)\n .map(([k]) => k);\n\n const tsInterface = propsSpecToTypeScript(propsSpec);\n const fieldInfo: string[] = [];\n if (requiredFields.length > 0) fieldInfo.push(`Required: ${requiredFields.join(', ')}`);\n if (optionalFields.length > 0) fieldInfo.push(`Optional: ${optionalFields.join(', ')}`);\n\n parts.push(\n `## Props Contract (MUST use these exact prop names and types)\\n\\n` +\n (fieldInfo.length > 0 ? fieldInfo.join('\\n') + '\\n\\n' : '') +\n `\\`\\`\\`typescript\\ninterface Props ${tsInterface}\\n\\`\\`\\``,\n );\n }\n\n if (contract.actionSpec && Object.keys(contract.actionSpec).length > 0) {\n const actions = Object.entries(contract.actionSpec)\n .map(([id, entry]: [string, ActionEntry]) => {\n let line = ` - ${id}: \"${entry.label ?? id}\"${entry.description ? ` — ${entry.description}` : ''}`;\n if (entry.example !== undefined) {\n line += `\\n Example payload: \\`${JSON.stringify(entry.example)}\\``;\n }\n if (entry.nextStep) {\n line += `\\n Next-step hint: agent intends to call \\`${entry.nextStep}\\``;\n }\n // ggui#1108: `confirm` was the one declared action field the contract\n // renderer dropped — every other author field (label, description,\n // example, nextStep) reaches the model and this one did not. It is\n // rendered as INFORMATION, not as a rule: the terminal-actions section\n // deliberately does NOT cite it, because citing it measured ZERO —\n // two wordings, n=4 each, `save` + `confirm: true` came back armed 0/4\n // and 0/4. The field's own doc says \"whether to show confirmation\n // BEFORE triggering\", which is gravity, not one-shot-ness, and the\n // model reads it that way. Emitted only when true, so an absent flag\n // leaves the prompt bytes identical to before this line existed.\n if (entry.confirm === true) {\n line += `\\n Author flag: \\`confirm\\` — the author marked this action as one to confirm with the user before it fires.`;\n }\n return line;\n })\n .join('\\n');\n parts.push(`## Action Contract (wire these callbacks)\\n${actions}`);\n }\n\n if (contract.streamSpec && Object.keys(contract.streamSpec).length > 0) {\n const channels = Object.entries(contract.streamSpec)\n .map(([name, entry]: [string, StreamChannelEntry]) => {\n const bits = [` - ${name}`];\n if (entry.description) bits.push(`— ${entry.description}`);\n if (entry.source?.tool) {\n bits.push(`(source tool: \\`${entry.source.tool}\\`)`);\n }\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## Stream Contract (real-time channels via \\`useStream(name)\\`)\\n${channels}\\n\\n` +\n `Payload schemas:\\n\\`\\`\\`json\\n${JSON.stringify(contract.streamSpec, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n const agentTools = contract.agentCapabilities?.tools;\n if (agentTools && Object.keys(agentTools).length > 0) {\n const tools = Object.entries(agentTools)\n .map(([name, entry]) => {\n const bits = [` - **\\`${name}\\`**`];\n if (entry.toolInfo?.description) bits.push(`— ${entry.toolInfo.description}`);\n if (entry.usage) bits.push(`(usage: ${entry.usage})`);\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## AgentTools Catalog (tools the contract references)\\n${tools}\\n\\n` +\n `These are referenced via \\`actionSpec[*].nextStep\\` (the tool the agent SHOULD call next ` +\n `after the action fires) and \\`streamSpec[*].source.tool\\` (the tool feeding a channel). ` +\n `The agent OWNS the call — your component does not invoke these directly.\\n\\n` +\n `Schemas:\\n\\`\\`\\`json\\n${JSON.stringify(agentTools, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n const clientCapabilities = contract.clientCapabilities?.gadgets;\n const gadgetUses = listContractGadgets(contract);\n if (gadgetUses.length > 0) {\n // The wire `clientCapabilities.gadgets` is a package-keyed\n // two-level map; `listContractGadgets` flattens it to `GadgetUse[]`.\n // Kind is discriminated by the export-name grammar — `use`-prefixed\n // names are hooks (imported + CALLED), PascalCase names are\n // components (imported + RENDERED as JSX). Both are direct-imported.\n const caps = gadgetUses\n .map((use) => {\n const bits = [` - **\\`${use.name}\\`**`];\n bits.push(\n HOOK_NAME_RE.test(use.name)\n ? `via direct-imported hook \\`${use.name}\\` from \\`${use.package}\\` (call it)`\n : `via direct-imported component \\`<${use.name} />\\` from \\`${use.package}\\` (render as JSX)`,\n );\n if (use.description) bits.push(`— ${use.description}`);\n return bits.join(' ');\n })\n .join('\\n');\n parts.push(\n `## ClientCapabilities Contract (browser-capability gadgets the UI mounts)\\n${caps}\\n\\n` +\n `Hook entries are imported + called; component entries are imported + rendered as JSX. ` +\n `Capability values reach the agent only when the UI threads them into an actionSpec ` +\n `payload or contextSpec slot.\\n\\n` +\n `Declarations:\\n\\`\\`\\`json\\n${JSON.stringify(clientCapabilities, null, 2)}\\n\\`\\`\\``,\n );\n }\n\n // Required-UI derivation — the architectural fix at the heart of v15.\n // Every contract surface implies a visible UI element; eval scores\n // against the contract regardless of what the user-prompt narrative\n // says. Emitting the implied UI surfaces explicitly makes the contract\n // the single source of truth for \"what must render\".\n const uiRequirements: string[] = [];\n if (contract.propsSpec?.properties) {\n for (const [name, entry] of Object.entries(contract.propsSpec.properties)) {\n // Opt-in required (`=== true`) — the wire gate's reading, ggui#528.\n // An omitted-`required` prop is OPTIONAL: the agent may leave it\n // out of a legitimate render, so the derived instruction must\n // demand a presence-safe component, not unconditional display.\n const required = (entry as { required?: boolean }).required === true;\n if (required) {\n uiRequirements.push(\n `- **Required prop \\`${name}\\`** must appear somewhere in rendered DOM (display the value, or use it to drive a label/aria/key).`,\n );\n } else {\n uiRequirements.push(\n `- **Optional prop \\`${name}\\`** may be ABSENT at runtime — the component must render without it (guard the access or apply its default) and must display it when it IS provided.`,\n );\n }\n }\n }\n if (contract.actionSpec) {\n for (const [name, entry] of Object.entries(contract.actionSpec)) {\n const label = (entry as ActionEntry).label ?? name;\n uiRequirements.push(\n `- **Action \\`${name}\\`** (\"${label}\") needs an interactive control that fires it (Button click, form submit, key press, etc.).`,\n );\n }\n }\n if (contract.streamSpec) {\n for (const name of Object.keys(contract.streamSpec)) {\n uiRequirements.push(\n `- **Stream \\`${name}\\`** must mirror into local state via \\`useEffect\\` and render the resulting state — \\`useStream\\` alone never re-renders the DOM.`,\n );\n }\n }\n // agentTools entries don't imply UI on their own — they're catalog\n // declarations the agent uses, not hooks the component invokes.\n // The implied UI lives on the referencing actionSpec entries\n // (already enumerated above) and streamSpec channels with `source`\n // declarations. No per-tool UI requirement is emitted here.\n for (const use of gadgetUses) {\n // Hook AND component exports are direct-imported; the\n // implied UI differs — a hook is called + its value threaded,\n // a component is rendered as a JSX element. Kind is discriminated\n // by the export-name grammar.\n const exportName = use.name;\n const pkg = use.package;\n uiRequirements.push(\n HOOK_NAME_RE.test(exportName)\n ? `- **ClientCapability \\`${exportName}\\`** — the boilerplate has direct-imported \\`${exportName}\\` from \\`${pkg}\\` above a \\`// DO NOT EDIT\\` banner; keep that import, call \\`${exportName}\\` inside the component, and thread the resulting value into a context slot or action payload if the agent needs to observe it.`\n : `- **ClientCapability \\`${exportName}\\`** — the boilerplate has direct-imported \\`${exportName}\\` from \\`${pkg}\\` above a \\`// DO NOT EDIT\\` banner; keep that import and RENDER \\`<${exportName} … />\\` as a JSX element in the tree you return.`,\n );\n }\n if (uiRequirements.length > 0) {\n parts.push(\n `## Required UI Surfaces (derived from contract — every surface below MUST have visible UI, eval will fail if any is missing)\\n` +\n uiRequirements.join('\\n'),\n );\n }\n\n return parts.join('\\n\\n');\n}\n\n/**\n * Append the contract-context block to a user prompt if a contract is present.\n *\n * `appGadgets` forwards into `buildContractsContext` so thin contract\n * refs (`{hook}` without per-binding `package`) resolve to the\n * registered descriptor's package in the prompt-emitted \"import X from\n * Y\" lines.\n */\nexport function injectContracts(\n userPrompt: string,\n contract?: DataContract,\n appGadgets?: readonly GadgetDescriptor[],\n): string {\n if (!contract) return userPrompt;\n const ctx = buildContractsContext(contract, appGadgets);\n if (!ctx) return userPrompt;\n return userPrompt + '\\n\\n' + ctx;\n}\n\n/**\n * Build the variance-context block. Surfaces the agent's declared\n * styling signals (persona, aesthetic, context, seedPrompt) so cold-gen\n * aligns the produced component with the requested variant. Each field\n * is optional; the block only renders when at least one is present.\n *\n * - `persona` names the USER mental model (e.g. \"data-analyst\",\n * \"mobile-first reader\") — drives copy register + density choices.\n * - `aesthetic` names the VISUAL treatment (e.g. \"glassmorphic\",\n * \"editorial\", \"brutalist\") — drives surface decoration, typographic\n * weight, color usage.\n * - `context` is structured key-value signal alongside the persona —\n * small JSON-safe shape, serialized verbatim into the prompt.\n * - `seedPrompt` is the operator's original natural-language steer that\n * produced this variant in cache storage — useful as a one-line\n * directive for cold-gen.\n *\n * Returns an empty string when no fields are populated so the inject\n * helper can no-op cleanly.\n */\nexport function buildVarianceContext(\n variance: BlueprintVariance,\n options?: { readonly profileDeclared?: boolean },\n): string {\n const lines: string[] = [];\n if (typeof variance.persona === 'string' && variance.persona.length > 0) {\n lines.push(`- **Persona**: ${variance.persona}`);\n }\n if (typeof variance.aesthetic === 'string' && variance.aesthetic.length > 0) {\n lines.push(`- **Aesthetic**: ${variance.aesthetic}`);\n }\n if (typeof variance.seedPrompt === 'string' && variance.seedPrompt.length > 0) {\n lines.push(`- **Seed prompt**: ${variance.seedPrompt}`);\n }\n if (\n variance.context !== undefined &&\n variance.context !== null &&\n typeof variance.context === 'object' &&\n !Array.isArray(variance.context) &&\n Object.keys(variance.context).length > 0\n ) {\n lines.push(`- **Context**: ${JSON.stringify(variance.context)}`);\n }\n if (lines.length === 0) return '';\n return [\n '## Variance (apply to styling, not contract shape)',\n '',\n 'The agent (or operator) declared these variant signals. Honor them in',\n 'the visual treatment + copy register — but the contract shape (props,',\n 'actions, contexts, streams) remains canonical. Variance changes HOW',\n 'the UI looks and reads, not WHAT it does.',\n ...(options?.profileDeclared === true ? [STYLING_PROFILE_PRECEDENCE_NOTE] : []),\n '',\n ...lines,\n ].join('\\n');\n}\n\n/** Append the variance-context block to a user prompt if variance is present. */\nexport function injectVariance(\n userPrompt: string,\n variance?: BlueprintVariance,\n options?: { readonly profileDeclared?: boolean },\n): string {\n if (!variance) return userPrompt;\n const block = buildVarianceContext(variance, options);\n if (!block) return userPrompt;\n return userPrompt + '\\n\\n' + block;\n}\n"]}
|