Add four group C patterns to the catalog as loose patterns #1549

Closed
opened 2026-11-04 21:46:53 +00:00 by jared · 2 comments
Owner

Problem

docs/system-design-format/pattern-landscape.md marks Builder, Mapper, Factory and Domain Service as expressible now, and catalog/ does not hold them. The detector cannot present these class-scale shapes.

The group C threshold of 1 comes from run 3: group C never reached convergence 2, and GATE alone confirmed 5 of 6 findings (pattern-matching.md section 5). At threshold 1, every misfit of a strict pattern goes straight into the ranking. A scratch run with the four patterns strict changed these group C rows: os-backlog 6 (15 to 19), os-me 2 (12 to 13), os-sdlc 12 (66 to 77). The scratch rows from Rule::BypassedFactory and Rule::CoupledSubsystems have no review behind them, and a new strict flag on a GATE line also doubles the score of that line.

Reproduce

n/a: this ticket adds catalog data; no defect exists to reproduce.

Approach

How the new patterns enter convergence. Each new pattern sets strict?: false, under the entry rule from catalog-group-a. Convergence#flags drops the matches of a loose pattern, so a new pattern adds no group C row, adds no convergence count, and changes no score. Threshold 1 stays valid. A scratch run confirmed identical group C rows over os-backlog, os-me and os-sdlc.

Effect on present. The new patterns are loose, so their rows go to the loose_present section that #1548 adds. The scratch run gave these rows: os-backlog 28, os-me 6, os-sdlc 19. Nobody reviewed them. The rows of the current patterns stay the same.

Order. Ship this ticket after catalog-group-a. That ticket adds ClassFacts#called_namespaces, and it replaces the test that says TXS is the one loose pattern.

Pattern roles and rules. Each item quotes the Revealing signals text of the landscape row.

  1. Builder (BLD): "Class keeps state across two or more step methods (return self or set ivars) and a final build/result/to_* that calls Product.new." Role Builder. Rule R1 flags a construct outside the final method. The predicate leaves out to_*: with to_*, OpportunityScan fit the role, because to_h and a memoized ivar give the same facts. The predicate needs pure?: without it, Daemon::LspSession fit the role because its interface lists build. With both limits, no class in os-backlog, os-me or os-sdlc fits. That miss is acceptable.
  2. Mapper (MAP): "A class calls two subsystems that do not call each other, and moves data between them." Roles Mapper, Subsystem. Rule R1 flags a call between the two namespaces. No fact shows "moves data". The predicate requires a stateless pure class that calls exactly two namespaces.
  3. Factory (FTY): "A class or class method whose job is to build a complex object with its invariants: method stereotype factory (new, build, for, from, create, parse); constructs two or more classes." Roles Factory, Client. The predicate leaves out new, because every class answers it. It counts only factory construct sites (Construct#factory?). Rule R1 flags another class in the component that constructs a product of the factory.
  4. Domain Service (DSV): "A stateless, pure class named for an operation that works on two or more domain objects." Roles DomainService, Edge. Rule R1 forbids I/O, except on Edge. A role predicate cannot see the class name, so "named for an operation" is not measured. The landscape calls the P&F Filter predicate "near-identical", so this pattern must stay out of convergence until a review separates them.

Required work:

  1. Add the Builder pattern — check: present id: "BLD" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/builder.rb
  2. Add the Mapper pattern — check: present id: "MAP" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/mapper.rb
  3. Add the Factory pattern — check: present id: "FTY" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/factory.rb
  4. Add the Domain Service pattern — check: present id: "DSV" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_service.rb
  5. Set every new pattern to loose — check: present strict?: false in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_service.rb
  6. Add a rule that flags a construct outside the final method — check: present StrayConstruct = Data.define in plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/stray_construct.rb
  7. Add a rule that flags a call between the two subsystems of a mapper — check: present CoupledSubsystems = Data.define in plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/coupled_subsystems.rb
  8. Add a rule that flags a construct of a product outside the factory — check: present BypassedFactory = Data.define in plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/bypassed_factory.rb
  9. List the four patterns in Catalog.patterns — check: present DomainService in plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb
  10. Update IDS in the catalog test — check: present BLD MAP FTY DSV in plugins/os-sdlc/tests/system_model_catalog_test.rb

Orchestrator work: add one History row to docs/implementation-status/os-sdlc-system-model.md.

Rejected: strict patterns at threshold 1 — each misfit enters the ranking with no review, and overlap with GATE doubles scores. Group C threshold 2 — run 3 never reached 2, so the threshold would remove the confirmed GATE findings.

Acceptance criteria

  1. Given the catalog with this batch, when a test selects the strict group C patterns, then the list is exactly VIS, GATE and SPEC.
  2. Given a line that GATE flags and one new pattern flags, when Convergence counts the line, then the count is 1.
  3. Given the opportunities --json output for os-backlog, os-me and os-sdlc before this ticket, when the command runs after this ticket, then the groups value of each plugin is the same.
  4. Given the opportunities output before this ticket, when the command runs after this ticket, then the present rows of strict patterns are the same, and each row of a new pattern is in the loose_present section.
  5. Given a class with no role, when shapes runs after this ticket, then the detail is still convergence A:3 C:0.
  6. Given a pure class with state writes after initialize, three public messages including build, and a construct, when the BLD Builder role tests it, then it fits.
  7. Given a class with to_h and a memoized ivar and a construct, and no build or result, when the BLD Builder role tests it, then it does not fit.
  8. Given a Builder class with a construct in a step method, when BLD matches the component, then R1 flags the line of that construct.
  9. Given a stateless pure non-entry class that calls exactly two namespaces, when the MAP Mapper role tests it, then it fits.
  10. Given a class in one of those namespaces that calls a class in the other, when MAP matches the component, then R1 flags the line of that call.
  11. Given a class with a public build and factory constructs of two classes, when the FTY Factory role tests it, then it fits.
  12. Given a class that answers only new and constructs two classes, when the FTY Factory role tests it, then it does not fit.
  13. Given another class in the component that constructs a product of the factory outside a parameter default, when FTY matches the component, then R1 flags the line of that construct.
  14. Given a stateless pure non-entry class with two calls, when the DSV DomainService role tests it, then it fits.
  15. Given a class that fits DomainService when its I/O is removed and that is not Edge, when DSV matches the component, then R1 flags the I/O line.

Owner sketch

class OsSdlc::SystemModel::Catalog
  FACTORY_MESSAGES = %w[build for from create parse].freeze

  Builder = Pattern.new(
    id: "BLD", group: :C, strict?: false,
    roles: [
      Pattern::Role.new(:Builder, ->(f) {
        names = f.interface.map { |message| message[:name] }
        f.pure? && f.mutates_state? && !f.constructs.empty? && names.size >= 3 && !(names & %w[build result]).empty?
      })
    ],
    rules: Pattern::Rules.new(
      allowed: [%i[Builder Builder]],
      forbidden: [Pattern::Rule.new("R1", "Only the final method builds the product.",
                                    Pattern::Rule::StrayConstruct.new(roles: [:Builder], finals: %w[build result]))]
    ),
    source: "Gamma et al., Builder, 1994"
  )
end

# Predicates of the other patterns.
Mapper::Mapper    = ->(f) { f.pure? && !f.entry? && !f.holds_state? && f.called_namespaces.size == 2 }
Mapper::Subsystem = ->(f) { !f.senders.empty? }
Factory::Factory  = ->(f) {
  !(f.interface.map { |message| message[:name] } & FACTORY_MESSAGES).empty? &&
    f.constructs.select(&:factory?).map(&:target).uniq.size >= 2
}
Factory::Client   = ->(f) { !f.calls.empty? }
DomainService::DomainService = ->(f) { f.pure? && !f.holds_state? && !f.value? && !f.entry? && f.calls.size >= 2 }

class OsSdlc::SystemModel::Pattern::Rule
  StrayConstruct = Data.define(:roles, :finals) do
    def sites(assignment) # constructs of a role class outside each def named in finals; skips default? sites
  end

  CoupledSubsystems = Data.define(:mappers) do
    def sites(assignment) # edges between the two namespaces that a mapper calls
  end

  BypassedFactory = Data.define(:factories) do
    def sites(assignment) # constructs of a factory target in another class; skips default? sites
  end
end

Non-goals

Do not set any new pattern to strict. Do not change Catalog.thresholds, the 0.05 threshold, or Match#present?. Do not add a fact for memoized ivars.

Retired tests

plugins/os-sdlc/tests/system_model_catalog_test.rb#test_the_added_patterns_are_in_the_class_group

Origin

  • Trigger: docs/system-design-format/pattern-landscape.md marks four group C rows expressible now, and catalog/ holds only VIS, GATE and SPEC of group C.
  • Improvised this session: none
  • Chain: a class-scale shape presents no match ← the catalog does not hold the pattern ← step 9 added only three group C patterns. DESIGN: docs/system-design-format/pattern-matching.md section 5.
  • Root candidate: none found; searched catalog, group C, Builder, Mapper, Factory, DomainService, loose
  • Where: plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb
  • Session: 16528b06-720e-4ee8-b195-838770c1efe8
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/16528b06-720e-4ee8-b195-838770c1efe8.jsonl

VERDICT: CREATE

## Problem `docs/system-design-format/pattern-landscape.md` marks Builder, Mapper, Factory and Domain Service as `expressible now`, and `catalog/` does not hold them. The detector cannot present these class-scale shapes. The group C threshold of 1 comes from run 3: group C never reached convergence 2, and GATE alone confirmed 5 of 6 findings (pattern-matching.md section 5). At threshold 1, every misfit of a strict pattern goes straight into the ranking. A scratch run with the four patterns strict changed these group C rows: os-backlog 6 (15 to 19), os-me 2 (12 to 13), os-sdlc 12 (66 to 77). The scratch rows from `Rule::BypassedFactory` and `Rule::CoupledSubsystems` have no review behind them, and a new strict flag on a GATE line also doubles the score of that line. ## Reproduce n/a: this ticket adds catalog data; no defect exists to reproduce. ## Approach **How the new patterns enter convergence.** Each new pattern sets `strict?: false`, under the entry rule from `catalog-group-a`. `Convergence#flags` drops the matches of a loose pattern, so a new pattern adds no group C row, adds no convergence count, and changes no score. Threshold 1 stays valid. A scratch run confirmed identical group C rows over os-backlog, os-me and os-sdlc. **Effect on `present`.** The new patterns are loose, so their rows go to the `loose_present` section that #1548 adds. The scratch run gave these rows: os-backlog 28, os-me 6, os-sdlc 19. Nobody reviewed them. The rows of the current patterns stay the same. **Order.** Ship this ticket after `catalog-group-a`. That ticket adds `ClassFacts#called_namespaces`, and it replaces the test that says TXS is the one loose pattern. **Pattern roles and rules.** Each item quotes the `Revealing signals` text of the landscape row. 1. Builder (`BLD`): "Class keeps state across two or more step methods (return `self` or set ivars) and a final `build`/`result`/`to_*` that calls `Product.new`." Role `Builder`. Rule R1 flags a construct outside the final method. The predicate leaves out `to_*`: with `to_*`, `OpportunityScan` fit the role, because `to_h` and a memoized ivar give the same facts. The predicate needs `pure?`: without it, `Daemon::LspSession` fit the role because its interface lists `build`. With both limits, no class in os-backlog, os-me or os-sdlc fits. That miss is acceptable. 2. Mapper (`MAP`): "A class calls two subsystems that do not call each other, and moves data between them." Roles `Mapper`, `Subsystem`. Rule R1 flags a call between the two namespaces. No fact shows "moves data". The predicate requires a stateless pure class that calls exactly two namespaces. 3. Factory (`FTY`): "A class or class method whose job is to build a complex object with its invariants: method stereotype `factory` (`new`, `build`, `for`, `from`, `create`, `parse`); constructs two or more classes." Roles `Factory`, `Client`. The predicate leaves out `new`, because every class answers it. It counts only factory construct sites (`Construct#factory?`). Rule R1 flags another class in the component that constructs a product of the factory. 4. Domain Service (`DSV`): "A stateless, pure class named for an operation that works on two or more domain objects." Roles `DomainService`, `Edge`. Rule R1 forbids I/O, except on `Edge`. A role predicate cannot see the class name, so "named for an operation" is not measured. The landscape calls the P&F Filter predicate "near-identical", so this pattern must stay out of convergence until a review separates them. Required work: 1. Add the Builder pattern — check: present `id: "BLD"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/builder.rb` 2. Add the Mapper pattern — check: present `id: "MAP"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/mapper.rb` 3. Add the Factory pattern — check: present `id: "FTY"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/factory.rb` 4. Add the Domain Service pattern — check: present `id: "DSV"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_service.rb` 5. Set every new pattern to loose — check: present `strict?: false` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_service.rb` 6. Add a rule that flags a construct outside the final method — check: present `StrayConstruct = Data.define` in `plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/stray_construct.rb` 7. Add a rule that flags a call between the two subsystems of a mapper — check: present `CoupledSubsystems = Data.define` in `plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/coupled_subsystems.rb` 8. Add a rule that flags a construct of a product outside the factory — check: present `BypassedFactory = Data.define` in `plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/bypassed_factory.rb` 9. List the four patterns in `Catalog.patterns` — check: present `DomainService` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb` 10. Update `IDS` in the catalog test — check: present `BLD MAP FTY DSV` in `plugins/os-sdlc/tests/system_model_catalog_test.rb` Orchestrator work: add one History row to `docs/implementation-status/os-sdlc-system-model.md`. Rejected: strict patterns at threshold 1 — each misfit enters the ranking with no review, and overlap with GATE doubles scores. Group C threshold 2 — run 3 never reached 2, so the threshold would remove the confirmed GATE findings. ## Acceptance criteria 1. Given the catalog with this batch, when a test selects the strict group C patterns, then the list is exactly VIS, GATE and SPEC. 2. Given a line that GATE flags and one new pattern flags, when `Convergence` counts the line, then the count is 1. 3. Given the `opportunities --json` output for os-backlog, os-me and os-sdlc before this ticket, when the command runs after this ticket, then the `groups` value of each plugin is the same. 4. Given the `opportunities` output before this ticket, when the command runs after this ticket, then the `present` rows of strict patterns are the same, and each row of a new pattern is in the `loose_present` section. 5. Given a class with no role, when `shapes` runs after this ticket, then the detail is still `convergence A:3 C:0`. 6. Given a pure class with state writes after `initialize`, three public messages including `build`, and a construct, when the `BLD` `Builder` role tests it, then it fits. 7. Given a class with `to_h` and a memoized ivar and a construct, and no `build` or `result`, when the `BLD` `Builder` role tests it, then it does not fit. 8. Given a `Builder` class with a construct in a step method, when `BLD` matches the component, then R1 flags the line of that construct. 9. Given a stateless pure non-entry class that calls exactly two namespaces, when the `MAP` `Mapper` role tests it, then it fits. 10. Given a class in one of those namespaces that calls a class in the other, when `MAP` matches the component, then R1 flags the line of that call. 11. Given a class with a public `build` and factory constructs of two classes, when the `FTY` `Factory` role tests it, then it fits. 12. Given a class that answers only `new` and constructs two classes, when the `FTY` `Factory` role tests it, then it does not fit. 13. Given another class in the component that constructs a product of the factory outside a parameter default, when `FTY` matches the component, then R1 flags the line of that construct. 14. Given a stateless pure non-entry class with two calls, when the `DSV` `DomainService` role tests it, then it fits. 15. Given a class that fits `DomainService` when its I/O is removed and that is not `Edge`, when `DSV` matches the component, then R1 flags the I/O line. ## Owner sketch ```ruby class OsSdlc::SystemModel::Catalog FACTORY_MESSAGES = %w[build for from create parse].freeze Builder = Pattern.new( id: "BLD", group: :C, strict?: false, roles: [ Pattern::Role.new(:Builder, ->(f) { names = f.interface.map { |message| message[:name] } f.pure? && f.mutates_state? && !f.constructs.empty? && names.size >= 3 && !(names & %w[build result]).empty? }) ], rules: Pattern::Rules.new( allowed: [%i[Builder Builder]], forbidden: [Pattern::Rule.new("R1", "Only the final method builds the product.", Pattern::Rule::StrayConstruct.new(roles: [:Builder], finals: %w[build result]))] ), source: "Gamma et al., Builder, 1994" ) end # Predicates of the other patterns. Mapper::Mapper = ->(f) { f.pure? && !f.entry? && !f.holds_state? && f.called_namespaces.size == 2 } Mapper::Subsystem = ->(f) { !f.senders.empty? } Factory::Factory = ->(f) { !(f.interface.map { |message| message[:name] } & FACTORY_MESSAGES).empty? && f.constructs.select(&:factory?).map(&:target).uniq.size >= 2 } Factory::Client = ->(f) { !f.calls.empty? } DomainService::DomainService = ->(f) { f.pure? && !f.holds_state? && !f.value? && !f.entry? && f.calls.size >= 2 } class OsSdlc::SystemModel::Pattern::Rule StrayConstruct = Data.define(:roles, :finals) do def sites(assignment) # constructs of a role class outside each def named in finals; skips default? sites end CoupledSubsystems = Data.define(:mappers) do def sites(assignment) # edges between the two namespaces that a mapper calls end BypassedFactory = Data.define(:factories) do def sites(assignment) # constructs of a factory target in another class; skips default? sites end end ``` ## Non-goals Do not set any new pattern to strict. Do not change `Catalog.thresholds`, the 0.05 threshold, or `Match#present?`. Do not add a fact for memoized ivars. ## Retired tests plugins/os-sdlc/tests/system_model_catalog_test.rb#test_the_added_patterns_are_in_the_class_group ## Origin - Trigger: `docs/system-design-format/pattern-landscape.md` marks four group C rows `expressible now`, and `catalog/` holds only VIS, GATE and SPEC of group C. - Improvised this session: none - Chain: a class-scale shape presents no match ← the catalog does not hold the pattern ← step 9 added only three group C patterns. DESIGN: `docs/system-design-format/pattern-matching.md` section 5. - Root candidate: none found; searched catalog, group C, Builder, Mapper, Factory, DomainService, loose - Where: `plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb` - Session: 16528b06-720e-4ee8-b195-838770c1efe8 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/16528b06-720e-4ee8-b195-838770c1efe8.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1549.

Work started on branch ticket-1549.
Author
Owner

Resolution

Done: Added four loose group C patterns and three rules.

Evidence: Implementation 233 merge-ready; suite gate and contract audit passed on branch ticket-1549.

Follow-ups: dropped: the Mapper precision gap the auditor noted, since the pattern is loose and gives no flags; review it in the calibration run

## Resolution **Done:** Added four loose group C patterns and three rules. **Evidence:** Implementation 233 merge-ready; suite gate and contract audit passed on branch ticket-1549. **Follow-ups:** dropped: the Mapper precision gap the auditor noted, since the pattern is loose and gives no flags; review it in the calibration run
jared closed this issue 2026-11-04 23:52:45 +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#1549
No description provided.