Resolve nested repeat state paths consistently across every renderer, validator, schema, prompt, and documentation surface.
Fixes#252
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
* feat(core): forward params to named onSuccess/onError actions
Named actions in onSuccess/onError only received their name, so params
never reached the handler. Forward the whole binding through the core
executor and every renderer bridge so a named handler receives params
the same way top-level bindings do.
Closes#301
* fix(core): forward params on the onError action form too
ActionOnErrorSchema omitted the optional params field, so onError
action params were silently stripped during Zod validation even though
the type allowed them. Mirror the onSuccess form and add schema-level
tests for both.
* fix(svelte): forward params to named onSuccess/onError actions
The Svelte ActionProvider bridge rebuilt the sub-binding from the name
only, dropping params, same gap as the other renderers. Its component
script block is type-checked more loosely, so it passed CI while wrong.
Forward the whole binding and add integration tests that named
onSuccess and onError handlers each receive their params.
* refactor(core): tighten executeAction context param to ActionBinding
Core always passes the resolved binding to the executeAction context
callback, so the string half of the union was false back-compat. Drop it
to (binding: ActionBinding): the runtime change becomes a compile error
for anyone with a custom renderer bridge instead of a silent break, and
every bridge collapses to execute(binding).
* autoFixSpec prunes children references to undefined elements
Dangling references are the dominant remaining first-attempt validation
failure in benchmarks, and models frequently fail to repair them even given
the exact error (observed: three repair turns, same dangling footer each
time). The renderer already skips missing children at runtime, so pruning
yields the identical rendered output while letting the spec validate. Each
removal is reported in fixes.
* Classify autoFixSpec fixes as lossy or lossless
Pruning a dangling child reference changes what renders; relocating a
misplaced field does not. Callers with a repair loop need to tell these
apart: accept lossless fixes silently, prefer re-prompting over lossy fixes,
and keep the lossy-fixed spec as a last resort. Adds fixDetails alongside the
existing fixes strings (additive, no signature break).
* Validate visible conditions in validateSpec; document filtered-list pattern
Malformed visible conditions (e.g. mixing $state and $item in one object)
silently evaluate to hidden at runtime: evaluateCondition dispatches on the
first recognized key and non-strict parsing strips the rest, so whole regions
of UI disappear with a valid-looking spec. Benchmarked worst case: a kanban
board that rendered zero task cards.
- core: VisibilityConditionStrictSchema (strict objects, exported)
- core: validateSpec rejects malformed visible with a repairable message
listing the valid forms (code: invalid_visible)
- framework prompts: FILTERED LISTS rule showing the repeat + per-item
visible pattern models keep reaching for and inventing syntax around
* Support filtered lists: repeat + $item visible on the same element
Models across vendors consistently write {repeat, visible: {$item: ...}} on
one container to mean a filtered list (kanban columns, status sections).
Previously $item had no meaning outside the repeat scope, the condition
evaluated false, and the whole region silently disappeared — the worst
visual failures in benchmarks were boards rendering zero cards this way.
Outside a repeat scope that spelling was always broken, so claiming it is
backward compatible: the renderer now applies such a condition per item,
preserving original indices for item state paths. Container-level $state
conditions and per-child $item conditions behave as before.
- core: conditionUsesItemScope helper (exported)
- react: RepeatChildren filters items by the container's $item condition
- framework prompts: FILTERED LISTS rule teaches the container spelling
- react: repeat-filter test suite (filtering, no-filter, $state container
visibility, per-child $item)
Other framework renderers (vue, svelte, solid, react-native) still evaluate
the container condition outside scope and should adopt the same semantics.
* Validate repeat containers: require children and matching state arrays
Two silent empty-region failures seen repeatedly in benchmarks, both passing
validation today: a repeat element with no children (nothing to clone per
item) and a repeat statePath pointing at a missing or non-array state value.
Both now fail validateSpec with repairable messages (repeat_without_children,
repeat_state_mismatch). State checks only run when the spec provides state;
runtime-fed state is unaffected.
* Address review: lossy-aware hook repair, react-only filtered-list rule, reuse getByPath
- ink/react-native useUIStream repair loops no longer accept lossy autofixes
unconditionally: lossless relocations apply immediately, pruned content
holds back while retries remain (so validation fails and the model repairs
the missing elements) and applies only as a last resort.
- FILTERED LISTS prompt rule removed from schemas whose renderers do not
implement the per-item filter yet (everything except react). Renderer
parity tracked as follow-up.
- repeat_state_mismatch validation reuses getByPath instead of a local JSON
Pointer lookup that skipped ~0/~1 unescaping.
* Address review: split mixed repeat visibility, apply lossless fixes eagerly
- splitRepeatVisibility (core, exported): AND-composed conditions on a repeat
container partition into a container gate ($state conjuncts, hides the
shell) and a per-item filter ($item/$index conjuncts). Mixed $or cannot
partition soundly and stays fully per-item, documented.
- react renderer uses the split, so {$and: [{$state gate}, {$item filter}]}
hides the empty shell when the gate is false instead of rendering a husk.
- autoFixSpec gains { lossy?: boolean } (default true, additive): ink and
react-native repair loops now apply lossless relocations immediately and
withhold only the pruning until retries are exhausted, matching the stated
intent.
* Address review: RN final-validation error path, repeat prune guard, docs
- react-native useUIStream mirrors ink: when retries are exhausted and the
spec still fails validation, report through onError instead of silently
calling onComplete with an invalid spec.
- autoFixSpec never prunes a repeat container to zero children; that would
trade missing_child for repeat_without_children and leave the last-resort
spec unrenderable. The dangling template reference stays visible to repair.
- Docs for the new surface: core README + skill (validateSpec issue codes,
fixDetails, lossy option), react README + skill and web visibility docs
(filtered-list pattern, mixed-condition splitting, framework support note).
* Fix visible validation depending on consumer zod version; strengthen children prompt rule
catalog.validate() behavior changed under consumers' zod resolution: z.any()
object keys are optional on zod 4.3 but nonoptional on zod 4.4+, so a spec
omitting an element's visible field validates on 4.3 and fails on 4.4. The
core test suite (zod 4.3.6) asserts the optional behavior, so optional is the
intent; pin it explicitly with .optional() so all zod 4.x agree.
children stays required (long-standing, zod-version-independent contract;
relaxing it would change InferSpec types for consumers). Instead the default
prompt rules now state explicitly that every element must include a children
array, with [] for leaves, which benchmarking shows models otherwise omit on
roughly a third of first attempts.
- core: InferSpecObject honors SchemaType.optional at the type level (additive;
no schema used optional before)
- all framework schemas: visible marked ...s.optional()
- framework prompt defaultRules: REQUIRED FIELDS rule for children
- core: regression tests locking visible-optional and children-required
* Fix Next schema optional fields
* feat: add custom directives API and @json-render/directives package
Add a `defineDirective` API that lets users register custom `$`-prefixed
dynamic values with schemas, resolvers, and prompt instructions — extending
the spec language without forking core.
* fixes
* perf(core): optimize findDirective to iterate registry instead of object keys
Flip the loop from O(object-keys) to O(registry-size) by iterating
the directive registry and checking `key in value` rather than scanning
all object keys with Object.keys() and startsWith("$").
* fix(core): reject directive names that conflict with built-in keys
defineDirective now throws at registration time if the name collides
with a built-in prop expression key ($state, $cond, etc.), making the
precedence contract explicit rather than relying on check ordering in
resolvePropValue.
* fix(directives): handle future dates in $format and warn on $math NaN coercion
$format relative dates now support future timestamps ("2h from now")
and return "just now" for zero diff. $math emits a console.warn in
dev mode when a non-numeric value is silently coerced to 0.
* fix(directives): remove process.env check that breaks DTS build
The directives package doesn't include @types/node, so referencing
process.env fails during tsup's DTS generation. The console.warn is
unconditional now — it only fires on actual misuse (non-numeric input)
so the cost is negligible.
* feat(directives): rename prompt to description, auto-describe schemas in prompt, add docs
- Rename `prompt` to `description` on DirectiveDefinition — a short
behavioral label rather than the full AI prompt blob
- Auto-generate directive schema signatures in the system prompt using
formatZodType, so the AI always sees every field, type, and optionality
- Add docs: guide page, API reference page, nav/title entries, docs-chat
* feat(directives): add standardDirectives export and composition hint
Export a pre-assembled standardDirectives array (all 7 non-factory
directives) for convenience. Add a composition hint to the generated
AI prompt so agents know directives can nest inside each other.
* docs: add directives skill, README entry, and docs-chat listing
- Add skills/directives/SKILL.md with full directive API reference
- Add @json-render/directives row to root README packages table
- Add "directives" to the Available skills list in docs-chat prompt
* perf(core): skip Zod parse in directive hot path
Resolvers are already defensive (coercion, fallbacks, switch defaults),
so runtime validation on every render adds overhead without safety.
The schema remains used for prompt generation and TypeScript inference.
Add ZodRecord and ZodDefault cases to formatZodType() in both core and
yaml packages. Fix ZodLiteral to support Zod 4's def.values array
in addition to Zod 3's def.value.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add yaml wire format and universal edit modes
**Summary**
- Add `@json-render/yaml` package with streaming parser, YAML prompt generation, and AI SDK transform
- Add universal edit modes (patch, merge, diff) to `@json-render/core`, usable across both JSONL and YAML formats
- Integrate YAML support and edit mode selection into the web playground with format toggle, token usage display, and prompt caching
* improvements
* fixes
* fixes ci
* docs
* feat: add strict mode to jsonSchema() for LLM structured outputs
This PR adds support for generating strict JSON schemas compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic, etc.).
## Problem
`catalog.jsonSchema()` was generating schemas that OpenAI's strict mode rejected due to:
- Use of `propertyNames` keyword (not permitted)
- Missing `additionalProperties: false` on nested objects
- Optional properties not being handled correctly for strict validation
## Changes
- Added `JsonSchemaOptions` interface with `strict` boolean flag
- Updated `catalog.jsonSchema()` to accept optional `JsonSchemaOptions` parameter
- When `strict: true`, the schema generator:
- Sets `additionalProperties: false` on all object types
- Removes `propertyNames` constraints
- Lists all properties (including optional ones) in `required` arrays, using nullable types for optionals
- Converts record types to fixed-key objects without additional property schemas
- Fixed Zod 4 compatibility issue in type name resolution
- Added comprehensive test suite covering all strict mode requirements
- Exported `JsonSchemaOptions` type from core package
## Usage
```typescript
// Default behavior (unchanged)
const schema = catalog.jsonSchema();
// Strict mode for LLM APIs
const strictSchema = catalog.jsonSchema({ strict: true });
```
The default behavior remains unchanged to maintain backward compatibility.
Fixes#180
* fix: document record type limitation and add anyOf nullable test
- Add clear JSDoc documenting that record/map types become opaque objects
in strict mode (LLM strict schemas require additionalProperties: false)
- Improve inline comment on the record case in zodToJsonSchema
- Add test exercising the anyOf nullable wrapping for optional properties
using a non-record schema so the path is directly verified
---------
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
* external store adapter for state management
Introduces a `StateStore` interface that lets users plug in their own state management (Redux, Zustand, XState, etc.) instead of being locked into the internal `useState`-based store.
- Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`
- `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode
- When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)
- When `store` is omitted, everything works exactly as before (fully backward compatible)
- Applied across all platform packages: react, react-native, react-pdf
* improvements
* update docs
* improvements
* fixes
* fix CI
* add store adapters
* fixes
* fixes
* fixes
* fixes
* e2e tests
* improvements
* fixes
* fixes
* fixes
* fixes
* update lockfile for widened react peer deps
* fix dashboard build
* add rate limits
* fix lint
* better
generate prompt
update examples
* fixes
* fixes
* new api
* update docs
* fixes
* fixes
* generated prompt
* Add tests for catalog validation and prompt generation
- Test generateSystemPrompt with components, actions, custom rules
- Test new defineCatalog API from schema system
- Test catalog.prompt() method with custom rules
- Test catalog.validate() for valid and invalid specs
- Test catalog.zodSchema() for custom validation
- Test catalog.jsonSchema() for structured outputs
- Add tests for nested specs with children
- Add tests for rejecting invalid component types
* Fix lint: pass children as nested elements in renderer
Change from children={...} prop to nested children pattern
to satisfy react/no-children-prop rule.
* Fix lint errors in dashboard and web apps
- Disable react/prop-types in both eslint configs (TypeScript handles this)
- Allow styled-jsx 'jsx' property in dashboard
- Add DATABASE_URL to turbo env allowlist
- Remove unused drizzle-orm imports (and, sql)
- Suppress unused variable warnings where intentional
* Add server entry point for @json-render/remotion
The main package entry includes React components that require
client-side context (React.createContext). This causes build
failures when importing in server-side API routes.
Added `@json-render/remotion/server` entry point that exports
only schema and catalog definitions without React dependencies:
- schema, RemotionSchema, RemotionSpec
- standardComponentDefinitions, standardTransitionDefinitions
- standardEffectDefinitions
- Type exports for catalogs
Updated remotion example to import from /server in catalog.ts
* Make dashboard database connection lazy-initialized
The database connection threw an error at module load time if
DATABASE_URL was not set, causing builds to fail in CI where
the database is not available.
Changed to lazy initialization using a Proxy so the error only
occurs when the database is actually used at runtime, not at
build time.
* Fix previousSpec property name and lazy rate limiting
- Fix property name mismatch: playground now passes previousSpec
instead of previousTree to match what useUIStream hook expects
- Make rate limiting lazy-initialized to avoid runtime errors when
Redis env vars (KV_REST_API_URL, KV_REST_API_TOKEN) are not set
- Rate limiting gracefully becomes a no-op when Redis is unavailable