Semantic Questions and Prepared Sets

Copy Markdown View Source

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.