Add related lessons and backstop hit to the ClassLength lesson #1324

Closed
opened 2026-10-25 16:44:43 +00:00 by jared · 3 comments
Owner

Problem

The ClassLength lesson cannot point to other cops that explain the same file. Lesson::Relay (plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb) renders one lesson per fired cop key. It has no support for related lessons. A file that Metrics/ClassLength flags, and that no family cop explains, gets no signal that the shape is new.

Reproduce

n/a: missing feature.

Approach

Add a related: key to the lesson schema. Lesson::Relay renders the signal line of each related lesson.
The ClassLength lesson lists the family: the repeated-method-skeleton cop (Sdlc/Dry/RepeatedRescueClause, #1321), the repeated-collaborator-construction cop (Sdlc/Dry/RepeatedCollaboratorConstruction, #1322), Sdlc/Structural/ModuleOwnsInstanceState, and Sdlc/Structural/ParameterThreadedThroughPrivateMethods.
The ClassLength lesson's related: list is the only family list. The backstop check reads it; it does not keep a second list.
The gate reports a "backstop hit" when Metrics/ClassLength fires in a file where no family cop fires. The lesson then tells the agent to run /os-sdlc:smell-analysis and capture a ticket for the uncovered shape.
This ticket is blocked by #1323 (the lessons it relates to).
"The gate" is the implement gate reply: OsSdlc::Gate::Reply#lint_output (plugins/os-sdlc/lib/os_sdlc/gate/reply.rb) renders the lint step output and calls OffenseDirective#guidance with the cop names. Find the backstop from the same rubocop JSON, per file, as Gate::LintOutput.cop_names does.
Lesson schema today: OsSdlc::Lesson (plugins/os-sdlc/lib/os_sdlc/lesson.rb) is Data.define(:key, :signal, :sign_of, :why, :decision, :correction, :example), and Lesson.from raises HarnessError for any missing member. related: and the backstop text must be optional members, so the 32 existing lesson files stay valid.
Metrics/ClassLength excludes test paths in plugins/os-sdlc/.rubocop.yml until #1317, so a backstop hit cannot occur in a test file. A target whose lint runs without Sdlc cops reports every ClassLength hit as a backstop hit; #1319 (earlier in the approved order) layers the os-sdlc config onto every target.

Required work:

  1. Add the related: key to the lesson schema and its validation — check: present related in plugins/os-sdlc/lib/os_sdlc/lesson.rb
  2. Make Lesson::Relay render the signal line of each related lesson — check: present related in plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb
  3. Add the related: list to the ClassLength lesson — check: present related: in plugins/os-sdlc/lint-lessons/metrics-class-length.yml
  4. Make the gate report a backstop hit when Metrics/ClassLength fires and no family cop fires in the same file — check: present backstop_paths in plugins/os-sdlc/lib/os_sdlc/gate/lint_output.rb
  5. Add backstop text to the ClassLength lesson — check: present smell-analysis in plugins/os-sdlc/lint-lessons/metrics-class-length.yml

Acceptance criteria

  1. Given a lesson with a related: key, when Lesson::Relay renders it, then the output has the signal line of each related lesson.
  2. Given a file where Metrics/ClassLength fires and a family cop fires, when the gate runs, then it reports no backstop hit.
  3. Given a file where Metrics/ClassLength fires and no family cop fires, when the gate runs, then it reports a backstop hit.
  4. Given a backstop hit, when the lesson renders, then it tells the agent to run /os-sdlc:smell-analysis and capture a ticket.
  5. Given an existing lesson file with no related: key and no backstop text, when Lesson.from loads it, then it loads with no error.

Owner sketch

class OsSdlc::Lesson < Data.define(:key, :signal, :sign_of, :why, :decision, :correction, :example, :related, :backstop)
  def self.from(path, data)
end

class OsSdlc::Lesson::Relay
  def render(keys)
end

class OsSdlc::Gate::LintOutput
  def self.backstop_paths(output, family)
end
  • OsSdlc::Lesson (lib/os_sdlc/lesson.rb): optional related (list of cop keys) and backstop (text). from defaults them when absent.
  • OsSdlc::Lesson::Relay#render (lib/os_sdlc/lesson/relay.rb): after a lesson's text, renders the signal of each related lesson that the catalog has.
  • OsSdlc::Gate::LintOutput.backstop_paths (lib/os_sdlc/gate/lint_output.rb): from the rubocop JSON, the paths where Metrics/ClassLength fires and no cop in family fires.
  • OsSdlc::Gate::Reply#lint_output (lib/os_sdlc/gate/reply.rb): when backstop_paths is not empty, adds a backstop hit: <path> line per path and the ClassLength lesson's backstop text.
  • Lesson file: plugins/os-sdlc/lint-lessons/metrics-class-length.yml gains related: and backstop:.

Origin

  • Trigger: Design of the ClassLength family in this session.
  • Improvised this session: none
  • Chain: Uncovered shapes stay silent ← Lesson::Relay has no related-lesson support ← DESIGN (plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb)
  • Root candidate: none found; searched ClassLength, Lesson::Relay, related, backstop, lesson, family cop, smell-analysis
    VERDICT: CREATE
  • Where: plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb
  • Session: f4187368-5e7e-459d-80c6-ddf7620389cd
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f4187368-5e7e-459d-80c6-ddf7620389cd.jsonl
## Problem The ClassLength lesson cannot point to other cops that explain the same file. Lesson::Relay (plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb) renders one lesson per fired cop key. It has no support for related lessons. A file that Metrics/ClassLength flags, and that no family cop explains, gets no signal that the shape is new. ## Reproduce n/a: missing feature. ## Approach Add a `related:` key to the lesson schema. Lesson::Relay renders the signal line of each related lesson. The ClassLength lesson lists the family: the repeated-method-skeleton cop (`Sdlc/Dry/RepeatedRescueClause`, #1321), the repeated-collaborator-construction cop (`Sdlc/Dry/RepeatedCollaboratorConstruction`, #1322), Sdlc/Structural/ModuleOwnsInstanceState, and Sdlc/Structural/ParameterThreadedThroughPrivateMethods. The ClassLength lesson's `related:` list is the only family list. The backstop check reads it; it does not keep a second list. The gate reports a "backstop hit" when Metrics/ClassLength fires in a file where no family cop fires. The lesson then tells the agent to run /os-sdlc:smell-analysis and capture a ticket for the uncovered shape. This ticket is blocked by #1323 (the lessons it relates to). "The gate" is the implement gate reply: `OsSdlc::Gate::Reply#lint_output` (plugins/os-sdlc/lib/os_sdlc/gate/reply.rb) renders the lint step output and calls `OffenseDirective#guidance` with the cop names. Find the backstop from the same rubocop JSON, per file, as `Gate::LintOutput.cop_names` does. Lesson schema today: `OsSdlc::Lesson` (plugins/os-sdlc/lib/os_sdlc/lesson.rb) is `Data.define(:key, :signal, :sign_of, :why, :decision, :correction, :example)`, and `Lesson.from` raises HarnessError for any missing member. `related:` and the backstop text must be optional members, so the 32 existing lesson files stay valid. Metrics/ClassLength excludes test paths in plugins/os-sdlc/.rubocop.yml until #1317, so a backstop hit cannot occur in a test file. A target whose lint runs without Sdlc cops reports every ClassLength hit as a backstop hit; #1319 (earlier in the approved order) layers the os-sdlc config onto every target. Required work: 1. Add the `related:` key to the lesson schema and its validation — check: present `related` in `plugins/os-sdlc/lib/os_sdlc/lesson.rb` 2. Make Lesson::Relay render the signal line of each related lesson — check: present `related` in `plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb` 3. Add the `related:` list to the ClassLength lesson — check: present `related:` in `plugins/os-sdlc/lint-lessons/metrics-class-length.yml` 4. Make the gate report a backstop hit when Metrics/ClassLength fires and no family cop fires in the same file — check: present `backstop_paths` in `plugins/os-sdlc/lib/os_sdlc/gate/lint_output.rb` 5. Add backstop text to the ClassLength lesson — check: present `smell-analysis` in `plugins/os-sdlc/lint-lessons/metrics-class-length.yml` ## Acceptance criteria 1. Given a lesson with a `related:` key, when Lesson::Relay renders it, then the output has the signal line of each related lesson. 2. Given a file where Metrics/ClassLength fires and a family cop fires, when the gate runs, then it reports no backstop hit. 3. Given a file where Metrics/ClassLength fires and no family cop fires, when the gate runs, then it reports a backstop hit. 4. Given a backstop hit, when the lesson renders, then it tells the agent to run /os-sdlc:smell-analysis and capture a ticket. 5. Given an existing lesson file with no `related:` key and no backstop text, when Lesson.from loads it, then it loads with no error. ## Owner sketch ```ruby class OsSdlc::Lesson < Data.define(:key, :signal, :sign_of, :why, :decision, :correction, :example, :related, :backstop) def self.from(path, data) end class OsSdlc::Lesson::Relay def render(keys) end class OsSdlc::Gate::LintOutput def self.backstop_paths(output, family) end ``` - `OsSdlc::Lesson` (lib/os_sdlc/lesson.rb): optional `related` (list of cop keys) and `backstop` (text). `from` defaults them when absent. - `OsSdlc::Lesson::Relay#render` (lib/os_sdlc/lesson/relay.rb): after a lesson's text, renders the `signal` of each related lesson that the catalog has. - `OsSdlc::Gate::LintOutput.backstop_paths` (lib/os_sdlc/gate/lint_output.rb): from the rubocop JSON, the paths where Metrics/ClassLength fires and no cop in `family` fires. - `OsSdlc::Gate::Reply#lint_output` (lib/os_sdlc/gate/reply.rb): when backstop_paths is not empty, adds a `backstop hit: <path>` line per path and the ClassLength lesson's `backstop` text. - Lesson file: plugins/os-sdlc/lint-lessons/metrics-class-length.yml gains `related:` and `backstop:`. ## Origin - Trigger: Design of the ClassLength family in this session. - Improvised this session: none - Chain: Uncovered shapes stay silent ← Lesson::Relay has no related-lesson support ← DESIGN (plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb) - Root candidate: none found; searched ClassLength, Lesson::Relay, related, backstop, lesson, family cop, smell-analysis VERDICT: CREATE - Where: plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb - Session: f4187368-5e7e-459d-80c6-ddf7620389cd - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f4187368-5e7e-459d-80c6-ddf7620389cd.jsonl
Author
Owner

Blocked by #1323: the ClassLength lesson rewrite must exist before it gets related: keys.

Blocked by #1323: the ClassLength lesson rewrite must exist before it gets related: keys.
Author
Owner

Work started on branch ticket-1324 through /os-sdlc:implement.

Work started on branch ticket-1324 through /os-sdlc:implement.
Author
Owner

Resolution

Done: Lessons take optional related: and backstop: fields. Lesson::Relay renders the signal of each related lesson. The ClassLength lesson lists the four family cops and carries backstop text that names /os-sdlc:smell-analysis and a ticket capture. The gate reply adds a backstop hit line per file where Metrics/ClassLength fires and no family cop fires.

Evidence: Commits cb4674b6 (Runner commit, implementation 164: implementation_complete, 10 dispatches, no escalation, suite-check pass) and 7dd2dcd2 (History row). Tests in plugins/os-sdlc/tests/gate/class_length_backstop_test.rb cover the five acceptance criteria. Orchestrator read the production diff.

Follow-ups: Captured #1391 (skeptic CREATE): the backstop check reads the new-offense-filtered lint JSON, so a family cop offense already on base is filtered out and the file gets a false backstop hit.

## Resolution **Done:** Lessons take optional related: and backstop: fields. Lesson::Relay renders the signal of each related lesson. The ClassLength lesson lists the four family cops and carries backstop text that names /os-sdlc:smell-analysis and a ticket capture. The gate reply adds a backstop hit line per file where Metrics/ClassLength fires and no family cop fires. **Evidence:** Commits cb4674b6 (Runner commit, implementation 164: implementation_complete, 10 dispatches, no escalation, suite-check pass) and 7dd2dcd2 (History row). Tests in plugins/os-sdlc/tests/gate/class_length_backstop_test.rb cover the five acceptance criteria. Orchestrator read the production diff. **Follow-ups:** Captured #1391 (skeptic CREATE): the backstop check reads the new-offense-filtered lint JSON, so a family cop offense already on base is filtered out and the file gets a false backstop hit.
jared closed this issue 2026-10-25 21:43:49 +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#1324
No description provided.