Add nine group A patterns to the catalog as loose patterns #1548

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

Problem

docs/system-design-format/pattern-landscape.md marks 12 group A rows as expressible now, and catalog/ does not hold them. The detector cannot present a pattern that the catalog does not hold, so a component that has one of these shapes gets no match.

The group A threshold of 3 comes from run 3, when the catalog held six group A patterns (pattern-matching.md section 5). Many new rows overlap current patterns. For example, the landscape calls FCIS "near-identical" to HEX R1. A strict overlapping pattern adds to convergence with no new evidence. A scratch run with all nine patterns strict changed 2 group A rows in os-backlog (2 to 4) and 4 in os-sdlc (7 to 8). No review confirmed those rows.

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. Convergence#flags keeps only matches whose pattern is strict, so a loose pattern adds no flag and cannot change a convergence count. OpportunityScan#findings rejects present matches one pattern at a time, so a new pattern does not remove the misfits of another pattern. The result: every group A row and its score stay the same, and threshold 3 stays valid. A scratch run over os-backlog, os-me and os-sdlc confirmed this: group A and group C rows were identical with all new patterns loose.

A loose pattern reaches a report through the loose_present section of opportunities and through system-model gauge (#1547). A later calibration run may set a pattern to strict after a review measures its precision (pattern-matching.md section 9). This ticket does not do that.

Shape::UnclearRole#groups also selects strict patterns only. Loose patterns therefore leave the unclear-role detail unchanged (convergence A:3 C:0).

Effect on present. OpportunityScan#present keeps strict patterns only, so the baseline stays comparable. A new OpportunityScan#loose_present holds the present matches of loose patterns. to_h and the text report print them in a separate section labeled unreviewed. This ticket does not change Match#present? or the 0.05 threshold. The scratch run gave these loose rows: os-backlog 107, os-me 21, os-sdlc 94. Nobody reviewed these rows, so they carry no precision figure. Ruling recorded in docs/decisions/rulings.tsv, 2026-11-04, ruled_by predicted.

Pattern roles and rules. Each item quotes the Revealing signals text of the landscape row. The Owner sketch gives the predicates.

  1. Facade (FAC): "One class calls three or more classes of one namespace; senders outside the namespace call only this class." Role Facade tests the first half. Rule R1 tests the second half, because only a rule sees class names.
  2. Mediator (MED): "One class holds three or more injected colleagues; colleagues hold a reference back to it; colleagues do not call each other." Role Mediator; rule R1 flags a call between two colleagues.
  3. Domain Model (DOM): "A web of classes that hold state and behavior, call each other, and do no I/O." Role DomainObject; rule R1 forbids I/O, except on Edge as in HEX R1.
  4. Service Layer (SVC): "Non-entry classes called by entry classes; each public method is one application operation; they call domain and data-source classes." Roles Entry, Service, DataSource; rule R1 forbids Entry to DataSource.
  5. Model View Controller (MVC): "Controller classes are entries and call model and view; view classes write the terminal; model classes are pure; model does not call view." Roles Controller, View, Model; rule R1 forbids Model to View.
  6. Page Controller (PGC): "One entry handler per command (its own bin script or Thor method)." Roles Handler, Worker; rule R1 forbids Handler to Handler.
  7. Front Controller (FRC): "One entry class receives every command and routes to handler classes by name." Roles Front, Handler; rule R1 forbids a decision in Front. routes_to works only in a Thor subclass, so the scratch run filled this pattern in no component. That miss is acceptable.
  8. Whole-Part (WHP): "The whole constructs and owns its parts in initialize (no injection); senders outside see only the whole." Role Whole, role Part; rule R1 flags an outside call to a part. Known instance: Extractor::FactScan, which fits Whole.
  9. Functional Core, Imperative Shell (FCIS): "Core classes are pure and hold no state; shell classes do I/O and call the core; the core never calls the shell." Roles Core, Shell; rule R1 forbids Core to Shell.

Rows left out of this batch.

  1. Module. The row says: "Measurable on a component from calls and class names; a role predicate cannot see its own name." It is a cohesion ratio of a namespace. It has no role, and no line is wrong, so it gives no site for a rule. A journey component also crosses namespaces by design. Catalog needs a metric, not a pattern, for this row.
  2. Presentation-Abstraction-Control. The row says: "A tree of agents; each has its own presentation, abstraction and control; controls talk only to parent and child controls." The agent tree is a relation between classes, and a role tests one class. A scratch pattern was present on 18 of 39 os-backlog components, and os-backlog has no such tree.
  3. View Handler. The row says: "One class creates, opens, closes and coordinates several view objects." A role cannot test that the constructed classes are views. A scratch pattern was present on 17 of 39 os-backlog components, and the row itself says "Rare in a CLI". The landscape rule requires that a predicate captures the defining signal. These last two rows fail it, and false positives cost more than misses.

Constraints for the implementing agent.

  1. A pattern id must hold no space, because Convergence.pattern_count splits each flag on whitespace.
  2. A role named in except: must also be in the roles of that pattern. Assignment#fits? raises KeyError otherwise. Catalog::Edge therefore goes in the DOM roles, as in HEX.
  3. Each catalog file defines the constant that matches its path (ADR-0090).

Required work:

  1. Add the Facade pattern — check: present id: "FAC" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/facade.rb
  2. Add the Mediator pattern — check: present id: "MED" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/mediator.rb
  3. Add the Domain Model pattern — check: present id: "DOM" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_model.rb
  4. Add the Service Layer pattern — check: present id: "SVC" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/service_layer.rb
  5. Add the Model View Controller pattern — check: present id: "MVC" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/model_view_controller.rb
  6. Add the Page Controller pattern — check: present id: "PGC" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/page_controller.rb
  7. Add the Front Controller pattern — check: present id: "FRC" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/front_controller.rb
  8. Add the Whole-Part pattern — check: present id: "WHP" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/whole_part.rb
  9. Add the Functional Core, Imperative Shell pattern — check: present id: "FCIS" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/functional_core.rb
  10. Set every new pattern to loose — check: present strict?: false in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/facade.rb
  11. Add a rule that flags an outside call to a part of an owner — check: present BypassedOwner = Data.define in plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/bypassed_owner.rb
  12. Add a rule that flags a call between two colleagues — check: present ColleagueCall = Data.define in plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/colleague_call.rb
  13. Add the derived readers that the Facade and Whole-Part roles need — check: present def called_namespaces in plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb
  14. List the nine patterns in Catalog.patterns — check: present FunctionalCore in plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb
  15. Update ARCHITECTURAL in the catalog test — check: present TXS FAC MED DOM SVC MVC PGC FRC WHP FCIS in plugins/os-sdlc/tests/system_model_catalog_test.rb
  16. Update IDS in the catalog test — check: present SPEC FAC MED DOM SVC MVC PGC FRC WHP FCIS in plugins/os-sdlc/tests/system_model_catalog_test.rb
  17. Record the entry rule in section 5 of the method document: a new pattern stays loose until a calibration run promotes it — check: present loose until a calibration run in docs/system-design-format/pattern-matching.md
  18. Split present rows by strict? and print the loose rows in a section labeled unreviewed — check: present def loose_present in plugins/os-sdlc/lib/os_sdlc/system_model/opportunity_scan.rb

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

Rejected: strict patterns with threshold 3 — overlap raises convergence with no evidence, and the threshold stops being valid. Strict patterns with a higher threshold — the threshold has no calibration run behind it, and it hides confirmed rows. Counting one overlap cluster as one pattern — this changes Convergence, and the scale-group rules, for a problem that strict? already solves.

Acceptance criteria

  1. Given the catalog with this batch, when a test selects the strict group A patterns, then the list is exactly HEX, LAY, P&F, CMD and REPO.
  2. Given a line that two strict group A patterns flag and that one new pattern flags, when Convergence counts the line, then the count is 2.
  3. Given a component with a finding at convergence 2 that a new pattern also flags, when OpportunityScan#ranked(:A) runs with threshold 3, then the finding is not ranked.
  4. 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.
  5. 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 loose pattern is in the loose_present section labeled unreviewed.
  6. Given a class with no role, when shapes runs after this ticket, then the detail is still convergence A:3 C:0.
  7. Given a non-entry class that calls three classes of one namespace, when the FAC Facade role tests it, then it fits.
  8. Given a component where a class outside that namespace calls a class inside it, when FAC matches the component, then R1 flags the line of that call.
  9. Given a class with three injected collaborators and one of them among its senders, when the MED Mediator role tests it, then it fits.
  10. Given two colleagues of a mediator, where each refers back to it and one calls the other, when MED matches the component, then R1 flags the line of that call.
  11. Given a class that builds two parts in initialize and has no injected collaborator, when the WHP Whole role tests it, then it fits.
  12. Given a class that builds two parts in a method other than initialize, when the WHP Whole role tests it, then it does not fit.
  13. Given a Model class that calls a View class, when MVC matches the component, then R1 flags the line of that call.
  14. Given a Core class that calls a Shell class, when FCIS matches the component, then R1 flags the line of that call.
  15. Given a class with state, a decision, a call and storage I/O that is not Edge, when DOM matches the component, then R1 flags the I/O line.
  16. Given an Entry class that calls a DataSource class, when SVC matches the component, then R1 flags the line of that call.
  17. Given a Handler class that calls a Handler class, when PGC matches the component, then R1 flags the line of that call.
  18. Given a Front class with a decision, when FRC matches the component, then R1 flags the decision line.

Owner sketch

class OsSdlc::SystemModel::ClassFacts
  def called_namespaces = calls.group_by { |name| name.rpartition("::").first }.reject { |space, _| space.empty? }
  def initialize_constructs # non-default constructs inside the `initialize` def span
end

class OsSdlc::SystemModel::Catalog
  Facade = Pattern.new(
    id: "FAC", group: :A, strict?: false,
    roles: [
      Pattern::Role.new(:Facade, ->(f) { !f.entry? && f.called_namespaces.values.any? { |names| names.size >= 3 } }),
      Pattern::Role.new(:Client, ->(f) { !f.calls.empty? })
    ],
    rules: Pattern::Rules.new(
      allowed: [%i[Client Facade]],
      forbidden: [Pattern::Rule.new("R1", "Senders outside the namespace call only the facade.",
                                    Pattern::Rule::BypassedOwner.new(owners: [:Facade], parts: :namespace))]
    ),
    source: "Gamma et al., Facade, 1994"
  )
end

# Predicates of the other patterns.
Mediator::Mediator        = ->(f) { f.injected.size >= 3 && !(f.senders & f.injected).empty? }
Mediator::Colleague       = ->(f) { !f.injected.empty? }
DomainModel::DomainObject = ->(f) { f.holds_state? && f.pure? && f.decides? && !f.calls.empty? }
ServiceLayer::Entry       = ->(f) { f.entry? && !f.calls.empty? }
ServiceLayer::Service     = ->(f) { !f.entry? && f.pure? && f.calls.size >= 2 && !f.senders.empty? }
ServiceLayer::DataSource  = ->(f) { f.storage? && !f.entry? }
ModelViewController::Controller = ->(f) { f.entry? && f.calls.size >= 2 }
ModelViewController::View       = ->(f) { f.terminal? && !f.entry? }
ModelViewController::Model      = ->(f) { f.pure? && f.holds_state? && f.decides? }
PageController::Handler   = ->(f) { f.entry? && !f.calls.empty? && f.routes_to.empty? }
PageController::Worker    = ->(f) { !f.entry? && !f.senders.empty? }
FrontController::Front    = ->(f) { f.entry? && !f.routes_to.empty? }
FrontController::Handler  = ->(f) { !f.entry? && f.routes_to.empty? && !f.calls.empty? }
WholePart::Whole          = ->(f) { f.injected.empty? && f.initialize_constructs.map(&:target).uniq.size >= 2 }
WholePart::Part           = ->(f) { !f.senders.empty? }
FunctionalCore::Core      = ->(f) { f.pure? && !f.holds_state? && !f.value? }
FunctionalCore::Shell     = ->(f) { !f.pure? && !f.calls.empty? }

class OsSdlc::SystemModel::Pattern::Rule
  BypassedOwner = Data.define(:owners, :parts) do
    def sites(assignment) # edges into a part from a class that is not the owner and not a part
  end

  ColleagueCall = Data.define(:mediators) do
    def sites(assignment) # edges between two injected classes that each refer back to the mediator
  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 Module, Presentation-Abstraction-Control or View Handler.

Retired tests

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

Origin

  • Trigger: system-model opportunities holds nine patterns; docs/system-design-format/pattern-landscape.md marks 33 rows expressible now.
  • Improvised this session: none
  • Chain: a group A shape presents no match ← the catalog does not hold the pattern ← the catalog was built from six run 1 patterns plus VIS, GATE and SPEC. DESIGN: docs/system-design-format/pattern-matching.md section 5.
  • Root candidate: none found; searched catalog, group A, FAC, MED, FCIS, pattern-landscape, loose, strict?, Convergence
  • 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 12 group A rows as `expressible now`, and `catalog/` does not hold them. The detector cannot present a pattern that the catalog does not hold, so a component that has one of these shapes gets no match. The group A threshold of 3 comes from run 3, when the catalog held six group A patterns (pattern-matching.md section 5). Many new rows overlap current patterns. For example, the landscape calls FCIS "near-identical" to HEX R1. A strict overlapping pattern adds to convergence with no new evidence. A scratch run with all nine patterns strict changed 2 group A rows in os-backlog (2 to 4) and 4 in os-sdlc (7 to 8). No review confirmed those rows. ## 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`. `Convergence#flags` keeps only matches whose pattern is strict, so a loose pattern adds no flag and cannot change a convergence count. `OpportunityScan#findings` rejects present matches one pattern at a time, so a new pattern does not remove the misfits of another pattern. The result: every group A row and its score stay the same, and threshold 3 stays valid. A scratch run over os-backlog, os-me and os-sdlc confirmed this: group A and group C rows were identical with all new patterns loose. A loose pattern reaches a report through the `loose_present` section of `opportunities` and through `system-model gauge` (#1547). A later calibration run may set a pattern to strict after a review measures its precision (pattern-matching.md section 9). This ticket does not do that. `Shape::UnclearRole#groups` also selects strict patterns only. Loose patterns therefore leave the `unclear-role` detail unchanged (`convergence A:3 C:0`). **Effect on `present`.** `OpportunityScan#present` keeps strict patterns only, so the baseline stays comparable. A new `OpportunityScan#loose_present` holds the present matches of loose patterns. `to_h` and the text report print them in a separate section labeled unreviewed. This ticket does not change `Match#present?` or the 0.05 threshold. The scratch run gave these loose rows: os-backlog 107, os-me 21, os-sdlc 94. Nobody reviewed these rows, so they carry no precision figure. Ruling recorded in `docs/decisions/rulings.tsv`, 2026-11-04, ruled_by predicted. **Pattern roles and rules.** Each item quotes the `Revealing signals` text of the landscape row. The Owner sketch gives the predicates. 1. Facade (`FAC`): "One class calls three or more classes of one namespace; senders outside the namespace call only this class." Role `Facade` tests the first half. Rule R1 tests the second half, because only a rule sees class names. 2. Mediator (`MED`): "One class holds three or more injected colleagues; colleagues hold a reference back to it; colleagues do not call each other." Role `Mediator`; rule R1 flags a call between two colleagues. 3. Domain Model (`DOM`): "A web of classes that hold state and behavior, call each other, and do no I/O." Role `DomainObject`; rule R1 forbids I/O, except on `Edge` as in HEX R1. 4. Service Layer (`SVC`): "Non-entry classes called by entry classes; each public method is one application operation; they call domain and data-source classes." Roles `Entry`, `Service`, `DataSource`; rule R1 forbids `Entry` to `DataSource`. 5. Model View Controller (`MVC`): "Controller classes are entries and call model and view; view classes write the terminal; model classes are pure; model does not call view." Roles `Controller`, `View`, `Model`; rule R1 forbids `Model` to `View`. 6. Page Controller (`PGC`): "One entry handler per command (its own `bin` script or Thor method)." Roles `Handler`, `Worker`; rule R1 forbids `Handler` to `Handler`. 7. Front Controller (`FRC`): "One entry class receives every command and routes to handler classes by name." Roles `Front`, `Handler`; rule R1 forbids a decision in `Front`. `routes_to` works only in a `Thor` subclass, so the scratch run filled this pattern in no component. That miss is acceptable. 8. Whole-Part (`WHP`): "The whole constructs and owns its parts in `initialize` (no injection); senders outside see only the whole." Role `Whole`, role `Part`; rule R1 flags an outside call to a part. Known instance: `Extractor::FactScan`, which fits `Whole`. 9. Functional Core, Imperative Shell (`FCIS`): "Core classes are pure and hold no state; shell classes do I/O and call the core; the core never calls the shell." Roles `Core`, `Shell`; rule R1 forbids `Core` to `Shell`. **Rows left out of this batch.** 1. Module. The row says: "Measurable on a component from `calls` and class names; a role predicate cannot see its own name." It is a cohesion ratio of a namespace. It has no role, and no line is wrong, so it gives no site for a rule. A journey component also crosses namespaces by design. `Catalog` needs a metric, not a pattern, for this row. 2. Presentation-Abstraction-Control. The row says: "A tree of agents; each has its own presentation, abstraction and control; controls talk only to parent and child controls." The agent tree is a relation between classes, and a role tests one class. A scratch pattern was present on 18 of 39 os-backlog components, and os-backlog has no such tree. 3. View Handler. The row says: "One class creates, opens, closes and coordinates several view objects." A role cannot test that the constructed classes are views. A scratch pattern was present on 17 of 39 os-backlog components, and the row itself says "Rare in a CLI". The landscape rule requires that a predicate captures the defining signal. These last two rows fail it, and false positives cost more than misses. **Constraints for the implementing agent.** 1. A pattern `id` must hold no space, because `Convergence.pattern_count` splits each flag on whitespace. 2. A role named in `except:` must also be in the roles of that pattern. `Assignment#fits?` raises `KeyError` otherwise. `Catalog::Edge` therefore goes in the `DOM` roles, as in HEX. 3. Each catalog file defines the constant that matches its path (ADR-0090). Required work: 1. Add the Facade pattern — check: present `id: "FAC"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/facade.rb` 2. Add the Mediator pattern — check: present `id: "MED"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/mediator.rb` 3. Add the Domain Model pattern — check: present `id: "DOM"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/domain_model.rb` 4. Add the Service Layer pattern — check: present `id: "SVC"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/service_layer.rb` 5. Add the Model View Controller pattern — check: present `id: "MVC"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/model_view_controller.rb` 6. Add the Page Controller pattern — check: present `id: "PGC"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/page_controller.rb` 7. Add the Front Controller pattern — check: present `id: "FRC"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/front_controller.rb` 8. Add the Whole-Part pattern — check: present `id: "WHP"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/whole_part.rb` 9. Add the Functional Core, Imperative Shell pattern — check: present `id: "FCIS"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/functional_core.rb` 10. Set every new pattern to loose — check: present `strict?: false` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/facade.rb` 11. Add a rule that flags an outside call to a part of an owner — check: present `BypassedOwner = Data.define` in `plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/bypassed_owner.rb` 12. Add a rule that flags a call between two colleagues — check: present `ColleagueCall = Data.define` in `plugins/os-sdlc/lib/os_sdlc/system_model/pattern/rule/colleague_call.rb` 13. Add the derived readers that the Facade and Whole-Part roles need — check: present `def called_namespaces` in `plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb` 14. List the nine patterns in `Catalog.patterns` — check: present `FunctionalCore` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb` 15. Update `ARCHITECTURAL` in the catalog test — check: present `TXS FAC MED DOM SVC MVC PGC FRC WHP FCIS` in `plugins/os-sdlc/tests/system_model_catalog_test.rb` 16. Update `IDS` in the catalog test — check: present `SPEC FAC MED DOM SVC MVC PGC FRC WHP FCIS` in `plugins/os-sdlc/tests/system_model_catalog_test.rb` 17. Record the entry rule in section 5 of the method document: a new pattern stays loose until a calibration run promotes it — check: present `loose until a calibration run` in `docs/system-design-format/pattern-matching.md` 18. Split present rows by `strict?` and print the loose rows in a section labeled unreviewed — check: present `def loose_present` in `plugins/os-sdlc/lib/os_sdlc/system_model/opportunity_scan.rb` Orchestrator work: add one History row to `docs/implementation-status/os-sdlc-system-model.md`. Rejected: strict patterns with threshold 3 — overlap raises convergence with no evidence, and the threshold stops being valid. Strict patterns with a higher threshold — the threshold has no calibration run behind it, and it hides confirmed rows. Counting one overlap cluster as one pattern — this changes `Convergence`, and the scale-group rules, for a problem that `strict?` already solves. ## Acceptance criteria 1. Given the catalog with this batch, when a test selects the strict group A patterns, then the list is exactly HEX, LAY, P&F, CMD and REPO. 2. Given a line that two strict group A patterns flag and that one new pattern flags, when `Convergence` counts the line, then the count is 2. 3. Given a component with a finding at convergence 2 that a new pattern also flags, when `OpportunityScan#ranked(:A)` runs with threshold 3, then the finding is not ranked. 4. 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. 5. 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 loose pattern is in the `loose_present` section labeled unreviewed. 6. Given a class with no role, when `shapes` runs after this ticket, then the detail is still `convergence A:3 C:0`. 7. Given a non-entry class that calls three classes of one namespace, when the `FAC` `Facade` role tests it, then it fits. 8. Given a component where a class outside that namespace calls a class inside it, when `FAC` matches the component, then R1 flags the line of that call. 9. Given a class with three injected collaborators and one of them among its senders, when the `MED` `Mediator` role tests it, then it fits. 10. Given two colleagues of a mediator, where each refers back to it and one calls the other, when `MED` matches the component, then R1 flags the line of that call. 11. Given a class that builds two parts in `initialize` and has no injected collaborator, when the `WHP` `Whole` role tests it, then it fits. 12. Given a class that builds two parts in a method other than `initialize`, when the `WHP` `Whole` role tests it, then it does not fit. 13. Given a `Model` class that calls a `View` class, when `MVC` matches the component, then R1 flags the line of that call. 14. Given a `Core` class that calls a `Shell` class, when `FCIS` matches the component, then R1 flags the line of that call. 15. Given a class with state, a decision, a call and storage I/O that is not `Edge`, when `DOM` matches the component, then R1 flags the I/O line. 16. Given an `Entry` class that calls a `DataSource` class, when `SVC` matches the component, then R1 flags the line of that call. 17. Given a `Handler` class that calls a `Handler` class, when `PGC` matches the component, then R1 flags the line of that call. 18. Given a `Front` class with a decision, when `FRC` matches the component, then R1 flags the decision line. ## Owner sketch ```ruby class OsSdlc::SystemModel::ClassFacts def called_namespaces = calls.group_by { |name| name.rpartition("::").first }.reject { |space, _| space.empty? } def initialize_constructs # non-default constructs inside the `initialize` def span end class OsSdlc::SystemModel::Catalog Facade = Pattern.new( id: "FAC", group: :A, strict?: false, roles: [ Pattern::Role.new(:Facade, ->(f) { !f.entry? && f.called_namespaces.values.any? { |names| names.size >= 3 } }), Pattern::Role.new(:Client, ->(f) { !f.calls.empty? }) ], rules: Pattern::Rules.new( allowed: [%i[Client Facade]], forbidden: [Pattern::Rule.new("R1", "Senders outside the namespace call only the facade.", Pattern::Rule::BypassedOwner.new(owners: [:Facade], parts: :namespace))] ), source: "Gamma et al., Facade, 1994" ) end # Predicates of the other patterns. Mediator::Mediator = ->(f) { f.injected.size >= 3 && !(f.senders & f.injected).empty? } Mediator::Colleague = ->(f) { !f.injected.empty? } DomainModel::DomainObject = ->(f) { f.holds_state? && f.pure? && f.decides? && !f.calls.empty? } ServiceLayer::Entry = ->(f) { f.entry? && !f.calls.empty? } ServiceLayer::Service = ->(f) { !f.entry? && f.pure? && f.calls.size >= 2 && !f.senders.empty? } ServiceLayer::DataSource = ->(f) { f.storage? && !f.entry? } ModelViewController::Controller = ->(f) { f.entry? && f.calls.size >= 2 } ModelViewController::View = ->(f) { f.terminal? && !f.entry? } ModelViewController::Model = ->(f) { f.pure? && f.holds_state? && f.decides? } PageController::Handler = ->(f) { f.entry? && !f.calls.empty? && f.routes_to.empty? } PageController::Worker = ->(f) { !f.entry? && !f.senders.empty? } FrontController::Front = ->(f) { f.entry? && !f.routes_to.empty? } FrontController::Handler = ->(f) { !f.entry? && f.routes_to.empty? && !f.calls.empty? } WholePart::Whole = ->(f) { f.injected.empty? && f.initialize_constructs.map(&:target).uniq.size >= 2 } WholePart::Part = ->(f) { !f.senders.empty? } FunctionalCore::Core = ->(f) { f.pure? && !f.holds_state? && !f.value? } FunctionalCore::Shell = ->(f) { !f.pure? && !f.calls.empty? } class OsSdlc::SystemModel::Pattern::Rule BypassedOwner = Data.define(:owners, :parts) do def sites(assignment) # edges into a part from a class that is not the owner and not a part end ColleagueCall = Data.define(:mediators) do def sites(assignment) # edges between two injected classes that each refer back to the mediator 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 Module, Presentation-Abstraction-Control or View Handler. ## Retired tests plugins/os-sdlc/tests/system_model_catalog_test.rb#test_transaction_script_is_the_one_loose_pattern_as_run_two_set_it ## Origin - Trigger: `system-model opportunities` holds nine patterns; `docs/system-design-format/pattern-landscape.md` marks 33 rows `expressible now`. - Improvised this session: none - Chain: a group A shape presents no match ← the catalog does not hold the pattern ← the catalog was built from six run 1 patterns plus VIS, GATE and SPEC. DESIGN: `docs/system-design-format/pattern-matching.md` section 5. - Root candidate: none found; searched catalog, group A, FAC, MED, FCIS, pattern-landscape, loose, strict?, Convergence - 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-1548.

Work started on branch ticket-1548.
Author
Owner

Resolution

Done: Added nine loose group A patterns, BypassedOwner and ColleagueCall rules, strict-only present, and a loose_present section labeled unreviewed.

Evidence: Implementation 231 merge-ready; suite gate passed twice; contract audit passed on branch ticket-1548.

Follow-ups: none

## Resolution **Done:** Added nine loose group A patterns, BypassedOwner and ColleagueCall rules, strict-only present, and a loose_present section labeled unreviewed. **Evidence:** Implementation 231 merge-ready; suite gate passed twice; contract audit passed on branch ticket-1548. **Follow-ups:** none
jared closed this issue 2026-11-04 22:33:49 +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#1548
No description provided.