Store the gate step classification in step_results #827

Closed
opened 2026-09-29 15:55:04 +00:00 by jared · 3 comments
Owner

Problem

Two places derive the same gate step classification. Gate::StepOutcome classifies each step when the gate runs. step_results stores only exit_status, output and duration_ms (plugins/os-sdlc/lib/os_sdlc/adapter/db.rb:280-284). So Implementation::Landing::UnexpectedPassReason must classify the stored rows again through StepResult#outcome to build the escalation reason :/. If the two classifications diverge, the reason text names the wrong cause.

Reproduce

n/a: design ticket; no failure observed yet.

Approach

Store the classification in step_results when the gate records a step. UnexpectedPassReason reads the stored value and does not classify again.

# Gate#classify: the outcome carries the classification the gate computed
@last_classification = outcome.classify(expect_red: @gate.red?)
@outcomes << outcome.with(classification: @last_classification)

# Dispatch#record_step!
add_step_result(step: ..., exit_status: ..., output: ..., duration_ms: ..., classification: outcome.classification&.to_s)

# UnexpectedPassReason#classify
@results.find { |row| row.step == expectation.name }&.classification&.to_sym
  • Gate::StepOutcome gains a classification member, default nil. Its custom initialize(duration_ms: nil, **rest) must also default classification: nil, or Data raises a missing-keyword error.
  • step_results gains a nullable String classification column in Db.add_step_result_outcome_columns. Db::SCHEMA_VERSION goes from 24 to 25. ADR-0129 wipe-and-recreate applies, so no migration code. ADR-0155 refuses the bump while an implementation is in flight.
  • The column holds the symbol name as a String. The reader converts it back with to_sym, because Gate::ESCALATING_CLASSIFICATIONS holds Symbols. Without the conversion, escalation never fires.
  • Gate-skip steps (Command::CompletionIntake#stamp_gate_skip!) and error steps (Command::Gate::Run#record_failure) carry no gate classification and store nil. A nil classification never escalates.
  • The reason text format pinned by ADR-0158 does not change.
  • Warning: never run a bin/ runner from this branch against the linked .sdlc/pipeline.db. The schema bump wipes the shared database. Tests use tmpdir databases.

Required work:

  1. Bump Db::SCHEMA_VERSION to 25 — check: grep -q "SCHEMA_VERSION = 25" plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  2. Delete StepResult#outcome, whose only caller is UnexpectedPassReason — check: ! grep -q "def outcome" plugins/os-sdlc/lib/os_sdlc/model/step_result.rb
  3. The seeded step_results rows in plugins/os-sdlc/tests/support/cli_next_instruction_helpers.rb carry the classification that each pipeline escalation test expects — check: grep -q "classification" plugins/os-sdlc/tests/support/cli_next_instruction_helpers.rb

Rejected: keep StepResult#outcome and classify on read — the two classifications can diverge, which is the defect.

Acceptance criteria

  1. Given a gate whose expect-red test step exits 0, when the dispatch records the gate and the step_results row is read back, then the row's classification is unexpected_pass.
  2. Given a gate-skip or error step recorded without a gate classification, when the step_results row is read back, then its classification is nil.
  3. Given a dispatch whose stored test step row carries classification green_report_nonzero_exit and output that would classify as failed, when UnexpectedPassReason builds the reason, then it returns green_report_nonzero_exit:<position>/test from the stored value.
  4. Given a dispatch whose stored rows carry only nil or passing classifications, when UnexpectedPassReason builds the reason, then it returns nil.

Owner sketch

class OsSdlc::Gate::StepOutcome < Data.define(:step, :exit_status, :output, :duration_ms, :classification)
  def initialize(duration_ms: nil, classification: nil, **rest)
end

class OsSdlc::Gate
  def classify(outcome)
end

class OsSdlc::Dispatch
  def record_step!(outcome)
end

class OsSdlc::Implementation::Landing::UnexpectedPassReason
  def call
end

Non-goals

  • The ADR for the stored classification (amends ADR-0158's derivation) is recorded by the orchestrator after the run, not by pipeline agents.

Origin

  • Trigger: #813 build (main f20cc962), 2026-09-29
  • Improvised this session: StepResult#outcome classifies stored rows again
  • Chain: duplicate classification ← classification not stored ← DESIGN (adapter/db.rb:280-284)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/adapter/db.rb; model/implementation/landing/unexpected_pass_reason.rb
  • Session: 0aa46819-aa36-4e80-9438-9ef8b7314bf2
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-takedown/0aa46819-aa36-4e80-9438-9ef8b7314bf2.jsonl

VERDICT: CREATE — user-created ticket; storing the classification at record time removes the divergence (ticket-skeptic, 2026-09-29).

## Problem Two places derive the same gate step classification. Gate::StepOutcome classifies each step when the gate runs. step_results stores only exit_status, output and duration_ms (plugins/os-sdlc/lib/os_sdlc/adapter/db.rb:280-284). So Implementation::Landing::UnexpectedPassReason must classify the stored rows again through StepResult#outcome to build the escalation reason <classification>:<position>/<gate>. If the two classifications diverge, the reason text names the wrong cause. ## Reproduce n/a: design ticket; no failure observed yet. ## Approach Store the classification in step_results when the gate records a step. UnexpectedPassReason reads the stored value and does not classify again. ```ruby # Gate#classify: the outcome carries the classification the gate computed @last_classification = outcome.classify(expect_red: @gate.red?) @outcomes << outcome.with(classification: @last_classification) # Dispatch#record_step! add_step_result(step: ..., exit_status: ..., output: ..., duration_ms: ..., classification: outcome.classification&.to_s) # UnexpectedPassReason#classify @results.find { |row| row.step == expectation.name }&.classification&.to_sym ``` - `Gate::StepOutcome` gains a `classification` member, default nil. Its custom `initialize(duration_ms: nil, **rest)` must also default `classification: nil`, or `Data` raises a missing-keyword error. - step_results gains a nullable String `classification` column in `Db.add_step_result_outcome_columns`. `Db::SCHEMA_VERSION` goes from 24 to 25. ADR-0129 wipe-and-recreate applies, so no migration code. ADR-0155 refuses the bump while an implementation is in flight. - The column holds the symbol name as a String. The reader converts it back with `to_sym`, because `Gate::ESCALATING_CLASSIFICATIONS` holds Symbols. Without the conversion, escalation never fires. - Gate-skip steps (`Command::CompletionIntake#stamp_gate_skip!`) and error steps (`Command::Gate::Run#record_failure`) carry no gate classification and store nil. A nil classification never escalates. - The reason text format pinned by ADR-0158 does not change. - Warning: never run a `bin/` runner from this branch against the linked `.sdlc/pipeline.db`. The schema bump wipes the shared database. Tests use tmpdir databases. Required work: 1. Bump `Db::SCHEMA_VERSION` to 25 — check: `grep -q "SCHEMA_VERSION = 25" plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 2. Delete `StepResult#outcome`, whose only caller is UnexpectedPassReason — check: `! grep -q "def outcome" plugins/os-sdlc/lib/os_sdlc/model/step_result.rb` 3. The seeded step_results rows in `plugins/os-sdlc/tests/support/cli_next_instruction_helpers.rb` carry the classification that each pipeline escalation test expects — check: `grep -q "classification" plugins/os-sdlc/tests/support/cli_next_instruction_helpers.rb` Rejected: keep StepResult#outcome and classify on read — the two classifications can diverge, which is the defect. ## Acceptance criteria 1. Given a gate whose expect-red test step exits 0, when the dispatch records the gate and the step_results row is read back, then the row's classification is `unexpected_pass`. 2. Given a gate-skip or error step recorded without a gate classification, when the step_results row is read back, then its classification is nil. 3. Given a dispatch whose stored test step row carries classification `green_report_nonzero_exit` and output that would classify as `failed`, when UnexpectedPassReason builds the reason, then it returns `green_report_nonzero_exit:<position>/test` from the stored value. 4. Given a dispatch whose stored rows carry only nil or passing classifications, when UnexpectedPassReason builds the reason, then it returns nil. ## Owner sketch ```ruby class OsSdlc::Gate::StepOutcome < Data.define(:step, :exit_status, :output, :duration_ms, :classification) def initialize(duration_ms: nil, classification: nil, **rest) end class OsSdlc::Gate def classify(outcome) end class OsSdlc::Dispatch def record_step!(outcome) end class OsSdlc::Implementation::Landing::UnexpectedPassReason def call end ``` ## Non-goals - The ADR for the stored classification (amends ADR-0158's derivation) is recorded by the orchestrator after the run, not by pipeline agents. ## Origin - Trigger: #813 build (main f20cc962), 2026-09-29 - Improvised this session: StepResult#outcome classifies stored rows again - Chain: duplicate classification ← classification not stored ← DESIGN (adapter/db.rb:280-284) - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/adapter/db.rb; model/implementation/landing/unexpected_pass_reason.rb - Session: 0aa46819-aa36-4e80-9438-9ef8b7314bf2 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-takedown/0aa46819-aa36-4e80-9438-9ef8b7314bf2.jsonl VERDICT: CREATE — user-created ticket; storing the classification at record time removes the divergence (ticket-skeptic, 2026-09-29).
Author
Owner

Work started on branch ticket-827 (worktree) via /os-sdlc:implement. This run is also the live test for #819.

Work started on branch `ticket-827` (worktree) via /os-sdlc:implement. This run is also the live test for #819.
Author
Owner

Re-decomposed 2026-09-29: added Approach code, Required work with checks, acceptance criteria 2-4, Owner sketch (implementation-open refused the missing Owner sketch). Skeptic: CREATE.

Re-decomposed 2026-09-29: added Approach code, Required work with checks, acceptance criteria 2-4, Owner sketch (implementation-open refused the missing Owner sketch). Skeptic: CREATE.
Author
Owner

Resolution

Done: step_results stores the gate classification of record. Gate#classify returns outcome.with(classification:), Dispatch#record_step! writes it, UnexpectedPassReason reads it, and StepResult#outcome is deleted. SCHEMA_VERSION 24 -> 25. ADR-0248 records the decision and its relation to ADR-0165.

Evidence: Commit 1c5142c1 on ticket-827. os-sdlc implementation 78: 8 dispatches (775-782), 0 retries, implementation_complete in 6 min 8 s. suite-check gate: required-work exit 0, suite exit 0. behavior-verifier pass on all 4 acceptance criteria.

Follow-ups: #836 (os-sdlc-implementation-report refuses after implementation_complete; ticket-skeptic VERDICT: CREATE)

## Resolution **Done:** step_results stores the gate classification of record. Gate#classify returns outcome.with(classification:), Dispatch#record_step! writes it, UnexpectedPassReason reads it, and StepResult#outcome is deleted. SCHEMA_VERSION 24 -> 25. ADR-0248 records the decision and its relation to ADR-0165. **Evidence:** Commit 1c5142c1 on ticket-827. os-sdlc implementation 78: 8 dispatches (775-782), 0 retries, implementation_complete in 6 min 8 s. suite-check gate: required-work exit 0, suite exit 0. behavior-verifier pass on all 4 acceptance criteria. **Follow-ups:** #836 (os-sdlc-implementation-report refuses after implementation_complete; ticket-skeptic VERDICT: CREATE)
jared closed this issue 2026-09-29 17:53:43 +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#827
No description provided.