Extract Project::Code, Project::Test, Project::Lint value objects; Codebase replaces Layout as the code-to-test pairing #595

Closed
opened 2026-09-15 14:29:41 +00:00 by jared · 6 comments
Owner

VERDICT: CREATE (ticket-skeptic: user-requested design decision, explicitly stated and approved this session — hard floor overrides the gauntlet)## Context
plugins/os-sdlc/lib/os_sdlc/project.rb is a Data.define with 13 keyword attributes after commit 5e867e6. Rubocop's Sdlc/Structural/ParameterListSignalsMissingObject cop fires on its initialize. Attributes that change together sit scattered across the flat list. test_path, test_command, test_framework, and red_assert_command move together when the test framework changes. lint_config and lint_paths move together too. Project::Layout, at plugins/os-sdlc/lib/os_sdlc/project/layout.rb, is a lookup, not a validation. Given a code file, it builds candidate test paths, mirrored and unnamespaced, and picks the first that exists. It falls back to scenario-split siblings, then raises. It needs both the code prefix and the test prefix, so it belongs to an object that holds both. The name "Layout" misnames this behavior. The YAML format stays flat; .sdlc/project.yaml keys do not change. Project::Yaml::Document#attrs, Project::Yaml::Document#shared_attrs, and Project::Config#project build the nested values at load time. The user decided this design on 2026-09-15. A separate session implements it through /os-sdlc:implement using the ticket map.

Observed

grep -n "ParameterListSignalsMissingObject" -r plugins/os-sdlc
Sdlc/Structural/ParameterListSignalsMissingObject fires on OsSdlc::Project#initialize (13 keyword args)

Reproduce

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

Expected

Given a project loaded from a flat project.yaml, when the reader accesses project.code.path, project.test.path, project.test.command, project.test.framework, project.lint.config, and project.lint.paths, then each nested value matches today's flat-attribute value, and project.tests_for(code_file) returns the same test paths that Project::Layout returns today.

Owner sketch

module OsSdlc
  Project = Data.define(:name, :root, :tracker, :code, :test, :lint, :map, :commit_on_complete) do
    def tests_for(code_file)
  end

  class Project
    Code = Data.define(:path) do
      def include?(path)
      def relative(path)
      def prefix
    end

    Test = Data.define(:path, :command, :framework) do
      def include?(path)
      def prefix
      def covering(code_relative)
      class Candidates
    end

    Lint = Data.define(:config, :paths) do
      def resolved_config(root)
    end

    class Codebase
      def initialize(code:, test:, root:)
      def tests_for(code_file)
    end
  end
end

Cases

  1. Given a project loaded from a flat project.yaml with code_path "lib" and test_path "tests", when project.code.path and project.test.path are read, then they return "lib" and "tests" and the yaml keys are unchanged.
  2. Given a yaml with no test_command, when the project loads, then project.test.command is "rake test" and project.test.framework is "minitest".
  3. Given project.code with path "lib", when include? is asked about "lib/a.rb" and "tests/a_test.rb", then it answers true and false, and relative("lib/a.rb") returns "a.rb".
  4. Given a code file "lib/os_sdlc/foo.rb" and an existing "tests/os_sdlc/foo_test.rb", when project.tests_for is called, then it returns ["tests/os_sdlc/foo_test.rb"].
  5. Given a code file whose mirrored test is absent but "tests/foo_a_test.rb" and "tests/foo_b_test.rb" exist, when project.tests_for is called, then it returns both scenario files sorted.
  6. Given a code file with no candidate and no scenario files, when project.tests_for is called, then it raises HarnessError naming the tried candidates.
  7. Given a path outside code.path, when project.tests_for is called, then it raises ArgumentError.
  8. Given lint_config "custom.yml" relative to root, when project.lint.resolved_config(root) is called, then it returns the absolute path, matching today's Project::LintConfig behavior.
  9. Given Project#to_json and Project.from_json round-trip, when a project with nested code/test/lint is dumped and reloaded, then the reloaded project equals the original.
  10. Given a project_config JSON blob that carries a key the record no longer has (for example reserved_paths or red_assert_command from a run that started before this change), when Project.from_json loads it, then the unknown key is dropped and the project loads.

Structural follow-through

  1. Delete plugins/os-sdlc/lib/os_sdlc/project/layout.rb and Project::LintConfig once their callers move.
  2. Update every caller of project.code_path, test_path, test_command, test_framework, red_assert_command, lint_config, and lint_paths across plugins/os-sdlc/lib to the nested readers. Grep first; the count is not yet known.
  3. Project::Yaml::Document#attrs, Project::Yaml::Document#shared_attrs, and Project::Config#project build the nested values. The compact-at-edge rule from 5e867e6 stays.
  4. Run /os-adr:find on project.rb before editing. ADR-0167 (Data.define for value objects) is satisfied. ADR-0187 through ADR-0189 (batch-4 owners) must not be reversed. Record the extraction as a new ADR amending ADR-0189.
  5. Remove reserved_paths and red_assert_command entirely. Both have zero readers under plugins/os-sdlc/lib (reserved_paths: ADR-0089 shard-wiring feature never built; red_assert_command: legacy pipeline, rejected for wiring in the v2 rebuild). Delete the red_assert_command lines from .sdlc/project.yaml (repo root, two entries) and plugins/os-sdlc/.sdlc/project.yaml (one entry). Delete the two reserved_paths tests in plugins/os-sdlc/tests/project_test.rb.

Origin

  • Trigger: attributes that change together sit apart in a flat 13-member record; originally surfaced by a rubocop offense later shown to be a cop bug (fixed dd6a445).
  • Improvised this session: none.
  • Chain: 13-keyword Project#initialize fails the cop ← attributes that change together are split across a flat list ← DESIGN: batch-4 responsibility-refactor left project.rb flat (ADR-0187-ADR-0189).
  • Root candidate: this ticket.
  • Where: plugins/os-sdlc/lib/os_sdlc/project.rb, plugins/os-sdlc/lib/os_sdlc/project/layout.rb
  • Session: b1c83b81-7be1-4f17-9e8e-cae36a934f25
  • Transcript: n/a
VERDICT: CREATE (ticket-skeptic: user-requested design decision, explicitly stated and approved this session — hard floor overrides the gauntlet)## Context `plugins/os-sdlc/lib/os_sdlc/project.rb` is a `Data.define` with 13 keyword attributes after commit `5e867e6`. Rubocop's `Sdlc/Structural/ParameterListSignalsMissingObject` cop fires on its `initialize`. Attributes that change together sit scattered across the flat list. `test_path`, `test_command`, `test_framework`, and `red_assert_command` move together when the test framework changes. `lint_config` and `lint_paths` move together too. `Project::Layout`, at `plugins/os-sdlc/lib/os_sdlc/project/layout.rb`, is a lookup, not a validation. Given a code file, it builds candidate test paths, mirrored and unnamespaced, and picks the first that exists. It falls back to scenario-split siblings, then raises. It needs both the code prefix and the test prefix, so it belongs to an object that holds both. The name "Layout" misnames this behavior. The YAML format stays flat; `.sdlc/project.yaml` keys do not change. `Project::Yaml::Document#attrs`, `Project::Yaml::Document#shared_attrs`, and `Project::Config#project` build the nested values at load time. The user decided this design on 2026-09-15. A separate session implements it through `/os-sdlc:implement` using the ticket map. ## Observed ``` grep -n "ParameterListSignalsMissingObject" -r plugins/os-sdlc ``` ``` Sdlc/Structural/ParameterListSignalsMissingObject fires on OsSdlc::Project#initialize (13 keyword args) ``` ## Reproduce n/a: this is a feature/design ticket, not a bug. ## Expected Given a project loaded from a flat `project.yaml`, when the reader accesses `project.code.path`, `project.test.path`, `project.test.command`, `project.test.framework`, `project.lint.config`, and `project.lint.paths`, then each nested value matches today's flat-attribute value, and `project.tests_for(code_file)` returns the same test paths that `Project::Layout` returns today. ## Owner sketch ```ruby module OsSdlc Project = Data.define(:name, :root, :tracker, :code, :test, :lint, :map, :commit_on_complete) do def tests_for(code_file) end class Project Code = Data.define(:path) do def include?(path) def relative(path) def prefix end Test = Data.define(:path, :command, :framework) do def include?(path) def prefix def covering(code_relative) class Candidates end Lint = Data.define(:config, :paths) do def resolved_config(root) end class Codebase def initialize(code:, test:, root:) def tests_for(code_file) end end end ``` ## Cases 1. Given a project loaded from a flat project.yaml with code_path "lib" and test_path "tests", when project.code.path and project.test.path are read, then they return "lib" and "tests" and the yaml keys are unchanged. 2. Given a yaml with no test_command, when the project loads, then project.test.command is "rake test" and project.test.framework is "minitest". 3. Given project.code with path "lib", when include? is asked about "lib/a.rb" and "tests/a_test.rb", then it answers true and false, and relative("lib/a.rb") returns "a.rb". 4. Given a code file "lib/os_sdlc/foo.rb" and an existing "tests/os_sdlc/foo_test.rb", when project.tests_for is called, then it returns ["tests/os_sdlc/foo_test.rb"]. 5. Given a code file whose mirrored test is absent but "tests/foo_a_test.rb" and "tests/foo_b_test.rb" exist, when project.tests_for is called, then it returns both scenario files sorted. 6. Given a code file with no candidate and no scenario files, when project.tests_for is called, then it raises HarnessError naming the tried candidates. 7. Given a path outside code.path, when project.tests_for is called, then it raises ArgumentError. 8. Given lint_config "custom.yml" relative to root, when project.lint.resolved_config(root) is called, then it returns the absolute path, matching today's Project::LintConfig behavior. 9. Given Project#to_json and Project.from_json round-trip, when a project with nested code/test/lint is dumped and reloaded, then the reloaded project equals the original. 10. Given a project_config JSON blob that carries a key the record no longer has (for example reserved_paths or red_assert_command from a run that started before this change), when Project.from_json loads it, then the unknown key is dropped and the project loads. ## Structural follow-through 1. Delete `plugins/os-sdlc/lib/os_sdlc/project/layout.rb` and `Project::LintConfig` once their callers move. 2. Update every caller of `project.code_path`, `test_path`, `test_command`, `test_framework`, `red_assert_command`, `lint_config`, and `lint_paths` across `plugins/os-sdlc/lib` to the nested readers. Grep first; the count is not yet known. 3. `Project::Yaml::Document#attrs`, `Project::Yaml::Document#shared_attrs`, and `Project::Config#project` build the nested values. The compact-at-edge rule from `5e867e6` stays. 4. Run `/os-adr:find` on `project.rb` before editing. ADR-0167 (Data.define for value objects) is satisfied. ADR-0187 through ADR-0189 (batch-4 owners) must not be reversed. Record the extraction as a new ADR amending ADR-0189. 5. Remove reserved_paths and red_assert_command entirely. Both have zero readers under plugins/os-sdlc/lib (reserved_paths: ADR-0089 shard-wiring feature never built; red_assert_command: legacy pipeline, rejected for wiring in the v2 rebuild). Delete the red_assert_command lines from .sdlc/project.yaml (repo root, two entries) and plugins/os-sdlc/.sdlc/project.yaml (one entry). Delete the two reserved_paths tests in plugins/os-sdlc/tests/project_test.rb. ## Origin - Trigger: attributes that change together sit apart in a flat 13-member record; originally surfaced by a rubocop offense later shown to be a cop bug (fixed dd6a445). - Improvised this session: none. - Chain: 13-keyword `Project#initialize` fails the cop ← attributes that change together are split across a flat list ← `DESIGN`: batch-4 responsibility-refactor left `project.rb` flat (ADR-0187-ADR-0189). - Root candidate: this ticket. - Where: `plugins/os-sdlc/lib/os_sdlc/project.rb`, `plugins/os-sdlc/lib/os_sdlc/project/layout.rb` - Session: b1c83b81-7be1-4f17-9e8e-cae36a934f25 - Transcript: n/a
Author
Owner

Body edited 2026-09-15: cop trigger was a false positive (fixed dd6a445); reserved_paths and red_assert_command dropped from the sketch as dead code (zero lib readers); case 10 and follow-through 5 added for the removal and old-blob tolerance.

Body edited 2026-09-15: cop trigger was a false positive (fixed dd6a445); reserved_paths and red_assert_command dropped from the sketch as dead code (zero lib readers); case 10 and follow-through 5 added for the removal and old-blob tolerance.
Author
Owner

Body edited 2026-09-15: cop trigger was a false positive (fixed dd6a445); reserved_paths and red_assert_command dropped from the sketch as dead code (zero lib readers); case 10 and follow-through 5 added for the removal and old-blob tolerance.

Body edited 2026-09-15: cop trigger was a false positive (fixed dd6a445); reserved_paths and red_assert_command dropped from the sketch as dead code (zero lib readers); case 10 and follow-through 5 added for the removal and old-blob tolerance.
Author
Owner

Work started 2026-09-15 on branch ticket-595 via /os-sdlc:implement.

Work started 2026-09-15 on branch ticket-595 via /os-sdlc:implement.
Author
Owner

implementation 21 (session 3affeb44) ended implementation_failed: bound_exhausted:test-reviewer/fail after 12 dispatches. Failed step: test-reviewer dispatches 271, 273, 275 rejected the second red round because the working tree held production changes relative to HEAD. Those changes were the programmer's green increment (dispatch 268), not test-writer edits. Test-writer dispatch 274 obeyed the objection and checked out project.rb and config.rb from HEAD, discarding the programmer's work. Worktree ticket-595 now holds only the two new test files. Harness defect filed separately. Waiting on a decision: rerun after the harness fix, or hand-finish.

implementation 21 (session 3affeb44) ended implementation_failed: bound_exhausted:test-reviewer/fail after 12 dispatches. Failed step: test-reviewer dispatches 271, 273, 275 rejected the second red round because the working tree held production changes relative to HEAD. Those changes were the programmer's green increment (dispatch 268), not test-writer edits. Test-writer dispatch 274 obeyed the objection and checked out project.rb and config.rb from HEAD, discarding the programmer's work. Worktree ticket-595 now holds only the two new test files. Harness defect filed separately. Waiting on a decision: rerun after the harness fix, or hand-finish.
Author
Owner

Resolution

Done: Project members are name, root, tracker, code, test, lint, map, commit_on_complete. Project::Code, Project::Test, Project::Lint own defaults and path predicates. Project::Codebase replaces Layout. LintConfig deleted. FlatAttrs nests the flat yaml and JSON keys at load. reserved_paths and red_assert_command removed with their three yaml lines. ADR-0191 amends ADR-0187 and ADR-0189.

Evidence: Branch ticket-595 commits 9443674, 433552c, fd78119. Suite 1211 runs, 0 failures, 0 errors. Rubocop clean on 21 changed files; ParameterListSignalsMissingObject silent on project.rb. Cases 1-10 covered by project_nested_code_test_lint_test.rb and project_nested_structural_followthrough_test.rb. Pipeline impl 21 failed bound_exhausted (test-reviewer); increment restored from the programmer transcript and hand-finished.

Follow-ups: #597 test-reviewer per-dispatch diff base (filed, P1). Document::FLAT_KEYS and FlatAttrs key maps list the flat keys in two places; dropped, revisit only if a third reader appears.

## Resolution **Done:** Project members are name, root, tracker, code, test, lint, map, commit_on_complete. Project::Code, Project::Test, Project::Lint own defaults and path predicates. Project::Codebase replaces Layout. LintConfig deleted. FlatAttrs nests the flat yaml and JSON keys at load. reserved_paths and red_assert_command removed with their three yaml lines. ADR-0191 amends ADR-0187 and ADR-0189. **Evidence:** Branch ticket-595 commits 9443674, 433552c, fd78119. Suite 1211 runs, 0 failures, 0 errors. Rubocop clean on 21 changed files; ParameterListSignalsMissingObject silent on project.rb. Cases 1-10 covered by project_nested_code_test_lint_test.rb and project_nested_structural_followthrough_test.rb. Pipeline impl 21 failed bound_exhausted (test-reviewer); increment restored from the programmer transcript and hand-finished. **Follow-ups:** #597 test-reviewer per-dispatch diff base (filed, P1). Document::FLAT_KEYS and FlatAttrs key maps list the flat keys in two places; dropped, revisit only if a third reader appears.
jared 2026-09-15 16:01:17 +00:00
  • closed this issue
  • removed the
    waiting
    label
Author
Owner

Correction: the ADR is ADR-0192, not ADR-0191 (main took 0191 for #594 while this branch was open). Merged to main 2026-09-15 via worktree finish; suite 1216 runs, 0 failures.

Correction: the ADR is ADR-0192, not ADR-0191 (main took 0191 for #594 while this branch was open). Merged to main 2026-09-15 via worktree finish; suite 1216 runs, 0 failures.
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#595
No description provided.