{"slug": "making-s1-model-judgements-a-rubylang-primitive-feature", "title": "Making s1-model 'Judgements' a RubyLang primitive feature", "summary": "Developer innocentdiaz released s1_ruby, a Ruby gem that adds System One (S1) measurement and a \"collapse\" operation over meaning as a native Ruby primitive, with a companion Ruby on Rails implementation at s1_rails. The library lets code ask a typed question about data and receive a calibrated probability from an S1 model that does not generate or decide, exposing verbs such as choose, score, and judge plus a ψ operator for preparing state, lenses via given/against, and late-collapse operators &, |, and ~. The project's README states that where it and the bundled THEORY.md disagree, the theory is authoritative.", "body_md": "AI built for interfacing with code (useful) — not with people (too good to be true).\n\nThe AI race has revealed the operation that computation was missing: `collapse` over meaning.\n\n**System One (S1) measurement** — and the *collapse* that follows it — native to Ruby.\nAn S1 model answers a typed question about data with a calibrated probability. It does not\ngenerate, and it does not decide. Code asks; code decides.\n\n- For the **Ruby on Rails** implementation, see:[s1-rails](https://github.com/innocentdiaz/s1_rails)\n- For the terms, see [THEORY.md](https://github.com/innocentdiaz/s1_ruby/blob/master/THEORY.md) . Where this README and the theory disagree, the theory is right.\n\n**TABLE of CONTENTS**\n\n- [TL;DR](#tldr)\n- [The idea: collapse](#the-idea-collapse) ·[Why](#why)\n- [Install](#install)\n- [Grammar: verbs, nouns, `?`](#grammar-verbs-nouns-)\n- [The three kinds](#the-three-kinds) — noul · choice · score ·[Level](#level) ·[Scales](#scales-one-place-for-the-labels) ·[Distribution](#distribution) ·[Batch](#batch-many-questions-one-call) ·[Structured state](#structured-state-and-structured-questions) ·[Reading distributions](#reading-distributions)\n- [Experimental: making it native](#experimental-making-it-native) — the core extension · ψ · pattern matching ·`===`\n- [Keeping the probability: collapse late](#keeping-the-probability-collapse-late) —`&``|``~` · ranges ·`undecided?` ·`collapse`\n- [Collections: judgments as predicates](#collections-judgments-as-predicates) — select / group_by / sort_by / sum / grep · the lens (`given:` )\n- [Dictionary and aliases](#dictionary-and-aliases)\n- [Errors](#errors) ·[Testing](#testing) ·[Observing calls](#observing-calls) ·[Providers](#providers) ·[Development](#development)\n\nIllustrative code:\n\n```\n# Pull from a database:\npeople = [\n  {\n    name: \"Michael\",\n    occupations: [\n      \"owner @ large self-sustaining family homestead\",\n      \"software engineer @ MedicalTech startup ($2M/arr)\"\n    ]\n  },\n  {\n    name: \"Bob\",\n    occupations: [\n      \"Exotic beast/animal hunter & trader (pokemon hunter)\",\n      \"Professional Sportsman (office ping pong master)\",\n      \"Front-Counter Point of Sale (POS) Operator (MacDonalds in Hunstville, Alabama)\"\n    ]\n  }\n]\n\n(ψ people).choose \"the most skilled individual\", categories: people.map { _1[:name] }   # => Michael — one call; .ranked lists everyone\npeople.max_by(&ψ.score(\"How skilled is this person?\", \"novice\", \"competent\", \"expert\", \"exceptional\"))   # => the Michael hash; one call per person\n```\n\n`(ψ people)` prepares the list: it is now a **state**, something questions can be asked about,\nnone asked yet. Measuring it — *choose* the most skilled — is, by default, the model's own sense\nof \"skilled\". To make the judgement relative, hand the model something to judge *against*: a\n**lens**, with `given` (or `against`):\n\n```\n(ψ people).given(rubric: \"skill = breadth of trades\").choose \"the most skilled, per `rubric`\", categories: people.map { _1[:name] }   # => Bob\n```\n\nThe same list, with the categories built from the data, is under [choice](#the-three-kinds).\n\nAnother example:\n\n``` python\nclass EscalateToHuman < StandardError; end\n\ndef handle_chat(chat)\n  triage = (ψ chat).measure do |q|                                        # several measurements, one call\n    q.judge  :escalate,   \"Is the customer asking for a supervisor?\"\n    q.judge  :new_matter, \"Is this a new matter?\"\n    q.score  :severity,   \"How severe is the injury?\", \"None\", \"Minor\", \"Serious\", \"Catastrophic\"\n  end\n\n  raise EscalateToHuman if triage.true?(:escalate)                        # ? collapses; a bare distribution is always truthy\n  create_case(triage[:severity].level) if triage.true?(:new_matter) && triage[:severity].level.position >= 1\nend\n\nchat = { messages: [] }\ninbox.each do |message|            # whatever feeds you messages: a queue, a webhook, a socket\n  chat[:messages] << message\n  handle_chat(chat)                # one call per message; every question answered fresh\nrescue EscalateToHuman\n  hand_to_person(chat)\nend\n```\n\nSoftware is rows and associations. Its input is human: a form, a phone call transcript, a\nreview, a chat, a résumé. Its output is what a person sees on the other side: a web UI, an\nAPI, an MCP. Between the two sits the one thing computers could never do — read the human\ninput and *judge* it.\n\nFor AI to be useful it has to tap that **stream** of human input — the data, one transcript\nor a list of calls, tickets, candidates — and categorize it, sort it, judge it. Not write about\nit: decide something about it that code can act on. That operation is the movement this gem\nis built around — four positions, three arrows:\n\n```\nevidence  ──prepare──▶  state  ──measure──▶  distribution  ──collapse──▶  category\n   x                     ψ(x)     by a question       over a scale         a point on it\n```\n\nTwo of the arrows have a symbol. **ψ prepares**: `(ψ chat)` is a state — the evidence rendered\nonce, fixed, with calibrated answers to any question, none yet taken. Rendering happens at\npreparation and never again: mutating `chat` afterwards does not change what is judged.\n**The verbs measure**: `judge`, `choose`, `score` — three kinds of measurement, always one of\nthe three — or several at once with `measure { |q| q.judge …; q.choose …; q.score … }`. Each\nreturns a distribution over its scale, calibrated and kept. **`?` collapses**: the distribution\nbecomes a category. That is Ruby's own suffix with Ruby's own meaning (`empty?`, `any?`: the\ndecision, not the thing). Between the last two arrows there is nothing new — arithmetic,\nranges, `sum`, `case` — because once a judgement is a number, Ruby already knows what to do\nwith a number.\n\n```\nHuman stream  +  Lens  ──ψ──▶  state  ──judge / choose / score──▶  distribution  ──?──▶  category\n                                                                        │\n                                                                 & | ~  ranges  sum  case\n```\n\nOne point where the physics image is loose: S1 answers are deterministic and repeatable; the\nprobability is calibrated credence, not a coin waiting to be flipped. \"Collapse\" names the\ncode's choice to stop carrying the distribution — and it is a choice; the section *Keeping the\nprobability* is about not making it too early.\n\nThe human adjusts a judgement in exactly two places. The **definition** attaches to the question\nand changes what the concept *means*: a clarification of yes / no, a description per category,\nthe ordered levels (`criteria:` is its wire word, and the keyword in code). The **lens** attaches\nto the state and changes what the concept is *applied to*: a firm's acceptance criteria, a\nrole's requirements, a return policy — `given:`, or a Rails form. If it changes the meaning, it\nis the definition; if it changes the evidence, it is the lens — a firm's \"criteria\" is a\nstandard to judge against, so it is a lens.\n\n**The judgement** is semantic categorization, with a probability. A regex categorizes by\ncharacters; this categorizes by meaning — the same *kind* of tool (a predicate you filter and\nmatch with), applied where characters run out. And the judgement is *probabilistic*: every\ndistribution is kept whole, so \"need more info / maybe / probably / sure\" is as native as\ntrue / false, and a sum of nouls is an expected count.\n\nThe thing that changes:\n\n```\nname = \"Andrew\"\nmale_name = true if ???                       # there was never a way to write this line\npreference_color = male_name ? \"blue\" : \"pink\"\nmale_name = (ψ name).is? \"a man's name\"       # now there is: ψ prepares, is measures, ? collapses\n(ψ name).is \"a man's name\"                    # => 0.97 — the distribution alone, when the number is what you want\n\"Andrew\".noul \"Is this a man's name?\"         # => 0.97 — the same distribution by its name, with the core extension on\n```\n\nThat is the whole foundation: make the movement native to the language, then give it the same\nsurface everything else in Ruby has — filter, group, sort, sum, match, pattern-match, batch — so\na stream can be judged with `Enumerable` the way it is counted with `Enumerable`. Everything in\nthis README is one of those two moves. The model behind it (jev, today) is what makes it\npossible; the grammar is what makes it usable.\n\nAn S1 model does one thing: measure (judge, choose, score). Three kinds cover it:\n\n- Choice — group, classify, route (`choose` )\n- Score — a level on an ordered scale (`score` )\n- Yes / no, with a probability (`judge` )\n\nIt is not a free-form assistant for people (an LLM). It interfaces with code. Code-in-the-loop, not human-in-the-loop.\n\nThe most basic usage:\n\n```\nchat = { messages: [ \"How may I help you?\" ... ]}\nescalate_to_human if chat.judge? \"is the customer asking for a human agent?\"    # core extension on (c.primitives = true)\nescalate_to_human if (ψ chat).is? \"asking for a human agent\"                     # ψ on (c.psi = true)\n```\n\nUnder the hood, both are:\n\n```\nstate = S1::State.new(\"I have asked three times now. Can I just talk to a real person?\")\nescalate_to_human if state.judge?(\"Is the customer asking for a human agent?\")\ngem \"s1\"\n# config/initializers/s1.rb (or anywhere at boot)\nS1.configure do |c|\n  c.provider   = :typesafe                  # default; a name under Providers, or an instance\n  c.timeout    = 30                         # seconds per request\n  c.threshold  = 0.5                        # a noul at or above this reads as true\n  c.logger     = Rails.logger               # optional; debug lines per request, warns on retry\n  c.primitives = false                      # true extends String, Hash, Array; see \"Experimental\"\n  c.psi        = false                      # true defines ψ(x); see \"Experimental\"\n\n  c.typesafe.api_key     = ENV[\"TYPESAFE_API_KEY\"]   # default: read from the environment\n  c.typesafe.model       = \"jev-latest\"\n  c.typesafe.base_url    = \"https://api.typesafe.ai\"\n  c.typesafe.max_retries = 2                         # transient failures before raising\n\n  c.cua.checkpoint = \"cua-s1-forms\"                  # only when c.provider = :cua\n  c.laya.base_url  = \"http://127.0.0.1:8765\"         # only when c.provider = :laya (self-hosted)\nend\n```\n\nEvery value has a default; an initializer is only needed to change one. Each provider keeps its\nown settings under its name (`c.typesafe`, `c.cua`), declared by the provider class. Ruby ≥ 3.2.\nNo runtime dependencies.\n\nOne rule names every method in this README: **a verb measures and returns the distribution; a\nnoun returns the thing it names; a `?` returns a boolean.**\n\n**States carry verbs.** A state is anything a question can be asked about: an `S1::State`; a\nString, Hash or Array with the core extension on; a Rails record. A `ψ.` predicate carries the\nsame verbs unapplied.\n\n| verb | asks | returns | \n|---|---|---|\n| `judge` (`is` and`same_as` fill the question in) | \"Is this true?\" | `Answer::Noul` | \n| `choose` | \"Which of these?\" | `Answer::Choice` | \n| `score` | \"Which level?\" | `Answer::Score` | \n| `measure` (`ask` ,`batch` ,`ask_about` ) | several at once | `Result` | \n\n**Distributions collapse.** One contract: `collapse(threshold)`. The threshold only matters to a\nnoul, and a noul carries the one it was measured under (`S1::State.new(x, threshold: 0.9)`; the\nconfig's as of the measure otherwise — stamped then, not read at collapse; a noul built by hand\ncarries the config's as of its construction); the others accept and ignore it, so a `Result`\ncollapses every distribution\nthrough one call, each noul at its own threshold, and `to_h`, `true?` and `case … in` follow.\nEach kind names its collapse and exposes Ruby's own conversions.\n\n| collapsable | `collapse` returns | its own name | Ruby idioms | \n|---|---|---|---|\n| `Answer::Noul` | `true` /`false` | `true?` (`false?` ) | `!` so`!!` ,`to_f` ,`Comparable` ,`&``\\|``~` | \n| `Answer::Choice` | a Symbol | `choice` | `to_sym` ,`to_s` , loose`==` | \n| `Answer::Score` | an `S1::Level` | `level` | `key` (the most likely level's rank),`to_f` (weighted position),`levels` | \n| `Result` | `{ id => collapsed value }` | `to_h` | `deconstruct_keys` , so`case … in` | \n\n**Nouns name; `?` decides.** On every state:\n\n```\nx.noul(q)            == x.judge(q)                        # the dichotomous distribution, by its proper name — an Answer::Noul\nx.choice(q, **cats)  == x.choose(q, **cats).collapse      # the category picked — a Symbol\nx.level(q, *levels)  == x.score(q, *levels).collapse      # the point on the ordinal scale — an S1::Level\nx.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\n```\n\nThe asymmetry is deliberate. Only the dichotomous distribution has a proper name — *noul* — so\nits noun returns the distribution itself. The nominal and ordinal distributions have none, so\ntheir nouns can only name the category: `choice` is a Symbol, `level` is a point on the scale.\nAnd a `?` method returns a boolean — Ruby's convention — so `?` exists only for a noul: there is\nno `choice?` and no `judge?` on a Choice or a Score.\n\nEvery question is one of three kinds — the first three of Stevens' scales: dichotomous, nominal,\nordinal. Pick by what the answer *is*. Each kind has a verb that measures and a noun that names.\nThe examples use `state = S1::State.new(text)`; a `# ψ:` line gives the same call with ψ on. A\n`Noul` prints as its probability, so `# => 0.98` below is a `Noul` at 0.98.\n\n**noul — \"Is this true?\"** The dichotomous distribution: a probability, 0 to 1, over\n`{ true, false }`. The probability is the signal. `judge` measures it; `noul` is its name, and\nreturns the same thing; `judge?` (`noul?`) is true at or above the threshold; `is` / `is?` take a\nphrase and ask \"Is this …?\".\n\n``` js\nstate.judge(\"Is the customer asking for a human agent?\")    # => #<S1::Answer::Noul 0.98>  compares like a number\nstate.noul(\"Is the customer asking for a human agent?\")     # the same distribution, by its name\nstate.judge?(\"Is the customer asking for a human agent?\")   # => true  (at or above the threshold)\n# ψ: (ψ text).judge \"…\" / (ψ text).is? \"the customer asking for a human agent\"\n\n# Optional definition — what counts as yes / no:\nstate.judge(\"Has the customer contacted support about this before?\",\n            true:  \"mentions a prior attempt, ticket, or having asked before\",\n            false: \"no sign of any previous contact\")\n```\n\n**choice — \"Which of these categories?\"** The nominal distribution: mass over an unordered set.\n`choose` measures — the pick plus a distribution over every category; `choice` is the pick alone,\na Symbol.\n\n```\ndept = state.choose(\"Which team should handle this?\",\n                    returns:  \"Exchanges, refunds, wrong or damaged items\",\n                    shipping: \"Delivery status, delays, lost packages\",\n                    billing:  \"Charges, invoices, payment problems\")\ndept.to_sym          # => :returns\ndept[:shipping]      # => 0.0\ndept.confidence      # => 1.0\nstate.choice(\"Which team should handle this?\", returns: \"…\", shipping: \"…\", billing: \"…\")   # => :returns\n# ψ: dept = (ψ text).choose \"Which team should handle this?\", returns: \"…\", shipping: \"…\", billing: \"…\"\n```\n\nCategories can be built from the state itself. `people` is the list from the [TL;DR](#tldr); that\nexample asks one question over the whole list, this one shows the four ways to pass the categories\n(core extension on, so the Array is the receiver):\n\n```\ncategories = people.to_h { |person| [person[:name], \"Occupations: #{person[:occupations].join(\", \")}\"] }\n\npeople.choose \"The most skilled individual\", **categories                          # => #<S1::Answer::Choice Michael>  ([:Michael] # => 0.98)\npeople.choose \"The most skilled individual\", categories: categories                # same\npeople.choose \"The most skilled individual\", categories: people.map { _1[:name] }  # labels only, no descriptions\ncategories.choose \"The most skilled individual\"                                    # choosing among the evidence: the state IS the categories\n```\n\n`categories:` takes `{ category => description }` or a bare list; `choices:`, and the wire name\n`criteria:`, still work. With none given, a state of that shape — an Array of Strings, or a Hash\nwith String keys and all-nil or all-String values — is the scale. A Symbol-keyed Hash is a\nrecord, not candidates: `{ ticket: \"…\", customer: \"…\" }.choose \"Which team?\"` raises until it is\ngiven `categories:`. A String-keyed Hash of Strings — a parsed JSON record — has the candidates'\nshape and would be asked as one; give it `categories:`.\n\n**score — \"Which level?\"** The ordinal distribution: mass over an ordered spectrum, worst → best.\n`score` measures — the most likely level, and the probability-weighted position; `level` is the\nlevel alone: an `S1::Level`, the label, that knows its position.\n\n```\nsev = state.score(\"How severe is the reported issue?\",\n                  \"Cosmetic; no impact to functionality\",\n                  \"Broken or degraded feature, but a workaround exists\",\n                  \"Blocking issue; no workaround exists\")\nsev.level    # => \"Blocking issue; no workaround exists\"   an S1::Level\nsev.key      # => 2      the rank of the most likely level — sev.level.position (`index` is the legacy name); the wire's key is in raw\nsev.to_f     # => 1.68   (expectation: the weighted position across the levels, always derived from the masses)\nstate.level(\"How severe is the reported issue?\", \"Cosmetic…\", \"Broken…\", \"Blocking…\")   # => \"Blocking issue; no workaround exists\"\n# ψ: sev = (ψ text).score \"How severe is the reported issue?\", \"Cosmetic…\", \"Broken…\", \"Blocking…\"\n```\n\nA score collapses to a `Level`: a String — the label — that knows its position on its scale.\nThat one fact is what makes a level usable both where code wants a number and where it wants\ntext.\n\n```\nsev = state.score(\"How severe is the reported issue?\", \"Cosmetic…\", \"Broken…\", \"Blocking…\")\nlvl = sev.collapse            # => \"Blocking issue; no workaround exists\", an S1::Level\n\nlvl >= 1                      # => true    by position, not alphabet\nlvl >= \"Broken or degraded feature, but a workaround exists\"   # => true    a label on the scale, by its position\nlvl.position                  # => 2       (lvl.to_i is the same)\nlvl.scale                     # => #<S1::Scale Cosmetic… < Broken… < Blocking…>   the S1::Scale (lvl.labels: the Strings)\nlvl.index(\"no\")               # => 16      String's own methods are all still String's\nYAML.dump(lvl)                # => \"--- Blocking issue; no workaround exists\\n\"   the label alone; safe_load reads it back\n\ncase state.measure { |q| q.score :severity, \"How severe?\", \"Cosmetic\", \"Broken\", \"Blocking\" }\nin { severity: \"Blocking\" }   then page_someone      # the label matches — the literal spelling; see Scales for the pinned form\nin { severity: 1.. }          then open_ticket       # so does an integer range (`when 2..`, never a bare `when 2`)\nend\n\ntickets.sort_by(&ψ.score(\"How severe?\", \"cosmetic\", \"broken\", \"blocking\"))   # ordered by expected position\ntickets.max_by(&ψ.score(\"How severe?\", \"cosmetic\", \"broken\", \"blocking\"))\nby_level = tickets.group_by(&ψ.level(\"How severe?\", \"cosmetic\", \"broken\", \"blocking\"))\nby_level[\"blocking\"]          # keys are the labels; a plain String looks them up\n\n\"severity: #{lvl}\"            # a String: interpolates\nticket.update!(severity: lvl) # stores as the label\n{ severity: lvl }.to_json     # => {\"severity\":\"Blocking issue; no workaround exists\"}\n```\n\nOne caveat. Put the level on the left of a comparison with a label: `lvl >= \"Broken…\"` is\nby position, `\"Broken…\" <= lvl` is String's own compare, by alphabet. Ranges of integers match\na level; ranges of labels do not.\n\nA label spelled twice is a foot-gun. The question declares `\"blocking\"`; somewhere else the\nprogram compares against it; then someone renames the level at the declaration:\n\n```\n# app/questions.rb — the declaration, renamed today\nSeverity = S1.scale :cosmetic, :degraded, :blocked                 # was :blocking\nlvl = (ψ text).level \"How severe?\", Severity\nr   = (ψ text).measure { |q| q.score :severity, \"How severe?\", Severity }\n\n# app/routing.rb — the literal spellings: silently false, and they stay false\npage_someone if lvl == \"blocking\"                                  # false: no error, no page\ncase lvl\nwhen \"blocking\" then page_someone                                  # falls through\nend\ncase r\nin { severity: \"blocking\" } then page_someone                      # never matches …\nin { severity: 1.. }        then open_ticket                       # … so a blocked issue opens a ticket\nend\n\n# the same three through the scale: each raises at the reference, the day of the rename\npage_someone if lvl == Severity[:blocking]                         # KeyError: :blocking is not on the scale (cosmetic, degraded, blocked)\npage_someone if lvl.blocking?                                      # NoMethodError\ncase r\nin { severity: ^(Severity.fetch(:blocking)) } then page_someone    # KeyError — pinned; unpinned, `in { severity: Severity[:blocking] }`\nend                                                                # is Ruby's array pattern on the constant and silently never matches\n```\n\n`S1.scale` makes the scale a value — the labels, ordered or not, with their definitions — so\nthe label is spelled once and every other mention is a lookup that fails loud. A question takes\nthe scale where the list or hash went:\n\n```\nSeverity = S1.scale :cosmetic, :degraded, :blocking                # ordinal: order is rank\nSeverity                                                           # => #<S1::Scale cosmetic < degraded < blocking>\nlvl = state.level(\"How severe?\", Severity)                         # => \"blocking\", an S1::Level on Severity\nlvl.scale.equal?(Severity)                                         # => true\n\nSeverity[:degraded]                                                # => \"degraded\"   an S1::Level, position 1; Severity[:sever] raises KeyError\nSeverity.fetch(:degraded)                                          # the same: \":sever is not on the scale (cosmetic, degraded, blocking)\"\nSeverity.to_a[1]                                                   # by position — [] is by label or key, never an index\nlvl.blocking?                                                      # => true        a predicate per label, by its snake-cased key (Severity.keys)\nlvl >= Severity[:degraded]                                         # => true        by position\nlvl < S1.scale(:low, :high)[:high]                                 # ArgumentError: different scales\ncase lvl when Severity then … end                                  # === is membership: a Level of this scale, or a label / key on it\nSeverity.to_a                                                      # the Levels; Enumerable, so Severity.max, Severity.map(&:position)\n\nTeam = S1.scale returns: \"Exchanges, refunds, wrong or damaged items\",   # nominal: label => definition\n                billing: \"Charges, invoices, payment problems\"\nTeam                                                               # => #<S1::Scale returns | billing>\ndept = state.choose(\"Which team should handle this?\", categories: Team)   # the Scale where the hash went\ndept.scale.equal?(Team)                                            # => true\nTeam[:billing]                                                     # => :billing    a nominal category is a Symbol; the scale checks it\nTeam.fetch(:biling)                                                # KeyError: :biling is not on the scale (returns, billing)\nTeam.fetch(row[\"team\"]) == Team[:billing]                          # a String from a column or the wire is a category once fetched — never `row[\"team\"] == Team[:billing]`\nTeam.definitions                                                   # => { \"returns\" => \"Exchanges, …\", \"billing\" => \"Charges, …\" }\n\ncase state.measure { |q| q.score :severity, \"How severe?\", Severity; q.choose :team, \"Which team?\", categories: Team }\nin { severity: ^(Severity.fetch(:blocking)) } then page_someone   # a pinned reference: a typo here raises\nin { team: ^(Team.fetch(:billing)) }           then route_to_billing\nend\n\ntickets.group_by(&ψ.level(\"How severe?\", Severity))              # keys are Levels on Severity; by_level[Severity[:blocking]]\n```\n\n`S1.scale(*labels)` is ordinal, `S1.scale(**definitions)` nominal (`ordered: false` makes a bare\nlist nominal, `ordered: true` a hash ordinal — its definitions are then what the model sees as\nthe levels, and the distribution speaks the labels); `name:` is what messages print, so a\ncategory named `name` or `ordered` goes in a positional Hash (`S1.scale({ name: \"…\", email: \"…\" })`;\nbeside keyword definitions `name:` is refused). Two scales are equal by kind and labels (a\nnominal one as a set; definitions and name are not identity — the digest carries them); a Scale\nis not an Array, and `==` against one is false (since 0.3.0 `Distribution#scale` and\n`Level#scale` are an `S1::Scale`, not an Array: compare with `.to_a` / `.labels`, index with\n`to_a[i]`). Labels are Strings, Symbols or Integers (a Scale, a list or a Hash beside other\nlabels is refused — `S1.scale(Severity)` raises), distinct and non-blank; so are the\ndefinitions, which are text (`Team.definition(:billing)` reads one; `definitions` / `to_h` are\nkeyed by label String), and labels do not mix with definitions (`S1.scale(\"a\", b: \"…\")` raises).\nA label whose key spells a method the Level already has (`\"empty\"`, `\"frozen\"`; `\"present\"`,\n`\"blank\"` under ActiveSupport) keeps that method's answer on the Level — reference it through\nthe scale, or a record's `<field>_<key>?`. A Level of another scale is off this one: `include?`\nsays false and `fetch` raises `KeyError`. A dynamic scale — `S1.scale(-> { … })` or\n`S1.scale(:method)` — holds its source and reads as dynamic until `resolve(record)` — a list\nresolves ordinal, a Hash nominal, unless `ordered:` said, and a Scale of the other kind than\n`ordered:` said raises, as a dynamic one does (in Rails, the declaration evaluates it per\nrecord, by the macro's kind); every other reader raises until then.\nA score given a Scale beside other levels is refused, as is a choose. The inline forms\nstill work and build a Scale: `state.score(\"…\", \"low\", \"mid\", \"high\").scale` is\n`#<S1::Scale low < mid < high>`, equal to `S1.scale(:low, :mid, :high)` — the difference is where\nthe labels are spelled. A label whose snake-cased key collides with another's (`\"A+\"`, `\"A-\"`),\nor has none (`\"🔥\"`), gets no key and no predicate; `Severity[\"A+\"]` still reaches it. Marshal\nkeeps a Scale whole; a YAML round trip reads its categories back as plain Strings.\n\nEvery measurement returns an `S1::Distribution` (`Answer::Base` remains an alias): mass over\nevery category of the scale, summing to 1 (`probabilities`, keyed by the wire name of each\ncategory), plus `scale` — an `S1::Scale` for a choice or a score, `[true, false]` for a noul —\nand `kind` — the scale kind by its wire name. *Wire* is what crosses to and from a provider; wire names are the provider's,\nthe rest of the vocabulary is ours.\n\n``` js\nstate.judge(\"Is the customer angry?\").scale                        # => [true, false]\nstate.judge(\"Is the customer angry?\").kind                         # => \"noul\"\nstate.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\nstate.choose(\"Which team?\", returns: \"…\", billing: \"…\").kind       # => \"choice\"\nstate.score(\"How severe?\", \"low\", \"mid\", \"high\").scale             # => #<S1::Scale low < mid < high>   to_a is the S1::Levels\nstate.score(\"How severe?\", \"low\", \"mid\", \"high\").kind              # => \"score\"\n```\n\nA measurement is always one of the three kinds — `judge`, `choose`, `score`; the kind is part\nof what a measurement is. `measure` is the plural: the same three, several at once.\n\n```\nmeasure  =  judge | choose | score              one measurement, one kind\nmeasure { judge; choose; score; … }             several, one call, independent\n```\n\nTwo things make the plural more than a loop. **One call**: the state goes over once, and N\nquestions cost one call. **Independence**: the model answers each question as if it were the\nonly one — one distribution is never hidden context for another. That is what makes the batch the\nnatural unit: several *independent* measurements of one state. It is not independence of what\nthey measure: `&` and `|` on the results are products of marginals, exact only when the two\nproperties are independent given the state — for overlapping properties, ask the conjunction as one\nquestion. `ask`, `batch` and `ask_about` are aliases of `measure`. It is the only way to get\nseveral distributions from one call — a verb per line is a call per line.\n\n```\nresult = state.measure do |q|        # ψ: (ψ text).measure do |q|\n  q.judge  :escalate,   \"Is the customer asking for a human agent?\"\n  q.judge  :repeat,     \"Has the customer contacted support about this before?\",\n                        true: \"mentions a prior attempt\", false: \"no sign of one\"\n  q.choose :department, \"Which team should handle this?\", returns: \"Refunds\", shipping: \"Delays\", billing: \"Charges\"\n  q.score  :severity,   \"How severe is the issue?\", \"Cosmetic\", \"Degraded, workaround exists\", \"Blocking\"\nend\n\nresult[:escalate].true?        # => true\nresult[:department].to_sym     # => :returns\nresult[:severity].level        # => \"Blocking\"\nresult.distributions           # => { escalate: #<S1::Answer::Noul …>, … }   (result.answers still works)\nresult.usage                   # => { input_tokens: 490, output_tokens: 86 }\nresult.duration_ms             # => 398\n```\n\nAsk **speculatively**: include questions whose answers you only need conditionally, then let\nyour code decide which to use. That keeps it to one call.\n\n```\nr = state.measure do |q|             # ψ: (ψ text).measure do |q|\n  q.judge :is_lead,  \"Is there a potential new case or matter?\"\n  q.judge :qualified, \"Is this a qualified lead, based on `firm.criteria`?\"\n  q.judge :prior_rep, \"Does the lead already have an attorney?\"   # asked regardless,\nend                                                               # used only when relevant\n\nif r[:is_lead].true? && r[:qualified] >= 0.85\n  flag_conflict if r[:prior_rep].true?\nend\n```\n\nA state can be a string, or a hash/array (braced — bare keywords to `State.new` are options,\nnot evidence). A measurement is not evidence either: a distribution or a `Result` placed in the\nstate or a lens is refused (`ValidationError`) — put its `collapse` or its `probabilities` there.\nWith a hash, instructions can point at fields with backticked paths:\n\n```\nstate = S1::State.new({\n  transcript:   utterances,\n  case_details: { date_of_incident: \"2026-08-15\", sol_deadline: \"2027-08-15\" }\n})\n\nstate.judge?(\"Judging from `transcript` and `case_details.sol_deadline`, is the claim still within the statute of limitations?\")\n# ψ: (ψ({ transcript: utterances, case_details: {...} })).is? \"still within the statute of limitations, judging from `transcript` and `case_details.sol_deadline`\"\n```\n\nThe hash is rendered once, when the state is built — `state.rendered` is that value, frozen —\nso appending to `utterances` afterwards does not change what any later question sees.\n\nInstructions can be structured too — the verification pattern:\n\n```\nstate = S1::State.new({ source_text: \"Invoice #4471 issued March 3, 2026 to Beaver Dam Logistics for $12,840.00, net 30.\" })\n\nstate.judge?({ field: { name: \"invoice_number\", type: \"string\", description: \"The identifier printed on the invoice.\" },\n               extracted_value: \"4471\",\n               question: \"Does `extracted_value` match the `field` as it appears in `source_text`?\" })\n# ψ: (ψ({ source_text: \"…\" })).judge?({ field: …, extracted_value: \"4471\", question: \"…\" })\n```\n\nAll distributions carry `probabilities` and `confident?`; use it to route — act automatically when\nconfident, escalate to a person or a reasoning model when not. Choice and score distributions carry\nthe provider's `confidence`, and `confident?(at)` is that confidence at or above `at` — and true when\nthe provider reported none (`confidence` is nil), so read `confidence` itself to escalate those. A noul has\nno separate confidence — distance from the threshold is it: `confident?(margin = 0.1)` is at least\n`margin` away from the threshold (its own, or `threshold:`), on either side; `undecided?` is its\nexact complement, and `decided?` is its alias — the name that says the positional is a margin, not\nthe floor `confident?(at)` takes on a choice or a score. (The positional is the margin: the pre-theory `confident?(threshold)`,\n`p >= t || p <= 1 - t`, was symmetric about 0.5, not about the threshold the noul was measured\nunder, and is gone — `confident?(0.8)` on a 0.85 noul is now false, 0.35 from a 0.5 threshold\nbeing under that margin.)\n\n| distribution | reads as | \n|---|---|\n| `Answer::Noul` | `to_f` ,`true?` /`false?` , compares to numbers (`a >= 0.85` ) | \n| `Answer::Choice` | `to_sym` ,`to_s` ,`[category]` ,`== :returns` ,`ranked` | \n| `Answer::Score` | `level` (an`S1::Level` ),`key` ,`to_f` ,`levels` | \n\n```\ndept = result[:department]\nif dept.confident?(0.8)\n  route_to(dept.to_sym)\nelse\n  hold_for_review(dept.probabilities)\nend\n```\n\nEverything above works through `S1::State`. The pieces below are opt-in — off by default,\neach behind its own config switch — and exist to make asking a question feel native to the\nlanguage. Use them where they read better; leave them off where a codebase would rather not\nextend core classes.\n\n**The core extension.** `c.primitives = true` extends `String`, `Hash` and `Array` with\n`judge` / `judge?` (`noul`, `noul?`, `ask?`), `choose` / `choice`, `score` / `level` and\n`measure` (`ask`, `batch`, `ask_about`). A class's own method with one of those names always\nwins, since the module is included beneath it. `to_s1` gives the State, for per-call options.\n\n```\nS1.configure { |c| c.primitives = true }      # String, Hash, Array; or a subset: [String]\nrequire \"s1/core_ext\"                                # equivalent, require-style\n\n\"Can I speak to a person?\".judge? \"the customer is asking for a human agent\"\n{ ticket: text }.choose \"Which team?\", returns: \"Refunds\", billing: \"Charges\"\nchat.measure { |q| q.judge :escalate, \"...\" }\nchat.to_s1(threshold: 0.9).judge?(\"...\")             # the State, for per-call options\n```\n\n**ψ.** `c.psi = true` defines **ψ** on `Kernel`, like `Integer()` or `Pathname()`: anything\nbecomes a State. ψ prepares; the verbs measure; the `?` on whatever follows is the collapse. Any\nidentifier works in its place (`c.psi = \"⍣\"`). Anything that defines `to_s1` converts itself —\nthat is how a typesafe-rails record becomes its default form. The explicit spelling is\n`S1.to_state(x)` — `x.to_s1` when `x` defines it, otherwise `S1::State.new(x)`.\n\nIs that just `is?` for the sake of reading like English? No — ψ is the convention the rest of\nthis section hangs off:\n\n- **any object** , not the three core classes the extension covers: a record,`params` , a`Mail::Message` ,\na Struct, a Time —`(ψ mail).is? \"an out-of-office reply\"` ;\n- **options at the point of asking** —`(ψ text, threshold: 0.95)` ,`provider:` ,`model:` ,`owner: phone_call` for the ledger; on a verb —`measure` included —`given:` is the fluent`.given(…)` inline\n(`choose(\"…\", a: \"A\", given: { p: 1 })` ,`(ψ text, given: { p: 1 })` ,`(ψ state, given: { p: 1 })` ), and`threshold:` belongs to a collapse alone\n(`judge?` ,`is?` ,`same_as?` — on`choose` /`score` /`judge` /`measure` it is an`ArgumentError` , never an option);\n- **a state** —`state = (ψ x)` is rendered once and asked many times; nothing about`x` is re-read\nbetween questions, and mutating`x` afterwards changes nothing the state will be asked about;\n- **the English forms** that live only on`State` :`is?` ,`is` ,`same_as?` ,`same_as` ;\n- **no monkeypatching** — one private`Kernel` method;`c.primitives` can stay off and code still\nreads as sentences.\n\n``` js\n(ψ\"Michael\").is? \"a man's name\"          # => true\n(ψ\"Michael\").is \"a man's name\"           # => 0.98\n(ψ text).is? \"a man's name\"              # a variable needs the space: ψtext is one identifier\n```\n\nThat is all ψ does: `(ψ x)` is `S1.to_state(x)` — `x.to_s1` when `x` defines it, otherwise\n`S1::State.new(x)` — and `ψ.` with no evidence builds a\n[predicate](#collections-judgments-as-predicates). It takes no block.\n\n**Pattern matching.** A batch `Result` deconstructs: nouls as booleans, each at its threshold, choices as\nsymbols, scores as Levels — which match an integer range and a label alike — so Ruby's own\n`case … in` is the gate logic.\n\n```\ncase (ψ chat).measure { |q| q.judge :escalate, \"…\"; q.choose :department, \"…\", returns: \"…\", billing: \"…\"; q.score :severity, \"…\", \"low\", \"mid\", \"high\" }\nin { escalate: true, severity: 2.. }  then page_someone\nin { department: :billing }           then route_to_billing\nelse                                       hold_for_triage\nend\n```\n\n**Semantic equality — with the right operator.** `(ψ \"Acme Inc\").same_as? \"ACME, Incorporated\"` asks\n\"Do `this` and `other` describe the same thing?\" (`same_as` for the distribution). The operator\nform is `===`, case equality — Ruby's \"does this match?\", the one `Range`, `Regexp` and `Proc`\ndefine, called only in explicit matching contexts:\n\n```\ncase vendor.name\nwhen (ψ \"Acme Inc\") then merge_into(acme)\nend\n\nnames.grep(ψ \"Acme Inc\")            # every name that describes the same company\nnames.any?(ψ \"Acme Inc\")\n```\n\nNot a regex. A regex (or `similarity()`) compares strings; `same_as?` compares what the strings\nare about. Two calls about one accident, transcribed a week apart:\n\n```\ntranscript = \"Caller: Hi, this is Juan, I was rear-ended on Michael Street on the fifteenth, my neck hurts, the other driver ran the light.\"\nrecent     = [\"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.\",\n              \"Caller: This is Maria, I slipped at the grocery store on Manor Street last week and hurt my knee.\",\n              \"Caller: Juan Perez, I want to know if you handle wills.\"]\n\nrecent.any?(ψ transcript)                             # => true   (one run: 0.67 / 0.03 / 0.06)\nrecent.any? { |t| t =~ /Juan.*Michael Street/ }       # => false  — \"John\", \"Manor Street\", \"August 15th\"\n```\n\nJuan/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.\n\nWhy not `==`? Ruby calls `==` for you — inside `Hash#[]`, `Array#include?`, `uniq`, RSpec's `eq` —\nso a model-backed `==` puts a network call, a cost and a non-deterministic answer into every one of\nthose, and `==` is expected to be reflexive, symmetric and transitive, which a judgment is not.\n`===` carries none of that: it is only ever a question about a match. `==` on a State is plain Ruby equality — never a model call.\n\n**Aliases.** The batch is `measure`, `ask`, `batch` or `ask_about` — `(ψ chat).ask_about { |q| … }` reads\nbest when the state is right there. Every alias is a plain Ruby `alias`; see the [dictionary](#dictionary-and-aliases).\n\nTyping ψ on macOS: add the *Unicode Hex Input* keyboard (System Settings → Keyboard → Input\nSources → +), switch to it, then hold Option and type `03C8`.\n\n**Explicit vs native.** Every form has an explicit spelling; the native one is the same call\nwith the plumbing removed.\n\n| you want | explicit | native | \n|---|---|---|\n| a yes/no | `S1::State.new(text).judge?(\"Is the customer angry?\")` | `(ψ text).is? \"angry\"` ·`text.judge? \"…\"` | \n| the noul | `S1::State.new(text).judge(\"…\")` | `(ψ text).judge \"…\"` | \n| the probability | `S1::State.new(text).judge(\"…\").to_f` | `(ψ text).judge(\"…\").to_f` | \n| one of a set | `S1::State.new(text).choose(\"Which team?\", returns: \"…\", billing: \"…\")` | `text.choose \"Which team?\", returns: \"…\", billing: \"…\"` | \n| the category alone | `S1::State.new(text).choose(\"…\", **cats).to_sym` | `text.choice \"…\", **cats` | \n| a level on a spectrum | `S1::State.new(text).score(\"…\", *levels).level` | `text.level \"…\", *levels` | \n| the best of a list | `S1::State.new(list).choose(\"the best\", categories: labels)` | `(ψ list).choose \"the best\", categories: labels` | \n| several at once | `S1::State.new(text).measure { \\|q\\| … }` | `(ψ text).measure { \\|q\\| … }` ·`text.measure { \\|q\\| … }` | \n| filter a stream | `list.select { \\|x\\| S1::State.new(x).judge?(\"…\") }` | `list.select(&ψ.is?(\"…\"))` | \n| bucket a stream | `list.group_by { \\|x\\| S1::State.new(x).choose(\"…\", **cats).to_sym }` | `list.group_by(&ψ.choice(\"…\", **cats))` | \n| rank a stream | `list.sort_by { \\|x\\| S1::State.new(x).score(\"…\", *levels).to_f }` | `list.sort_by(&ψ.score(\"…\", *levels))` | \n| 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)` | \n| against a lens | `S1::State.new({ this: x, prefs: p }).judge?(\"… per prefs\")` | `(ψ x).given(prefs: p).is? \"… per prefs\"` | \n\n**What Ruby will not do.** `ψ\"Michael\".is? \"…\"` without parens is one method call whose argument is\n`\"Michael\".is?(\"…\")` — the dot binds before any prefix, symbol or operator — so the parens around the\nevidence are load-bearing: `(ψ\"…\")`, `(ψ text)`. A hash literal needs its own parens too — `(ψ({ … }))` — because\n`ψ { … }` is a block to Ruby, and ψ takes none. And `is?` (the English form: \"Is this …?\") lives on `State` only, never on\ncore classes, where it would sit beside equality methods.\n\nCollapse is one operation, and it is lossy. The measurement returns a distribution; turning it\ninto a boolean, a symbol or a level throws the rest away. Both layers are tools, and the\nconvention that separates them is Ruby's own: **no `?` keeps the probability, `?` collapses.**\n`collapse` is the same step with a name, for when you want to see it. There is no second symbol\nfor it because Ruby already has one — the trailing `?` — and a reader who has never seen this\ngem reads `judge` / `judge?`, `is` / `is?` correctly (see [The idea](#the-idea-collapse)).\n\n**What the collapse hides.** Three calls, one question, one boolean each:\n\n```\nclear = \"I was rear-ended yesterday, the other driver ran a red light and got a ticket, my neck hurts.\"\nmurky = \"there was a fender bender, not sure who was at fault, I feel a bit sore maybe.\"\nnone  = \"I want to know your office hours.\"\n\n(ψ clear).judge \"Does the caller have a viable injury claim?\"    # => 0.84   collapse → true\n(ψ murky).judge \"Does the caller have a viable injury claim?\"    # => 0.52   collapse → true\n(ψ none).judge  \"Does the caller have a viable injury claim?\"    # => 0.03   collapse → false\n```\n\n`clear` and `murky` collapse to the same `true`. What the boolean threw away: that one is a\ncase and the other is a coin flip — the difference between \"call them now\" and \"have someone\nlook\". `(ψ murky).judge(…).undecided?(0.15)` is `true`; `collapse` cannot say so. Every\ndownstream count, dashboard and decision built on the booleans inherits that erasure. The\ndistribution is the information; the collapse is a summary of it — take it last.\n\n**The five conventional ways to use the distribution** — three keep it, and are plain Ruby;\ntwo end it, and are `?` s:\n\n|  | you write | what it is | \n|---|---|---|\n| compose | `r[:is_lead] & r[:qualified] & ~r[:prior_rep]` | products of marginals, exact only for independent properties — keeps the distribution | \n| route | `case p when 0.85.. then … when 0.5...0.85 then … else … end` | `Comparable` + ranges — keeps it | \n| count | `calls.sum(&ψ.judge(\"…\"))` | expected count from calibrated probabilities — keeps it | \n| abstain | `p.undecided?(0.1)` → hand off | a `?` whose answer is \"not by me\" | \n| collapse | `p.collapse` ,`p.true?` ,`judge?` ,`is?` ,`choice` ,`level` (an`S1::Level` ),`r.collapse` /`r.to_h` ,`case r in { … }` | the decision — ends it | \n\n```\nr = (ψ transcript).measure do |q|\n  q.judge :is_lead,   \"Is this a potential new personal-injury client?\"\n  q.judge :qualified, \"Was the caller not at fault and injured?\"\n  q.judge :prior_rep, \"Does the caller already have an attorney?\"\nend\n```\n\n**Compose before you collapse.** `&` both, `|` either, `~` not — products of marginals. jev\ndocuments batched distributions as independent in the sense that no answer is context for another;\nthe products are exact only when the two properties are independent given the state, which the\nprovider does not promise. For overlapping properties (`is_lead` and `qualified` nest), ask the\nconjunction as one question.\n\n```\nviable = r[:is_lead] & r[:qualified] & ~r[:prior_rep]     # still a Noul\nr[:is_lead] & 0.5                                           # a number in 0..1 is a marginal already known; nil or true raises\n```\n\n**Route on the number, not the bit.** A Noul compares like a Float, so `case`/` when` with\nranges is the routing table — three outcomes from one probability, where a boolean gives two.\n\n```\ncase viable\nwhen 0.85..      then call_now        # act automatically\nwhen 0.5...0.85  then queue_review    # a person decides\nelse                  archive\nend\n```\n\n**Abstain near the threshold.** `undecided?(margin)` is \"too close to call\": hand off instead of\ncollapsing. `decided?(margin)` — `confident?(margin)` is the same method — is its exact complement\non a noul; `confident?(at)` is the counterpart for choices and scores, a floor on the provider's\nconfidence rather than a margin.\n\n```\nreturn hold_for_human if viable.undecided?(0.1)\n```\n\n**Count without collapsing.** A sum of calibrated probabilities is an expected count; a count of\ncollapsed booleans rounds every 0.6 up and every 0.4 down.\n\n``` js\ncalls.sum(&ψ.judge(\"Is the customer angry?\"))     # => 37.4 expected angry calls\ncalls.count(&ψ.is?(\"an angry customer\"))          # => 41, with the rounding baked in\n```\n\n**Then collapse, explicitly.** `collapse` on a distribution or a whole `Result`; `?` on a method;\n`!!` on a noul (`!noul` is \"not true at the threshold\"); `case … in { escalate: true }` on a\nbatch. All four are the same step. What is *not* a collapse: a bare `if noul` — Ruby's `if`\nnever calls `!`, so a distribution is always truthy there.\n\n``` js\nviable.collapse            # => true          (at the noul's own threshold — the State's, else the config's when measured — or pass one)\nviable.collapse(0.9)       # => false\nr.collapse                 # => { is_lead: true, qualified: true, prior_rep: false }   (r.to_h is the same)\n```\n\nA choice collapses to its Symbol, a score to its `S1::Level` (the label, ordered by position),\nand a `Result` to a Hash of all three.\n\nS1 models are at their best over *streams* — transcripts, tickets, candidates, calls — where\neach item gets the same typed question and the distributions are calibrated enough to filter,\nbucket, rank and count on. This section is the data-science surface: `Enumerable` with\njudgments in the blocks.\n\nψ with no argument is a question not yet applied to a state — a **predicate**, with `to_proc` and\n`===`, so it goes wherever Ruby expects a block or a pattern. Applied to an element it follows\nthe naming rule: a verb yields one distribution per element — `judge` a noul (which sums as its\nprobability), `choose` a choice (which buckets by category), `score` a score (which sorts by\nexpected position) — a noun the thing it names (`choice` the Symbol, `level` the `S1::Level`,\nso `group_by` keys by category or label), a `?` a boolean. `measure(x)` on a predicate is the\ndistribution under any name. A verb in a boolean slot is always truthy — a distribution is\nnever `false` — so `select`, `grep`, `count`, `find` and `partition` take the `?` form; `sum`\nand `sort_by` take the verb; `group_by` takes the noun.\n\n```\nangry = ψ.is?(\"an angry customer\")\n\ncalls.select(&angry)                                   # filter        (or calls.grep(angry), via ===)\ncalls.partition(&ψ.is?(\"a new matter\"))\ncalls.count(&ψ.judge?(\"Was it resolved on the call?\"))\ninbox.find(&ψ.is?(\"a cancellation request\"))\n\ncalls.group_by(&ψ.choice(\"Which team?\", returns: \"Refunds, exchanges\", shipping: \"Delivery, damage\", billing: \"Charges\"))\n# => { returns: [...], shipping: [...] }               # classify\n\ntickets.sort_by(&ψ.score(\"How urgent?\", \"can wait\", \"today\", \"right now\")).reverse   # rank\ntickets.max_by(&ψ.score(\"How severe?\", \"cosmetic\", \"degraded\", \"blocking\"))\n\ncalls.sum(&ψ.judge(\"Is the customer angry?\"))          # => 2.0   expected count, no threshold\ncalls.sum(&ψ.judge(\"…\")) / calls.size                  # share\n\ncalls.each_with_object([]) { |c, seen| seen << c unless seen.any?(ψ c) }   # dedupe, via ===\n```\n\n**The lens.** A stream is judged *against* something — a firm's acceptance criteria, a role's\nrequirements, a return policy. That is the lens, and it attaches to the state: `given` (or\n`against` — \"judged against\") puts the element under `this` and the lens beside it, so\ninstructions can name both (a lens keyed `this`, or `other` on `same_as`, is refused — it would\nreplace the facts). It is distinct from the definition, which attaches to the question:\n`true:`/` false:` on a noul, the descriptions of a choice, the levels of a score. (In\ntypesafe-rails a form does the lens's job: `measurable_as(:qualification) { { transcript:, preferences: } }`.)\n\n```\nqualifications = { must_have: [\"5+ years Ruby\", \"shipped a Rails app\"], disqualifiers: [\"cannot work US hours\"] }\n\nqualified = ψ.is?(\"qualified for the role, per `qualifications`\", given: { qualifications: qualifications })\ncandidates.select(&qualified)                                                # => Michael, Dana\n\ncandidates.group_by(&ψ.choice(\"Per `qualifications`, which bucket?\",\n                              qualified: \"meets every must_have, no disqualifier\",\n                              disqualified: \"hits a disqualifier\",\n                              unclear: \"not enough information\",\n                              given: { qualifications: qualifications }))    # => { qualified: [...], disqualified: [Bob] }\n\nS1::State.new({ candidates: candidates, qualifications: qualifications })\n  .choose(\"the candidate most qualified per `qualifications`\", categories: %w[Michael Bob Dana]).ranked   # one call\n```\n\nSame shape for intake: calls stream → the firm's qualification preferences → qualified / disqualified / unclear, and every other judgment the firm configures.\n\nTwo of these deserve a note. **Expected counts**: nouls are calibrated probabilities, so their sum\nis the expected number of positives — a fractional headcount with no cutoff bias, where\n`count(&ψ.is?(…))` would round every 0.6 up and every 0.4 down. **Ranking a set** is one call, not\nN: `choose` over the items returns a distribution over all of them, and `ranked` reads it out.\n\n```\nS1::State.new(calls).choose(\"the call most likely to become a chargeback\").ranked\n# => [:\"Caller: …\", …]   the transcripts as Symbols, most likely first; one call for the whole list\ncandidates.choose(\"the most qualified for this role\", categories: names).ranked.first   # add given: for the role's requirements\n```\n\nCost model: every `&predicate` is one call per element (~400ms, run in parallel where you can —\ntypesafe-rails' `where_judged` takes `concurrency:`); prefer one `choose` over the items when the\nquestion is \"which of these\", and reserve per-element predicates for \"which of these are\".\n\n**One question, one or many.** A predicate is the question as a value, so define it once and\napply it to a single state with `[]` (what `Enumerable` sees: the distribution for a verb, the thing\nnamed for a noun, a boolean for a `?`) or `measure` (always the distribution), and to a stream with\n`&`. No question text is written twice.\n\n```\nteam  = ψ.choice \"Which team?\", returns: \"Refunds\", support: \"Product help\", billing: \"Charges\"\nangry = ψ.judge  \"Is the customer angry?\"\n\nteam[ticket]                          # => :returns\nteam.measure(ticket).probabilities    # => { \"returns\" => 0.91, \"support\" => 0.09, \"billing\" => 0.0 }\ntickets.group_by(&team)               # => { returns: [...], support: [...] }\n\nangry[ticket]                         # => Answer::Noul, 0.98\ntickets.sum(&angry)                   # => 2.29\n```\n\nPredicates are built by `ψ` with no argument, or without ψ by `S1.predicates`.\nInside `select(…)`, write the predicate with parentheses — `&ψ.is?(\"…\")` — Ruby's grammar does not\nallow a command call after `&`.\n\nThe terms are defined in [THEORY.md](https://github.com/innocentdiaz/s1_ruby/blob/master/THEORY.md); this is the short form, with the code names.\nThe classes, one per idea:\n\n```\nψ(x)         S1::State            the state — judge · choose · score · measure\n             S1::Predicate        a question not yet applied to a state (ψ.is? \"…\"), for select / group_by / sum\n             S1::Question::*      the question on the wire: Noul | Choice | Score\n             S1::Providers::*     who measures (TypeSafe's jev, Laya, cua-s1-forms, the Stub)\n\nmeasure →    S1::Distribution     what comes back: mass over the scale, and collapse\n               Answer::Noul         the dichotomous distribution    collapse → true / false   (the `?`)\n               Answer::Choice       the nominal distribution        collapse → the category, a Symbol\n               Answer::Score        the ordinal distribution        collapse → the level (S1::Level)\n               Result               several, from measure { }       collapse → { id => value }; pattern-matches\n             S1::Level            the label, a String that knows its position on the scale\n             S1::Scale            the scale as a value: the labels, ordered or not, with their definitions (S1.scale)\n```\n\nEvidence is prepared into a state; a state is measured by a question; measuring returns a distribution over a scale; collapsing picks a category.\n\n| term | meaning | code | \n|---|---|---|\n| **evidence** | the particular — a transcript, a record, a list of candidates — before any presentation | any object | \n| **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)` with`c.psi = true` ;`x.to_s1` ;`rendered` is the value | \n| **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` | \n| **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` | \n| **prepare** | ψ — makes evidence measurable: renders once and attaches the lens. Measures nothing | `S1::State.new` ,`ψ(x)` ,`to_s1` , a Rails form | \n| **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, a`Result` | \n| **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` | \n| **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 | \n| **lens** | evidence added to the state to judge *against* ; makes the judgement relative | `given(…)` /`against(…)` ;`given:` on any verb (`measure` too), predicate, or`State.new` | \n| **question** | a concept on a scale, with its definition; immutable; the wire form | `S1::Question::Noul` /`Choice` /`Score` :`{ type, instructions, criteria }` | \n| **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's`categories` are the labels; a question's`levels` are the texts shown (the labels when the scale has no definitions), a distribution's`levels` the Levels | \n| **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 word`criteria:` ) on a choose;`*levels` on a score; a Scale's`definitions` | \n| **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 takes`categories:` | \n| **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) | \n| **collapsable** | anything that holds a distribution and can collapse | `S1::Collapsable` : every distribution, and a`Result` elementwise | \n| **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 noul`confident?(margin)` /`decided?` , the complement of`undecided?` | \n| **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` | \n| **level** | a point on an ordinal scale — a label with a rank, on a scale it knows | `S1::Level` : a String with`position` (`to_i` ),`scale` (an`S1::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 | \n| **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?` is`judge?` | \n| **choice** | the category a choose picked | `Answer::Choice#choice` , a Symbol;`x.choice(q, …) == x.choose(q, …).collapse` | \n| **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)` | \n| **stream** | many particulars under one question; filtering, ranking, bucketing and counting are the collection's own operations | any `Enumerable` with`&predicate` | \n| **form** | a named rendering of a persistent particular | typesafe-rails `measurable_as(:name) { … }` | \n| **provider** | an implementation of *measure* that honours calibration; owns its transport and wire format | `call(Request) → Result` :`:typesafe` ,`:laya` ,`:cua` , the`Stub` | \n| **result** | a batch of distributions from one measure, plus telemetry | `S1::Result` :`distributions` (`answers` is an alias),`usage` ,`model` ,`provider` ,`duration_ms` ,`raw` | \n| **request** | what a provider receives: rendered state, questions, options | `S1::Request` | \n| **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 | \n| **core extension** | String, Hash and Array as receivers of the verbs — sugar | `c.primitives = true` , or`require \"s1/core_ext\"` | \n| **psi** | the setting that installs ψ | `c.psi = true` (`c.symbol` is an alias), or any identifier | \n\nAliases are plain Ruby `alias` es, so a class's own method with the same name always wins.\n\nRescue by intent, not by HTTP code:\n\n``` js\nbegin\n  result = state.measure { |q| ... }\nrescue S1::TransientError => e     # RateLimitError, ServerError, ConnectionError, TimeoutError — retry later\n  retry_later(e)\nrescue S1::PermanentError => e     # AuthenticationError, InvalidRequestError, ValidationError — fix the request\n  raise\nend\n```\n\nTransient failures already retry inside the provider (`c.typesafe.max_retries`, honoring `Retry-After`) before surfacing.\n\n`Providers::Stub` answers without the network. Give it the distributions that matter; everything\nelse gets a neutral default (noul 0.5, the first category, the first level).\n\n```\nS1.configure do |c|\n  c.provider = S1::Providers::Stub.new(escalate: 0.9, department: :billing, severity: 2)\nend\n```\n\nSingle-question calls are keyed by the wire name of their kind — `noul` (for `judge`, `judge?`,\n`is?`, `same_as?`), `choice`, `score`: `Stub.new(noul: 0.9, choice: :billing, score: 2)`. A\nscore's shorthand is a position, a label (`\"blocking\"`, `Severity[:blocking]`) or the text\nshown; 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\nscore's expectation is derived from the masses, so a `score:` or `expectation:` in it is read\nonly from `raw`.\n\nA block form receives the request when a distribution should depend on the state:\n\n```\nS1::Providers::Stub.new { |req| { escalate: req.state.include?(\"real person\") ? 0.95 : 0.1 } }\n```\n\nHook every completed call for telemetry or a cost ledger. Extra keyword arguments to\n`State.new` ride along on the request, so you can attribute a call to its owner:\n\n```\nS1.on_result do |result, request|\n  Ledger.record(owner: request.options[:owner], model: result.model, **result.usage)\nend\n\nS1::State.new(transcript, owner: phone_call).judge?(\"...\")\n# ψ: (ψ transcript, owner: phone_call).is? \"…\"\n```\n\nA provider is the code that talks to a model: any object responding to `call(request) → Result`.\nThe gem owns the shape — `Request`, `Question`, `Distribution`, `Result`, the method signatures\non `State`, the error taxonomy; a provider owns only translation: how the state and questions go\non the wire, how answers come back as distributions, how failures map to `TransientError` /\n`PermanentError`. A provider declares which question kinds it answers (` supports?`); the rest\nare refused before any call (`UnsupportedError`). Configure by name — `:typesafe` resolves to\n`S1::Providers::TypeSafe`, built from its section of the config — or pass an instance.\n\n```\nS1.configure { |c| c.provider = :typesafe }\nS1::State.new(text, provider: MyProvider.new)   # per-state override   ψ: (ψ text, provider: MyProvider.new)\n```\n\nFour ship. TypeSafe and Laya share one thing — the System One HTTP contract (`Providers::SystemOneHTTP`,\nthe `/v1/systemone` JSON that jev defined and Laya adopted) — and are otherwise separate\nclasses under `Base`, each with its own settings, auth and models; Cua and Stub share nothing\nwith them but the distributions:\n\n| provider | what | kinds | transport | \n|---|---|---|---|\n| `TypeSafe` | TypeSafe's jev, hosted | noul, choice, score | HTTPS `/v1/systemone` | \n| `Cua` | [cua-s1-forms](https://huggingface.co/cua-ai/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` ; needs`cua-s1` + torch, checkpoint as safetensors + json) | \n| `Laya` | [Laya](https://github.com/NandhaKishorM/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` wraps`pip install laya` in the same`/v1/systemone` contract; no key | \n| `Stub` | canned distributions for tests | all | none | \n\n```\nS1.configure { |c| c.provider = :cua; c.cua.checkpoint = \"cua-s1-forms\" }\nS1.configure { |c| c.provider = :laya; c.laya.base_url = \"http://127.0.0.1:8765\" }   # python support/laya_server.py\nS1::State.new('ELEMENT Edit \"Phone number\"').choose(\"Fill the form: which entity?\", phone: \"555-0100\", email: \"a@b.c\", skip: nil)\n```\n\nTranslation Cua owns, as an example of what a provider decides: the context string is in the\ncheckpoint's own shape — a `TASK <text>` line, then the state on the next (as given, or JSON\nfor structured state), the delimiter cua-s1's `render_context` uses — with the question's\ninstructions as the task, the model having no other slot for the concept; a state that already\nopens with a `TASK` line keeps it, the instructions spliced in after `;` , so the model never\nsees two; category descriptions become\n`\"key: description\"`; the whole context is held to the model's byte limit; the argmax is the\npick and its probability the confidence; and noul / score are refused rather than emulated —\nthe model is trained on form elements, not propositions.\n\n**Writing one.** Subclass `S1::Providers::Base` and do five things: declare `settings` (its\nsection of `S1.config`, with defaults — `settings :name, key: default` defines `c.name.key`,\nhanded to `new` when the provider is named); answer `supports?(question)` honestly; implement\n`call(request) → Result` by building every distribution with `distribution(id, question, raw:, **fields)` —\nthe only constructor, which is what makes every provider's distributions identical and keeps a\nvendor's wire keys out of consumers' hands — and returning `build_result(distributions:, model:, usage:, raw:)`; map failures onto `S1::TransientError` / `S1::PermanentError`; leave `name`\nalone. Then run the conformance suite the gem ships:\n\n```\nrequire \"s1/rspec\"\nRSpec.describe MyProvider do\n  it_behaves_like \"an S1 provider\", -> { MyProvider.new(client: fake) }             # all three kinds\n  it_behaves_like \"an S1 provider\", -> { ChoiceOnly.new }, supports: %i[choice]      # or fewer\nend\n```\n\nIt checks the shape, not the wisdom: a `Result` that is a `Collapsable`, every id answered with\nthe class its kind demands, probabilities in [0, 1] summing to 1, choices keyed by category and\nscores by rank whatever keys the wire used (`key` is the level's position), every distribution\ncarrying its question's `scale`, `collapse` yielding a\nboolean / Symbol / `S1::Level`, `supports?` telling the truth, integer usage. Shape only:\ncalibration and batch independence are the two clauses no offline suite can test — they are the\nprovider's warranty. All four providers here pass it; that is the standard.\n\n`bin/setup`, then `bundle exec rake` runs the specs and rubocop. `bin/console` opens IRB with the\ngem loaded. `TYPESAFE_LIVE=1 TYPESAFE_API_KEY=… bundle exec rspec spec/s1/live_spec.rb`\nhits the real API.\n\nMIT.", "url": "https://wpnews.pro/news/making-s1-model-judgements-a-rubylang-primitive-feature", "canonical_source": "https://github.com/innocentdiaz/s1_ruby", "published_at": "2026-09-21 23:56:27+00:00", "updated_at": "2026-09-22 00:24:21.974205+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "artificial-intelligence", "ai-products"], "entities": ["s1_ruby", "s1_rails", "Ruby", "Ruby on Rails", "innocentdiaz", "System One (S1)"], "alternates": {"html": "https://wpnews.pro/news/making-s1-model-judgements-a-rubylang-primitive-feature", "markdown": "https://wpnews.pro/news/making-s1-model-judgements-a-rubylang-primitive-feature.md", "text": "https://wpnews.pro/news/making-s1-model-judgements-a-rubylang-primitive-feature.txt", "jsonld": "https://wpnews.pro/news/making-s1-model-judgements-a-rubylang-primitive-feature.jsonld"}}