Closing the gaps in dynamic content: validation, scheduling, and AI indexing

When we shipped the dynamic content substrate in v1.41.0, DynamicTypeRepo accepted any map[string]any you handed it — no validation against the registered schema. This release closes those gaps: field validation, scheduling support, and AI index registration at boot.

Release: smeldr.dev/core v1.54.0 · Amendment A202


When we shipped the dynamic content substrate in v1.41.0 (A153), DynamicTypeRepo accepted any map[string]any you handed it — no validation against the registered schema. You could create a recipe without a title, or pass a number where the schema expected a string, and the data would silently persist. That was the pragmatic call at the time. This release closes those gaps.

Field validation on create and update

Two new functions in schemas.go:

// create path — all required fields must be present
err := smeldr.ValidateFields(schema, fields)

// update path — only provided fields are checked (partial update semantics)
err := smeldr.ValidatePartialFields(schema, patch)

Both return *ValidationError when the input doesn't conform to the schema: unknown fields, missing required fields, type mismatches. Both return nil when schema is nil — so if you have dynamic types without a schema, nothing breaks.

DynamicTypeRepo.CreateDraft and UpdateFields now call these automatically. You don't need to call them yourself — they're there for cases where you want to validate a field map before handing it to the repo.

Scheduling support

DynamicTypeRepo now has a ScheduleContent method alongside SetStatus:

err = repo.ScheduleContent(ctx, id, time.Now().Add(48*time.Hour))

It uses the same validateTransition logic as SetStatus — if your type has a registered state flow that doesn't include draft → scheduled, you'll get ErrConflict. State-flow enforcement is consistent across the whole lifecycle.

AI index at boot

If you have runtime-defined content types with a URLPrefix, their /llms.txt compact fragment is now wired at boot time when loadDynamicTypes runs. Previously, the AI index was only populated for compiled Module[T] types. Dynamic types were invisible to the AI index until content was published and a refresh was triggered. Now they're registered on startup.


*v1.54.0 ships with 96.1% test coverage.*