Add group D to the catalog with Active Record and Data Mapper #1550

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

Problem

docs/system-design-format/pattern-landscape.md marks Active Record and Data Mapper as expressible now, and catalog/ holds no group D pattern. The detector cannot present a data-mapping shape.

Group D has no threshold. OpportunityScan#groups calls @thresholds.fetch(group) for every group of Catalog.patterns, strict or loose. One group D pattern without a threshold therefore raises KeyError, and pattern-matching.md section 5 says "A group with no threshold is an error".

Section 9 of the same document says to measure precision "whenever a scale group is added". No run has measured group D.

Reproduce

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

Approach

Threshold. Add D: 1 to Catalog.thresholds. This is the one open point of this ticket. Predicted value: 1. Reasons:

  1. Group D holds two patterns in this batch, so the highest convergence is 2. A scratch run with both patterns strict gave 0 rows at convergence 2 over os-backlog, os-me and os-sdlc. Threshold 2 would rank nothing.
  2. Group C has the same shape: few patterns, no overlap, threshold 1.
  3. The value is inert while both patterns are loose. A later calibration run may change it with no risk to current reports.

How the new patterns enter convergence. Both patterns set strict?: false, under the entry rule from catalog-group-a. Convergence#flags drops the matches of a loose pattern, so ranked(:D) is empty. Group D has no calibrated threshold to protect, but it also has no precision figure, so loose is the safe entry. Convergence counts one group only (Convergence#lines groups by pattern.group), so group D cannot change a group A or group C count in either case.

The scratch run with both patterns strict gave these group D rows: os-backlog 0, os-me 1, os-sdlc 16. No review has read them, so they stay out of the ranking until a calibration run measures them.

opportunities prints a header group D (convergence >= 1) with no rows. Shape::UnclearRole#groups selects strict patterns only, so the unclear-role detail stays convergence A:3 C:0 and does not gain D:0.

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 29, os-me 8, os-sdlc 47. Nobody reviewed them. The rows of the current patterns stay the same.

Order. Ship this ticket after catalog-group-a, which 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. Active Record (AR): "A class holds state and domain behavior and does its own storage I/O; or Sequel::Model/ActiveRecord::Base superclass (F1 sharpens)." Roles Record, Client. Rule R1 reads "does its own storage I/O": a class that calls a record does no I/O of the same kind (Rule::BypassedIo, as in GATE R1). The role does not use the superclass, because the landscape names the first signal as the core signal. A first draft of R1 used ForbiddenIo on Client. It flagged every storage line of a calling class (24 lines in os-backlog). Do not use that form.
  2. Data Mapper (DM): "A mapper does storage I/O, builds domain objects (Domain.new(row)), and reads their attributes to write; domain classes are pure." Roles Mapper, Domain, Edge. Rule R1 reads "domain classes are pure": a Domain class does no I/O, except on Edge. Rule R2 forbids Domain to Mapper, because a domain class that calls its mapper reaches storage. No fact shows "reads their attributes to write", so a predicate cannot measure it.

Required work:

  1. Add the Active Record pattern — check: present id: "AR" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/active_record.rb
  2. Add the Data Mapper pattern — check: present id: "DM" in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/data_mapper.rb
  3. Set each new pattern to loose — check: present strict?: false in plugins/os-sdlc/lib/os_sdlc/system_model/catalog/data_mapper.rb
  4. Add the group D threshold — check: present D: 1 in plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb
  5. List the two patterns in Catalog.patterns — check: present DataMapper in plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb
  6. Update IDS in the catalog test — check: present AR DM in plugins/os-sdlc/tests/system_model_catalog_test.rb
  7. Record the group D threshold and its prediction in section 5 of the method document — check: present group D threshold in docs/system-design-format/pattern-matching.md

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

Rejected: strict patterns at threshold 1 — section 9 requires a precision measurement for a new scale group, and none exists. Threshold 2 — two patterns reach 2 only when both flag one file, and the scratch run found no such file. Skip a group with no threshold in OpportunityScan#groups — section 5 calls that case an error.

Acceptance criteria

  1. Given the catalog with this batch, when Catalog.thresholds is read, then it holds { A: 3, C: 1, D: 1 }.
  2. Given the catalog with this batch, when a test reads each catalog group, then each group has a threshold.
  3. Given the catalog with this batch, when opportunities runs over os-backlog, then the command exits 0 and prints group D (convergence >= 1).
  4. Given both group D patterns, when a test reads strict?, then each value is false.
  5. Given a component with a group D misfit, when OpportunityScan#ranked(:D) runs, then the result is empty.
  6. 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 A and C entries of groups are the same.
  7. 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.
  8. Given a class with no role, when shapes runs after this ticket, then the detail is still convergence A:3 C:0.
  9. Given a non-entry class with state, a decision and storage I/O, when the AR Record role tests it, then it fits.
  10. Given a class with state and a decision and no storage I/O, when the AR Record role tests it, then it does not fit.
  11. Given a class that calls a Record class and does storage I/O, when AR matches the component, then R1 flags the I/O line of the caller.
  12. Given a non-entry class with storage I/O and a construct, when the DM Mapper role tests it, then it fits.
  13. Given a class that fits Domain when its I/O is removed and that is not Edge, when DM matches the component, then R1 flags the I/O line.
  14. Given a Domain class that calls a Mapper class, when DM matches the component, then R2 flags the line of that call.

Owner sketch

class OsSdlc::SystemModel::Catalog
  def self.thresholds = { A: 3, C: 1, D: 1 }.freeze

  ActiveRecord = Pattern.new(
    id: "AR", group: :D, strict?: false,
    roles: [
      Pattern::Role.new(:Record, ->(f) { f.holds_state? && f.decides? && f.storage? && !f.entry? && !f.value? }),
      Pattern::Role.new(:Client, ->(f) { !f.storage? && !f.calls.empty? })
    ],
    rules: Pattern::Rules.new(
      allowed: [%i[Client Record]],
      forbidden: [Pattern::Rule.new("R1", "A class that calls a record does no storage I/O itself.",
                                    Pattern::Rule::BypassedIo.new(clients: [:Client], owners: [:Record]))]
    ),
    source: "Fowler, PoEAA ch. 10, Active Record"
  )

  DataMapper = Pattern.new(
    id: "DM", group: :D, strict?: false,
    roles: [
      Pattern::Role.new(:Mapper, ->(f) { f.storage? && !f.entry? && !f.constructs.empty? }),
      Pattern::Role.new(:Domain, ->(f) { f.pure? && f.holds_state? && f.decides? }),
      Catalog::Edge
    ],
    rules: Pattern::Rules.new(
      allowed: [%i[Mapper Domain]],
      forbidden: [
        Pattern::Rule.new("R1", "Domain classes are pure.",
                          Pattern::Rule::ForbiddenIo.new(roles: [:Domain], kinds: ClassFacts::Io::KINDS, except: [:Edge])),
        Pattern::Rule.new("R2", "A domain class does not call its mapper.",
                          Pattern::Rule::ForbiddenEdge.new(pairs: [%i[Domain Mapper]]))
      ]
    ),
    source: "Fowler, PoEAA ch. 10, Data Mapper"
  )
end

Non-goals

Do not set a group D pattern to strict. Do not change the group A or group C threshold, the 0.05 threshold, or Match#present?. Do not add a schema fact. Do not add a rule class.

Retired tests

plugins/os-sdlc/tests/system_model_opportunity_scan_test.rb#test_thresholds_are_three_for_the_architectural_group_and_one_for_the_class_group

Origin

  • Trigger: docs/system-design-format/pattern-landscape.md marks two group D rows expressible now, and Catalog.thresholds has no group D entry.
  • Improvised this session: none
  • Chain: a data-mapping shape presents no match ← the catalog holds no group D pattern ← group D has no threshold, and OpportunityScan#groups needs one. DESIGN: docs/system-design-format/pattern-matching.md section 5.
  • Root candidate: none found; searched catalog, group D, ActiveRecord, DataMapper, Catalog.thresholds
  • 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 Active Record and Data Mapper as `expressible now`, and `catalog/` holds no group D pattern. The detector cannot present a data-mapping shape. Group D has no threshold. `OpportunityScan#groups` calls `@thresholds.fetch(group)` for every group of `Catalog.patterns`, strict or loose. One group D pattern without a threshold therefore raises `KeyError`, and pattern-matching.md section 5 says "A group with no threshold is an error". Section 9 of the same document says to measure precision "whenever a scale group is added". No run has measured group D. ## Reproduce n/a: this ticket adds catalog data; no defect exists to reproduce. ## Approach **Threshold.** Add `D: 1` to `Catalog.thresholds`. This is the one open point of this ticket. Predicted value: 1. Reasons: 1. Group D holds two patterns in this batch, so the highest convergence is 2. A scratch run with both patterns strict gave 0 rows at convergence 2 over os-backlog, os-me and os-sdlc. Threshold 2 would rank nothing. 2. Group C has the same shape: few patterns, no overlap, threshold 1. 3. The value is inert while both patterns are loose. A later calibration run may change it with no risk to current reports. **How the new patterns enter convergence.** Both patterns set `strict?: false`, under the entry rule from `catalog-group-a`. `Convergence#flags` drops the matches of a loose pattern, so `ranked(:D)` is empty. Group D has no calibrated threshold to protect, but it also has no precision figure, so loose is the safe entry. Convergence counts one group only (`Convergence#lines` groups by `pattern.group`), so group D cannot change a group A or group C count in either case. The scratch run with both patterns strict gave these group D rows: os-backlog 0, os-me 1, os-sdlc 16. No review has read them, so they stay out of the ranking until a calibration run measures them. `opportunities` prints a header `group D (convergence >= 1)` with no rows. `Shape::UnclearRole#groups` selects strict patterns only, so the `unclear-role` detail stays `convergence A:3 C:0` and does not gain `D:0`. **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 29, os-me 8, os-sdlc 47. Nobody reviewed them. The rows of the current patterns stay the same. **Order.** Ship this ticket after `catalog-group-a`, which 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. Active Record (`AR`): "A class holds state and domain behavior and does its own storage I/O; or `Sequel::Model`/`ActiveRecord::Base` superclass (F1 sharpens)." Roles `Record`, `Client`. Rule R1 reads "does its own storage I/O": a class that calls a record does no I/O of the same kind (`Rule::BypassedIo`, as in GATE R1). The role does not use the superclass, because the landscape names the first signal as the core signal. A first draft of R1 used `ForbiddenIo` on `Client`. It flagged every storage line of a calling class (24 lines in os-backlog). Do not use that form. 2. Data Mapper (`DM`): "A mapper does storage I/O, builds domain objects (`Domain.new(row)`), and reads their attributes to write; domain classes are pure." Roles `Mapper`, `Domain`, `Edge`. Rule R1 reads "domain classes are pure": a `Domain` class does no I/O, except on `Edge`. Rule R2 forbids `Domain` to `Mapper`, because a domain class that calls its mapper reaches storage. No fact shows "reads their attributes to write", so a predicate cannot measure it. Required work: 1. Add the Active Record pattern — check: present `id: "AR"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/active_record.rb` 2. Add the Data Mapper pattern — check: present `id: "DM"` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/data_mapper.rb` 3. Set each new pattern to loose — check: present `strict?: false` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog/data_mapper.rb` 4. Add the group D threshold — check: present `D: 1` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb` 5. List the two patterns in `Catalog.patterns` — check: present `DataMapper` in `plugins/os-sdlc/lib/os_sdlc/system_model/catalog.rb` 6. Update `IDS` in the catalog test — check: present `AR DM` in `plugins/os-sdlc/tests/system_model_catalog_test.rb` 7. Record the group D threshold and its prediction in section 5 of the method document — check: present `group D threshold` in `docs/system-design-format/pattern-matching.md` Orchestrator work: add one History row to `docs/implementation-status/os-sdlc-system-model.md`. Rejected: strict patterns at threshold 1 — section 9 requires a precision measurement for a new scale group, and none exists. Threshold 2 — two patterns reach 2 only when both flag one file, and the scratch run found no such file. Skip a group with no threshold in `OpportunityScan#groups` — section 5 calls that case an error. ## Acceptance criteria 1. Given the catalog with this batch, when `Catalog.thresholds` is read, then it holds `{ A: 3, C: 1, D: 1 }`. 2. Given the catalog with this batch, when a test reads each catalog group, then each group has a threshold. 3. Given the catalog with this batch, when `opportunities` runs over os-backlog, then the command exits 0 and prints `group D (convergence >= 1)`. 4. Given both group D patterns, when a test reads `strict?`, then each value is false. 5. Given a component with a group D misfit, when `OpportunityScan#ranked(:D)` runs, then the result is empty. 6. 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 `A` and `C` entries of `groups` are the same. 7. 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. 8. Given a class with no role, when `shapes` runs after this ticket, then the detail is still `convergence A:3 C:0`. 9. Given a non-entry class with state, a decision and storage I/O, when the `AR` `Record` role tests it, then it fits. 10. Given a class with state and a decision and no storage I/O, when the `AR` `Record` role tests it, then it does not fit. 11. Given a class that calls a `Record` class and does storage I/O, when `AR` matches the component, then R1 flags the I/O line of the caller. 12. Given a non-entry class with storage I/O and a construct, when the `DM` `Mapper` role tests it, then it fits. 13. Given a class that fits `Domain` when its I/O is removed and that is not `Edge`, when `DM` matches the component, then R1 flags the I/O line. 14. Given a `Domain` class that calls a `Mapper` class, when `DM` matches the component, then R2 flags the line of that call. ## Owner sketch ```ruby class OsSdlc::SystemModel::Catalog def self.thresholds = { A: 3, C: 1, D: 1 }.freeze ActiveRecord = Pattern.new( id: "AR", group: :D, strict?: false, roles: [ Pattern::Role.new(:Record, ->(f) { f.holds_state? && f.decides? && f.storage? && !f.entry? && !f.value? }), Pattern::Role.new(:Client, ->(f) { !f.storage? && !f.calls.empty? }) ], rules: Pattern::Rules.new( allowed: [%i[Client Record]], forbidden: [Pattern::Rule.new("R1", "A class that calls a record does no storage I/O itself.", Pattern::Rule::BypassedIo.new(clients: [:Client], owners: [:Record]))] ), source: "Fowler, PoEAA ch. 10, Active Record" ) DataMapper = Pattern.new( id: "DM", group: :D, strict?: false, roles: [ Pattern::Role.new(:Mapper, ->(f) { f.storage? && !f.entry? && !f.constructs.empty? }), Pattern::Role.new(:Domain, ->(f) { f.pure? && f.holds_state? && f.decides? }), Catalog::Edge ], rules: Pattern::Rules.new( allowed: [%i[Mapper Domain]], forbidden: [ Pattern::Rule.new("R1", "Domain classes are pure.", Pattern::Rule::ForbiddenIo.new(roles: [:Domain], kinds: ClassFacts::Io::KINDS, except: [:Edge])), Pattern::Rule.new("R2", "A domain class does not call its mapper.", Pattern::Rule::ForbiddenEdge.new(pairs: [%i[Domain Mapper]])) ] ), source: "Fowler, PoEAA ch. 10, Data Mapper" ) end ``` ## Non-goals Do not set a group D pattern to strict. Do not change the group A or group C threshold, the 0.05 threshold, or `Match#present?`. Do not add a schema fact. Do not add a rule class. ## Retired tests plugins/os-sdlc/tests/system_model_opportunity_scan_test.rb#test_thresholds_are_three_for_the_architectural_group_and_one_for_the_class_group ## Origin - Trigger: `docs/system-design-format/pattern-landscape.md` marks two group D rows `expressible now`, and `Catalog.thresholds` has no group D entry. - Improvised this session: none - Chain: a data-mapping shape presents no match ← the catalog holds no group D pattern ← group D has no threshold, and `OpportunityScan#groups` needs one. DESIGN: `docs/system-design-format/pattern-matching.md` section 5. - Root candidate: none found; searched catalog, group D, ActiveRecord, DataMapper, Catalog.thresholds - 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-1550.

Work started on branch ticket-1550.
Author
Owner

Resolution

Done: Group D loose patterns Active Record and Data Mapper in the catalog; thresholds gain D: 1 with an amendment in pattern-matching.md.

Evidence: Implementation 234 complete; commit d27dd847; suite gate passed; contract audit passed.

Follow-ups: none

## Resolution **Done:** Group D loose patterns Active Record and Data Mapper in the catalog; thresholds gain D: 1 with an amendment in pattern-matching.md. **Evidence:** Implementation 234 complete; commit d27dd847; suite gate passed; contract audit passed. **Follow-ups:** none
jared closed this issue 2026-11-05 00:12:37 +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#1550
No description provided.