Port the inward-dependency lint to a rubocop cop #1073

Closed
opened 2026-10-17 10:40:23 +00:00 by jared · 2 comments
Owner

Problem

The inward-dependency check judges code: it reports a reference that crosses from an inner layer to an outer layer. Every other code judgment in os-sdlc is a rubocop cop that the gate runs. This one is a separate script, plugins/os-sdlc/bin/lint-inward-dependencies, called by hand as gate step 3 of the crc-refactor skill. It prints one summary line per file, not an offense on the referencing line. It runs only during a CRC run, so no other Ruby codebase gets the check.

Reproduce

n/a: design ticket. The script runs today with bundle exec bin/lint-inward-dependencies ../../docs/crc/os-sdlc/ledger.tsv from plugins/os-sdlc.

Approach

Port the check to a cop, Sdlc/Structural/InwardDependency, and delete the script. Decided design:

  1. Layer source. When LedgerPath: is configured, the layer is the ledger role column for the file path. Otherwise the layer is the first directory under lib/os_sdlc/ of the file (model/, presenter/, adapter/, and so on), the same rule build_role_map uses today. The layer table is the existing ROLE_LAYER constant, moved to the cop.
  2. The file's own constant comes from the Zeitwerk loader's expected cpath, never from parsing. That rule was fixed in a40f736e on 2026-10-17. Keep it.
  3. ADR-0090 governs the loader as the source of the role map. ADR-0082 and ADR-0066 govern how a Sdlc/* cop reaches target projects.
  4. Once the cop is green, delete the executable and gate step 3 from skills/crc-refactor/SKILL.md and references/parameters.md.
  5. Ship a lint lesson in lint-lessons/ with the cop. Lesson, code shape only: a reader that finds "the constant this file defines" by walking only class and module nodes names the wrong constant for Name = Struct.new, Name = Data.define, and Name = Class.new. Those are the same definition in a different spelling. Ask the loader for the constant instead of parsing. Test with one file of each spelling and assert the same name for all three.
  6. The cop enters .rubocop.yml enabled, with Exclude: entries for the files that carry the 69 edges reported on 2026-10-17, under a comment naming Phase 9 of docs/crc/PLAN.md as the burn-down. The gate must stay green.

Required work:

  1. Cop class with loader-sourced file constant and a two-source layer lookup — check: present all_expected_cpaths in plugins/os-sdlc/lib/os_sdlc/cops/inward_dependency*.rb
  2. Cop registered and enabled in the plugin rubocop config — check: present Sdlc/Structural/InwardDependency in plugins/os-sdlc/.rubocop.yml
  3. Script and its test seam removed — check: absent lint-inward-dependencies in plugins/os-sdlc/bin/*
  4. Lint lesson shipped — check: present all_expected_cpaths in plugins/os-sdlc/lint-lessons/*inward*

Orchestrator work: skill edits in item 4; ADR check against 0090, 0082, 0066 before the first edit.

Acceptance criteria

  1. Given a file in layer model that references a constant in layer command, when rubocop runs with the cop enabled, then an offense appears on the referencing line and names both layers.
  2. Given the same file spelled as Name = Struct.new, Name = Data.define, or class Name, when rubocop runs, then the file's own layer is the same in all three cases.
  3. Given no LedgerPath: in config, when rubocop runs on an A15 plugin, then the layer comes from the file's role directory.
  4. Given the cop is green on os-sdlc, when the crc-refactor skill runs its gate, then no step calls bin/lint-inward-dependencies and the file does not exist.

Owner sketch

class RuboCop::Cop::Sdlc::Structural::InwardDependency
  def on_const(node)
end

Non-goals

Ruling on the 69 edges the check reports today. That is a Phase 9 task in docs/crc/PLAN.md.
Surveying crc-sweep for a cop port. Follow-up ticket if the survey says yes.

Origin

  • Trigger: Phase 9 review of the inward lint after the a40f736e fix; Jared asked why the check is an executable and not a cop
  • Improvised this session: none
  • Chain: check runs only in CRC runs ← check is a script called by gate step 3 ← DESIGN (grew from the #748 ratchet with a baseline file; no ADR records the choice)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/lint/inward_dependency_check.rb
  • Session: f508dad4-7906-450e-bd5a-0bcf879917b0
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f508dad4-7906-450e-bd5a-0bcf879917b0.jsonl
## Problem The inward-dependency check judges code: it reports a reference that crosses from an inner layer to an outer layer. Every other code judgment in os-sdlc is a rubocop cop that the gate runs. This one is a separate script, `plugins/os-sdlc/bin/lint-inward-dependencies`, called by hand as gate step 3 of the crc-refactor skill. It prints one summary line per file, not an offense on the referencing line. It runs only during a CRC run, so no other Ruby codebase gets the check. ## Reproduce n/a: design ticket. The script runs today with `bundle exec bin/lint-inward-dependencies ../../docs/crc/os-sdlc/ledger.tsv` from `plugins/os-sdlc`. ## Approach Port the check to a cop, `Sdlc/Structural/InwardDependency`, and delete the script. Decided design: 1. Layer source. When `LedgerPath:` is configured, the layer is the ledger `role` column for the file path. Otherwise the layer is the first directory under `lib/os_sdlc/` of the file (`model/`, `presenter/`, `adapter/`, and so on), the same rule `build_role_map` uses today. The layer table is the existing `ROLE_LAYER` constant, moved to the cop. 2. The file's own constant comes from the Zeitwerk loader's expected cpath, never from parsing. That rule was fixed in a40f736e on 2026-10-17. Keep it. 3. ADR-0090 governs the loader as the source of the role map. ADR-0082 and ADR-0066 govern how a `Sdlc/*` cop reaches target projects. 4. Once the cop is green, delete the executable and gate step 3 from `skills/crc-refactor/SKILL.md` and `references/parameters.md`. 5. Ship a lint lesson in `lint-lessons/` with the cop. Lesson, code shape only: a reader that finds "the constant this file defines" by walking only class and module nodes names the wrong constant for `Name = Struct.new`, `Name = Data.define`, and `Name = Class.new`. Those are the same definition in a different spelling. Ask the loader for the constant instead of parsing. Test with one file of each spelling and assert the same name for all three. 6. The cop enters `.rubocop.yml` enabled, with `Exclude:` entries for the files that carry the 69 edges reported on 2026-10-17, under a comment naming Phase 9 of docs/crc/PLAN.md as the burn-down. The gate must stay green. Required work: 1. Cop class with loader-sourced file constant and a two-source layer lookup — check: present `all_expected_cpaths` in `plugins/os-sdlc/lib/os_sdlc/cops/inward_dependency*.rb` 2. Cop registered and enabled in the plugin rubocop config — check: present `Sdlc/Structural/InwardDependency` in `plugins/os-sdlc/.rubocop.yml` 3. Script and its test seam removed — check: absent `lint-inward-dependencies` in `plugins/os-sdlc/bin/*` 4. Lint lesson shipped — check: present `all_expected_cpaths` in `plugins/os-sdlc/lint-lessons/*inward*` Orchestrator work: skill edits in item 4; ADR check against 0090, 0082, 0066 before the first edit. ## Acceptance criteria 1. Given a file in layer `model` that references a constant in layer `command`, when rubocop runs with the cop enabled, then an offense appears on the referencing line and names both layers. 2. Given the same file spelled as `Name = Struct.new`, `Name = Data.define`, or `class Name`, when rubocop runs, then the file's own layer is the same in all three cases. 3. Given no `LedgerPath:` in config, when rubocop runs on an A15 plugin, then the layer comes from the file's role directory. 4. Given the cop is green on os-sdlc, when the crc-refactor skill runs its gate, then no step calls `bin/lint-inward-dependencies` and the file does not exist. ## Owner sketch ```ruby class RuboCop::Cop::Sdlc::Structural::InwardDependency def on_const(node) end ``` ## Non-goals Ruling on the 69 edges the check reports today. That is a Phase 9 task in `docs/crc/PLAN.md`. Surveying `crc-sweep` for a cop port. Follow-up ticket if the survey says yes. ## Origin - Trigger: Phase 9 review of the inward lint after the a40f736e fix; Jared asked why the check is an executable and not a cop - Improvised this session: none - Chain: check runs only in CRC runs ← check is a script called by gate step 3 ← DESIGN (grew from the #748 ratchet with a baseline file; no ADR records the choice) - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/lint/inward_dependency_check.rb - Session: f508dad4-7906-450e-bd5a-0bcf879917b0 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f508dad4-7906-450e-bd5a-0bcf879917b0.jsonl
Author
Owner

Work started on branch ticket-1073 via /os-sdlc:implement.

Work started on branch ticket-1073 via /os-sdlc:implement.
Author
Owner

Resolution

Done: Inward-dependency lint ported to the rubocop cop Sdlc/Structural/InwardDependency; script, lib and test deleted; loader construction extracted to lib/os_sdlc_loader.rb so the cop loads without bundler; LedgerPath set in .rubocop.yml with 39 files Excluded for Phase 9 burn-down; lint lesson shipped; crc-refactor skill text uses INWARD_LEDGER_PATH

Evidence: ticket-1073 commit 7fc5e28b; suite 1774 runs 0 failures 0 errors; whole-plugin rubocop 89 offenses, all pre-existing under eval/ and tests/support; cop alone on lib: no offenses with Excludes, 70 offenses in 39 files without; design audit (contract-auditor) passed, its findings applied; ADR find: no one-way door reversed (0090, 0166, 0082 inform). Hand-finished after Runner escalation 27 (unexpected pass) on implementation 52.

Follow-ups: crc_tickets still accepts --inward-lint-command (remove or repoint): captured as Phase 9 note in docs/crc/PLAN.md, no ticket; format-exemplar.md 'inward-lint count' wording stays as a historical example: dropped; burn down the 39 Excluded files: Phase 9 task already in docs/crc/PLAN.md

## Resolution **Done:** Inward-dependency lint ported to the rubocop cop Sdlc/Structural/InwardDependency; script, lib and test deleted; loader construction extracted to lib/os_sdlc_loader.rb so the cop loads without bundler; LedgerPath set in .rubocop.yml with 39 files Excluded for Phase 9 burn-down; lint lesson shipped; crc-refactor skill text uses INWARD_LEDGER_PATH **Evidence:** ticket-1073 commit 7fc5e28b; suite 1774 runs 0 failures 0 errors; whole-plugin rubocop 89 offenses, all pre-existing under eval/ and tests/support; cop alone on lib: no offenses with Excludes, 70 offenses in 39 files without; design audit (contract-auditor) passed, its findings applied; ADR find: no one-way door reversed (0090, 0166, 0082 inform). Hand-finished after Runner escalation 27 (unexpected pass) on implementation 52. **Follow-ups:** crc_tickets still accepts --inward-lint-command (remove or repoint): captured as Phase 9 note in docs/crc/PLAN.md, no ticket; format-exemplar.md 'inward-lint count' wording stays as a historical example: dropped; burn down the 39 Excluded files: Phase 9 task already in docs/crc/PLAN.md
jared closed this issue 2026-10-17 11:42:26 +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#1073
No description provided.