decision-system S4: levers and os-me-route in predict #1246

Closed
opened 2026-10-23 15:17:26 +00:00 by jared · 2 comments
Owner

Problem

A prediction does not record where its answer can come from or how much a wrong ruling costs. Thus no caller can set the confidence that a row needs. The design names four objects for these labels: AnswerSource, WrongCostAssessor, WrongCost, and RequiredBand (docs/decision-system/levers.md, levers 1 to 3). None of them exists. This ticket is the first of three S4 tickets. S4b adds the evidence-loop objects. S4c adds the parser fields and the os-me-route CLI.

Approach

The model-backed objects use the DecisionModel port that exists today, not the sketch in levers.md. The port has one shape: Client#ask(question, rules) takes one DecisionModel::Question.new(text:, options:) and returns DecisionModel::Answer.new(choice:). The answer has no probability. Thus:

  • AnswerSource#classify(row) asks one question. Its options are %w[rule precedent facts experiment taste], in this order. Its text holds the row text and the five criterion sentences from levers.md, lever 1. It returns AnswerSource::Result.new(source: <choice as a symbol>). Result has no probability field.
  • WrongCostAssessor#assess(row) asks three questions, in this order: hard_to_undo, compounds, stays_hidden. Each question has the options %w[yes no]. Its text holds the row text and the instruction sentence for that key from levers.md, lever 2. The choice yes gives true. The choice no gives false. There is no threshold.
  • An error from the client goes to the caller. No object gives a default label.

The lever tables give these values:

Source max_rung novel?
rule 1 false
precedent 1 false
facts 2 false
experiment 3 false
taste 0 true
Yes answers WrongCost#level RequiredBand#for
0 :low nil
1 :medium 70
2 :high 85
3 :high 85

Row is the input of the model-backed levers. It holds the row text only. O-9 is settled: the levers live in os-me, and os-sdlc calls the os-me-route CLI (S4c). The ADR challenge rejected an ADR.

Required work:

  1. Add Row, a value that holds the row text — check: present Data.define(:text) in plugins/os-me/lib/os_me/row.rb
  2. Add AnswerSource with MAX_RUNG and Result — check: present MAX_RUNG = { rule: 1, precedent: 1, facts: 2, experiment: 3, taste: 0 } in plugins/os-me/lib/os_me/answer_source.rb
  3. Add WrongCost with yes_count and level — check: present LEVELS = { 0 => :low, 1 => :medium, 2 => :high, 3 => :high } in plugins/os-me/lib/os_me/wrong_cost.rb
  4. Add WrongCostAssessor that asks the three yes/no questions — check: present class WrongCostAssessor in plugins/os-me/lib/os_me/wrong_cost_assessor.rb
  5. Add RequiredBand — check: present BY_LEVEL = { low: nil, medium: 70, high: 85 } in plugins/os-me/lib/os_me/required_band.rb

Acceptance criteria

  1. Given a fake adapter that answers rule, precedent, facts, experiment, and taste in turn, when AnswerSource#classify runs once for each answer, then the results have source :rule, :precedent, :facts, :experiment, :taste; max_rung 1, 1, 2, 3, 0; and novel? false, false, false, false, true.
  2. Given the row text Move Gate::StepOutcome to OsSdlc::StepOutcome., when AnswerSource#classify runs, then the adapter gets one question with the options ["rule", "precedent", "facts", "experiment", "taste"], and the question text includes the row text and the sentence Jared ruled the same kind of case in an earlier session.
  3. Given a fake adapter that answers yes, no, yes in turn, when WrongCostAssessor#assess runs, then it returns WrongCost.new(hard_to_undo: true, compounds: false, stays_hidden: true).
  4. Given the row text Delete the OsSdlc::Error root., when WrongCostAssessor#assess runs, then the adapter gets three questions, each with the options ["yes", "no"]. Each question text includes the row text. The first question text includes deletes code or policy. The second includes sets a pattern. The third includes No test, lint cop, or later gate.
  5. Given WrongCost.new(hard_to_undo: false, compounds: false, stays_hidden: false), when level runs, then it returns :low, and yes_count returns 0.
  6. Given each WrongCost with exactly one true field (three cases), when level runs, then each returns :medium.
  7. Given WrongCost.new(hard_to_undo: true, compounds: true, stays_hidden: false) and WrongCost.new(hard_to_undo: true, compounds: true, stays_hidden: true), when level runs, then both return :high.
  8. Given wrong costs at the levels :low, :medium, and :high, when RequiredBand#for runs, then it returns nil, 70, and 85.
  9. Given a fake adapter that answers no, yes, no (the S22 worked example in levers.md), when WrongCostAssessor#assess runs and RequiredBand#for reads the result, then the level is :medium and the band is 70.
  10. Given a fake adapter that raises OsMe::DecisionModel::UnreadableAnswer, when WrongCostAssessor#assess runs, then the error reaches the caller.

Owner sketch

OsMe::Row = Data.define(:text)

class OsMe::AnswerSource
  MAX_RUNG = { rule: 1, precedent: 1, facts: 2, experiment: 3, taste: 0 }.freeze
  CRITERIA = { rule: '...', precedent: '...', facts: '...', experiment: '...', taste: '...' }.freeze
  Result = Data.define(:source) do
    def max_rung = MAX_RUNG.fetch(source)
    def novel? = source == :taste
  end
  def initialize(client); end
  def classify(row); end
end

class OsMe::WrongCost < Data.define(:hard_to_undo, :compounds, :stays_hidden)
  LEVELS = { 0 => :low, 1 => :medium, 2 => :high, 3 => :high }.freeze
  def yes_count; end
  def level; end
end

class OsMe::WrongCostAssessor
  INSTRUCTIONS = { hard_to_undo: '...', compounds: '...', stays_hidden: '...' }.freeze
  def initialize(client); end
  def assess(row); end
end

class OsMe::RequiredBand
  BY_LEVEL = { low: nil, medium: 70, high: 85 }.freeze
  def for(wrong_cost); end
end

Non-goals

  • The evidence-loop objects: ConfidenceBand.claimed, EvidenceLadder, StopRule, StakeReducer, CostModel (S4b).
  • Parser fields, log fields, and the os-me-route CLI (S4c).
  • A probability on AnswerSource::Result. The DecisionModel port gives none.
  • Changes to the DecisionModel port.
  • Routing in os-sdlc flows (S5).

Origin

  • Trigger: User asked for the decision-system build tickets this session. Rewritten 2026-10-23: split #1246 with exact criteria after the #1245 lesson.
  • Improvised this session: none
  • Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at DESIGN (docs/decision-system/levers.md).
  • Root candidate: none found; searched WrongCost, levers, os-me, predict
  • Where: docs/decision-system/build-plan.md S4; docs/decision-system/levers.md levers 1 to 3; plugins/os-me/lib/os_me/
  • Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl

VERDICT: CREATE (user-approved; backlog search only)

## Problem A prediction does not record where its answer can come from or how much a wrong ruling costs. Thus no caller can set the confidence that a row needs. The design names four objects for these labels: `AnswerSource`, `WrongCostAssessor`, `WrongCost`, and `RequiredBand` (`docs/decision-system/levers.md`, levers 1 to 3). None of them exists. This ticket is the first of three S4 tickets. S4b adds the evidence-loop objects. S4c adds the parser fields and the `os-me-route` CLI. ## Approach The model-backed objects use the `DecisionModel` port that exists today, not the sketch in `levers.md`. The port has one shape: `Client#ask(question, rules)` takes one `DecisionModel::Question.new(text:, options:)` and returns `DecisionModel::Answer.new(choice:)`. The answer has no probability. Thus: - `AnswerSource#classify(row)` asks one question. Its options are `%w[rule precedent facts experiment taste]`, in this order. Its text holds the row text and the five criterion sentences from `levers.md`, lever 1. It returns `AnswerSource::Result.new(source: <choice as a symbol>)`. `Result` has no `probability` field. - `WrongCostAssessor#assess(row)` asks three questions, in this order: `hard_to_undo`, `compounds`, `stays_hidden`. Each question has the options `%w[yes no]`. Its text holds the row text and the instruction sentence for that key from `levers.md`, lever 2. The choice `yes` gives `true`. The choice `no` gives `false`. There is no threshold. - An error from the client goes to the caller. No object gives a default label. The lever tables give these values: | Source | `max_rung` | `novel?` | |---|---|---| | `rule` | 1 | false | | `precedent` | 1 | false | | `facts` | 2 | false | | `experiment` | 3 | false | | `taste` | 0 | true | | Yes answers | `WrongCost#level` | `RequiredBand#for` | |---|---|---| | 0 | `:low` | `nil` | | 1 | `:medium` | 70 | | 2 | `:high` | 85 | | 3 | `:high` | 85 | `Row` is the input of the model-backed levers. It holds the row text only. O-9 is settled: the levers live in os-me, and os-sdlc calls the `os-me-route` CLI (S4c). The ADR challenge rejected an ADR. Required work: 1. Add `Row`, a value that holds the row text — check: present `Data.define(:text)` in `plugins/os-me/lib/os_me/row.rb` 2. Add `AnswerSource` with `MAX_RUNG` and `Result` — check: present `MAX_RUNG = { rule: 1, precedent: 1, facts: 2, experiment: 3, taste: 0 }` in `plugins/os-me/lib/os_me/answer_source.rb` 3. Add `WrongCost` with `yes_count` and `level` — check: present `LEVELS = { 0 => :low, 1 => :medium, 2 => :high, 3 => :high }` in `plugins/os-me/lib/os_me/wrong_cost.rb` 4. Add `WrongCostAssessor` that asks the three yes/no questions — check: present `class WrongCostAssessor` in `plugins/os-me/lib/os_me/wrong_cost_assessor.rb` 5. Add `RequiredBand` — check: present `BY_LEVEL = { low: nil, medium: 70, high: 85 }` in `plugins/os-me/lib/os_me/required_band.rb` ## Acceptance criteria 1. Given a fake adapter that answers `rule`, `precedent`, `facts`, `experiment`, and `taste` in turn, when `AnswerSource#classify` runs once for each answer, then the results have `source` `:rule`, `:precedent`, `:facts`, `:experiment`, `:taste`; `max_rung` 1, 1, 2, 3, 0; and `novel?` false, false, false, false, true. 2. Given the row text `Move Gate::StepOutcome to OsSdlc::StepOutcome.`, when `AnswerSource#classify` runs, then the adapter gets one question with the options `["rule", "precedent", "facts", "experiment", "taste"]`, and the question text includes the row text and the sentence `Jared ruled the same kind of case in an earlier session.` 3. Given a fake adapter that answers `yes`, `no`, `yes` in turn, when `WrongCostAssessor#assess` runs, then it returns `WrongCost.new(hard_to_undo: true, compounds: false, stays_hidden: true)`. 4. Given the row text `Delete the OsSdlc::Error root.`, when `WrongCostAssessor#assess` runs, then the adapter gets three questions, each with the options `["yes", "no"]`. Each question text includes the row text. The first question text includes `deletes code or policy`. The second includes `sets a pattern`. The third includes `No test, lint cop, or later gate`. 5. Given `WrongCost.new(hard_to_undo: false, compounds: false, stays_hidden: false)`, when `level` runs, then it returns `:low`, and `yes_count` returns 0. 6. Given each `WrongCost` with exactly one `true` field (three cases), when `level` runs, then each returns `:medium`. 7. Given `WrongCost.new(hard_to_undo: true, compounds: true, stays_hidden: false)` and `WrongCost.new(hard_to_undo: true, compounds: true, stays_hidden: true)`, when `level` runs, then both return `:high`. 8. Given wrong costs at the levels `:low`, `:medium`, and `:high`, when `RequiredBand#for` runs, then it returns `nil`, 70, and 85. 9. Given a fake adapter that answers `no`, `yes`, `no` (the S22 worked example in `levers.md`), when `WrongCostAssessor#assess` runs and `RequiredBand#for` reads the result, then the level is `:medium` and the band is 70. 10. Given a fake adapter that raises `OsMe::DecisionModel::UnreadableAnswer`, when `WrongCostAssessor#assess` runs, then the error reaches the caller. ## Owner sketch ```ruby OsMe::Row = Data.define(:text) class OsMe::AnswerSource MAX_RUNG = { rule: 1, precedent: 1, facts: 2, experiment: 3, taste: 0 }.freeze CRITERIA = { rule: '...', precedent: '...', facts: '...', experiment: '...', taste: '...' }.freeze Result = Data.define(:source) do def max_rung = MAX_RUNG.fetch(source) def novel? = source == :taste end def initialize(client); end def classify(row); end end class OsMe::WrongCost < Data.define(:hard_to_undo, :compounds, :stays_hidden) LEVELS = { 0 => :low, 1 => :medium, 2 => :high, 3 => :high }.freeze def yes_count; end def level; end end class OsMe::WrongCostAssessor INSTRUCTIONS = { hard_to_undo: '...', compounds: '...', stays_hidden: '...' }.freeze def initialize(client); end def assess(row); end end class OsMe::RequiredBand BY_LEVEL = { low: nil, medium: 70, high: 85 }.freeze def for(wrong_cost); end end ``` ## Non-goals - The evidence-loop objects: `ConfidenceBand.claimed`, `EvidenceLadder`, `StopRule`, `StakeReducer`, `CostModel` (S4b). - Parser fields, log fields, and the `os-me-route` CLI (S4c). - A probability on `AnswerSource::Result`. The `DecisionModel` port gives none. - Changes to the `DecisionModel` port. - Routing in os-sdlc flows (S5). ## Origin - Trigger: User asked for the decision-system build tickets this session. Rewritten 2026-10-23: split #1246 with exact criteria after the #1245 lesson. - Improvised this session: none - Chain: off-design S3 build ← acceptance criteria too loose ← first ticket draft. Stop at `DESIGN` (`docs/decision-system/levers.md`). - Root candidate: none found; searched WrongCost, levers, os-me, predict - Where: docs/decision-system/build-plan.md S4; docs/decision-system/levers.md levers 1 to 3; plugins/os-me/lib/os_me/ - Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl VERDICT: CREATE (user-approved; backlog search only)
Author
Owner

Work started on branch ticket-1246 (/os-sdlc:implement, S4a).

Work started on branch ticket-1246 (/os-sdlc:implement, S4a).
Author
Owner

Resolution

Done: S4a built: Row, AnswerSource (+Result), WrongCost, WrongCostAssessor, RequiredBand on the current DecisionModel port; implementation 127 complete.

Evidence: Branch ticket-1246, Runner commit; suite and lint gates green; contract audit pass after one refactor round; 10 acceptance criteria covered in answer_source_test.rb and wrong_cost_test.rb.

Follow-ups: S4b #1262 and S4c #1263 continue S4. A lens proposal on A3 (prefix-sharing assessor under a ticket-pinned name) goes through the lens challenge. S8 #1250 swaps the DecisionModel port to the decision-model gem, which changes AnswerSource and WrongCostAssessor asks.

## Resolution **Done:** S4a built: Row, AnswerSource (+Result), WrongCost, WrongCostAssessor, RequiredBand on the current DecisionModel port; implementation 127 complete. **Evidence:** Branch ticket-1246, Runner commit; suite and lint gates green; contract audit pass after one refactor round; 10 acceptance criteria covered in answer_source_test.rb and wrong_cost_test.rb. **Follow-ups:** S4b #1262 and S4c #1263 continue S4. A lens proposal on A3 (prefix-sharing assessor under a ticket-pinned name) goes through the lens challenge. S8 #1250 swaps the DecisionModel port to the decision-model gem, which changes AnswerSource and WrongCostAssessor asks.
jared closed this issue 2026-10-23 17:17:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1246
No description provided.