@sembl/core
v0.5.0
Published
Semantic coercion for TypeScript: turn unstructured input into validated instances of your types.
Maintainers
Readme
@sembl/core
Runtime for SEMBL — semantic coercion for TypeScript. Describe what a type means, and turn unstructured input into a validated instance of it.
const draft = await sembl(listingHtml).partialCoerceTo(StayDetailsSchema);This package holds the decorators, the runtime schema types, the coerce API,
validation, and tracing. Schemas are produced from your decorated classes by
@sembl/compiler;
the LLM call is made by a provider package
(Anthropic,
OpenAI).
See the project README for the full walkthrough.
Install
pnpm add @sembl/core
pnpm add -D @sembl/compilerDescribing a type
@Schema says what a type is for, @Describe what each field means,
@Constrain bounds a value beyond its type, and @ValuesFrom says the legal
values come from somewhere resolved at runtime.
import { Schema, Describe, Constrain, ValuesFrom } from "@sembl/core";
@Schema("A short-term rental listing as a host would describe it.")
export class Listing {
@Describe("Display name for the listing.")
@Constrain({ maxLength: 40 })
name!: string;
@Describe("Amenities the property offers.")
@ValuesFrom("amenities")
@Constrain({ maxItems: 5 })
amenities!: string[];
}Coercing
coerce throws if a required field is missing; partialCoerce doesn't, and
returns Partial<T> with nulls stripped — the right one for pre-filling a form
a human will review. Both throw CoerceError on a type mismatch or a violated
constraint, with a FieldValidationIssue[] a form can render per field.
import { sembl, SemblConfig } from "@sembl/core";
SemblConfig.configure({
provider,
bundle,
// Called once per distinct source per coercion; you own any caching.
enumResolver: async (sourceId) => (await cms.taxonomy(sourceId)).map((d) => d.slug),
});
const draft = await sembl(listingHtml).partialCoerceTo<Listing>(listingSchema);If a source backing a required field fails to resolve, coercion throws
EnumResolutionError rather than quietly widening the field to a free-form
string. A source backing only optional fields widens and records a trace event.
Repair and provenance
maxRepairAttempts sends validation failures back to the model with its own
rejected output and the reasons. It only spends a call when validation actually
failed:
await coerce<Listing>(scrapedHtml, { provider, schema, maxRepairAttempts: 1 });partialCoerceWithProvenance (and its strict sibling) additionally reports how
well the input supported each field, so a review UI can flag the guesses:
const { data, provenance } = await partialCoerceWithProvenance<Listing>(html, {
provider,
schema,
});
// provenance.name → { confidence: "high", evidence: "the Sea Cabin sleeps 6" }Provenance works by requesting a derived schema that wraps each field as
{ value, confidence, evidence }, then splitting the response apart and
validating the values against your original schema — no provider is involved.
Top-level fields only.
Tracing
Pass traceSinks to see prompt construction, schema build, enum resolution,
the LLM call with token usage, and validation as nested spans. Implement
TraceSink (one write(span) method) to forward them anywhere.
