Add lint lessons for the four Sdlc/Structural cops that n8n-api hit #991

Closed
opened 2026-10-11 14:43:59 +00:00 by jared · 1 comment
Owner

Problem

plugins/os-sdlc/lint-lessons/ holds lessons for 3 Sequel cops only.
The gate relays no lesson for Sdlc/Structural/UnusedPrivateMethod, Sdlc/Structural/ParameterThreadedThroughPrivateMethods, Sdlc/Structural/CollaboratorConstructionInInitialize, or Sdlc/Structural/RequireRelativeDensity.
In n8n-api (jaredswanson/n8n-api#2, fix-lints run), the AI treated these four offenses as obstacles.
A human review found that each offense named a real smell.

  1. type_class was passed down through private methods only to receive new. The record class should own from_api.
  2. An unused private test helper was evidence of 9 lost list tests.
  3. instance_variable_set(:@faraday, …) in a test helper showed a missing injection seam.
  4. 35 require_relative lines showed a hand-kept load list. The fix is Zeitwerk.
    The policy is that lessons are written when a real pain point appears.
    This is that pain point.

Approach

The library resolves the registered name Sdlc/Structural/<Cop> to sdlc-structural-<kebab>.yml.
The drafter verified that Lesson::Library loads all four files and that Lesson::Relay renders them.
Add the four drafted lesson files below verbatim.

Required work:

  1. Add the lesson file for Sdlc/Structural/UnusedPrivateMethod. check: present cop: Sdlc/Structural/UnusedPrivateMethod in plugins/os-sdlc/lint-lessons/sdlc-structural-unused-private-method.yml
  2. Add the lesson file for Sdlc/Structural/ParameterThreadedThroughPrivateMethods. check: present cop: Sdlc/Structural/ParameterThreadedThroughPrivateMethods in plugins/os-sdlc/lint-lessons/sdlc-structural-parameter-threaded-through-private-methods.yml
  3. Add the lesson file for Sdlc/Structural/CollaboratorConstructionInInitialize. check: present cop: Sdlc/Structural/CollaboratorConstructionInInitialize in plugins/os-sdlc/lint-lessons/sdlc-structural-collaborator-construction-in-initialize.yml
  4. Add the lesson file for Sdlc/Structural/RequireRelativeDensity. check: present cop: Sdlc/Structural/RequireRelativeDensity in plugins/os-sdlc/lint-lessons/sdlc-structural-require-relative-density.yml

Drafted content of each file follows.

plugins/os-sdlc/lint-lessons/sdlc-structural-unused-private-method.yml

cop: Sdlc/Structural/UnusedPrivateMethod
signal: >-
  A private method has no call site in its own file. Before you delete it, run `git log -S` to find
  the caller that it lost.
sign_of: >-
  One of two conditions. The last caller was deleted, often in a refactor, and the method stayed
  behind. Or the callers are in other files, for example subclasses that call a private helper of
  their parent class. Then the parent and the subclasses share a contract that neither file shows.
why: >-
  A method with no caller tells the reader that a behavior exists when it does not. If the caller was
  deleted by mistake, the method is the last evidence of the lost test or behavior; when you delete
  the method, you also delete that evidence. A private method that other files call has an interface
  that no one can see. A change to it can break a subclass, and the file that changed shows no
  failure.
decision: >-
  Find out why the method has no caller. Run `git log -S '<method_name>'` and read the commit that
  removed the last call. If the removal was intended, delete the method. If the removal was a mistake,
  restore the caller, for example the deleted test. If other files call the method, the offense is a
  false positive, because the cop examines one file at a time. Report the false positive to the
  human. Do not add an `Exclude` or a disable comment without a human. Then ask a second question:
  does the helper belong to the object that it builds or acts on?
correction: >-
  For a lost caller, restore the caller or delete the method. For a helper that subclasses call, make
  the contract visible: move the behavior to the object that it acts on, or make it a public method
  of a named collaborator. Do not make the method public only to stop the offense.
example:
  before: |-
    class OrdersTest < Minitest::Test
      def test_lists_orders
        stub_get("/orders", { "data" => [] })
        assert_empty client.orders.list
      end

      private

      def order_payload
        { "id" => 1, "status" => "shipped" }
      end
    end
  after: |-
    # git log -S order_payload: a refactor deleted test_finds_one_order.
    class OrdersTest < Minitest::Test
      def test_lists_orders
        stub_get("/orders", { "data" => [] })
        assert_empty client.orders.list
      end

      def test_finds_one_order
        stub_get("/orders/1", order_payload)
        assert_equal "shipped", client.orders.find(1).status
      end

      private

      def order_payload
        { "id" => 1, "status" => "shipped" }
      end
    end

plugins/os-sdlc/lint-lessons/sdlc-structural-parameter-threaded-through-private-methods.yml

cop: Sdlc/Structural/ParameterThreadedThroughPrivateMethods
signal: >-
  One parameter goes through 3 or more private methods. Find the message that the last method sends
  to it: the parameter is probably the receiver that should own the job.
sign_of: >-
  The class does work that belongs to the object it passes down. The methods carry the parameter only
  so that the last method can send it one message, often `new`. Vague names such as `klass`, `type`,
  `data`, or `options` hide which object that is.
why: >-
  Each method in the chain takes a parameter that it does not use. The chain couples the methods to
  each other, and the object that should own the behavior does not have it. A new caller must copy the
  chain or reach into the private methods.
decision: >-
  Rename vague names first, so that you can see what the parameter is. Then follow it to the end of
  the chain and name the message it receives there. If it receives one message, the parameter can
  own the whole job, and each caller sends that message to it directly. If several methods use the
  parameter as shared context for different steps, an object that holds it as instance state is
  correct. The cop message suggests that holder object; when the parameter is only a receiver, the
  holder is the weaker fix. If the parameter is a plain value, such as a string or a count, that each
  method uses for its own reason, the offense can be a false positive. Report it to the human. Do not
  add an `Exclude` or a disable comment without a human.
correction: >-
  Move the behavior to the object that was passed down, and send it the message. When several
  classes can take the place of the parameter, give each one the same method name (a duck type), and
  pass the class as an argument to the object that needs it.
example:
  before: |-
    class Resource
      private

      def build_page(model_class, body)
        Page.new(items: build_all(model_class, body["data"]), next_cursor: body["nextCursor"])
      end

      def build_all(model_class, rows)
        rows.map { |row| build_one(model_class, row) }
      end

      def build_one(model_class, row)
        model_class.new(**row.transform_keys(&:to_sym))
      end
    end

    class Orders < Resource
      def list = build_page(Order, connection.get("/orders"))
    end
  after: |-
    class Order
      def self.from_api(row) = new(**row.transform_keys(&:to_sym))
    end

    class Page
      def self.from_api(body, of:)
        new(items: body["data"].map { |row| of.from_api(row) }, next_cursor: body["nextCursor"])
      end
    end

    class Orders < Resource
      def list = Page.from_api(connection.get("/orders"), of: Order)
    end

plugins/os-sdlc/lint-lessons/sdlc-structural-collaborator-construction-in-initialize.yml

cop: Sdlc/Structural/CollaboratorConstructionInInitialize
signal: >-
  `initialize` calls `.new` on 2 or more collaborator classes. Check whether a test can give this
  object a double without access to its private state.
sign_of: >-
  The object decides both what it does and which concrete collaborators it uses. A test cannot replace
  a collaborator. The evidence is a test that calls `instance_variable_set`, calls a private method
  with `send`, or stubs a class globally to put a double in place.
why: >-
  When an object has no seam, its tests depend on its private state, and the tests break when the
  internals change. Callers also cannot use the object with a different collaborator.
decision: >-
  Decide whether this class is a composition root: the one place that turns primitive arguments, such
  as a URL, a key, or options, into a set of connected objects. A composition root must build objects;
  that is its job. The offense is still correct if tests cannot give the object a double; then add a
  seam. If the class is a composition root and no test or caller needs a different collaborator, the
  offense is a false positive. Report it to the human. Do not add an `Exclude` or a disable comment
  without a human.
correction: >-
  Move construction into a class factory method (`.for`) that builds the real objects and passes them
  to `new`. Or accept each collaborator as a keyword argument whose default is the real object. Then
  change the tests to pass the double through that seam, and remove each `instance_variable_set`.
example:
  before: |-
    class Client
      def initialize(base_url:, api_key:)
        @config = Configuration.new(base_url:, api_key:)
        @connection = Connection.new(@config)
      end
    end

    client = Client.new(base_url: "https://shop.test", api_key: "key")
    client.connection.instance_variable_set(:@http, fake_http)
  after: |-
    class Client
      def self.for(base_url:, api_key:)
        new(connection: Connection.new(Configuration.new(base_url:, api_key:)))
      end

      def initialize(connection:)
        @connection = connection
      end
    end

    client = Client.new(connection: Connection.new(config, http: fake_http))

plugins/os-sdlc/lint-lessons/sdlc-structural-require-relative-density.yml

cop: Sdlc/Structural/RequireRelativeDensity
signal: >-
  A file has more top-level `require` and `require_relative` calls than `Max` (default 15). The project
  keeps its load list by hand; plan a move to Zeitwerk and do not increase `Max`.
sign_of: >-
  The project keeps its load list by hand. Each new file needs a new line in the list, and the order
  of the lines is a hidden dependency.
why: >-
  The list drifts from the files on disk. A missing line does not fail when the library loads. It
  fails later with `NameError`, and only when a code path uses the constant that the missing file
  defines.
decision: >-
  A move to Zeitwerk is an architecture decision. Record an ADR before you change the code. Zeitwerk
  requires that the namespace of each constant agrees with its file path: `lib/shop/orders/item.rb`
  must define `Shop::Orders::Item`. List each file that does not agree, and plan those moves in the
  same change. Do not increase `Max`, and do not add an `Exclude` or a disable comment without a human.
  If most of the lines load external gems, or the file is an intentional manifest such as a test
  helper, the offense can be a false positive. Report it to the human.
correction: >-
  Add `zeitwerk` as a dependency. Set up a loader in the entry file, and delete the `require_relative`
  lines for project files. Keep `require` for external gems. Add a test that calls
  `loader.eager_load`; it fails for each file whose namespace does not agree with its path.
example:
  before: |-
    require "json"
    require_relative "shop/version"
    require_relative "shop/configuration"
    require_relative "shop/connection"
    require_relative "shop/orders/item"
    require_relative "shop/orders/order"
    # ... 30 more require_relative lines

    module Shop
    end
  after: |-
    require "json"
    require "zeitwerk"

    loader = Zeitwerk::Loader.for_gem
    loader.setup

    module Shop
    end

Acceptance criteria

  1. Given the four lesson files exist, when Lesson::Library loads the lesson directory, then it returns a lesson for each of the four Sdlc/Structural cops.
  2. Given an offense from one of the four cops, when the gate runs, then Lesson::Relay renders the lesson for that cop.
  3. Given the existing 3 Sequel lessons, when the four new files are added, then the Sequel lessons are unchanged and still load.

Notes for review

These are format gaps found while drafting. They are open questions, not required work.

  1. The lesson format has no field for the false-positive branch. The branch sits at the end of decision.
  2. The format allows one example per lesson. UnusedPrivateMethod has two distinct cases: a lost caller, and a helper that subclasses call (see #990).
  3. Later sightings show only signal. Key guidance is packed into the signals for that reason.
  4. The ParameterThreadedThroughPrivateMethods cop message suggests a holder object. The lesson calls the holder the weaker fix when the parameter receives only one message.
  5. Two examples contain a comment line. The lesson after example for UnusedPrivateMethod and the before example for RequireRelativeDensity each have one.

Origin

  • Trigger: Jared explicitly asked for this ticket after the n8n-api fix-lints run (jaredswanson/n8n-api#2).
  • Improvised this session: The four lesson files were drafted by hand because the gate relayed no lesson for these cops.
  • Chain: fix-lints run in n8n-api, then human review of the four offense classes, then this ticket.
  • Root candidate: plugins/os-sdlc/lint-lessons/ has no lessons for the Sdlc/Structural cops.
  • Where: plugins/os-sdlc/lint-lessons/
  • Session: 44cf3646-a8fe-4f26-8550-e92296c57f86
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-ruby-gems-n8n-api/44cf3646-a8fe-4f26-8550-e92296c57f86.jsonl
  1. #989: lessons reach every lint-fixing skill and agent.
  2. #990: UnusedPrivateMethod and base-class helpers.

Discoverer: /home/jared/dev/ruby-gems/n8n-api, session 44cf3646-a8fe-4f26-8550-e92296c57f86, 2026-10-11. The gate relays no lesson for four Sdlc/Structural cops that n8n-api hit.

## Problem `plugins/os-sdlc/lint-lessons/` holds lessons for 3 Sequel cops only. The gate relays no lesson for `Sdlc/Structural/UnusedPrivateMethod`, `Sdlc/Structural/ParameterThreadedThroughPrivateMethods`, `Sdlc/Structural/CollaboratorConstructionInInitialize`, or `Sdlc/Structural/RequireRelativeDensity`. In n8n-api (jaredswanson/n8n-api#2, fix-lints run), the AI treated these four offenses as obstacles. A human review found that each offense named a real smell. 1. `type_class` was passed down through private methods only to receive `new`. The record class should own `from_api`. 2. An unused private test helper was evidence of 9 lost list tests. 3. `instance_variable_set(:@faraday, …)` in a test helper showed a missing injection seam. 4. 35 `require_relative` lines showed a hand-kept load list. The fix is Zeitwerk. The policy is that lessons are written when a real pain point appears. This is that pain point. ## Approach The library resolves the registered name `Sdlc/Structural/<Cop>` to `sdlc-structural-<kebab>.yml`. The drafter verified that `Lesson::Library` loads all four files and that `Lesson::Relay` renders them. Add the four drafted lesson files below verbatim. Required work: 1. Add the lesson file for `Sdlc/Structural/UnusedPrivateMethod`. check: present `cop: Sdlc/Structural/UnusedPrivateMethod` in `plugins/os-sdlc/lint-lessons/sdlc-structural-unused-private-method.yml` 2. Add the lesson file for `Sdlc/Structural/ParameterThreadedThroughPrivateMethods`. check: present `cop: Sdlc/Structural/ParameterThreadedThroughPrivateMethods` in `plugins/os-sdlc/lint-lessons/sdlc-structural-parameter-threaded-through-private-methods.yml` 3. Add the lesson file for `Sdlc/Structural/CollaboratorConstructionInInitialize`. check: present `cop: Sdlc/Structural/CollaboratorConstructionInInitialize` in `plugins/os-sdlc/lint-lessons/sdlc-structural-collaborator-construction-in-initialize.yml` 4. Add the lesson file for `Sdlc/Structural/RequireRelativeDensity`. check: present `cop: Sdlc/Structural/RequireRelativeDensity` in `plugins/os-sdlc/lint-lessons/sdlc-structural-require-relative-density.yml` Drafted content of each file follows. `plugins/os-sdlc/lint-lessons/sdlc-structural-unused-private-method.yml` ```yaml cop: Sdlc/Structural/UnusedPrivateMethod signal: >- A private method has no call site in its own file. Before you delete it, run `git log -S` to find the caller that it lost. sign_of: >- One of two conditions. The last caller was deleted, often in a refactor, and the method stayed behind. Or the callers are in other files, for example subclasses that call a private helper of their parent class. Then the parent and the subclasses share a contract that neither file shows. why: >- A method with no caller tells the reader that a behavior exists when it does not. If the caller was deleted by mistake, the method is the last evidence of the lost test or behavior; when you delete the method, you also delete that evidence. A private method that other files call has an interface that no one can see. A change to it can break a subclass, and the file that changed shows no failure. decision: >- Find out why the method has no caller. Run `git log -S '<method_name>'` and read the commit that removed the last call. If the removal was intended, delete the method. If the removal was a mistake, restore the caller, for example the deleted test. If other files call the method, the offense is a false positive, because the cop examines one file at a time. Report the false positive to the human. Do not add an `Exclude` or a disable comment without a human. Then ask a second question: does the helper belong to the object that it builds or acts on? correction: >- For a lost caller, restore the caller or delete the method. For a helper that subclasses call, make the contract visible: move the behavior to the object that it acts on, or make it a public method of a named collaborator. Do not make the method public only to stop the offense. example: before: |- class OrdersTest < Minitest::Test def test_lists_orders stub_get("/orders", { "data" => [] }) assert_empty client.orders.list end private def order_payload { "id" => 1, "status" => "shipped" } end end after: |- # git log -S order_payload: a refactor deleted test_finds_one_order. class OrdersTest < Minitest::Test def test_lists_orders stub_get("/orders", { "data" => [] }) assert_empty client.orders.list end def test_finds_one_order stub_get("/orders/1", order_payload) assert_equal "shipped", client.orders.find(1).status end private def order_payload { "id" => 1, "status" => "shipped" } end end ``` `plugins/os-sdlc/lint-lessons/sdlc-structural-parameter-threaded-through-private-methods.yml` ```yaml cop: Sdlc/Structural/ParameterThreadedThroughPrivateMethods signal: >- One parameter goes through 3 or more private methods. Find the message that the last method sends to it: the parameter is probably the receiver that should own the job. sign_of: >- The class does work that belongs to the object it passes down. The methods carry the parameter only so that the last method can send it one message, often `new`. Vague names such as `klass`, `type`, `data`, or `options` hide which object that is. why: >- Each method in the chain takes a parameter that it does not use. The chain couples the methods to each other, and the object that should own the behavior does not have it. A new caller must copy the chain or reach into the private methods. decision: >- Rename vague names first, so that you can see what the parameter is. Then follow it to the end of the chain and name the message it receives there. If it receives one message, the parameter can own the whole job, and each caller sends that message to it directly. If several methods use the parameter as shared context for different steps, an object that holds it as instance state is correct. The cop message suggests that holder object; when the parameter is only a receiver, the holder is the weaker fix. If the parameter is a plain value, such as a string or a count, that each method uses for its own reason, the offense can be a false positive. Report it to the human. Do not add an `Exclude` or a disable comment without a human. correction: >- Move the behavior to the object that was passed down, and send it the message. When several classes can take the place of the parameter, give each one the same method name (a duck type), and pass the class as an argument to the object that needs it. example: before: |- class Resource private def build_page(model_class, body) Page.new(items: build_all(model_class, body["data"]), next_cursor: body["nextCursor"]) end def build_all(model_class, rows) rows.map { |row| build_one(model_class, row) } end def build_one(model_class, row) model_class.new(**row.transform_keys(&:to_sym)) end end class Orders < Resource def list = build_page(Order, connection.get("/orders")) end after: |- class Order def self.from_api(row) = new(**row.transform_keys(&:to_sym)) end class Page def self.from_api(body, of:) new(items: body["data"].map { |row| of.from_api(row) }, next_cursor: body["nextCursor"]) end end class Orders < Resource def list = Page.from_api(connection.get("/orders"), of: Order) end ``` `plugins/os-sdlc/lint-lessons/sdlc-structural-collaborator-construction-in-initialize.yml` ```yaml cop: Sdlc/Structural/CollaboratorConstructionInInitialize signal: >- `initialize` calls `.new` on 2 or more collaborator classes. Check whether a test can give this object a double without access to its private state. sign_of: >- The object decides both what it does and which concrete collaborators it uses. A test cannot replace a collaborator. The evidence is a test that calls `instance_variable_set`, calls a private method with `send`, or stubs a class globally to put a double in place. why: >- When an object has no seam, its tests depend on its private state, and the tests break when the internals change. Callers also cannot use the object with a different collaborator. decision: >- Decide whether this class is a composition root: the one place that turns primitive arguments, such as a URL, a key, or options, into a set of connected objects. A composition root must build objects; that is its job. The offense is still correct if tests cannot give the object a double; then add a seam. If the class is a composition root and no test or caller needs a different collaborator, the offense is a false positive. Report it to the human. Do not add an `Exclude` or a disable comment without a human. correction: >- Move construction into a class factory method (`.for`) that builds the real objects and passes them to `new`. Or accept each collaborator as a keyword argument whose default is the real object. Then change the tests to pass the double through that seam, and remove each `instance_variable_set`. example: before: |- class Client def initialize(base_url:, api_key:) @config = Configuration.new(base_url:, api_key:) @connection = Connection.new(@config) end end client = Client.new(base_url: "https://shop.test", api_key: "key") client.connection.instance_variable_set(:@http, fake_http) after: |- class Client def self.for(base_url:, api_key:) new(connection: Connection.new(Configuration.new(base_url:, api_key:))) end def initialize(connection:) @connection = connection end end client = Client.new(connection: Connection.new(config, http: fake_http)) ``` `plugins/os-sdlc/lint-lessons/sdlc-structural-require-relative-density.yml` ```yaml cop: Sdlc/Structural/RequireRelativeDensity signal: >- A file has more top-level `require` and `require_relative` calls than `Max` (default 15). The project keeps its load list by hand; plan a move to Zeitwerk and do not increase `Max`. sign_of: >- The project keeps its load list by hand. Each new file needs a new line in the list, and the order of the lines is a hidden dependency. why: >- The list drifts from the files on disk. A missing line does not fail when the library loads. It fails later with `NameError`, and only when a code path uses the constant that the missing file defines. decision: >- A move to Zeitwerk is an architecture decision. Record an ADR before you change the code. Zeitwerk requires that the namespace of each constant agrees with its file path: `lib/shop/orders/item.rb` must define `Shop::Orders::Item`. List each file that does not agree, and plan those moves in the same change. Do not increase `Max`, and do not add an `Exclude` or a disable comment without a human. If most of the lines load external gems, or the file is an intentional manifest such as a test helper, the offense can be a false positive. Report it to the human. correction: >- Add `zeitwerk` as a dependency. Set up a loader in the entry file, and delete the `require_relative` lines for project files. Keep `require` for external gems. Add a test that calls `loader.eager_load`; it fails for each file whose namespace does not agree with its path. example: before: |- require "json" require_relative "shop/version" require_relative "shop/configuration" require_relative "shop/connection" require_relative "shop/orders/item" require_relative "shop/orders/order" # ... 30 more require_relative lines module Shop end after: |- require "json" require "zeitwerk" loader = Zeitwerk::Loader.for_gem loader.setup module Shop end ``` ## Acceptance criteria 1. Given the four lesson files exist, when `Lesson::Library` loads the lesson directory, then it returns a lesson for each of the four `Sdlc/Structural` cops. 2. Given an offense from one of the four cops, when the gate runs, then `Lesson::Relay` renders the lesson for that cop. 3. Given the existing 3 Sequel lessons, when the four new files are added, then the Sequel lessons are unchanged and still load. ## Notes for review These are format gaps found while drafting. They are open questions, not required work. 1. The lesson format has no field for the false-positive branch. The branch sits at the end of `decision`. 2. The format allows one example per lesson. `UnusedPrivateMethod` has two distinct cases: a lost caller, and a helper that subclasses call (see #990). 3. Later sightings show only `signal`. Key guidance is packed into the signals for that reason. 4. The `ParameterThreadedThroughPrivateMethods` cop message suggests a holder object. The lesson calls the holder the weaker fix when the parameter receives only one message. 5. Two examples contain a comment line. The lesson `after` example for `UnusedPrivateMethod` and the `before` example for `RequireRelativeDensity` each have one. ## Origin - Trigger: Jared explicitly asked for this ticket after the n8n-api fix-lints run (jaredswanson/n8n-api#2). - Improvised this session: The four lesson files were drafted by hand because the gate relayed no lesson for these cops. - Chain: fix-lints run in n8n-api, then human review of the four offense classes, then this ticket. - Root candidate: `plugins/os-sdlc/lint-lessons/` has no lessons for the `Sdlc/Structural` cops. - Where: `plugins/os-sdlc/lint-lessons/` - Session: 44cf3646-a8fe-4f26-8550-e92296c57f86 - Transcript: /home/jared/.claude/projects/-home-jared-dev-ruby-gems-n8n-api/44cf3646-a8fe-4f26-8550-e92296c57f86.jsonl ## Related 1. #989: lessons reach every lint-fixing skill and agent. 2. #990: `UnusedPrivateMethod` and base-class helpers. -------- **Discoverer:** /home/jared/dev/ruby-gems/n8n-api, session 44cf3646-a8fe-4f26-8550-e92296c57f86, 2026-10-11. The gate relays no lesson for four Sdlc/Structural cops that n8n-api hit.
Author
Owner

Resolution

Done: Added four lesson files under plugins/os-sdlc/lint-lessons/ for Sdlc/Structural/UnusedPrivateMethod, ParameterThreadedThroughPrivateMethods, CollaboratorConstructionInInitialize, RequireRelativeDensity, written from the n8n-api session discussion (lost test, inverted responsibility, missing seam, hand-kept load list).

Evidence: Commit 6f63a09d on main. Lesson::Library returns a lesson for all four cops and the three Sequel cops; Lesson::Relay renders all four (full text when unseen, signal only when seen). tests/presenter/lesson and tests/adapter/lesson: 14 runs, 40 assertions, 0 failures.

Follow-ups: Format gaps (no false-positive field, one example per cop, signal-only on repeat) stay as notes on #989/#990; none filed. A Naming/PredicateMethod lesson is not in scope here and is tracked by #989's stock-cop concern.

## Resolution **Done:** Added four lesson files under plugins/os-sdlc/lint-lessons/ for Sdlc/Structural/UnusedPrivateMethod, ParameterThreadedThroughPrivateMethods, CollaboratorConstructionInInitialize, RequireRelativeDensity, written from the n8n-api session discussion (lost test, inverted responsibility, missing seam, hand-kept load list). **Evidence:** Commit 6f63a09d on main. Lesson::Library returns a lesson for all four cops and the three Sequel cops; Lesson::Relay renders all four (full text when unseen, signal only when seen). tests/presenter/lesson and tests/adapter/lesson: 14 runs, 40 assertions, 0 failures. **Follow-ups:** Format gaps (no false-positive field, one example per cop, signal-only on repeat) stay as notes on #989/#990; none filed. A Naming/PredicateMethod lesson is not in scope here and is tracked by #989's stock-cop concern.
jared closed this issue 2026-10-11 22:08:47 +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#991
No description provided.