Add Sdlc/ProcessBoundary cop restricting process-starting calls to named boundary classes #637

Closed
opened 2026-09-23 12:50:54 +00:00 by jared · 2 comments
Owner

Problem

The 2026-09-23 shell/ENV self-calling audit (#631-#636) found the repository's process-starting calls scattered and confirmed which classes are legitimate boundaries: Git::Repo (plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb, sole git caller, ADR-0174), ProjectShell (plugins/os-sdlc/lib/os_sdlc/model/project_shell.rb, ADR-0191), the ruby-lsp/lsp daemon classes under plugins/os-sdlc/lib/os_sdlc/lsp_probe/daemon/ (ADR-0197/0198/0201), and several ast-grep callers (for example plugins/os-sdlc/lib/os_sdlc/lsp_probe/ast_definition.rb, plugins/os-sdlc/lib/os_sdlc/evidence/probe/rows.rb). The pb-mirror (pbm) boundary lives in the separate pb-mirror gem, outside this repo, and is out of scope for this cop.
Today's audit relied on manual grep. There is no static check that stops a new process-starting call from landing outside the allowed boundary classes at the next commit.
os-sdlc already has a precedent for a structural custom cop: Sdlc/RequireInMethod at plugins/os-sdlc/lib/os_sdlc/cops/require_in_method.rb, with its test at plugins/os-sdlc/tests/cops/require_in_method_test.rb.
An adr-find search for "custom rubocop cop", "lint", and a file-scoped search against require_in_method.rb returned no candidates, so no constraining ADR came back from the CLI. ADR-0154 (lint lessons) and ADR-0194 (one-constant-per-file) are named by the requester as likely-relevant precedent for cop design and should be read before implementation, even though the finder did not surface them.

Observed

n/a: this is a design/feature ticket, not a bug report.

Reproduce

n/a: no existing cop enforces the boundary-class rule; there is nothing to reproduce.

Expected

Add a new custom cop, Sdlc/ProcessBoundary (or equivalent name), that:

  • Flags any process-starting call — backticks, %x, Kernel#system, exec, spawn, Process.spawn, Open3.*, IO.popen — found in a class not on an explicit allowlist.
  • Scopes to lib and tests only.
  • Emits a message that prescribes the fix: name the boundary class the call should route through, or instruct the author to add a new named boundary class to the allowlist.
def test_flags_system_call_outside_boundary_class
  # arrange: a lib file with `class Foo; def bar; system("git status"); end; end`
  # act: run Sdlc/ProcessBoundary over the file
  # assert: one offense, message names an existing boundary class (e.g. Git::Repo) or says "add a boundary class"
end

def test_allows_system_call_inside_allowlisted_boundary_class
  # arrange: `class Git::Repo; def bar; system("git status"); end; end`
  # act: run Sdlc/ProcessBoundary over the file
  # assert: no offense
end

Origin

  • Trigger: user request during the 2026-09-23 shell/ENV self-calling audit (#631-#636) session, following up on the runtime self-fork guard test landed in #631.
  • Improvised this session: none.
  • Chain: no static check on process-starting calls ← boundary classes for process calls exist but are enforced only by convention and manual audit (DESIGN, Git::Repo ADR-0174, ProjectShell ADR-0191, lsp daemon ADR-0197/0198/0201) ← this ticket adds the missing static enforcement.
  • Root candidate: this ticket.
  • Where: plugins/os-sdlc/lib/os_sdlc/cops/ (new cop file alongside require_in_method.rb); plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb; plugins/os-sdlc/lib/os_sdlc/model/project_shell.rb; plugins/os-sdlc/lib/os_sdlc/lsp_probe/daemon/
  • Session: 5bb052ff-ddf2-4d47-a283-d00d071e9b79
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/5bb052ff-ddf2-4d47-a283-d00d071e9b79.jsonl

Owner sketch

class OsSdlc::Cops::ProcessBoundary < RuboCop::Cop::Base
  ALLOWED_BOUNDARY_CLASSES = %w[Git::Repo ProjectShell LspProbe::Daemon::Spawner ...].freeze
  def on_send(node)
end

Acceptance criteria

  1. Given a system("git ...") call inside a class not on the allowlist, when the cop runs, then it reports one offense whose message names an existing boundary class to route the call through instead.
  2. Given a `git status` backtick call inside Git::Repo, when the cop runs, then it reports no offense.
  3. Given an Open3.capture2 call inside ProjectShell, when the cop runs, then it reports no offense.
  4. Given a Process.spawn call inside a brand-new class with no existing boundary fit, when the cop runs, then the offense message tells the author to add a new named boundary class rather than only flagging the violation.
  5. Given a process-starting call inside a file under tests/, when the cop runs, then the same boundary-class rule applies as in lib/.
  6. Given a process-starting call inside a file outside lib/ and tests/ (for example a bin/ script), when the cop runs, then it is out of scope and reports no offense.

Structural follow-through

  • Complements the runtime self-fork guard test landed in #631: this cop is the static lint-time check; #631's guard is the runtime check that catches forks of the repo's own binaries through computed paths.
  • Relates to #632 (hook/pipe.rb), #635, #636 — coordinate boundary-class naming with any classes those tickets introduce or touch.
  • Read ADR-0154 (lint lessons) and ADR-0194 (one-constant-per-file) before design; confirm via os-adr:find at implementation time whether either constrains cop shape, since the CLI search run during capture returned no candidates.
## Problem The 2026-09-23 shell/ENV self-calling audit (#631-#636) found the repository's process-starting calls scattered and confirmed which classes are legitimate boundaries: `Git::Repo` (`plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb`, sole git caller, ADR-0174), `ProjectShell` (`plugins/os-sdlc/lib/os_sdlc/model/project_shell.rb`, ADR-0191), the ruby-lsp/lsp daemon classes under `plugins/os-sdlc/lib/os_sdlc/lsp_probe/daemon/` (ADR-0197/0198/0201), and several ast-grep callers (for example `plugins/os-sdlc/lib/os_sdlc/lsp_probe/ast_definition.rb`, `plugins/os-sdlc/lib/os_sdlc/evidence/probe/rows.rb`). The pb-mirror (pbm) boundary lives in the separate `pb-mirror` gem, outside this repo, and is out of scope for this cop. Today's audit relied on manual grep. There is no static check that stops a new process-starting call from landing outside the allowed boundary classes at the next commit. `os-sdlc` already has a precedent for a structural custom cop: `Sdlc/RequireInMethod` at `plugins/os-sdlc/lib/os_sdlc/cops/require_in_method.rb`, with its test at `plugins/os-sdlc/tests/cops/require_in_method_test.rb`. An `adr-find` search for "custom rubocop cop", "lint", and a file-scoped search against `require_in_method.rb` returned no candidates, so no constraining ADR came back from the CLI. ADR-0154 (lint lessons) and ADR-0194 (one-constant-per-file) are named by the requester as likely-relevant precedent for cop design and should be read before implementation, even though the finder did not surface them. ## Observed n/a: this is a design/feature ticket, not a bug report. ## Reproduce n/a: no existing cop enforces the boundary-class rule; there is nothing to reproduce. ## Expected Add a new custom cop, `Sdlc/ProcessBoundary` (or equivalent name), that: - Flags any process-starting call — backticks, `%x`, `Kernel#system`, `exec`, `spawn`, `Process.spawn`, `Open3.*`, `IO.popen` — found in a class not on an explicit allowlist. - Scopes to `lib` and `tests` only. - Emits a message that prescribes the fix: name the boundary class the call should route through, or instruct the author to add a new named boundary class to the allowlist. ```ruby def test_flags_system_call_outside_boundary_class # arrange: a lib file with `class Foo; def bar; system("git status"); end; end` # act: run Sdlc/ProcessBoundary over the file # assert: one offense, message names an existing boundary class (e.g. Git::Repo) or says "add a boundary class" end def test_allows_system_call_inside_allowlisted_boundary_class # arrange: `class Git::Repo; def bar; system("git status"); end; end` # act: run Sdlc/ProcessBoundary over the file # assert: no offense end ``` ## Origin - Trigger: user request during the 2026-09-23 shell/ENV self-calling audit (#631-#636) session, following up on the runtime self-fork guard test landed in #631. - Improvised this session: none. - Chain: no static check on process-starting calls ← boundary classes for process calls exist but are enforced only by convention and manual audit (DESIGN, `Git::Repo` ADR-0174, `ProjectShell` ADR-0191, lsp daemon ADR-0197/0198/0201) ← this ticket adds the missing static enforcement. - Root candidate: this ticket. - Where: plugins/os-sdlc/lib/os_sdlc/cops/ (new cop file alongside require_in_method.rb); plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb; plugins/os-sdlc/lib/os_sdlc/model/project_shell.rb; plugins/os-sdlc/lib/os_sdlc/lsp_probe/daemon/ - Session: 5bb052ff-ddf2-4d47-a283-d00d071e9b79 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/5bb052ff-ddf2-4d47-a283-d00d071e9b79.jsonl ## Owner sketch ```ruby class OsSdlc::Cops::ProcessBoundary < RuboCop::Cop::Base ALLOWED_BOUNDARY_CLASSES = %w[Git::Repo ProjectShell LspProbe::Daemon::Spawner ...].freeze def on_send(node) end ``` ## Acceptance criteria 1. Given a `system("git ...")` call inside a class not on the allowlist, when the cop runs, then it reports one offense whose message names an existing boundary class to route the call through instead. 2. Given a `` `git status` `` backtick call inside `Git::Repo`, when the cop runs, then it reports no offense. 3. Given an `Open3.capture2` call inside `ProjectShell`, when the cop runs, then it reports no offense. 4. Given a `Process.spawn` call inside a brand-new class with no existing boundary fit, when the cop runs, then the offense message tells the author to add a new named boundary class rather than only flagging the violation. 5. Given a process-starting call inside a file under `tests/`, when the cop runs, then the same boundary-class rule applies as in `lib/`. 6. Given a process-starting call inside a file outside `lib/` and `tests/` (for example a `bin/` script), when the cop runs, then it is out of scope and reports no offense. ## Structural follow-through - Complements the runtime self-fork guard test landed in #631: this cop is the static lint-time check; #631's guard is the runtime check that catches forks of the repo's own binaries through computed paths. - Relates to #632 (hook/pipe.rb), #635, #636 — coordinate boundary-class naming with any classes those tickets introduce or touch. - Read ADR-0154 (lint lessons) and ADR-0194 (one-constant-per-file) before design; confirm via `os-adr:find` at implementation time whether either constrains cop shape, since the CLI search run during capture returned no candidates.
Author
Owner

Absorbed by #1647 part B (branch ticket-1647-lesson, commit 7f055cd1d, not merged yet). The cop is Sdlc/Structural/ProcessStartOutsideAdapter. It uses no allowlist of boundary classes. It uses a value rule: a process start in a lib/ file is allowed only in an adapter, a class whose every public method reaches the start (constructor, builder, and helper methods of the start do not count), or an object that keeps a started process (blockless popen). Differences from this ticket: (1) scope is lib/ only; tests are covered by the #1647 runtime guard in test_helper, which has no exceptions (ruling 88, ruling 92); (2) no named-boundary list, so tool-named adapters need no entry; (3) a second cop, Sdlc/Structural/AdapterBuiltOutsideEntry, flags an adapter built or named outside the entry. Whole-plugins probe 2026-11-09: 4 hits, 0 false positives. Close this ticket when #1647 merges.

Absorbed by #1647 part B (branch ticket-1647-lesson, commit 7f055cd1d, not merged yet). The cop is `Sdlc/Structural/ProcessStartOutsideAdapter`. It uses no allowlist of boundary classes. It uses a value rule: a process start in a `lib/` file is allowed only in an adapter, a class whose every public method reaches the start (constructor, builder, and helper methods of the start do not count), or an object that keeps a started process (blockless popen). Differences from this ticket: (1) scope is `lib/` only; tests are covered by the #1647 runtime guard in test_helper, which has no exceptions (ruling 88, ruling 92); (2) no named-boundary list, so tool-named adapters need no entry; (3) a second cop, `Sdlc/Structural/AdapterBuiltOutsideEntry`, flags an adapter built or named outside the entry. Whole-plugins probe 2026-11-09: 4 hits, 0 false positives. Close this ticket when #1647 merges.
Author
Owner

Resolution

Done: Absorbed by #1647: Sdlc/Structural/ProcessStartOutsideAdapter cop, structural adapter rule instead of a boundary-class list

Evidence: main 7f055cd1; 0 hits on main after 38b3284d

Follow-ups: none

## Resolution **Done:** Absorbed by #1647: Sdlc/Structural/ProcessStartOutsideAdapter cop, structural adapter rule instead of a boundary-class list **Evidence:** main 7f055cd1; 0 hits on main after 38b3284d **Follow-ups:** none <!-- os-backlog kind: resolution -->
jared closed this issue 2026-11-09 20:46:04 +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#637
No description provided.