Add CodeShape::LexicalScope; move ConceptCycleCheck::References onto it #1729

Closed
opened 2026-11-12 21:37:43 +00:00 by jared · 2 comments
Owner

Problem

Seven pieces of os-sdlc each build their own table of constant definitions and their own lexical-scope rule, and they resolve the same name to different targets (#1718). No object owns facts read from parsed Ruby code. Jared named that owner OsSdlc::CodeShape (ruling 145). This ticket builds the first part of it and moves one caller onto it.

Design

/home/jared/dev/cc-os/docs/takedown/1729-20261112-163805/final-design.json

Approach

Add OsSdlc::CodeShape::LexicalScope as the single owner of Ruby's lexical constant-lookup rule (one frame per module/class node, rooted :: bypasses scope) and move ConceptCycleCheck::References onto it, deleting its scope string and nesting rule.

Required work:

  1. Write plugins/os-sdlc/lib/os_sdlc/code_shape/lexical_scope.rb
  2. Write plugins/os-sdlc/lib/os_sdlc/lint/concept_cycle_check/references.rb
  3. Write test plugins/os-sdlc/test/os_sdlc/code_shape/lexical_scope_test.rb
  4. Write test plugins/os-sdlc/test/os_sdlc/lint/concept_cycle_check/references_test.rb

Acceptance criteria

  • module A::B referencing X, with A::X defined: no edge (matches Ruby)
  • module A; module B::C referencing X: candidates A::B::C::X, A::X, X
  • ::Helper inside module M: Helper only
  • dynamic constant path: nil, skipped as before
  • top-level reference: X

Owner sketch

Responsibility Owner
Lexical scope frames, nesting a compact path, candidate full names for a reference, rooted :: CodeShape::LexicalScope
Reading constant nodes into names with the anchor kept ConstantPath.rooted_parts (existing)
Collecting definitions and references from files, resolving against the definitions ConceptCycleCheck::References (until a later CodeShape part takes the table)

plugins/os-sdlc/test/os_sdlc/code_shape/lexical_scope_test.rb

# frozen_string_literal: true

require_relative "../../test_helper"

module OsSdlc
  class CodeShapeLexicalScopeTest < Minitest::Test
    def test_a_compact_path_is_one_frame
      scope = CodeShape::LexicalScope.root.nest(%w[A B])

      assert_equal "A::B", scope.name
      assert_equal ["A::B::X", "X"], scope.candidates(["X"])
    end

    def test_outer_frames_stay_visible
      scope = CodeShape::LexicalScope.root.nest(["A"]).nest(%w[B C])

      assert_equal ["A::B::C::X", "A::X", "X"], scope.candidates(["X"])
    end

    def test_a_rooted_name_skips_every_scope
      scope = CodeShape::LexicalScope.root.nest(["M"])

      assert_equal ["Helper"], scope.candidates(["::Helper"])
    end

    def test_a_rooted_nest_resets_the_base
      scope = CodeShape::LexicalScope.root.nest(["M"]).nest(["::Foo"])

      assert_equal "Foo", scope.name
    end
  end
end

plugins/os-sdlc/test/os_sdlc/lint/concept_cycle_check/references_test.rb

# frozen_string_literal: true

require_relative "../../../test_helper"

module OsSdlc
  class LintConceptCycleCheckReferencesTest < Minitest::Test
    include TmpdirHelpers

    def test_a_superclass_naming_a_sibling_concept_resolves_to_it
      source = "module Zed\n  class Alpha < Beta\n  end\n  class Beta\n  end\nend\n"

      assert_equal [["Zed", "Zed::Beta"]], resolved(source)
    end

    def test_a_constant_written_in_a_module_resolves_a_reference_to_it
      source = "module Zed\n  Beta = 1\n  class Alpha\n    def call = Beta\n  end\nend\n"

      assert_equal [["Zed::Alpha", "Zed::Beta"]], resolved(source)
    end

    def test_a_constant_write_value_reads_its_constant_as_a_reference
      source = "module Zed\n  class Alpha\n    Shared = Beta\n  end\n  class Beta\n  end\nend\n"

      assert_equal [["Zed::Alpha", "Zed::Beta"]], resolved(source)
    end

    def test_a_compact_module_does_not_see_its_first_segment
      source = "module A\n  X = 1\nend\nmodule A::B\n  def f = X\nend\n"

      assert_empty resolved(source)
    end

    def test_a_rooted_name_resolves_to_the_top_level_constant
      source = "Helper = 1\nmodule M\n  Helper = 2\n  def f = ::Helper\nend\n"

      assert_equal [["M", "Helper"]], resolved(source)
    end

    private

    def resolved(source)
      with_tmpdir do |dir|
        File.write(File.join(dir, "zed.rb"), source)
        OsSdlc::Lint::ConceptCycleCheck::References.new(dir).resolved
      end
    end
  end
end
## Problem Seven pieces of os-sdlc each build their own table of constant definitions and their own lexical-scope rule, and they resolve the same name to different targets (#1718). No object owns facts read from parsed Ruby code. Jared named that owner `OsSdlc::CodeShape` (ruling 145). This ticket builds the first part of it and moves one caller onto it. ## Design /home/jared/dev/cc-os/docs/takedown/1729-20261112-163805/final-design.json ## Approach Add OsSdlc::CodeShape::LexicalScope as the single owner of Ruby's lexical constant-lookup rule (one frame per module/class node, rooted `::` bypasses scope) and move ConceptCycleCheck::References onto it, deleting its scope string and nesting rule. Required work: 1. Write `plugins/os-sdlc/lib/os_sdlc/code_shape/lexical_scope.rb` 2. Write `plugins/os-sdlc/lib/os_sdlc/lint/concept_cycle_check/references.rb` 3. Write test `plugins/os-sdlc/test/os_sdlc/code_shape/lexical_scope_test.rb` 4. Write test `plugins/os-sdlc/test/os_sdlc/lint/concept_cycle_check/references_test.rb` ## Acceptance criteria - `module A::B` referencing `X`, with `A::X` defined: no edge (matches Ruby) - `module A; module B::C` referencing `X`: candidates `A::B::C::X`, `A::X`, `X` - `::Helper` inside `module M`: `Helper` only - dynamic constant path: nil, skipped as before - top-level reference: `X` ## Owner sketch | Responsibility | Owner | |---|---| | Lexical scope frames, nesting a compact path, candidate full names for a reference, rooted `::` | `CodeShape::LexicalScope` | | Reading constant nodes into names with the anchor kept | `ConstantPath.rooted_parts` (existing) | | Collecting definitions and references from files, resolving against the definitions | `ConceptCycleCheck::References` (until a later CodeShape part takes the table) | `plugins/os-sdlc/test/os_sdlc/code_shape/lexical_scope_test.rb` ```ruby # frozen_string_literal: true require_relative "../../test_helper" module OsSdlc class CodeShapeLexicalScopeTest < Minitest::Test def test_a_compact_path_is_one_frame scope = CodeShape::LexicalScope.root.nest(%w[A B]) assert_equal "A::B", scope.name assert_equal ["A::B::X", "X"], scope.candidates(["X"]) end def test_outer_frames_stay_visible scope = CodeShape::LexicalScope.root.nest(["A"]).nest(%w[B C]) assert_equal ["A::B::C::X", "A::X", "X"], scope.candidates(["X"]) end def test_a_rooted_name_skips_every_scope scope = CodeShape::LexicalScope.root.nest(["M"]) assert_equal ["Helper"], scope.candidates(["::Helper"]) end def test_a_rooted_nest_resets_the_base scope = CodeShape::LexicalScope.root.nest(["M"]).nest(["::Foo"]) assert_equal "Foo", scope.name end end end ``` `plugins/os-sdlc/test/os_sdlc/lint/concept_cycle_check/references_test.rb` ```ruby # frozen_string_literal: true require_relative "../../../test_helper" module OsSdlc class LintConceptCycleCheckReferencesTest < Minitest::Test include TmpdirHelpers def test_a_superclass_naming_a_sibling_concept_resolves_to_it source = "module Zed\n class Alpha < Beta\n end\n class Beta\n end\nend\n" assert_equal [["Zed", "Zed::Beta"]], resolved(source) end def test_a_constant_written_in_a_module_resolves_a_reference_to_it source = "module Zed\n Beta = 1\n class Alpha\n def call = Beta\n end\nend\n" assert_equal [["Zed::Alpha", "Zed::Beta"]], resolved(source) end def test_a_constant_write_value_reads_its_constant_as_a_reference source = "module Zed\n class Alpha\n Shared = Beta\n end\n class Beta\n end\nend\n" assert_equal [["Zed::Alpha", "Zed::Beta"]], resolved(source) end def test_a_compact_module_does_not_see_its_first_segment source = "module A\n X = 1\nend\nmodule A::B\n def f = X\nend\n" assert_empty resolved(source) end def test_a_rooted_name_resolves_to_the_top_level_constant source = "Helper = 1\nmodule M\n Helper = 2\n def f = ::Helper\nend\n" assert_equal [["M", "Helper"]], resolved(source) end private def resolved(source) with_tmpdir do |dir| File.write(File.join(dir, "zed.rb"), source) OsSdlc::Lint::ConceptCycleCheck::References.new(dir).resolved end end end end ```
jared changed title from Add CodeShape with one definition table and lexical-scope rule; move ConceptCycleCheck::References onto it to Add CodeShape::LexicalScope; move ConceptCycleCheck::References onto it 2026-11-12 21:48:19 +00:00
Author
Owner

Work started on branch ticket-1729 (os-sdlc takedown-implement pipeline).

Work started on branch ticket-1729 (os-sdlc takedown-implement pipeline).
Author
Owner

Resolution

Done: CodeShape::LexicalScope owns Ruby's lexical constant-lookup rule (compact module A::B is one frame; rooted ::Name skips every scope). Lint::ConceptCycleCheck::References resolves through it; its scope string and prefix walk are deleted.

Evidence: Takedown 1729-20261112-163805 (cause_removed). Implementation 286: 5 dispatches, all pass, suite green, 2 min 25 s. Commits 7766138c4 (code + tests), 217d1f5ca (history row) on branch ticket-1729.

Follow-ups: #1741 CodeShape::Definitions (the References definition table moves there); #1730-#1740 remaining CodeShape children; A15 lens occurrence posted on #1712.

## Resolution **Done:** CodeShape::LexicalScope owns Ruby's lexical constant-lookup rule (compact module A::B is one frame; rooted ::Name skips every scope). Lint::ConceptCycleCheck::References resolves through it; its scope string and prefix walk are deleted. **Evidence:** Takedown 1729-20261112-163805 (cause_removed). Implementation 286: 5 dispatches, all pass, suite green, 2 min 25 s. Commits 7766138c4 (code + tests), 217d1f5ca (history row) on branch ticket-1729. **Follow-ups:** #1741 CodeShape::Definitions (the References definition table moves there); #1730-#1740 remaining CodeShape children; A15 lens occurrence posted on #1712. <!-- os-backlog kind: resolution -->
jared closed this issue 2026-11-12 21:53:35 +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#1729
No description provided.