Lesson catalog: default lesson for Sdlc cops with no lesson, and a false-positive lesson per batch #1414

Closed
opened 2026-10-27 15:56:12 +00:00 by jared · 2 comments
Owner

Problem

Lesson::Catalog#lesson(key) returns nil when a cop has no lesson file. Lesson::Relay#ordered_keys (lesson/relay.rb:35) then drops the cop without a message. Six enabled Sdlc cops have no lesson today: HandRolledFetch, TautologicalAssertion, TestMethodAfterPrivate, UnreachableBranchAfterGuard, InstanceVariableAssumption, SimulatedPolymorphism. An agent that hits one of them gets no guidance and can fix the smoke instead of the fire. Also, no lesson tells an agent what to do when it thinks a hit is a false positive.

Reproduce

n/a: design gap; no lesson text reaches the agent for the six cops above.

Approach

Two data files carry the process text, so a process change edits one file.

class Lesson::Catalog
  NO_LESSON_YET = "_no-lesson-yet"

  def lesson(key)
    path = File.join(@dir, filename_for(key))
    return Lesson.from(path, YamlFile.read(path)) if File.file?(path)
    return nil unless key.start_with?("Sdlc/")

    no_lesson_yet(key)
  end

  def false_positive = read_required("_false-positive")
end

# Lesson::Relay, after the batch's lessons are collected:
lessons.compact!
lessons << @library.false_positive if lessons.any?
  • _no-lesson-yet.yml is found by file-name convention. Catalog fills in the cop name. The text refers to "the offense reported above"; the offense site is not passed in, because Relay gets keys only.
  • _no-lesson-yet.yml steps: capture a ticket with the cop, and the file:line and snippet of the offense reported above; run /os-sdlc:smell-analysis on the cop and that offense to find the smoke and the fire; update the ticket; tell Jared.
  • _false-positive.yml text: do not suppress; capture a ticket with the snippet and one true positive from the cop's tests; smell-analyze both; answer (a) what distinguishes this hit (sharpen the matcher and the lesson) and (b) is it smoke for a different problem (new cop and lesson). It includes the sentence "The cop's pattern matched but its intent did not; something unusual in the code often caused the match." It agrees with the "No false positives" section of plugins/os-sdlc/skills/mine-lints/references/implement-cop.md.
  • A missing _no-lesson-yet.yml or _false-positive.yml raises HarnessError that names the path.
  • Keys outside Sdlc/* (Layout, Metrics, detector keys) keep today's nil result.

Required work:

  1. Add the default lesson data file — check: present smell-analysis in plugins/os-sdlc/lint-lessons/_no-lesson-yet.yml
  2. Add the false-positive lesson data file — check: present intent did not in plugins/os-sdlc/lint-lessons/_false-positive.yml

Rejected: thread the offense file:line into the default lesson — needs new plumbing through OffenseDirective and Gate::Run; the gate output already shows the site above the lessons.
Rejected: default lesson for every key — a ticket per Layout offense is noise.

Acceptance criteria

  1. Given an enabled Sdlc/* cop with no lesson file, when the catalog looks it up, then it returns the default lesson, and the lesson text names that cop.
  2. Given a key outside Sdlc/* with no lesson file, when the catalog looks it up, then it returns nil.
  3. Given a lessons directory without _no-lesson-yet.yml, when the catalog looks up an Sdlc/* cop that has no lesson, then it raises an error that names the missing path.
  4. Given a batch of offenses where at least one has a lesson, when the relay builds the lesson text, then the false-positive lesson appears once, after the other lessons.
  5. Given a batch of offenses where no key has a lesson, when the relay builds the lesson text, then no false-positive lesson appears.
  6. Given the plugin's .rubocop.yml, when every enabled Sdlc/* cop is looked up in the default catalog, then each one resolves to a lesson.
  7. Given the real _no-lesson-yet.yml and _false-positive.yml, when each is loaded, then both pass Lesson.from validation.

Owner sketch

class OsSdlc::Lesson::Catalog
  def lesson(key)
  def false_positive
end

class OsSdlc::Lesson::Relay
  def ordered_keys
end

Non-goals

  • Real lessons for the six cops: increment 6 of the lint-cops track.
  • Passing the offense site into lessons.

Origin

  • Trigger: lint-cops track handoff .cc-os/os/handoffs/2026-10-27-1542-lint-cops-track.md, increment 1; blast radius 2026-10-27.
  • Improvised this session: none
  • Chain: agent gets no guidance for six cops ← Relay#ordered_keys drops nil lessons (plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb:35) ← Catalog#lesson returns nil for a missing file (plugins/os-sdlc/lib/os_sdlc/lesson/catalog.rb:11-15) ← DESIGN (lesson/catalog.rb)
  • Root candidate: none found; searched lesson, no lesson, false positive, Catalog, Relay, ordered_keys, _no-lesson, the six cop names
  • Where: plugins/os-sdlc/lib/os_sdlc/lesson/catalog.rb, plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb
  • Session: 0b4d7950-df1c-4eba-a561-ccc19b386a9a
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/0b4d7950-df1c-4eba-a561-ccc19b386a9a.jsonl

VERDICT: CREATE

## Problem `Lesson::Catalog#lesson(key)` returns nil when a cop has no lesson file. `Lesson::Relay#ordered_keys` (`lesson/relay.rb:35`) then drops the cop without a message. Six enabled Sdlc cops have no lesson today: HandRolledFetch, TautologicalAssertion, TestMethodAfterPrivate, UnreachableBranchAfterGuard, InstanceVariableAssumption, SimulatedPolymorphism. An agent that hits one of them gets no guidance and can fix the smoke instead of the fire. Also, no lesson tells an agent what to do when it thinks a hit is a false positive. ## Reproduce n/a: design gap; no lesson text reaches the agent for the six cops above. ## Approach Two data files carry the process text, so a process change edits one file. ```ruby class Lesson::Catalog NO_LESSON_YET = "_no-lesson-yet" def lesson(key) path = File.join(@dir, filename_for(key)) return Lesson.from(path, YamlFile.read(path)) if File.file?(path) return nil unless key.start_with?("Sdlc/") no_lesson_yet(key) end def false_positive = read_required("_false-positive") end # Lesson::Relay, after the batch's lessons are collected: lessons.compact! lessons << @library.false_positive if lessons.any? ``` - `_no-lesson-yet.yml` is found by file-name convention. Catalog fills in the cop name. The text refers to "the offense reported above"; the offense site is not passed in, because Relay gets keys only. - `_no-lesson-yet.yml` steps: capture a ticket with the cop, and the file:line and snippet of the offense reported above; run `/os-sdlc:smell-analysis` on the cop and that offense to find the smoke and the fire; update the ticket; tell Jared. - `_false-positive.yml` text: do not suppress; capture a ticket with the snippet and one true positive from the cop's tests; smell-analyze both; answer (a) what distinguishes this hit (sharpen the matcher and the lesson) and (b) is it smoke for a different problem (new cop and lesson). It includes the sentence "The cop's pattern matched but its intent did not; something unusual in the code often caused the match." It agrees with the "No false positives" section of `plugins/os-sdlc/skills/mine-lints/references/implement-cop.md`. - A missing `_no-lesson-yet.yml` or `_false-positive.yml` raises HarnessError that names the path. - Keys outside `Sdlc/*` (Layout, Metrics, detector keys) keep today's nil result. Required work: 1. Add the default lesson data file — check: present `smell-analysis` in `plugins/os-sdlc/lint-lessons/_no-lesson-yet.yml` 2. Add the false-positive lesson data file — check: present `intent did not` in `plugins/os-sdlc/lint-lessons/_false-positive.yml` Rejected: thread the offense file:line into the default lesson — needs new plumbing through `OffenseDirective` and `Gate::Run`; the gate output already shows the site above the lessons. Rejected: default lesson for every key — a ticket per Layout offense is noise. ## Acceptance criteria 1. Given an enabled `Sdlc/*` cop with no lesson file, when the catalog looks it up, then it returns the default lesson, and the lesson text names that cop. 2. Given a key outside `Sdlc/*` with no lesson file, when the catalog looks it up, then it returns nil. 3. Given a lessons directory without `_no-lesson-yet.yml`, when the catalog looks up an `Sdlc/*` cop that has no lesson, then it raises an error that names the missing path. 4. Given a batch of offenses where at least one has a lesson, when the relay builds the lesson text, then the false-positive lesson appears once, after the other lessons. 5. Given a batch of offenses where no key has a lesson, when the relay builds the lesson text, then no false-positive lesson appears. 6. Given the plugin's `.rubocop.yml`, when every enabled `Sdlc/*` cop is looked up in the default catalog, then each one resolves to a lesson. 7. Given the real `_no-lesson-yet.yml` and `_false-positive.yml`, when each is loaded, then both pass `Lesson.from` validation. ## Owner sketch ```ruby class OsSdlc::Lesson::Catalog def lesson(key) def false_positive end class OsSdlc::Lesson::Relay def ordered_keys end ``` ## Non-goals - Real lessons for the six cops: increment 6 of the lint-cops track. - Passing the offense site into lessons. ## Origin - Trigger: lint-cops track handoff `.cc-os/os/handoffs/2026-10-27-1542-lint-cops-track.md`, increment 1; blast radius 2026-10-27. - Improvised this session: none - Chain: agent gets no guidance for six cops ← `Relay#ordered_keys` drops nil lessons (`plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb:35`) ← `Catalog#lesson` returns nil for a missing file (`plugins/os-sdlc/lib/os_sdlc/lesson/catalog.rb:11-15`) ← DESIGN (`lesson/catalog.rb`) - Root candidate: none found; searched lesson, no lesson, false positive, Catalog, Relay, ordered_keys, _no-lesson, the six cop names - Where: `plugins/os-sdlc/lib/os_sdlc/lesson/catalog.rb`, `plugins/os-sdlc/lib/os_sdlc/lesson/relay.rb` - Session: 0b4d7950-df1c-4eba-a561-ccc19b386a9a - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/0b4d7950-df1c-4eba-a561-ccc19b386a9a.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1414 through /os-sdlc:implement (poodr-ticket-implementation).

Work started on branch ticket-1414 through /os-sdlc:implement (poodr-ticket-implementation).
Author
Owner

Resolution

Done: Lesson::Catalog returns the _no-lesson-yet default (cop name filled in) for an Sdlc/* key with no lesson file and raises HarnessError naming the path when a required default file is missing; Lesson::Relay appends the _false-positive lesson once per batch when any lesson exists. Two data files added under plugins/os-sdlc/lint-lessons/.

Evidence: Implementation 182 complete (13 dispatches, 20 min 38 s); commits ca4ebfa8 (production, Runner) and 1f148fc6 (test fixtures hand-fixed after a programmer test-defect escalation and reroute); os-sdlc suite green twice at suite-check (dispatches 1603, 1614); contract-auditor pass (1615).

Follow-ups: #1419: widen the default lesson to every acted-on offense except autocorrectable Layout/Style cops (Jared ruled 2026-10-27).

## Resolution **Done:** Lesson::Catalog returns the _no-lesson-yet default (cop name filled in) for an Sdlc/* key with no lesson file and raises HarnessError naming the path when a required default file is missing; Lesson::Relay appends the _false-positive lesson once per batch when any lesson exists. Two data files added under plugins/os-sdlc/lint-lessons/. **Evidence:** Implementation 182 complete (13 dispatches, 20 min 38 s); commits ca4ebfa8 (production, Runner) and 1f148fc6 (test fixtures hand-fixed after a programmer test-defect escalation and reroute); os-sdlc suite green twice at suite-check (dispatches 1603, 1614); contract-auditor pass (1615). **Follow-ups:** #1419: widen the default lesson to every acted-on offense except autocorrectable Layout/Style cops (Jared ruled 2026-10-27).
jared closed this issue 2026-10-27 16:19:19 +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#1414
No description provided.