AI built for interfacing with code (useful) β not with people (too good to be true).
The AI race has revealed the operation that computation was missing: collapse over meaning.
System One (S1) measurement β and the collapse that follows it β native to Ruby. An S1 model answers a typed question about data with a calibrated probability. It does not generate, and it does not decide. Code asks; code decides.
- For the Ruby on Rails implementation, see:s1-rails
- For the terms, see THEORY.md . Where this README and the theory disagree, the theory is right.
TABLE of CONTENTS
- TL;DR
- The idea: collapse Β·Why
- Install
- Grammar: verbs, nouns,
? - The three kinds β noul Β· choice Β· score Β·Level Β·Scales Β·Distribution Β·Batch Β·Structured state Β·Reading distributions
- Experimental: making it native β the core extension Β· Ο Β· pattern matching Β·
=== - Keeping the probability: collapse late β
&``|``~Β· ranges Β·undecided?Β·collapse - Collections: judgments as predicates β select / group_by / sort_by / sum / grep Β· the lens (
given:) - Dictionary and aliases
- Errors Β·Testing Β·Observing calls Β·Providers Β·Development
Illustrative code:
people = [
{
name: "Michael",
occupations: [
"owner @ large self-sustaining family homestead",
"software engineer @ MedicalTech startup ($2M/arr)"
]
},
{
name: "Bob",
occupations: [
"Exotic beast/animal hunter & trader (pokemon hunter)",
"Professional Sportsman (office ping pong master)",
"Front-Counter Point of Sale (POS) Operator (MacDonalds in Hunstville, Alabama)"
]
}
]
(Ο people).choose "the most skilled individual", categories: people.map { _1[:name] } # => Michael β one call; .ranked lists everyone
people.max_by(&Ο.score("How skilled is this person?", "novice", "competent", "expert", "exceptional")) # => the Michael hash; one call per person
(Ο people) prepares the list: it is now a state, something questions can be asked about,
none asked yet. Measuring it β choose the most skilled β is, by default, the model's own sense
of "skilled". To make the judgement relative, hand the model something to judge against: a
lens, with given (or against):
(Ο people).given(rubric: "skill = breadth of trades").choose "the most skilled, per `rubric`", categories: people.map { _1[:name] } # => Bob
The same list, with the categories built from the data, is under choice.
Another example:
class EscalateToHuman < StandardError; end
def handle_chat(chat)
triage = (Ο chat).measure do |q| # several measurements, one call
q.judge :escalate, "Is the customer asking for a supervisor?"
q.judge :new_matter, "Is this a new matter?"
q.score :severity, "How severe is the injury?", "None", "Minor", "Serious", "Catastrophic"
end
raise EscalateToHuman if triage.true?(:escalate) # ? collapses; a bare distribution is always truthy
create_case(triage[:severity].level) if triage.true?(:new_matter) && triage[:severity].level.position >= 1
end
chat = { messages: [] }
inbox.each do |message| # whatever feeds you messages: a queue, a webhook, a socket
chat[:messages] << message
handle_chat(chat) # one call per message; every question answered fresh
rescue EscalateToHuman
hand_to_person(chat)
end
Software is rows and associations. Its input is human: a form, a phone call transcript, a review, a chat, a rΓ©sumΓ©. Its output is what a person sees on the other side: a web UI, an API, an MCP. Between the two sits the one thing computers could never do β read the human input and judge it.
For AI to be useful it has to tap that stream of human input β the data, one transcript or a list of calls, tickets, candidates β and categorize it, sort it, judge it. Not write about it: decide something about it that code can act on. That operation is the movement this gem is built around β four positions, three arrows:
evidence ββprepareβββΆ state ββmeasureβββΆ distribution ββcollapseβββΆ category
x Ο(x) by a question over a scale a point on it
Two of the arrows have a symbol. Ο prepares: (Ο chat) is a state β the evidence rendered
once, fixed, with calibrated answers to any question, none yet taken. Rendering happens at
preparation and never again: mutating chat afterwards does not change what is judged.
The verbs measure: judge, choose, score β three kinds of measurement, always one of
the three β or several at once with measure { |q| q.judge β¦; q.choose β¦; q.score β¦ }. Each
returns a distribution over its scale, calibrated and kept. ? collapses: the distribution
becomes a category. That is Ruby's own suffix with Ruby's own meaning (empty?, any?: the
decision, not the thing). Between the last two arrows there is nothing new β arithmetic,
ranges, sum, case β because once a judgement is a number, Ruby already knows what to do
with a number.
Human stream + Lens ββΟβββΆ state ββjudge / choose / scoreβββΆ distribution ββ?βββΆ category
β
& | ~ ranges sum case
One point where the physics image is loose: S1 answers are deterministic and repeatable; the probability is calibrated credence, not a coin waiting to be flipped. "Collapse" names the code's choice to stop carrying the distribution β and it is a choice; the section Keeping the probability is about not making it too early.
The human adjusts a judgement in exactly two places. The definition attaches to the question
and changes what the concept means: a clarification of yes / no, a description per category,
the ordered levels (criteria: is its wire word, and the keyword in code). The lens attaches
to the state and changes what the concept is applied to: a firm's acceptance criteria, a
role's requirements, a return policy β given:, or a Rails form. If it changes the meaning, it
is the definition; if it changes the evidence, it is the lens β a firm's "criteria" is a
standard to judge against, so it is a lens.
The judgement is semantic categorization, with a probability. A regex categorizes by characters; this categorizes by meaning β the same kind of tool (a predicate you filter and match with), applied where characters run out. And the judgement is probabilistic: every distribution is kept whole, so "need more info / maybe / probably / sure" is as native as true / false, and a sum of nouls is an expected count.
The thing that changes:
name = "Andrew"
male_name = true if ??? # there was never a way to write this line
preference_color = male_name ? "blue" : "pink"
male_name = (Ο name).is? "a man's name" # now there is: Ο prepares, is measures, ? collapses
(Ο name).is "a man's name" # => 0.97 β the distribution alone, when the number is what you want
"Andrew".noul "Is this a man's name?" # => 0.97 β the same distribution by its name, with the core extension on
That is the whole foundation: make the movement native to the language, then give it the same
surface everything else in Ruby has β filter, group, sort, sum, match, pattern-match, batch β so
a stream can be judged with Enumerable the way it is counted with Enumerable. Everything in
this README is one of those two moves. The model behind it (jev, today) is what makes it
possible; the grammar is what makes it usable.
An S1 model does one thing: measure (judge, choose, score). Three kinds cover it:
- Choice β group, classify, route (
choose) - Score β a level on an ordered scale (
score) - Yes / no, with a probability (
judge)
It is not a free-form assistant for people (an LLM). It interfaces with code. Code-in-the-loop, not human-in-the-loop.
The most basic usage:
chat = { messages: [ "How may I help you?" ... ]}
escalate_to_human if chat.judge? "is the customer asking for a human agent?" # core extension on (c.primitives = true)
escalate_to_human if (Ο chat).is? "asking for a human agent" # Ο on (c.psi = true)
Under the hood, both are:
state = S1::State.new("I have asked three times now. Can I just talk to a real person?")
escalate_to_human if state.judge?("Is the customer asking for a human agent?")
gem "s1"
S1.configure do |c|
c.provider = :typesafe # default; a name under Providers, or an instance
c.timeout = 30 # seconds per request
c.threshold = 0.5 # a noul at or above this reads as true
c.logger = Rails.logger # optional; debug lines per request, warns on retry
c.primitives = false # true extends String, Hash, Array; see "Experimental"
c.psi = false # true defines Ο(x); see "Experimental"
c.typesafe.api_key = ENV["TYPESAFE_API_KEY"] # default: read from the environment
c.typesafe.model = "jev-latest"
c.typesafe.base_url = "https://api.typesafe.ai"
c.typesafe.max_retries = 2 # transient failures before raising
c.cua.checkpoint = "cua-s1-forms" # only when c.provider = :cua
c.laya.base_url = "http://127.0.0.1:8765" # only when c.provider = :laya (self-hosted)
end
Every value has a default; an initializer is only needed to change one. Each provider keeps its
own settings under its name (c.typesafe, c.cua), declared by the provider class. Ruby β₯ 3.2.
No runtime dependencies.
One rule names every method in this README: a verb measures and returns the distribution; a
noun returns the thing it names; a ? returns a boolean.
States carry verbs. A state is anything a question can be asked about: an S1::State; a
String, Hash or Array with the core extension on; a Rails record. A Ο. predicate carries the
same verbs unapplied.
| verb | asks | returns |
|---|---|---|
judge (is andsame_as fill the question in) |
"Is this true?" | Answer::Noul |
choose |
"Which of these?" | Answer::Choice |
score |
"Which level?" | Answer::Score |
measure (ask ,batch ,ask_about ) |
several at once | Result |
Distributions collapse. One contract: collapse(threshold). The threshold only matters to a
noul, and a noul carries the one it was measured under (S1::State.new(x, threshold: 0.9); the
config's as of the measure otherwise β stamped then, not read at collapse; a noul built by hand
carries the config's as of its construction); the others accept and ignore it, so a Result
collapses every distribution
through one call, each noul at its own threshold, and to_h, true? and case β¦ in follow.
Each kind names its collapse and exposes Ruby's own conversions.
| collapsable | collapse returns |
its own name | Ruby idioms |
|---|---|---|---|
Answer::Noul |
true /false |
true? (false? ) |
! so!! ,to_f ,Comparable ,&``|``~ |
Answer::Choice |
a Symbol | choice |
to_sym ,to_s , loose== |
Answer::Score |
an S1::Level |
level |
key (the most likely level's rank),to_f (weighted position),levels |
Result |
{ id => collapsed value } |
to_h |
deconstruct_keys , socase β¦ in |
Nouns name; ? decides. On every state:
x.noul(q) == x.judge(q) # the dichotomous distribution, by its proper name β an Answer::Noul
x.choice(q, **cats) == x.choose(q, **cats).collapse # the category picked β a Symbol
x.level(q, *levels) == x.score(q, *levels).collapse # the point on the ordinal scale β an S1::Level
x.judge?(q) == x.judge(q).collapse(threshold) # a boolean; noul?, ask? are the same; on an S1::State (and a Ο. predicate) is? and same_as? fill the question in
The asymmetry is deliberate. Only the dichotomous distribution has a proper name β noul β so
its noun returns the distribution itself. The nominal and ordinal distributions have none, so
their nouns can only name the category: choice is a Symbol, level is a point on the scale.
And a ? method returns a boolean β Ruby's convention β so ? exists only for a noul: there is
no choice? and no judge? on a Choice or a Score.
Every question is one of three kinds β the first three of Stevens' scales: dichotomous, nominal,
ordinal. Pick by what the answer is. Each kind has a verb that measures and a noun that names.
The examples use state = S1::State.new(text); a # Ο: line gives the same call with Ο on. A
Noul prints as its probability, so # => 0.98 below is a Noul at 0.98.
noul β "Is this true?" The dichotomous distribution: a probability, 0 to 1, over
{ true, false }. The probability is the signal. judge measures it; noul is its name, and
returns the same thing; judge? (noul?) is true at or above the threshold; is / is? take a
phrase and ask "Is this β¦?".
state.judge("Is the customer asking for a human agent?") # => #<S1::Answer::Noul 0.98> compares like a number
state.noul("Is the customer asking for a human agent?") # the same distribution, by its name
state.judge?("Is the customer asking for a human agent?") # => true (at or above the threshold)
state.judge("Has the customer contacted support about this before?",
true: "mentions a prior attempt, ticket, or having asked before",
false: "no sign of any previous contact")
choice β "Which of these categories?" The nominal distribution: mass over an unordered set.
choose measures β the pick plus a distribution over every category; choice is the pick alone,
a Symbol.
dept = state.choose("Which team should handle this?",
returns: "Exchanges, refunds, wrong or damaged items",
shipping: "Delivery status, delays, lost packages",
billing: "Charges, invoices, payment problems")
dept.to_sym # => :returns
dept[:shipping] # => 0.0
dept.confidence # => 1.0
state.choice("Which team should handle this?", returns: "β¦", shipping: "β¦", billing: "β¦") # => :returns
Categories can be built from the state itself. people is the list from the TL;DR; that
example asks one question over the whole list, this one shows the four ways to pass the categories
(core extension on, so the Array is the receiver):
categories = people.to_h { |person| [person[:name], "Occupations: #{person[:occupations].join(", ")}"] }
people.choose "The most skilled individual", **categories # => #<S1::Answer::Choice Michael> ([:Michael] # => 0.98)
people.choose "The most skilled individual", categories: categories # same
people.choose "The most skilled individual", categories: people.map { _1[:name] } # labels only, no descriptions
categories.choose "The most skilled individual" # choosing among the evidence: the state IS the categories
categories: takes { category => description } or a bare list; choices:, and the wire name
criteria:, still work. With none given, a state of that shape β an Array of Strings, or a Hash
with String keys and all-nil or all-String values β is the scale. A Symbol-keyed Hash is a
record, not candidates: { ticket: "β¦", customer: "β¦" }.choose "Which team?" raises until it is
given categories:. A String-keyed Hash of Strings β a parsed JSON record β has the candidates'
shape and would be asked as one; give it categories:.
score β "Which level?" The ordinal distribution: mass over an ordered spectrum, worst β best.
score measures β the most likely level, and the probability-weighted position; level is the
level alone: an S1::Level, the label, that knows its position.
sev = state.score("How severe is the reported issue?",
"Cosmetic; no impact to functionality",
"Broken or degraded feature, but a workaround exists",
"Blocking issue; no workaround exists")
sev.level # => "Blocking issue; no workaround exists" an S1::Level
sev.key # => 2 the rank of the most likely level β sev.level.position (`index` is the legacy name); the wire's key is in raw
sev.to_f # => 1.68 (expectation: the weighted position across the levels, always derived from the masses)
state.level("How severe is the reported issue?", "Cosmeticβ¦", "Brokenβ¦", "Blockingβ¦") # => "Blocking issue; no workaround exists"
A score collapses to a Level: a String β the label β that knows its position on its scale.
That one fact is what makes a level usable both where code wants a number and where it wants
text.
sev = state.score("How severe is the reported issue?", "Cosmeticβ¦", "Brokenβ¦", "Blockingβ¦")
lvl = sev.collapse # => "Blocking issue; no workaround exists", an S1::Level
lvl >= 1 # => true by position, not alphabet
lvl >= "Broken or degraded feature, but a workaround exists" # => true a label on the scale, by its position
lvl.position # => 2 (lvl.to_i is the same)
lvl.scale # => #<S1::Scale Cosmeticβ¦ < Brokenβ¦ < Blockingβ¦> the S1::Scale (lvl.labels: the Strings)
lvl.index("no") # => 16 String's own methods are all still String's
YAML.dump(lvl) # => "--- Blocking issue; no workaround exists\n" the label alone; safe_load reads it back
case state.measure { |q| q.score :severity, "How severe?", "Cosmetic", "Broken", "Blocking" }
in { severity: "Blocking" } then page_someone # the label matches β the literal spelling; see Scales for the pinned form
in { severity: 1.. } then open_ticket # so does an integer range (`when 2..`, never a bare `when 2`)
end
tickets.sort_by(&Ο.score("How severe?", "cosmetic", "broken", "blocking")) # ordered by expected position
tickets.max_by(&Ο.score("How severe?", "cosmetic", "broken", "blocking"))
by_level = tickets.group_by(&Ο.level("How severe?", "cosmetic", "broken", "blocking"))
by_level["blocking"] # keys are the labels; a plain String looks them up
"severity: #{lvl}" # a String: interpolates
ticket.update!(severity: lvl) # stores as the label
{ severity: lvl }.to_json # => {"severity":"Blocking issue; no workaround exists"}
One caveat. Put the level on the left of a comparison with a label: lvl >= "Brokenβ¦" is
by position, "Brokenβ¦" <= lvl is String's own compare, by alphabet. Ranges of integers match
a level; ranges of labels do not.
A label spelled twice is a foot-gun. The question declares "blocking"; somewhere else the
program compares against it; then someone renames the level at the declaration:
Severity = S1.scale :cosmetic, :degraded, :blocked # was :blocking
lvl = (Ο text).level "How severe?", Severity
r = (Ο text).measure { |q| q.score :severity, "How severe?", Severity }
page_someone if lvl == "blocking" # false: no error, no page
case lvl
when "blocking" then page_someone # falls through
end
case r
in { severity: "blocking" } then page_someone # never matches β¦
in { severity: 1.. } then open_ticket # β¦ so a blocked issue opens a ticket
end
page_someone if lvl == Severity[:blocking] # KeyError: :blocking is not on the scale (cosmetic, degraded, blocked)
page_someone if lvl.blocking? # NoMethodError
case r
in { severity: ^(Severity.fetch(:blocking)) } then page_someone # KeyError β pinned; unpinned, `in { severity: Severity[:blocking] }`
end # is Ruby's array pattern on the constant and silently never matches
S1.scale makes the scale a value β the labels, ordered or not, with their definitions β so
the label is spelled once and every other mention is a lookup that fails loud. A question takes
the scale where the list or hash went:
Severity = S1.scale :cosmetic, :degraded, :blocking # ordinal: order is rank
Severity # => #<S1::Scale cosmetic < degraded < blocking>
lvl = state.level("How severe?", Severity) # => "blocking", an S1::Level on Severity
lvl.scale.equal?(Severity) # => true
Severity[:degraded] # => "degraded" an S1::Level, position 1; Severity[:sever] raises KeyError
Severity.fetch(:degraded) # the same: ":sever is not on the scale (cosmetic, degraded, blocking)"
Severity.to_a[1] # by position β [] is by label or key, never an index
lvl.blocking? # => true a predicate per label, by its snake-cased key (Severity.keys)
lvl >= Severity[:degraded] # => true by position
lvl < S1.scale(:low, :high)[:high] # ArgumentError: different scales
case lvl when Severity then β¦ end # === is membership: a Level of this scale, or a label / key on it
Severity.to_a # the Levels; Enumerable, so Severity.max, Severity.map(&:position)
Team = S1.scale returns: "Exchanges, refunds, wrong or damaged items", # nominal: label => definition
billing: "Charges, invoices, payment problems"
Team # => #<S1::Scale returns | billing>
dept = state.choose("Which team should handle this?", categories: Team) # the Scale where the hash went
dept.scale.equal?(Team) # => true
Team[:billing] # => :billing a nominal category is a Symbol; the scale checks it
Team.fetch(:biling) # KeyError: :biling is not on the scale (returns, billing)
Team.fetch(row["team"]) == Team[:billing] # a String from a column or the wire is a category once fetched β never `row["team"] == Team[:billing]`
Team.definitions # => { "returns" => "Exchanges, β¦", "billing" => "Charges, β¦" }
case state.measure { |q| q.score :severity, "How severe?", Severity; q.choose :team, "Which team?", categories: Team }
in { severity: ^(Severity.fetch(:blocking)) } then page_someone # a pinned reference: a typo here raises
in { team: ^(Team.fetch(:billing)) } then route_to_billing
end
tickets.group_by(&Ο.level("How severe?", Severity)) # keys are Levels on Severity; by_level[Severity[:blocking]]
S1.scale(*labels) is ordinal, S1.scale(**definitions) nominal (ordered: false makes a bare
list nominal, ordered: true a hash ordinal β its definitions are then what the model sees as
the levels, and the distribution speaks the labels); name: is what messages print, so a
category named name or ordered goes in a positional Hash (S1.scale({ name: "β¦", email: "β¦" });
beside keyword definitions name: is refused). Two scales are equal by kind and labels (a
nominal one as a set; definitions and name are not identity β the digest carries them); a Scale
is not an Array, and == against one is false (since 0.3.0 Distribution#scale and
Level#scale are an S1::Scale, not an Array: compare with .to_a / .labels, index with
to_a[i]). Labels are Strings, Symbols or Integers (a Scale, a list or a Hash beside other
labels is refused β S1.scale(Severity) raises), distinct and non-blank; so are the
definitions, which are text (Team.definition(:billing) reads one; definitions / to_h are
keyed by label String), and labels do not mix with definitions (S1.scale("a", b: "β¦") raises).
A label whose key spells a method the Level already has ("empty", "frozen"; "present",
"blank" under ActiveSupport) keeps that method's answer on the Level β reference it through
the scale, or a record's <field>_<key>?. A Level of another scale is off this one: include?
says false and fetch raises KeyError. A dynamic scale β S1.scale(-> { β¦ }) or
S1.scale(:method) β holds its source and reads as dynamic until resolve(record) β a list
resolves ordinal, a Hash nominal, unless ordered: said, and a Scale of the other kind than
ordered: said raises, as a dynamic one does (in Rails, the declaration evaluates it per
record, by the macro's kind); every other reader raises until then.
A score given a Scale beside other levels is refused, as is a choose. The inline forms
still work and build a Scale: state.score("β¦", "low", "mid", "high").scale is
#<S1::Scale low < mid < high>, equal to S1.scale(:low, :mid, :high) β the difference is where
the labels are spelled. A label whose snake-cased key collides with another's ("A+", "A-"),
or has none ("π₯"), gets no key and no predicate; Severity["A+"] still reaches it. Marshal
keeps a Scale whole; a YAML round trip reads its categories back as plain Strings.
Every measurement returns an S1::Distribution (Answer::Base remains an alias): mass over
every category of the scale, summing to 1 (probabilities, keyed by the wire name of each
category), plus scale β an S1::Scale for a choice or a score, [true, false] for a noul β
and kind β the scale kind by its wire name. Wire is what crosses to and from a provider; wire names are the provider's,
the rest of the vocabulary is ours.
state.judge("Is the customer angry?").scale # => [true, false]
state.judge("Is the customer angry?").kind # => "noul"
state.choose("Which team?", returns: "β¦", billing: "β¦").scale # => #<S1::Scale returns | billing> an S1::Scale; to_a is [:returns, :billing]; a category the wire left out has mass 0
state.choose("Which team?", returns: "β¦", billing: "β¦").kind # => "choice"
state.score("How severe?", "low", "mid", "high").scale # => #<S1::Scale low < mid < high> to_a is the S1::Levels
state.score("How severe?", "low", "mid", "high").kind # => "score"
A measurement is always one of the three kinds β judge, choose, score; the kind is part
of what a measurement is. measure is the plural: the same three, several at once.
measure = judge | choose | score one measurement, one kind
measure { judge; choose; score; β¦ } several, one call, independent
Two things make the plural more than a loop. One call: the state goes over once, and N
questions cost one call. Independence: the model answers each question as if it were the
only one β one distribution is never hidden context for another. That is what makes the batch the
natural unit: several independent measurements of one state. It is not independence of what
they measure: & and | on the results are products of marginals, exact only when the two
properties are independent given the state β for overlapping properties, ask the conjunction as one
question. ask, batch and ask_about are aliases of measure. It is the only way to get
several distributions from one call β a verb per line is a call per line.
result = state.measure do |q| # Ο: (Ο text).measure do |q|
q.judge :escalate, "Is the customer asking for a human agent?"
q.judge :repeat, "Has the customer contacted support about this before?",
true: "mentions a prior attempt", false: "no sign of one"
q.choose :department, "Which team should handle this?", returns: "Refunds", shipping: "Delays", billing: "Charges"
q.score :severity, "How severe is the issue?", "Cosmetic", "Degraded, workaround exists", "Blocking"
end
result[:escalate].true? # => true
result[:department].to_sym # => :returns
result[:severity].level # => "Blocking"
result.distributions # => { escalate: #<S1::Answer::Noul β¦>, β¦ } (result.answers still works)
result.usage # => { input_tokens: 490, output_tokens: 86 }
result.duration_ms # => 398
Ask speculatively: include questions whose answers you only need conditionally, then let your code decide which to use. That keeps it to one call.
r = state.measure do |q| # Ο: (Ο text).measure do |q|
q.judge :is_lead, "Is there a potential new case or matter?"
q.judge :qualified, "Is this a qualified lead, based on `firm.criteria`?"
q.judge :prior_rep, "Does the lead already have an attorney?" # asked regardless,
end # used only when relevant
if r[:is_lead].true? && r[:qualified] >= 0.85
flag_conflict if r[:prior_rep].true?
end
A state can be a string, or a hash/array (braced β bare keywords to State.new are options,
not evidence). A measurement is not evidence either: a distribution or a Result placed in the
state or a lens is refused (ValidationError) β put its collapse or its probabilities there.
With a hash, instructions can point at fields with backticked paths:
state = S1::State.new({
transcript: utterances,
case_details: { date_of_incident: "2026-08-15", sol_deadline: "2027-08-15" }
})
state.judge?("Judging from `transcript` and `case_details.sol_deadline`, is the claim still within the statute of limitations?")
The hash is rendered once, when the state is built β state.rendered is that value, frozen β
so appending to utterances afterwards does not change what any later question sees.
Instructions can be structured too β the verification pattern:
state = S1::State.new({ source_text: "Invoice #4471 issued March 3, 2026 to Beaver Dam Logistics for $12,840.00, net 30." })
state.judge?({ field: { name: "invoice_number", type: "string", description: "The identifier printed on the invoice." },
extracted_value: "4471",
question: "Does `extracted_value` match the `field` as it appears in `source_text`?" })
All distributions carry probabilities and confident?; use it to route β act automatically when
confident, escalate to a person or a reasoning model when not. Choice and score distributions carry
the provider's confidence, and confident?(at) is that confidence at or above at β and true when
the provider reported none (confidence is nil), so read confidence itself to escalate those. A noul has
no separate confidence β distance from the threshold is it: confident?(margin = 0.1) is at least
margin away from the threshold (its own, or threshold:), on either side; undecided? is its
exact complement, and decided? is its alias β the name that says the positional is a margin, not
the floor confident?(at) takes on a choice or a score. (The positional is the margin: the pre-theory confident?(threshold),
p >= t || p <= 1 - t, was symmetric about 0.5, not about the threshold the noul was measured
under, and is gone β confident?(0.8) on a 0.85 noul is now false, 0.35 from a 0.5 threshold
being under that margin.)
| distribution | reads as |
|---|---|
Answer::Noul |
to_f ,true? /false? , compares to numbers (a >= 0.85 ) |
Answer::Choice |
to_sym ,to_s ,[category] ,== :returns ,ranked |
Answer::Score |
level (anS1::Level ),key ,to_f ,levels |
dept = result[:department]
if dept.confident?(0.8)
route_to(dept.to_sym)
else
hold_for_review(dept.probabilities)
end
Everything above works through S1::State. The pieces below are opt-in β off by default,
each behind its own config switch β and exist to make asking a question feel native to the
language. Use them where they read better; leave them off where a codebase would rather not
extend core classes.
The core extension. c.primitives = true extends String, Hash and Array with
judge / judge? (noul, noul?, ask?), choose / choice, score / level and
measure (ask, batch, ask_about). A class's own method with one of those names always
wins, since the module is included beneath it. to_s1 gives the State, for per-call options.
S1.configure { |c| c.primitives = true } # String, Hash, Array; or a subset: [String]
require "s1/core_ext" # equivalent, require-style
"Can I speak to a person?".judge? "the customer is asking for a human agent"
{ ticket: text }.choose "Which team?", returns: "Refunds", billing: "Charges"
chat.measure { |q| q.judge :escalate, "..." }
chat.to_s1(threshold: 0.9).judge?("...") # the State, for per-call options
Ο. c.psi = true defines Ο on Kernel, like Integer() or Pathname(): anything
becomes a State. Ο prepares; the verbs measure; the ? on whatever follows is the collapse. Any
identifier works in its place (c.psi = "β£"). Anything that defines to_s1 converts itself β
that is how a typesafe-rails record becomes its default form. The explicit spelling is
S1.to_state(x) β x.to_s1 when x defines it, otherwise S1::State.new(x).
Is that just is? for the sake of reading like English? No β Ο is the convention the rest of
this section hangs off:
- any object , not the three core classes the extension covers: a record,
params, aMail::Message, a Struct, a Time β(Ο mail).is? "an out-of-office reply"; - options at the point of asking β
(Ο text, threshold: 0.95),provider:,model:,owner: phone_callfor the ledger; on a verb βmeasureincluded βgiven:is the fluent.given(β¦)inline (choose("β¦", a: "A", given: { p: 1 }),(Ο text, given: { p: 1 }),(Ο state, given: { p: 1 })), andthreshold:belongs to a collapse alone (judge?,is?,same_as?β onchoose/score/judge/measureit is anArgumentError, never an option); - a state β
state = (Ο x)is rendered once and asked many times; nothing aboutxis re-read between questions, and mutatingxafterwards changes nothing the state will be asked about; - the English forms that live only on
State:is?,is,same_as?,same_as; - no monkeypatching β one private
Kernelmethod;c.primitivescan stay off and code still reads as sentences.
(Ο"Michael").is? "a man's name" # => true
(Ο"Michael").is "a man's name" # => 0.98
(Ο text).is? "a man's name" # a variable needs the space: Οtext is one identifier
That is all Ο does: (Ο x) is S1.to_state(x) β x.to_s1 when x defines it, otherwise
S1::State.new(x) β and Ο. with no evidence builds a
predicate. It takes no block.
Pattern matching. A batch Result deconstructs: nouls as booleans, each at its threshold, choices as
symbols, scores as Levels β which match an integer range and a label alike β so Ruby's own
case β¦ in is the gate logic.
case (Ο chat).measure { |q| q.judge :escalate, "β¦"; q.choose :department, "β¦", returns: "β¦", billing: "β¦"; q.score :severity, "β¦", "low", "mid", "high" }
in { escalate: true, severity: 2.. } then page_someone
in { department: :billing } then route_to_billing
else hold_for_triage
end
Semantic equality β with the right operator. (Ο "Acme Inc").same_as? "ACME, Incorporated" asks
"Do this and other describe the same thing?" (same_as for the distribution). The operator
form is ===, case equality β Ruby's "does this match?", the one Range, Regexp and Proc
define, called only in explicit matching contexts:
case vendor.name
when (Ο "Acme Inc") then merge_into(acme)
end
names.grep(Ο "Acme Inc") # every name that describes the same company
names.any?(Ο "Acme Inc")
Not a regex. A regex (or similarity()) compares strings; same_as? compares what the strings
are about. Two calls about one accident, transcribed a week apart:
transcript = "Caller: Hi, this is Juan, I was rear-ended on Michael Street on the fifteenth, my neck hurts, the other driver ran the light."
recent = ["Caller: John here, calling back about my accident on Manor Street, August 15th, the guy went through the red light and hit me from behind.",
"Caller: This is Maria, I slipped at the grocery store on Manor Street last week and hurt my knee.",
"Caller: Juan Perez, I want to know if you handle wills."]
recent.any?(Ο transcript) # => true (one run: 0.67 / 0.03 / 0.06)
recent.any? { |t| t =~ /Juan.*Michael Street/ } # => false β "John", "Manor Street", "August 15th"
Juan/John and Michael/Manor are transcription noise; "the fifteenth" and "August 15th" are the same day; the third caller shares the name and nothing else. No pattern over characters gets that right, and every regex you tighten toward one case breaks another. One call per candidate, so narrow with SQL first (same firm, same week, same phone) and ask about the survivors.
Why not ==? Ruby calls == for you β inside Hash#[], Array#include?, uniq, RSpec's eq β
so a model-backed == puts a network call, a cost and a non-deterministic answer into every one of
those, and == is expected to be reflexive, symmetric and transitive, which a judgment is not.
=== carries none of that: it is only ever a question about a match. == on a State is plain Ruby equality β never a model call.
Aliases. The batch is measure, ask, batch or ask_about β (Ο chat).ask_about { |q| β¦ } reads
best when the state is right there. Every alias is a plain Ruby alias; see the dictionary.
Typing Ο on macOS: add the Unicode Hex Input keyboard (System Settings β Keyboard β Input
Sources β +), switch to it, then hold Option and type 03C8.
Explicit vs native. Every form has an explicit spelling; the native one is the same call with the plumbing removed.
| you want | explicit | native |
|---|---|---|
| a yes/no | S1::State.new(text).judge?("Is the customer angry?") |
(Ο text).is? "angry" Β·text.judge? "β¦" |
| the noul | S1::State.new(text).judge("β¦") |
(Ο text).judge "β¦" |
| the probability | S1::State.new(text).judge("β¦").to_f |
(Ο text).judge("β¦").to_f |
| one of a set | S1::State.new(text).choose("Which team?", returns: "β¦", billing: "β¦") |
text.choose "Which team?", returns: "β¦", billing: "β¦" |
| the category alone | S1::State.new(text).choose("β¦", **cats).to_sym |
text.choice "β¦", **cats |
| a level on a spectrum | S1::State.new(text).score("β¦", *levels).level |
text.level "β¦", *levels |
| the best of a list | S1::State.new(list).choose("the best", categories: labels) |
(Ο list).choose "the best", categories: labels |
| several at once | S1::State.new(text).measure { |q| β¦ } |
(Ο text).measure { |q| β¦ } Β·text.measure { |q| β¦ } |
| filter a stream | list.select { |x| S1::State.new(x).judge?("β¦") } |
list.select(&Ο.is?("β¦")) |
| bucket a stream | list.group_by { |x| S1::State.new(x).choose("β¦", **cats).to_sym } |
list.group_by(&Ο.choice("β¦", **cats)) |
| rank a stream | list.sort_by { |x| S1::State.new(x).score("β¦", *levels).to_f } |
list.sort_by(&Ο.score("β¦", *levels)) |
| the same thing? | S1::State.new({ this: a, other: b }).judge?("Do this and other describe the same thing?") |
(Ο a).same_as? b Β·case b when (Ο a) |
| against a lens | S1::State.new({ this: x, prefs: p }).judge?("β¦ per prefs") |
(Ο x).given(prefs: p).is? "β¦ per prefs" |
What Ruby will not do. Ο"Michael".is? "β¦" without parens is one method call whose argument is
"Michael".is?("β¦") β the dot binds before any prefix, symbol or operator β so the parens around the
evidence are load-bearing: (Ο"β¦"), (Ο text). A hash literal needs its own parens too β (Ο({ β¦ })) β because
Ο { β¦ } is a block to Ruby, and Ο takes none. And is? (the English form: "Is this β¦?") lives on State only, never on
core classes, where it would sit beside equality methods.
Collapse is one operation, and it is lossy. The measurement returns a distribution; turning it
into a boolean, a symbol or a level throws the rest away. Both layers are tools, and the
convention that separates them is Ruby's own: no ? keeps the probability, ? collapses.
collapse is the same step with a name, for when you want to see it. There is no second symbol
for it because Ruby already has one β the trailing ? β and a reader who has never seen this
gem reads judge / judge?, is / is? correctly (see The idea).
What the collapse hides. Three calls, one question, one boolean each:
clear = "I was rear-ended yesterday, the other driver ran a red light and got a ticket, my neck hurts."
murky = "there was a fender bender, not sure who was at fault, I feel a bit sore maybe."
none = "I want to know your office hours."
(Ο clear).judge "Does the caller have a viable injury claim?" # => 0.84 collapse β true
(Ο murky).judge "Does the caller have a viable injury claim?" # => 0.52 collapse β true
(Ο none).judge "Does the caller have a viable injury claim?" # => 0.03 collapse β false
clear and murky collapse to the same true. What the boolean threw away: that one is a
case and the other is a coin flip β the difference between "call them now" and "have someone
look". (Ο murky).judge(β¦).undecided?(0.15) is true; collapse cannot say so. Every
downstream count, dashboard and decision built on the booleans inherits that erasure. The
distribution is the information; the collapse is a summary of it β take it last.
The five conventional ways to use the distribution β three keep it, and are plain Ruby;
two end it, and are ? s:
| you write | what it is | |
|---|---|---|
| compose | r[:is_lead] & r[:qualified] & ~r[:prior_rep] |
products of marginals, exact only for independent properties β keeps the distribution |
| route | case p when 0.85.. then β¦ when 0.5...0.85 then β¦ else β¦ end |
Comparable + ranges β keeps it |
| count | calls.sum(&Ο.judge("β¦")) |
expected count from calibrated probabilities β keeps it |
| abstain | p.undecided?(0.1) β hand off |
a ? whose answer is "not by me" |
| collapse | p.collapse ,p.true? ,judge? ,is? ,choice ,level (anS1::Level ),r.collapse /r.to_h ,case r in { β¦ } |
the decision β ends it |
r = (Ο transcript).measure do |q|
q.judge :is_lead, "Is this a potential new personal-injury client?"
q.judge :qualified, "Was the caller not at fault and injured?"
q.judge :prior_rep, "Does the caller already have an attorney?"
end
Compose before you collapse. & both, | either, ~ not β products of marginals. jev
documents batched distributions as independent in the sense that no answer is context for another;
the products are exact only when the two properties are independent given the state, which the
provider does not promise. For overlapping properties (is_lead and qualified nest), ask the
conjunction as one question.
viable = r[:is_lead] & r[:qualified] & ~r[:prior_rep] # still a Noul
r[:is_lead] & 0.5 # a number in 0..1 is a marginal already known; nil or true raises
Route on the number, not the bit. A Noul compares like a Float, so case/ when with
ranges is the routing table β three outcomes from one probability, where a boolean gives two.
case viable
when 0.85.. then call_now # act automatically
when 0.5...0.85 then queue_review # a person decides
else archive
end
Abstain near the threshold. undecided?(margin) is "too close to call": hand off instead of
collapsing. decided?(margin) β confident?(margin) is the same method β is its exact complement
on a noul; confident?(at) is the counterpart for choices and scores, a floor on the provider's
confidence rather than a margin.
return hold_for_human if viable.undecided?(0.1)
Count without collapsing. A sum of calibrated probabilities is an expected count; a count of collapsed booleans rounds every 0.6 up and every 0.4 down.
calls.sum(&Ο.judge("Is the customer angry?")) # => 37.4 expected angry calls
calls.count(&Ο.is?("an angry customer")) # => 41, with the rounding baked in
Then collapse, explicitly. collapse on a distribution or a whole Result; ? on a method;
!! on a noul (!noul is "not true at the threshold"); case β¦ in { escalate: true } on a
batch. All four are the same step. What is not a collapse: a bare if noul β Ruby's if
never calls !, so a distribution is always truthy there.
viable.collapse # => true (at the noul's own threshold β the State's, else the config's when measured β or pass one)
viable.collapse(0.9) # => false
r.collapse # => { is_lead: true, qualified: true, prior_rep: false } (r.to_h is the same)
A choice collapses to its Symbol, a score to its S1::Level (the label, ordered by position),
and a Result to a Hash of all three.
S1 models are at their best over streams β transcripts, tickets, candidates, calls β where
each item gets the same typed question and the distributions are calibrated enough to filter,
bucket, rank and count on. This section is the data-science surface: Enumerable with
judgments in the blocks.
Ο with no argument is a question not yet applied to a state β a predicate, with to_proc and
===, so it goes wherever Ruby expects a block or a pattern. Applied to an element it follows
the naming rule: a verb yields one distribution per element β judge a noul (which sums as its
probability), choose a choice (which buckets by category), score a score (which sorts by
expected position) β a noun the thing it names (choice the Symbol, level the S1::Level,
so group_by keys by category or label), a ? a boolean. measure(x) on a predicate is the
distribution under any name. A verb in a boolean slot is always truthy β a distribution is
never false β so select, grep, count, find and partition take the ? form; sum
and sort_by take the verb; group_by takes the noun.
angry = Ο.is?("an angry customer")
calls.select(&angry) # filter (or calls.grep(angry), via ===)
calls.partition(&Ο.is?("a new matter"))
calls.count(&Ο.judge?("Was it resolved on the call?"))
inbox.find(&Ο.is?("a cancellation request"))
calls.group_by(&Ο.choice("Which team?", returns: "Refunds, exchanges", shipping: "Delivery, damage", billing: "Charges"))
tickets.sort_by(&Ο.score("How urgent?", "can wait", "today", "right now")).reverse # rank
tickets.max_by(&Ο.score("How severe?", "cosmetic", "degraded", "blocking"))
calls.sum(&Ο.judge("Is the customer angry?")) # => 2.0 expected count, no threshold
calls.sum(&Ο.judge("β¦")) / calls.size # share
calls.each_with_object([]) { |c, seen| seen << c unless seen.any?(Ο c) } # dedupe, via ===
The lens. A stream is judged against something β a firm's acceptance criteria, a role's
requirements, a return policy. That is the lens, and it attaches to the state: given (or
against β "judged against") puts the element under this and the lens beside it, so
instructions can name both (a lens keyed this, or other on same_as, is refused β it would
replace the facts). It is distinct from the definition, which attaches to the question:
true:/ false: on a noul, the descriptions of a choice, the levels of a score. (In
typesafe-rails a form does the lens's job: measurable_as(:qualification) { { transcript:, preferences: } }.)
qualifications = { must_have: ["5+ years Ruby", "shipped a Rails app"], disqualifiers: ["cannot work US hours"] }
qualified = Ο.is?("qualified for the role, per `qualifications`", given: { qualifications: qualifications })
candidates.select(&qualified) # => Michael, Dana
candidates.group_by(&Ο.choice("Per `qualifications`, which bucket?",
qualified: "meets every must_have, no disqualifier",
disqualified: "hits a disqualifier",
unclear: "not enough information",
given: { qualifications: qualifications })) # => { qualified: [...], disqualified: [Bob] }
S1::State.new({ candidates: candidates, qualifications: qualifications })
.choose("the candidate most qualified per `qualifications`", categories: %w[Michael Bob Dana]).ranked # one call
Same shape for intake: calls stream β the firm's qualification preferences β qualified / disqualified / unclear, and every other judgment the firm configures.
Two of these deserve a note. Expected counts: nouls are calibrated probabilities, so their sum
is the expected number of positives β a fractional headcount with no cutoff bias, where
count(&Ο.is?(β¦)) would round every 0.6 up and every 0.4 down. Ranking a set is one call, not
N: choose over the items returns a distribution over all of them, and ranked reads it out.
S1::State.new(calls).choose("the call most likely to become a chargeback").ranked
candidates.choose("the most qualified for this role", categories: names).ranked.first # add given: for the role's requirements
Cost model: every &predicate is one call per element (~400ms, run in parallel where you can β
typesafe-rails' where_judged takes concurrency:); prefer one choose over the items when the
question is "which of these", and reserve per-element predicates for "which of these are".
One question, one or many. A predicate is the question as a value, so define it once and
apply it to a single state with [] (what Enumerable sees: the distribution for a verb, the thing
named for a noun, a boolean for a ?) or measure (always the distribution), and to a stream with
&. No question text is written twice.
team = Ο.choice "Which team?", returns: "Refunds", support: "Product help", billing: "Charges"
angry = Ο.judge "Is the customer angry?"
team[ticket] # => :returns
team.measure(ticket).probabilities # => { "returns" => 0.91, "support" => 0.09, "billing" => 0.0 }
tickets.group_by(&team) # => { returns: [...], support: [...] }
angry[ticket] # => Answer::Noul, 0.98
tickets.sum(&angry) # => 2.29
Predicates are built by Ο with no argument, or without Ο by S1.predicates.
Inside select(β¦), write the predicate with parentheses β &Ο.is?("β¦") β Ruby's grammar does not
allow a command call after &.
The terms are defined in THEORY.md; this is the short form, with the code names. The classes, one per idea:
Ο(x) S1::State the state β judge Β· choose Β· score Β· measure
S1::Predicate a question not yet applied to a state (Ο.is? "β¦"), for select / group_by / sum
S1::Question::* the question on the wire: Noul | Choice | Score
S1::Providers::* who measures (TypeSafe's jev, Laya, cua-s1-forms, the Stub)
measure β S1::Distribution what comes back: mass over the scale, and collapse
Answer::Noul the dichotomous distribution collapse β true / false (the `?`)
Answer::Choice the nominal distribution collapse β the category, a Symbol
Answer::Score the ordinal distribution collapse β the level (S1::Level)
Result several, from measure { } collapse β { id => value }; pattern-matches
S1::Level the label, a String that knows its position on the scale
S1::Scale the scale as a value: the labels, ordered or not, with their definitions (S1.scale)
Evidence is prepared into a state; a state is measured by a question; measuring returns a distribution over a scale; collapsing picks a category.
| term | meaning | code |
|---|---|---|
| evidence | the particular β a transcript, a record, a list of candidates β before any presentation | any object |
| state | the evidence as presented for judgement: rendered once, fixed, with its lens attached. Whatever the rendering omitted does not exist to the judgement | S1::State.new(x) (Subject is an alias);Ο(x) withc.psi = true ;x.to_s1 ;rendered is the value |
| distribution | the product of a judgement: mass over every category of the scale, kept whole; scale ,kind , and on nominal and ordinal scales a confidence |
S1::Distribution (Answer::Base is an alias):Answer::Noul /Choice /Score |
| category | one point on the scale β the verdict; what the program acts on. Spelled as a bare literal it is unchecked; referenced through the scale ( Severity[:degraded] ,lvl.degraded? ) a rename fails where it is used |
a boolean, a Symbol, an S1::Level |
| prepare | Ο β makes evidence measurable: renders once and attaches the lens. Measures nothing | S1::State.new ,Ο(x) ,to_s1 , a Rails form |
| measure | the judgement: the degree to which the state falls under each category of a scale, in one provider call. Named by scale kind (judge, choose, score) or generically when several questions share a state | judge /choose /score ;measure (ask ,batch ,ask_about ) for a batch, aResult |
| collapse | the decision rule: the distribution becomes one category. The moment information is discarded, so the moment to postpone | collapse(threshold) on every collapsable;? ,!! ,case β¦ in |
| rendering | the function from evidence to state β what is shown, in what shape; done once at preparation | S1::Rendering.render : Strings copied, Hashes and Arrays rebuilt and frozen |
| lens | evidence added to the state to judge against ; makes the judgement relative | given(β¦) /against(β¦) ;given: on any verb (measure too), predicate, orState.new |
| question | a concept on a scale, with its definition; immutable; the wire form | S1::Question::Noul /Choice /Score :{ type, instructions, criteria } |
| scale | the finite set of categories: dichotomous { true, false } , nominal (unordered), ordinal (ordered). Three kinds, only three. A value β the labels with their definitions β that a category is a point on |
S1::Scale :S1.scale(*labels) ordinal,S1.scale(**definitions) nominal,S1.scale(-> { β¦ }) dynamic;[] ,fetch ,=== ,keys ,definitions ;Distribution#scale ,Level#scale ; a choice'scategories are the labels; a question'slevels are the texts shown (the labels when the scale has no definitions), a distribution'slevels the Levels |
| definition | the working definition of the scale β what counts as yes, what each category means, the ordered levels. Attaches to the question; criteria is the wire word | true: /false: on a judge;categories: (or the wire wordcriteria: ) on a choose;*levels on a score; a Scale'sdefinitions |
| choosing among the evidence | when the state is itself the set of candidates β a list of labels, or label β description β its labels are the scale | names.choose "the most skilled" βnames the labels themselves,%w[Michael Bob] : an Array of Strings, or a Hash with String keys and all-nil or all-String values; a Symbol-keyed Hash is a record and takescategories: |
| calibration | the axiom: 0.7 means seven in ten such judgements are true; what licenses arithmetic before collapse (sums, thresholds; & / ` |
` only for properties independent given the state) |
| collapsable | anything that holds a distribution and can collapse | S1::Collapsable : every distribution, and aResult elementwise |
| confidence | a scalar the provider reports beside a nominal or ordinal distribution, derived from the distribution's shape (jev) or the winning mass (cua) β it carries nothing the masses do not; a noul has none β distance from the threshold is it | confidence ,confident?(at) β a floor; on a noulconfident?(margin) /decided? , the complement ofundecided? |
| threshold | the parameter of the dichotomous decision rule β the mass at or above which "true" is the verdict; travels with the measurement, stamped at measure (the config's when none was given) | S1.config.threshold ,S1::State.new(x, threshold: 0.9) ,judge?("β¦", threshold: 0.9) ,Answer::Noul#threshold |
| level | a point on an ordinal scale β a label with a rank, on a scale it knows | S1::Level : a String withposition (to_i ),scale (anS1::Scale ;labels the Strings), a predicate per label (lvl.blocking? ); compares by rank on its own scale, across scales<=> raises; matches integer ranges and labels; keep it on the left of a comparison with a label |
| noul | the dichotomous distribution, by its proper name β the product of a judge; also the wire name of the kind | Answer::Noul ;x.noul(q) == x.judge(q) ;noul? isjudge? |
| choice | the category a choose picked | Answer::Choice#choice , a Symbol;x.choice(q, β¦) == x.choose(q, β¦).collapse |
| predicate | a question not yet applied to a state; applied to each element of a stream a verb yields one distribution per element, a noun the thing it names, a ? a boolean |
Ο.is?(β¦) ,Ο.judge(β¦) ,Ο.choose(β¦) ,Ο.score(β¦) , the nounsΟ.choice ,Ο.level ;S1.predicates ;to_proc ,=== ,[x] ,measure(x) |
| stream | many particulars under one question; filtering, ranking, bucketing and counting are the collection's own operations | any Enumerable with&predicate |
| form | a named rendering of a persistent particular | typesafe-rails measurable_as(:name) { β¦ } |
| provider | an implementation of measure that honours calibration; owns its transport and wire format | call(Request) β Result ::typesafe ,:laya ,:cua , theStub |
| result | a batch of distributions from one measure, plus telemetry | S1::Result :distributions (answers is an alias),usage ,model ,provider ,duration_ms ,raw |
| request | what a provider receives: rendered state, questions, options | S1::Request |
| wire | what crosses to and from a provider; wire names are the provider's, the rest are ours | noul ,choice ,score ,criteria ,answers ;Distribution#raw keeps the payload |
| core extension | String, Hash and Array as receivers of the verbs β sugar | c.primitives = true , orrequire "s1/core_ext" |
| psi | the setting that installs Ο | c.psi = true (c.symbol is an alias), or any identifier |
Aliases are plain Ruby alias es, so a class's own method with the same name always wins.
Rescue by intent, not by HTTP code:
begin
result = state.measure { |q| ... }
rescue S1::TransientError => e # RateLimitError, ServerError, ConnectionError, TimeoutError β retry later
retry_later(e)
rescue S1::PermanentError => e # AuthenticationError, InvalidRequestError, ValidationError β fix the request
raise
end
Transient failures already retry inside the provider (c.typesafe.max_retries, honoring Retry-After) before surfacing.
Providers::Stub answers without the network. Give it the distributions that matter; everything
else gets a neutral default (noul 0.5, the first category, the first level).
S1.configure do |c|
c.provider = S1::Providers::Stub.new(escalate: 0.9, department: :billing, severity: 2)
end
Single-question calls are keyed by the wire name of their kind β noul (for judge, judge?,
is?, same_as?), choice, score: Stub.new(noul: 0.9, choice: :billing, score: 2). A
score's shorthand is a position, a label ("blocking", Severity[:blocking]) or the text
shown; a choice's the category. The full fields β { probability: }, { choice:, probabilities:, confidence: }, { legend:, probabilities:, confidence: } β are taken as a Hash, kept whole as the distribution's raw; a
score's expectation is derived from the masses, so a score: or expectation: in it is read
only from raw.
A block form receives the request when a distribution should depend on the state:
S1::Providers::Stub.new { |req| { escalate: req.state.include?("real person") ? 0.95 : 0.1 } }
Hook every completed call for telemetry or a cost ledger. Extra keyword arguments to
State.new ride along on the request, so you can attribute a call to its owner:
S1.on_result do |result, request|
Ledger.record(owner: request.options[:owner], model: result.model, **result.usage)
end
S1::State.new(transcript, owner: phone_call).judge?("...")
A provider is the code that talks to a model: any object responding to call(request) β Result.
The gem owns the shape β Request, Question, Distribution, Result, the method signatures
on State, the error taxonomy; a provider owns only translation: how the state and questions go
on the wire, how answers come back as distributions, how failures map to TransientError /
PermanentError. A provider declares which question kinds it answers ( supports?); the rest
are refused before any call (UnsupportedError). Configure by name β :typesafe resolves to
S1::Providers::TypeSafe, built from its section of the config β or pass an instance.
S1.configure { |c| c.provider = :typesafe }
S1::State.new(text, provider: MyProvider.new) # per-state override Ο: (Ο text, provider: MyProvider.new)
Four ship. TypeSafe and Laya share one thing β the System One HTTP contract (Providers::SystemOneHTTP,
the /v1/systemone JSON that jev defined and Laya adopted) β and are otherwise separate
classes under Base, each with its own settings, auth and models; Cua and Stub share nothing
with them but the distributions:
| provider | what | kinds | transport |
|---|---|---|---|
TypeSafe |
TypeSafe's jev, hosted | noul, choice, score | HTTPS /v1/systemone |
Cua |
cua-s1-forms , a 2.8 MB jev-like option scorer for GUI forms | choice | a local Python sidecar over stdin/stdout ( support/cua_s1_sidecar.py ; needscua-s1 + torch, checkpoint as safetensors + json) |
Laya |
Laya , self-hosted, Apache-2.0: the same three kinds and the same System One HTTP contract as jev; 322β421M-parameter encoder (English / 100+ languages / typed-decisions checkpoints) | noul, choice, score | HTTP to a server you run β support/laya_server.py wrapspip install laya in the same/v1/systemone contract; no key |
Stub |
canned distributions for tests | all | none |
S1.configure { |c| c.provider = :cua; c.cua.checkpoint = "cua-s1-forms" }
S1.configure { |c| c.provider = :laya; c.laya.base_url = "http://127.0.0.1:8765" } # python support/laya_server.py
S1::State.new('ELEMENT Edit "Phone number"').choose("Fill the form: which entity?", phone: "555-0100", email: "a@b.c", skip: nil)
Translation Cua owns, as an example of what a provider decides: the context string is in the
checkpoint's own shape β a TASK <text> line, then the state on the next (as given, or JSON
for structured state), the delimiter cua-s1's render_context uses β with the question's
instructions as the task, the model having no other slot for the concept; a state that already
opens with a TASK line keeps it, the instructions spliced in after ; , so the model never
sees two; category descriptions become
"key: description"; the whole context is held to the model's byte limit; the argmax is the
pick and its probability the confidence; and noul / score are refused rather than emulated β
the model is trained on form elements, not propositions.
Writing one. Subclass S1::Providers::Base and do five things: declare settings (its
section of S1.config, with defaults β settings :name, key: default defines c.name.key,
handed to new when the provider is named); answer supports?(question) honestly; implement
call(request) β Result by building every distribution with distribution(id, question, raw:, **fields) β
the only constructor, which is what makes every provider's distributions identical and keeps a
vendor's wire keys out of consumers' hands β and returning build_result(distributions:, model:, usage:, raw:); map failures onto S1::TransientError / S1::PermanentError; leave name
alone. Then run the conformance suite the gem ships:
require "s1/rspec"
RSpec.describe MyProvider do
it_behaves_like "an S1 provider", -> { MyProvider.new(client: fake) } # all three kinds
it_behaves_like "an S1 provider", -> { ChoiceOnly.new }, supports: %i[choice] # or fewer
end
It checks the shape, not the wisdom: a Result that is a Collapsable, every id answered with
the class its kind demands, probabilities in [0, 1] summing to 1, choices keyed by category and
scores by rank whatever keys the wire used (key is the level's position), every distribution
carrying its question's scale, collapse yielding a
boolean / Symbol / S1::Level, supports? telling the truth, integer usage. Shape only:
calibration and batch independence are the two clauses no offline suite can test β they are the
provider's warranty. All four providers here pass it; that is the standard.
bin/setup, then bundle exec rake runs the specs and rubocop. bin/console opens IRB with the
gem loaded. TYPESAFE_LIVE=1 TYPESAFE_API_KEY=β¦ bundle exec rspec spec/s1/live_spec.rb
hits the real API.
MIT.