Extract Class.new constants as class cards in system-model #1513

Closed
opened 2026-11-02 22:35:16 +00:00 by jared · 2 comments
Owner

Problem

The system-model extractor does not see a constant made with Class.new(...).
DataDeclaration::KINDS holds only Data.define and Struct.new.
The os-backlog plugin has six such constants, for example TrackerCommandFailed, Close::Failed and Refused.
None of them has a card in the model.
Each one is an error class.
The model misses them in cards, in inheritance facts, and in class counts.

Observed

plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9:

KINDS = { %i[Data define] => :data, %i[Struct new] => :struct }.freeze

The six os-backlog constants (found with grep -rnE "[A-Z]\w* = Class\.new" plugins/os-backlog/lib):

lib/backlog/tracker_command_failed.rb:4:  TrackerCommandFailed = Class.new(StandardError)
lib/backlog/issue_create/refused.rb:5:    Refused = Class.new(StandardError)
lib/backlog/config/write_command/refused.rb:6:      Refused = Class.new(StandardError)
lib/backlog/issue_show/command_failed.rb:5:    CommandFailed = Class.new(StandardError)
lib/backlog/close/failed.rb:5:    Failed = Class.new(StandardError)
lib/backlog/resolution/missing_parts.rb:5:    MissingParts = Class.new(StandardError) do

Reproduce

grep -c "Backlog::Close::Failed" <(plugins/os-sdlc/bin/system-model coverage plugins/os-backlog)

The count is 0 because no card exists for the class.

Approach

Fact kind choice, decided from the code. Foo = Class.new(Parent) defines a class, so its card has kind :class and superclass "Parent" (the same card that class Foo < Parent; end gives via ClassDeclaration, extractor/class_declaration.rb:6-10). It does not get kind :data or :struct, and it does not get the value? fact: FactScan#visit_constant_write_node (extractor/fact_scan.rb:28-34) calls @ledger.value_object for every DataDeclaration match, and an error class is not a value object. FactLedger#within(name, superclass:) (extractor/fact_ledger.rb:13-19) already records the superclass, so Subclasses sees it. The block form (MissingParts = Class.new(StandardError) do ... end) uses the block body for methods, as DataDeclaration#body does.
A new declaration object takes the Class.new match, so DataDeclaration::KINDS stays a lookup of fixed kinds. The three call sites that test DataDeclaration.match? (extractor/walk.rb:22, extractor/body.rb:39, extractor/fact_scan.rb:29) also route Class.new constants. A nested form (module A; B = Class.new(StandardError); end) gets the name A::B through the existing nesting.
The argument must be a constant path (StandardError, Foo::Bar); Class.new with no argument or a non-constant argument gives superclass nil.

Required work:

  1. Add the declaration object for Class.new constants — check: present Class in plugins/os-sdlc/lib/os_sdlc/system_model/extractor/class_new_declaration.rb
  2. Route it from the walk — check: present ClassNewDeclaration in plugins/os-sdlc/lib/os_sdlc/system_model/extractor/walk.rb
  3. Route it from the fact scan — check: present ClassNewDeclaration in plugins/os-sdlc/lib/os_sdlc/system_model/extractor/fact_scan.rb
  4. Route it from the body visitor — check: present ClassNewDeclaration in plugins/os-sdlc/lib/os_sdlc/system_model/extractor/body.rb
  5. Add tests — check: present Class.new(StandardError) in plugins/os-sdlc/tests/system_model_*_test.rb

Orchestrator work: run bin/refresh-plugins; add one History row to docs/implementation-status/os-sdlc-system-model.md; run /os-adr:find once (blast radius found no ADR on extractor kinds).

Rejected: add %i[Class new] => :class to KINDS — the superclass is the first argument and DataDeclaration#superclass returns nil, and the value? fact would be wrong for an error class.

Acceptance criteria

  1. Given Foo = Class.new(StandardError) at the top level of a file under lib/, when the extractor runs, then the model has a card Foo with kind :class and superclass StandardError.

  2. Given the same constant inside module Shop, when the extractor runs, then the card is named Shop::Foo with the same kind and superclass.

  3. Given Foo = Class.new(StandardError) do def message = "x" end, when the extractor runs, then the card for Foo lists message as a public method.

  4. Given Foo = Class.new(StandardError), when the extractor runs, then the facts for Foo do not have value? set.

  5. Given the plugins/os-backlog root, when the extractor runs, then the model has a card for each of Backlog::TrackerCommandFailed, Backlog::IssueCreate::Refused, Backlog::Config::WriteCommand::Refused, Backlog::IssueShow::CommandFailed, Backlog::Close::Failed and Backlog::Resolution::MissingParts, each with superclass StandardError.

  6. Given a Data.define or Struct.new constant, when the extractor runs, then its card and its value? fact are unchanged.

  7. Given plugins/os-sdlc/lib/os_sdlc/detector/reach_through/parent_answers.rb:37 (Forwarders = Class.new(Prism::Visitor) do, the one block-form constant in os-sdlc), when the extractor runs, then Forwarders has its own :class card and the suite and fast-gate stay green.

  8. Given a plugin whose design/generated output is checked for drift, when the output is regenerated in the same change, then the drift check passes.

Owner sketch

class OsSdlc::SystemModel::Extractor::ClassNewDeclaration
  def self.match?(node)
  def kind = :class
  def superclass
  def body
end

Non-goals

Other constructors (Module.new, Class.new assigned to a non-constant). The Boot::Commands lazy loads.

Origin

  • Trigger: plugins/os-sdlc/bin/system-model coverage plugins/os-backlog lists no error class that Class.new(StandardError) defines.
  • Improvised this session: none
  • Chain: error classes missing from cards ← DataDeclaration::KINDS has only Data.define and Struct.new ← DESIGN (plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9)
  • Root candidate: none found; searched Class.new, system-model, extractor, class card, DataDeclaration, KINDS
    VERDICT: CREATE
  • Where: plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9
  • Session: d077e084-9195-4803-b761-6ce01eca0249
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/d077e084-9195-4803-b761-6ce01eca0249.jsonl
## Problem The system-model extractor does not see a constant made with `Class.new(...)`. `DataDeclaration::KINDS` holds only `Data.define` and `Struct.new`. The os-backlog plugin has six such constants, for example `TrackerCommandFailed`, `Close::Failed` and `Refused`. None of them has a card in the model. Each one is an error class. The model misses them in cards, in inheritance facts, and in class counts. ## Observed `plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9`: ``` KINDS = { %i[Data define] => :data, %i[Struct new] => :struct }.freeze ``` The six os-backlog constants (found with `grep -rnE "[A-Z]\w* = Class\.new" plugins/os-backlog/lib`): ``` lib/backlog/tracker_command_failed.rb:4: TrackerCommandFailed = Class.new(StandardError) lib/backlog/issue_create/refused.rb:5: Refused = Class.new(StandardError) lib/backlog/config/write_command/refused.rb:6: Refused = Class.new(StandardError) lib/backlog/issue_show/command_failed.rb:5: CommandFailed = Class.new(StandardError) lib/backlog/close/failed.rb:5: Failed = Class.new(StandardError) lib/backlog/resolution/missing_parts.rb:5: MissingParts = Class.new(StandardError) do ``` ## Reproduce ``` grep -c "Backlog::Close::Failed" <(plugins/os-sdlc/bin/system-model coverage plugins/os-backlog) ``` The count is 0 because no card exists for the class. ## Approach Fact kind choice, decided from the code. `Foo = Class.new(Parent)` defines a class, so its card has kind `:class` and `superclass` "Parent" (the same card that `class Foo < Parent; end` gives via `ClassDeclaration`, `extractor/class_declaration.rb:6-10`). It does not get kind `:data` or `:struct`, and it does not get the `value?` fact: `FactScan#visit_constant_write_node` (`extractor/fact_scan.rb:28-34`) calls `@ledger.value_object` for every `DataDeclaration` match, and an error class is not a value object. `FactLedger#within(name, superclass:)` (`extractor/fact_ledger.rb:13-19`) already records the superclass, so `Subclasses` sees it. The block form (`MissingParts = Class.new(StandardError) do ... end`) uses the block body for methods, as `DataDeclaration#body` does. A new declaration object takes the `Class.new` match, so `DataDeclaration::KINDS` stays a lookup of fixed kinds. The three call sites that test `DataDeclaration.match?` (`extractor/walk.rb:22`, `extractor/body.rb:39`, `extractor/fact_scan.rb:29`) also route `Class.new` constants. A nested form (`module A; B = Class.new(StandardError); end`) gets the name `A::B` through the existing nesting. The argument must be a constant path (`StandardError`, `Foo::Bar`); `Class.new` with no argument or a non-constant argument gives superclass nil. Required work: 1. Add the declaration object for `Class.new` constants — check: present `Class` in `plugins/os-sdlc/lib/os_sdlc/system_model/extractor/class_new_declaration.rb` 2. Route it from the walk — check: present `ClassNewDeclaration` in `plugins/os-sdlc/lib/os_sdlc/system_model/extractor/walk.rb` 3. Route it from the fact scan — check: present `ClassNewDeclaration` in `plugins/os-sdlc/lib/os_sdlc/system_model/extractor/fact_scan.rb` 4. Route it from the body visitor — check: present `ClassNewDeclaration` in `plugins/os-sdlc/lib/os_sdlc/system_model/extractor/body.rb` 5. Add tests — check: present `Class.new(StandardError)` in `plugins/os-sdlc/tests/system_model_*_test.rb` Orchestrator work: run `bin/refresh-plugins`; add one History row to `docs/implementation-status/os-sdlc-system-model.md`; run `/os-adr:find` once (blast radius found no ADR on extractor kinds). Rejected: add `%i[Class new] => :class` to `KINDS` — the superclass is the first argument and `DataDeclaration#superclass` returns nil, and the `value?` fact would be wrong for an error class. ## Acceptance criteria 1. Given `Foo = Class.new(StandardError)` at the top level of a file under `lib/`, when the extractor runs, then the model has a card `Foo` with kind `:class` and superclass `StandardError`. 2. Given the same constant inside `module Shop`, when the extractor runs, then the card is named `Shop::Foo` with the same kind and superclass. 3. Given `Foo = Class.new(StandardError) do def message = "x" end`, when the extractor runs, then the card for `Foo` lists `message` as a public method. 4. Given `Foo = Class.new(StandardError)`, when the extractor runs, then the facts for `Foo` do not have `value?` set. 5. Given the `plugins/os-backlog` root, when the extractor runs, then the model has a card for each of `Backlog::TrackerCommandFailed`, `Backlog::IssueCreate::Refused`, `Backlog::Config::WriteCommand::Refused`, `Backlog::IssueShow::CommandFailed`, `Backlog::Close::Failed` and `Backlog::Resolution::MissingParts`, each with superclass `StandardError`. 6. Given a `Data.define` or `Struct.new` constant, when the extractor runs, then its card and its `value?` fact are unchanged. 7. Given `plugins/os-sdlc/lib/os_sdlc/detector/reach_through/parent_answers.rb:37` (`Forwarders = Class.new(Prism::Visitor) do`, the one block-form constant in os-sdlc), when the extractor runs, then `Forwarders` has its own `:class` card and the suite and `fast-gate` stay green. 8. Given a plugin whose `design/generated` output is checked for drift, when the output is regenerated in the same change, then the drift check passes. ## Owner sketch ```ruby class OsSdlc::SystemModel::Extractor::ClassNewDeclaration def self.match?(node) def kind = :class def superclass def body end ``` ## Non-goals Other constructors (`Module.new`, `Class.new` assigned to a non-constant). The `Boot::Commands` lazy loads. ## Origin - Trigger: `plugins/os-sdlc/bin/system-model coverage plugins/os-backlog` lists no error class that `Class.new(StandardError)` defines. - Improvised this session: none - Chain: error classes missing from cards ← `DataDeclaration::KINDS` has only Data.define and Struct.new ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9`) - Root candidate: none found; searched Class.new, system-model, extractor, class card, DataDeclaration, KINDS VERDICT: CREATE - Where: plugins/os-sdlc/lib/os_sdlc/system_model/extractor/data_declaration.rb:9 - Session: d077e084-9195-4803-b761-6ce01eca0249 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/d077e084-9195-4803-b761-6ce01eca0249.jsonl
Author
Owner

Work started on branch ticket-1513 through /os-sdlc:implement.

Work started on branch `ticket-1513` through /os-sdlc:implement.
Author
Owner

Resolution

Done: system-model extracts a constant assigned Class.new(...) { ... } as a class card (name, superclass from the first argument, body facts), beside Data.define value objects. New Extractor::ClassNewDeclaration; Walk, Body and FactScan route it.

Evidence: Commit a463d6c1 on ticket-1513 (implementation 224). New tests in plugins/os-sdlc/tests/system_model_class_new_declaration_test.rb. Suite gate green (dispatch 2050); behavior-verifier pass (2049); contract audit pass (2051).

Follow-ups: none

## Resolution **Done:** system-model extracts a constant assigned Class.new(...) { ... } as a class card (name, superclass from the first argument, body facts), beside Data.define value objects. New Extractor::ClassNewDeclaration; Walk, Body and FactScan route it. **Evidence:** Commit a463d6c1 on ticket-1513 (implementation 224). New tests in plugins/os-sdlc/tests/system_model_class_new_declaration_test.rb. Suite gate green (dispatch 2050); behavior-verifier pass (2049); contract audit pass (2051). **Follow-ups:** none
jared closed this issue 2026-11-03 14:53:36 +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#1513
No description provided.