system-model: Edge asks PrecedenceInEdge whether a class holds a domain rule #1517

Closed
opened 2026-11-02 23:48:24 +00:00 by jared · 2 comments
Owner

Problem

ClassFacts#leaks_io? decides whether a class holds a domain rule beside I/O.
It uses ClassFacts::IoReach, a call-reach heuristic over self-sends.
Commit 1814bfef (2026-11-01, Run 4) added it with no ticket.
Its one consumer is Catalog::Edge (!pure? && !value? && !leaks_io?), which feeds Domain, Repository and Hex.
The cop PrecedenceInEdge answers the same question with better precision.
Jared ruled option D: Edge asks PrecedenceInEdge whether the class holds a domain rule.

Observed

Measured over os-sdlc, os-backlog and os-me, and scored against docs/system-design-format/proof/2026-11-02-detector-a19/findings.tsv:

variant Edge Domain Repository Hex misfits TRUE FALSE
current 128 538 96 57 15 22
D 179 487 136 10 6 0

The 6 TRUE classes under D: LayerLedger::Ledger, Crc::ProgressTable, Project::Config::Document, Project::ConfigCommand, Tracker::IssueSource::TrackerLookup, bin/wakeup-poll.
The in-process cop run cost about +11 s on a 2 s extractor run (os-sdlc).

Reproduce

n/a: design change. The measurement script is described under Approach.

Approach

Mechanism (prototype, scratch only, not permanent): run the cop in-process, map each offense to the class whose def span covers it.

sites = OsSdlc::Detector::CopSites.new([RuboCop::Cop::Sdlc::Structural::PrecedenceInEdge], {})
hits  = paths.flat_map { |p| sites.findings(p).map { |f| Site.new(p.delete_prefix("#{root}/"), f.line) } }
# owner = the class whose defs cover the hit; among several, the one with the smallest def span.
# Fallback: for a script, the class named by the script path.
flagged = hits.filter_map { |h| classes.select { |_, f| f.defs.any? { |d| d.covers?(h) } }.min_by { |_, f| f.defs.select { |d| d.covers?(h) }.map { |d| d.last_line - d.line }.min }&.last || classes[h.path] }

CopSites (plugins/os-sdlc/lib/os_sdlc/detector/cop_sites.rb) gives the same hits as the rubocop CLI.
The scratch prototypes were variants.rb and inproc.rb (session scratchpad); this body carries the essential part.

Design:

  • ClassFacts#leaks_io? becomes true when PrecedenceInEdge reports an offense inside one of the class's def spans, or in the script file for a script.
  • One new extractor class (15-20 lines) maps offenses to classes. Wire it in through Extractor and Scripts (2-3 lines). class_facts.rb gets one field and a one-line leaks_io?.
  • The list of cops is one constant ([PrecedenceInEdge]). Cops O and E (#1510, #1511, in progress in another session) may join later by a one-line change. Do not add them here.
  • Size limit from Jared: no file sprawl. About 25 lines of library code, 1 new file.
  • Hand-count fixtures (system_model_pattern_match_hand_counts_test.rb) have no defs. The implementer states in the PR how those fixtures get cop sites (a real fixture file on disk, or a stub offense list) and why.

Delete what only IoReach uses. Verify each has no other caller before deleting (grep at ticket time found none):

  • plugins/os-sdlc/lib/os_sdlc/system_model/class_facts/io_reach.rb
  • Def#io_after? and send_lines (class_facts/def.rb)
  • Decision#sends (class_facts/decision.rb) and Branching#sends (extractor/branching.rb)
  • the send_lines: argument and SelfSends#first_lines (extractor/frame.rb:34, extractor/self_sends.rb:15)

Required work:

  1. Delete the call-reach heuristic — check: absent IoReach in plugins/os-sdlc/lib/os_sdlc/**/*.rb
  2. Delete the send-line bookkeeping — check: absent send_lines in plugins/os-sdlc/lib/os_sdlc/**/*.rb
  3. Delete the first-line helper — check: absent first_lines in plugins/os-sdlc/lib/os_sdlc/**/*.rb
  4. Name the cop list once — check: present PrecedenceInEdge in plugins/os-sdlc/lib/os_sdlc/system_model/**/*.rb
  5. Rewrite the 17 pinned tests that break: 7 leaks_io tests in plugins/os-sdlc/tests/system_model_class_facts_edge_test.rb, 6 in system_model_catalog_test.rb, 7 in system_model_pattern_match_hand_counts_test.rb (counts from Jared, re-count at start). Add 1-2 tests with a fixture file.

Orchestrator work: regenerate plugins/os-sdlc/design/generated; measure the os-sdlc system-model run time before and after; re-measure the run-4 agreement figure (17/18 at 2026-11-01) and record it on this ticket.

Rejected: keep IoReach and tune it — 22 FALSE against findings.tsv; the cop already encodes the rule.

Acceptance criteria

  1. Given os-sdlc, os-backlog and os-me, when the system model runs, then Edge, Domain, Repository and Hex-misfit counts are 179, 487, 136 and 10 (the D row).
  2. Given findings.tsv, when the flagged classes are scored, then TRUE is 6 and FALSE is 0.
  3. Given the full os-sdlc suite, when it runs, then it is green.
  4. Given the os-sdlc system-model run, when timed before and after, then both times are on this ticket (measured cost was about +11 s on a 2 s extractor).
  5. Given the run-4 agreement measurement, when it is re-run, then the new figure (was 17/18) is recorded on this ticket.
  6. Given bin/system-model regeneration, when it finishes, then plugins/os-sdlc/design/generated is current.
  7. Given a fixture file with a domain rule beside I/O in a class, when the extractor runs, then that class has leaks_io? true and is not an Edge.

Owner sketch

class OsSdlc::SystemModel::Extractor::CopOffenses
  def initialize(sites, classes) = ...
  def flagged_classes(paths) = ...
end

ClassFacts holds the flag; leaks_io? returns it.

Non-goals

Cops O and E (#1510, #1511). Class.new constants (#1513).

Blocks

Blocks: #1513 (both change the system_model extractor wiring; implement this first).

Origin

  • Trigger: Run 4 review of ClassFacts#leaks_io?; Jared ruled option D after the variant measurement.
  • Improvised this session: none
  • Chain: Edge misclassifies classes (22 FALSE) ← leaks_io? uses IoReach call-reach heuristic ← DESIGN (plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb:24, commit 1814bfef, no ticket)
  • Root candidate: none found; searched leaks_io, IoReach, io_reach, ClassFacts, PrecedenceInEdge, domain rule, Edge
    VERDICT: CREATE
  • Where: plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb:24
  • Session: d077e084-9195-4803-b761-6ce01eca0249
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/d077e084-9195-4803-b761-6ce01eca0249.jsonl
## Problem `ClassFacts#leaks_io?` decides whether a class holds a domain rule beside I/O. It uses `ClassFacts::IoReach`, a call-reach heuristic over self-sends. Commit `1814bfef` (2026-11-01, Run 4) added it with no ticket. Its one consumer is `Catalog::Edge` (`!pure? && !value? && !leaks_io?`), which feeds Domain, Repository and Hex. The cop `PrecedenceInEdge` answers the same question with better precision. Jared ruled option D: Edge asks `PrecedenceInEdge` whether the class holds a domain rule. ## Observed Measured over os-sdlc, os-backlog and os-me, and scored against `docs/system-design-format/proof/2026-11-02-detector-a19/findings.tsv`: | variant | Edge | Domain | Repository | Hex misfits | TRUE | FALSE | |---|---|---|---|---|---|---| | current | 128 | 538 | 96 | 57 | 15 | 22 | | D | 179 | 487 | 136 | 10 | 6 | 0 | The 6 TRUE classes under D: `LayerLedger::Ledger`, `Crc::ProgressTable`, `Project::Config::Document`, `Project::ConfigCommand`, `Tracker::IssueSource::TrackerLookup`, `bin/wakeup-poll`. The in-process cop run cost about +11 s on a 2 s extractor run (os-sdlc). ## Reproduce n/a: design change. The measurement script is described under Approach. ## Approach Mechanism (prototype, scratch only, not permanent): run the cop in-process, map each offense to the class whose def span covers it. ```ruby sites = OsSdlc::Detector::CopSites.new([RuboCop::Cop::Sdlc::Structural::PrecedenceInEdge], {}) hits = paths.flat_map { |p| sites.findings(p).map { |f| Site.new(p.delete_prefix("#{root}/"), f.line) } } # owner = the class whose defs cover the hit; among several, the one with the smallest def span. # Fallback: for a script, the class named by the script path. flagged = hits.filter_map { |h| classes.select { |_, f| f.defs.any? { |d| d.covers?(h) } }.min_by { |_, f| f.defs.select { |d| d.covers?(h) }.map { |d| d.last_line - d.line }.min }&.last || classes[h.path] } ``` `CopSites` (`plugins/os-sdlc/lib/os_sdlc/detector/cop_sites.rb`) gives the same hits as the rubocop CLI. The scratch prototypes were `variants.rb` and `inproc.rb` (session scratchpad); this body carries the essential part. Design: - `ClassFacts#leaks_io?` becomes true when `PrecedenceInEdge` reports an offense inside one of the class's def spans, or in the script file for a script. - One new extractor class (15-20 lines) maps offenses to classes. Wire it in through `Extractor` and `Scripts` (2-3 lines). `class_facts.rb` gets one field and a one-line `leaks_io?`. - The list of cops is one constant (`[PrecedenceInEdge]`). Cops O and E (#1510, #1511, in progress in another session) may join later by a one-line change. Do not add them here. - Size limit from Jared: no file sprawl. About 25 lines of library code, 1 new file. - Hand-count fixtures (`system_model_pattern_match_hand_counts_test.rb`) have no defs. The implementer states in the PR how those fixtures get cop sites (a real fixture file on disk, or a stub offense list) and why. Delete what only `IoReach` uses. Verify each has no other caller before deleting (grep at ticket time found none): - `plugins/os-sdlc/lib/os_sdlc/system_model/class_facts/io_reach.rb` - `Def#io_after?` and `send_lines` (`class_facts/def.rb`) - `Decision#sends` (`class_facts/decision.rb`) and `Branching#sends` (`extractor/branching.rb`) - the `send_lines:` argument and `SelfSends#first_lines` (`extractor/frame.rb:34`, `extractor/self_sends.rb:15`) Required work: 1. Delete the call-reach heuristic — check: absent `IoReach` in `plugins/os-sdlc/lib/os_sdlc/**/*.rb` 2. Delete the send-line bookkeeping — check: absent `send_lines` in `plugins/os-sdlc/lib/os_sdlc/**/*.rb` 3. Delete the first-line helper — check: absent `first_lines` in `plugins/os-sdlc/lib/os_sdlc/**/*.rb` 4. Name the cop list once — check: present `PrecedenceInEdge` in `plugins/os-sdlc/lib/os_sdlc/system_model/**/*.rb` 5. Rewrite the 17 pinned tests that break: 7 `leaks_io` tests in `plugins/os-sdlc/tests/system_model_class_facts_edge_test.rb`, 6 in `system_model_catalog_test.rb`, 7 in `system_model_pattern_match_hand_counts_test.rb` (counts from Jared, re-count at start). Add 1-2 tests with a fixture file. Orchestrator work: regenerate `plugins/os-sdlc/design/generated`; measure the os-sdlc system-model run time before and after; re-measure the run-4 agreement figure (17/18 at 2026-11-01) and record it on this ticket. Rejected: keep `IoReach` and tune it — 22 FALSE against findings.tsv; the cop already encodes the rule. ## Acceptance criteria 1. Given os-sdlc, os-backlog and os-me, when the system model runs, then Edge, Domain, Repository and Hex-misfit counts are 179, 487, 136 and 10 (the D row). 2. Given findings.tsv, when the flagged classes are scored, then TRUE is 6 and FALSE is 0. 3. Given the full os-sdlc suite, when it runs, then it is green. 4. Given the os-sdlc system-model run, when timed before and after, then both times are on this ticket (measured cost was about +11 s on a 2 s extractor). 5. Given the run-4 agreement measurement, when it is re-run, then the new figure (was 17/18) is recorded on this ticket. 6. Given `bin/system-model` regeneration, when it finishes, then `plugins/os-sdlc/design/generated` is current. 7. Given a fixture file with a domain rule beside I/O in a class, when the extractor runs, then that class has `leaks_io?` true and is not an Edge. ## Owner sketch ```ruby class OsSdlc::SystemModel::Extractor::CopOffenses def initialize(sites, classes) = ... def flagged_classes(paths) = ... end ``` `ClassFacts` holds the flag; `leaks_io?` returns it. ## Non-goals Cops O and E (#1510, #1511). Class.new constants (#1513). ## Blocks Blocks: #1513 (both change the system_model extractor wiring; implement this first). ## Origin - Trigger: Run 4 review of `ClassFacts#leaks_io?`; Jared ruled option D after the variant measurement. - Improvised this session: none - Chain: Edge misclassifies classes (22 FALSE) ← `leaks_io?` uses `IoReach` call-reach heuristic ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb:24`, commit `1814bfef`, no ticket) - Root candidate: none found; searched leaks_io, IoReach, io_reach, ClassFacts, PrecedenceInEdge, domain rule, Edge VERDICT: CREATE - Where: plugins/os-sdlc/lib/os_sdlc/system_model/class_facts.rb:24 - 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-1517 through /os-sdlc:implement.

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

Resolution

Done: Catalog::Edge asks PrecedenceInEdge (run in-process by Extractor::CopOffenses) whether a class holds a domain rule; ClassFacts::IoReach deleted. Measured 2026-11-03 (snapshot, not target): Edge 179, Domain 492, Repository 136, Hex 10, 6 TRUE / 0 FALSE vs findings.tsv. AC1 said Domain 487: the tree gained six DomainOperandInEdge classes and lost IoReach since the prototype; Jared accepted that counts move as code evolves. AC4: extractor 1.5 s -> 2.8 s on os-sdlc (+1.3 s; estimate was +11 s). AC5: run-4 agreement 13/18 (was 17/18); the five disagreements are cop misses (LabelFile, RouteComparison, Scorer, ReplyShape::EvalRun, Census::SourceRow), no false hits.

Evidence: main 24d18dfe (commit 5ff021b3). Implementation 223 escalated on a test defect and was hand-finished, then abandoned. Suite 3305 runs 0 failures on the merged branch; rubocop 0 offenses; POODR design audit findings (cop-only leaks_io?, decides? reverted, in-memory catalog fixture) fixed; design/generated rebuilt with drift 0.

Follow-ups: #1526 add DomainOperandInEdge and DomainEffectInEdge to the Edge cop list (held for Jared's map review).

## Resolution **Done:** Catalog::Edge asks PrecedenceInEdge (run in-process by Extractor::CopOffenses) whether a class holds a domain rule; ClassFacts::IoReach deleted. Measured 2026-11-03 (snapshot, not target): Edge 179, Domain 492, Repository 136, Hex 10, 6 TRUE / 0 FALSE vs findings.tsv. AC1 said Domain 487: the tree gained six DomainOperandInEdge classes and lost IoReach since the prototype; Jared accepted that counts move as code evolves. AC4: extractor 1.5 s -> 2.8 s on os-sdlc (+1.3 s; estimate was +11 s). AC5: run-4 agreement 13/18 (was 17/18); the five disagreements are cop misses (LabelFile, RouteComparison, Scorer, ReplyShape::EvalRun, Census::SourceRow), no false hits. **Evidence:** main 24d18dfe (commit 5ff021b3). Implementation 223 escalated on a test defect and was hand-finished, then abandoned. Suite 3305 runs 0 failures on the merged branch; rubocop 0 offenses; POODR design audit findings (cop-only leaks_io?, decides? reverted, in-memory catalog fixture) fixed; design/generated rebuilt with drift 0. **Follow-ups:** #1526 add DomainOperandInEdge and DomainEffectInEdge to the Edge cop list (held for Jared's map review).
jared closed this issue 2026-11-03 14:40:02 +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#1517
No description provided.