Add cop NestedConstantOutsideConcept for a reference to a nested constant of another concept #1743
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1743
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
A file in one concept can name an object that is nested inside a different concept (
B::X). A concept is the first namespace segment below the library root. The caller then depends on how the other concept is built, and nothing in that concept shows the outside caller. Lens A2 defect (2) and lens A15 already state this rule, but no cop checks it. Agents add new reaches because nothing stops them.Observed
bin/andhooks/scripts, and names from gems.docs/research/lint-lessons/2026-11-12-domain-boundary-cops/smell-1-cross-domain-reach.md.Reproduce
n/a: feature ticket.
Approach
Add the cop
Sdlc/Structural/NestedConstantOutsideConceptand its lint lesson. The cop gets its facts from the Zeitwerk loader, asSdlc/Structural/InwardDependencydoes (lib/os_sdlc/cops/structural/inward_dependency.rb:9,lib/os_sdlc/cops/structural/layer_ledger/layer_map.rb:11). It uses no hand list and noExcludelist.The value rule:
Consequences of the rule, with no special case in the code:
bin/script, a hook) is not a source. The cop is silent there.B), or a value constant directly on a root (B::LIMIT, sonamedisB), is 1 segment below the root. The cop is silent.B::X::LIMITfires one time, and the offense namesB::X.rescue B::X,raise B::X, andclass Y < B::Xfire.Constraints:
const_getand do not autoload a constant. Some library constants are Sequel models that need a database connection.OsSdlc. Sdlc cops run in every target project (ADR-0082, ADR-0298). Use the loader whose expected constants contain the file. When no loader contains the file, report nothing.external_dependency_checksum, so the RuboCop result cache does not keep an old answer after a file moves.Offense message:
Lesson file
lint-lessons/sdlc-structural-nested-constant-outside-concept.yml(fields as inlib/os_sdlc/lesson.rb:4; modellint-lessons/sdlc-structural-inward-dependency.yml):Enable the cop in the sweep tier only (
.rubocop-sweep.yml, asSdlc/Structural/InwardDependencyat:7, with noExclude). The tree has 198 true offenses, so the cop does not go in.rubocop.ymlin this ticket.Required work:
Sdlc/Structural/NestedConstantOutsideConceptinplugins/os-sdlc/lint-lessons/sdlc-structural-nested-constant-outside-concept.ymlSdlc/Structural/NestedConstantOutsideConceptinplugins/os-sdlc/.rubocop-sweep.ymlexternal_dependency_checksuminplugins/os-sdlc/lib/os_sdlc/cops/structural/nested_constant_outside_concept.rbOrchestrator work: none.
Rejected: a cop that uses syntax only — 2,421 false hits of 2,621. Rejected: an exemption list for root values or for test files — the loader rule gives the same result with no list. Rejected: must-pass in
.rubocop.ymlnow — 198 true offenses exist; the move to must-pass is a later ticket.Acceptance criteria
B::X, whereB::Xhas its own file in the same loader, when the cop runs on that file, then it reports one offense on the reference, and the message namesB::X, conceptB, and conceptA.B, when the cop runs, then it reports no offense.B::LIMIT, whereLIMITis a value in the file ofBand has no file of its own, when the cop runs, then it reports no offense.A::X,A::X::Y, and a sibling by its bare name, when the cop runs, then it reports no offense.B::X::LIMIT, when the cop runs, then it reports one offense, and the message namesB::X.rescue B::Xand a class with the superclassB::X, when the cop runs, then it reports one offense for each of the two references.Gem::Thing), when the cop runs, then it reports no offense.B::Xat file level, outside eachclassandmodulebody, when the cop runs, then it reports one offense, because the source concept comes from the constant of the file.Owner sketch
Non-goals
.rubocop.yml.bin/scripts, and hooks.Origin
plugins/os-sdlc/skills/lens-architecture/SKILL.mdA2, A15)VERDICT: CREATE (ticket-skeptic, 2026-11-12, backlog search only; Jared asked for this ticket): no open ticket proposes a cop for a library file that names a nested constant of another concept.
Work started on branch ticket-1743 through the os-sdlc implement pipeline.
Resolution
Done: Added the cop Sdlc/Structural/NestedConstantOutsideConcept, its two collaborators (Library, Library::Crossing), its lint lesson, and its tests. The cop reads the Zeitwerk loader and reports a library file that names a constant nested below the root of a different concept. It is enabled in the sweep tier only.
Evidence: Implementation 289 ended implementation_complete; commit
f4f00c15con branch ticket-1743 (not merged yet). Sweep run on the real tree: 200 offenses in 115 lib files, none in test, bin, or hooks. rubocop --show-cops under .rubocop.yml shows Enabled: false. One site more than the probe count (198 + 1) is not traced.Follow-ups: #1745 (repair the offenses and move the three boundary cops from sweep to gate; ticket-skeptic verdict CREATE). The ticket rule line was corrected during escalation 129 to 'named is 2 or more segments below the root'. No other follow-up.