evaluate is the strict Elixir interface. system_one remains the Python-parity
escape hatch. Both use generated operations and the same Pristine client.
Constructor contracts
{:ok, q} = TypeSafeSDK.Question.Noul.new("Urgent?", true: "Time-sensitive")
q = TypeSafeSDK.Question.Noul.new!("Urgent?", false: "Routine")
q = TypeSafeSDK.noul("Urgent?", criteria: %{"true" => "Time-sensitive"})
{:ok, team} = TypeSafeSDK.Question.Choice.new("Which team?",
billing: "Money", technical: "Product")
severity = TypeSafeSDK.score("Impact?", [
{"Low", "Cosmetic only"}, {"High", %{impact: "Blocking", workaround: false}}
])Strict Choice accepts 2..255 distinct atom/string options. Pair lists preserve
caller order; maps sort by wire key. Order is carried into JSON, including maps
larger than the small-map representation. Noul accepts only true/false criteria;
explicit criteria: cannot be combined with shorthand true/false options.
Score accepts 2..10 levels in caller order. A {label, description} level becomes
a structured JSON object, retaining the local label for answers.
These are semantic-layer constraints, not changes to committed OpenAPI or the
legacy constructors (whose Score minimum remains one).
Instructions/descriptions accept nonblank strings, JSON maps/lists or nil where
optional. Score levels cannot be nil. JSON objects allow atom/string keys but
reject collisions such as :team and "team". JSON values reject arbitrary
atoms, tuples, structs, PIDs, functions and invalid UTF-8. Nesting is bounded to
64 levels. Structured state is sent as JSON, never stringified as Elixir terms.
Extras, unknown types and validation paths
q = TypeSafeSDK.noul("Is this relevant?", extra: %{future_flag: true})
{:ok, prepared} = TypeSafeSDK.prepare(%{
"future" => %{"type" => "future_primitive", "instructions" => "Evaluate", "new_field" => 3}
})Question extra cannot replace type, instructions or criteria. Strict
evaluate similarly prevents extra_body from replacing state, model or
questions. Legacy system_one retains its last-write-wins top-level merge.
Unknown question types remain usable, while unknown answer types remain in raw
response data and unknown_answers rather than being coerced to known structs.
Question.validate/1 returns :ok or {:error, Error}; validate!/1 returns its
input or raises. Constructor/preparation failures use :invalid_request and
carry both error.path components and error.field_path for display. Prefer the
component list when IDs contain dots. Details describe the violated invariant;
validation never converts incoming strings into atoms.
Prepare once
prepared = TypeSafeSDK.prepare!(team: team, severity: severity)
TypeSafeSDK.evaluate(client, "first state", prepared)
TypeSafeSDK.evaluate(client, "second state", prepared)Preparation validates and JSON-encodes questions once, stores finite identity
registries, and injects only SDK-produced JSON through Jason.Fragment during
subsequent calls. Never construct or mutate Prepared fields directly. Each state
still receives JSON validation; each known answer receives relational checks.
Legacy question structs also work with prepare, subject to strict constraints.
Improper lists are rejected as invalid arrays, keyed collections or Score levels; they are not forwarded or treated as partially valid input. Response validation also retains unambiguous path components when an answer ID itself contains dots.
Run mix run examples/live_semantic.exs for a complete live walkthrough.
See the live example catalog for setup and API-call costs.