Implementation#handoffs is the Sequel association; Handoff answers items and summary #591

Closed
opened 2026-09-15 13:41:18 +00:00 by jared · 2 comments
Owner

Context

Responsibility-audit run 18 on plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb reported finding 2: handoffs is declared twice, and the Sequel association loses. The user chose Option B on 2026-09-15: repair the association, add no new record class.

Observed

plugins/os-sdlc/lib/os_sdlc/runner/models.rb:87 declares klass.one_to_many :handoffs, class: handoff, key: :implementation_id. plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:76 defines an instance method with the same name. The mixin is included at models.rb:77, before the association is wired, so the mixin method wins and the association reader has no callers. Which declaration runs depends on include order in another file. Lines 77 and 78 of implementation.rb reach two hops through Handoff#content_object into HandoffContent to answer handoff_items and merge_ready_summary for Implementation::Commit.

implementation.rb:76    def handoffs = Models.for(db).handoff.for_implementation(id)
implementation.rb:77    def handoff_items = handoffs.flat_map { |handoff| handoff.content_object.items }
implementation.rb:78    def merge_ready_summary = handoffs.last&.content_object&.summary
models.rb:87            klass.one_to_many :handoffs, class: handoff, key: :implementation_id
commit.rb:70            @implementation.handoff_items.filter_map { |entry| Item.new(entry).path }.uniq
commit.rb:149           @summary ||= @implementation.merge_ready_summary.to_s.strip

Reproduce

n/a: design defect, no failing command. The shadowing is visible by reading models.rb:77 and models.rb:87 against implementation.rb:76.

Tasks

  • models.rb:87 becomes klass.one_to_many :handoffs, class: handoff, key: :implementation_id, order: :id.
  • Delete implementation.rb:76-78 (handoffs, handoff_items, merge_ready_summary).
  • Add to Handoff::InstanceMethods in plugins/os-sdlc/lib/os_sdlc/runner/handoff.rb: def items = content_object.items and def summary = content_object.summary.
  • plugins/os-sdlc/lib/os_sdlc/runner/implementation/commit.rb:70 becomes @implementation.handoffs.flat_map(&:items).filter_map { |entry| Item.new(entry).path }.uniq.
  • commit.rb:149 becomes @summary ||= @implementation.handoffs.last&.summary.to_s.strip.
  • Delete Handoff::ClassMethods#for_implementation (handoff.rb:49-51) once it has no caller.

Class sketch

# models.rb
def wire_implementation_associations(klass)
  klass.one_to_many :dispatches, class: dispatch, key: :implementation_id
  klass.one_to_many :handoffs, class: handoff, key: :implementation_id, order: :id
  klass.one_to_many :edge_spends, class: edge_spend, key: :implementation_id
end

# handoff.rb  (InstanceMethods)
def items   = content_object.items
def summary = content_object.summary

# commit.rb
def call = @implementation.handoffs.flat_map(&:items).filter_map { |entry| Item.new(entry).path }.uniq
def summary = @summary ||= @implementation.handoffs.last&.summary.to_s.strip

Expected test cases

  • Implementation#handoffs returns the handoffs for that implementation ordered by id, oldest first, when two handoffs are inserted in reverse creation order.
  • Implementation#handoffs returns an empty array for an implementation with no handoffs.
  • Handoff#items returns the items list from the handoff content; returns [] when content has no items.
  • Handoff#summary returns the summary string from the handoff content; returns nil when absent.
  • Implementation::Commit::ListedPaths#call returns the unique paths from all handoffs' items, in handoff order.
  • Implementation::Commit::Message#summary returns the stripped summary of the last handoff, and "" when the implementation has no handoffs.
  • Implementation instances do not respond to handoff_items or merge_ready_summary.
  • Handoff class does not respond to for_implementation.

Expected

def test_handoffs_are_ordered_oldest_first_through_the_association
  later = create_handoff(implementation, summary: "b")
  earlier = create_handoff(implementation, summary: "a")
  earlier.update(id: later.id - 1) if earlier.id > later.id
  assert_equal %w[a b], implementation.refresh.handoffs.map(&:summary)
end

Acceptance criteria

  • plugins/os-sdlc/tests/runner/implementation_test.rb:103 keeps passing through the association.
  • Every expected test case above has one Minitest example and is green.
  • ruby plugins/os-sdlc/tests/all.rb is green; rubocop with plugins/os-sdlc/.rubocop.yml reports zero offenses on touched files.

Constraints

  • ADR-0167: content readers are methods on the Sequel model, no new plain-Ruby record class.
  • ADR-0171: Commit stages only handoff-listed paths; the staged set must not change.
  • ADR-0168: no change to transition! or the terminal writers.
  • ADR-0127: no comments in source.
  • Map: plugins/os-sdlc/maps/poodr-ticket-implementation.yaml.

Out of scope

  • Moving model mixins out of the Runner namespace (separate decision ticket).
  • The other audit findings from run 18 (Implementation::Stamp, Implementation::Admission, defect rows).

Origin

  • Trigger: /os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb (run 18, artifact .sdlc/tmp/responsibility-audit/run-18/analysis.yaml)
  • Improvised this session: none
  • Chain: shadowed association with zero readers ← mixin included before associations wired (models.rb:77 vs :87) ← DESIGN (ADR-0167 model mixin registry, models.rb)
  • Root candidate: this ticket
  • Where: OsSdlc::Runner::Implementation::InstanceMethods, plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:76-78
  • Session: b8f5c70a-9904-4ac3-9cea-77ba0aa5025c
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/b8f5c70a-9904-4ac3-9cea-77ba0aa5025c.jsonl
## Context Responsibility-audit run 18 on `plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb` reported finding 2: `handoffs` is declared twice, and the Sequel association loses. The user chose Option B on 2026-09-15: repair the association, add no new record class. ## Observed `plugins/os-sdlc/lib/os_sdlc/runner/models.rb:87` declares `klass.one_to_many :handoffs, class: handoff, key: :implementation_id`. `plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:76` defines an instance method with the same name. The mixin is included at `models.rb:77`, before the association is wired, so the mixin method wins and the association reader has no callers. Which declaration runs depends on include order in another file. Lines 77 and 78 of implementation.rb reach two hops through `Handoff#content_object` into `HandoffContent` to answer `handoff_items` and `merge_ready_summary` for `Implementation::Commit`. ``` implementation.rb:76 def handoffs = Models.for(db).handoff.for_implementation(id) implementation.rb:77 def handoff_items = handoffs.flat_map { |handoff| handoff.content_object.items } implementation.rb:78 def merge_ready_summary = handoffs.last&.content_object&.summary models.rb:87 klass.one_to_many :handoffs, class: handoff, key: :implementation_id commit.rb:70 @implementation.handoff_items.filter_map { |entry| Item.new(entry).path }.uniq commit.rb:149 @summary ||= @implementation.merge_ready_summary.to_s.strip ``` ## Reproduce n/a: design defect, no failing command. The shadowing is visible by reading `models.rb:77` and `models.rb:87` against `implementation.rb:76`. ## Tasks - [ ] `models.rb:87` becomes `klass.one_to_many :handoffs, class: handoff, key: :implementation_id, order: :id`. - [ ] Delete `implementation.rb:76-78` (`handoffs`, `handoff_items`, `merge_ready_summary`). - [ ] Add to `Handoff::InstanceMethods` in `plugins/os-sdlc/lib/os_sdlc/runner/handoff.rb`: `def items = content_object.items` and `def summary = content_object.summary`. - [ ] `plugins/os-sdlc/lib/os_sdlc/runner/implementation/commit.rb:70` becomes `@implementation.handoffs.flat_map(&:items).filter_map { |entry| Item.new(entry).path }.uniq`. - [ ] `commit.rb:149` becomes `@summary ||= @implementation.handoffs.last&.summary.to_s.strip`. - [ ] Delete `Handoff::ClassMethods#for_implementation` (`handoff.rb:49-51`) once it has no caller. ## Class sketch ```ruby # models.rb def wire_implementation_associations(klass) klass.one_to_many :dispatches, class: dispatch, key: :implementation_id klass.one_to_many :handoffs, class: handoff, key: :implementation_id, order: :id klass.one_to_many :edge_spends, class: edge_spend, key: :implementation_id end # handoff.rb (InstanceMethods) def items = content_object.items def summary = content_object.summary # commit.rb def call = @implementation.handoffs.flat_map(&:items).filter_map { |entry| Item.new(entry).path }.uniq def summary = @summary ||= @implementation.handoffs.last&.summary.to_s.strip ``` ## Expected test cases - `Implementation#handoffs` returns the handoffs for that implementation ordered by id, oldest first, when two handoffs are inserted in reverse creation order. - `Implementation#handoffs` returns an empty array for an implementation with no handoffs. - `Handoff#items` returns the `items` list from the handoff content; returns `[]` when content has no items. - `Handoff#summary` returns the `summary` string from the handoff content; returns nil when absent. - `Implementation::Commit::ListedPaths#call` returns the unique paths from all handoffs' items, in handoff order. - `Implementation::Commit::Message#summary` returns the stripped summary of the last handoff, and `""` when the implementation has no handoffs. - `Implementation` instances do not respond to `handoff_items` or `merge_ready_summary`. - `Handoff` class does not respond to `for_implementation`. ## Expected ```ruby def test_handoffs_are_ordered_oldest_first_through_the_association later = create_handoff(implementation, summary: "b") earlier = create_handoff(implementation, summary: "a") earlier.update(id: later.id - 1) if earlier.id > later.id assert_equal %w[a b], implementation.refresh.handoffs.map(&:summary) end ``` ## Acceptance criteria - `plugins/os-sdlc/tests/runner/implementation_test.rb:103` keeps passing through the association. - Every expected test case above has one Minitest example and is green. - `ruby plugins/os-sdlc/tests/all.rb` is green; rubocop with `plugins/os-sdlc/.rubocop.yml` reports zero offenses on touched files. ## Constraints - ADR-0167: content readers are methods on the Sequel model, no new plain-Ruby record class. - ADR-0171: `Commit` stages only handoff-listed paths; the staged set must not change. - ADR-0168: no change to `transition!` or the terminal writers. - ADR-0127: no comments in source. - Map: `plugins/os-sdlc/maps/poodr-ticket-implementation.yaml`. ## Out of scope - Moving model mixins out of the `Runner` namespace (separate decision ticket). - The other audit findings from run 18 (`Implementation::Stamp`, `Implementation::Admission`, defect rows). ## Origin - Trigger: `/os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb` (run 18, artifact `.sdlc/tmp/responsibility-audit/run-18/analysis.yaml`) - Improvised this session: none - Chain: shadowed association with zero readers ← mixin included before associations wired (`models.rb:77` vs `:87`) ← `DESIGN` (ADR-0167 model mixin registry, `models.rb`) - Root candidate: this ticket - Where: `OsSdlc::Runner::Implementation::InstanceMethods`, `plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:76-78` - Session: b8f5c70a-9904-4ac3-9cea-77ba0aa5025c - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/b8f5c70a-9904-4ac3-9cea-77ba0aa5025c.jsonl
Author
Owner

Work started via /os-sdlc:implement on branch ticket-591 (map: poodr-ticket-implementation.yaml).

Work started via /os-sdlc:implement on branch ticket-591 (map: poodr-ticket-implementation.yaml).
Author
Owner

Resolution

Done: Option B shipped on branch ticket-591, commit f4fa431: one_to_many :handoffs carries order: :id and is the sole definition; Handoff#items and Handoff#summary added; Implementation#handoffs/#handoff_items/#merge_ready_summary and Handoff.for_implementation deleted; Commit reads handoffs.flat_map(&:items) and handoffs.last&.summary.

Evidence: Pipeline implementation 18 (poodr-ticket-implementation map, 16 dispatches): tests red then green, behavior-verifier pass x2, suite-check pass (1200 runs, 0 failures), rubocop 7 files no offenses. Contract-auditor settled error because a programmer relaxed HandoffContent#validate_summary! out of scope to satisfy the ticket's 'summary nil when absent' case; hand-finished by restoring the validator to HEAD and deleting that one test, then suite rerun green.

Follow-ups: The ticket's expected case 'Handoff#summary returns nil when absent' contradicted the HandoffContent contract (summary required); dropped, no ticket. Contract-auditor has no route for a correction that needs a test edit and settles error instead; same shape already noted on #555, no new ticket.

## Resolution **Done:** Option B shipped on branch ticket-591, commit f4fa431: one_to_many :handoffs carries order: :id and is the sole definition; Handoff#items and Handoff#summary added; Implementation#handoffs/#handoff_items/#merge_ready_summary and Handoff.for_implementation deleted; Commit reads handoffs.flat_map(&:items) and handoffs.last&.summary. **Evidence:** Pipeline implementation 18 (poodr-ticket-implementation map, 16 dispatches): tests red then green, behavior-verifier pass x2, suite-check pass (1200 runs, 0 failures), rubocop 7 files no offenses. Contract-auditor settled error because a programmer relaxed HandoffContent#validate_summary! out of scope to satisfy the ticket's 'summary nil when absent' case; hand-finished by restoring the validator to HEAD and deleting that one test, then suite rerun green. **Follow-ups:** The ticket's expected case 'Handoff#summary returns nil when absent' contradicted the HandoffContent contract (summary required); dropped, no ticket. Contract-auditor has no route for a correction that needs a test edit and settles error instead; same shape already noted on #555, no new ticket.
jared closed this issue 2026-09-15 14:08:57 +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#591
No description provided.