Project::Config owns the implementations.project_config blob; empty config is a state, not a crash (amends ADR-0169) #566

Closed
opened 2026-09-13 23:18:11 +00:00 by jared · 2 comments
Owner

Map: #568
Blocked by #565

Context

Responsibility audit of project.rb on 2026-09-13. Batch position 3 of 4. The implementations.project_config column has two decoders with different vocabularies and defaults.

Observed

Project#to_json defines the blob. Runner::Implementation reads it through Project.from_json and again with a hand-written JSON.parse on a string key. The second reader tolerates the column default "{}". The first raises ArgumentError: missing keyword: :name, reached through BriefFacts#project and GateCommands#project.

plugins/os-sdlc/lib/os_sdlc/project.rb:142                       to_h.except(:root, :map).to_json
plugins/os-sdlc/lib/os_sdlc/project.rb:41                        data = JSON.parse(json, symbolize_names: true)
plugins/os-sdlc/lib/os_sdlc/runner/open_implementation.rb:39     project ? { project_config: project.to_json } : {}
plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:69          def project(root) = Project.from_json(root, project_config)
plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:74          def commit_on_complete? = JSON.parse(project_config || "{}")["commit_on_complete"] == true
plugins/os-sdlc/lib/os_sdlc/runner/db.rb:260                     t.String :project_config, text: true, null: false, default: "{}"

Reproduce

cd plugins/os-sdlc && ruby -Ilib -e 'require "os_sdlc"; OsSdlc::Project.from_json(Dir.pwd, "{}")'

Expected

def test_empty_project_config_yields_nil_project_not_argument_error
  implementation = build_implementation(project_config: "{}")
  assert_nil implementation.project(root)
end

def test_both_readers_agree_on_commit_on_complete
  # Implementation#commit_on_complete? and Implementation#project(root).commit_on_complete return the same value
end

Illustration

# illustration, not spec
class OsSdlc::Project
  class Config                                  # lib/os_sdlc/project/config.rb
    TRAVELLING = ATTR_DEFAULTS.keys - %i[map]   # the one key vocabulary
    def self.dump(project)
    def initialize(json)                        # "{}" and nil are one state here
    def empty?
    def commit_on_complete?
    def project(root)                           # nil when empty?, never ArgumentError
  end
end

Callers that delegate: OpenImplementation#project_config_kwargs, Implementation#project, Implementation#commit_on_complete?, BriefFacts#project, GateCommands#project.

Tasks

  • Failing tests: empty config yields nil, not ArgumentError; both readers agree on commit_on_complete
  • Build OsSdlc::Project::Config at lib/os_sdlc/project/config.rb
  • Delegate the five callers
  • BriefFacts.new(implementation) stays the whole constructor (ADR-0169)
  • Create the amending ADR with /os-adr:create when the ticket lands

Acceptance criteria

  • Full suite green; one decoder for the blob
  • ADR case: amends ADR-0169

Origin

  • Trigger: /os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/project.rb on 2026-09-13; analysis at .sdlc/tmp/responsibility-audit/2026-09-13-project/analysis.yaml
  • Improvised this session: none
  • Chain: mixed responsibilities in OsSdlc::Project ← DESIGN (plugins/os-sdlc/lib/os_sdlc/project.rb, ADR-0055/0074/0088/0169/0174)
  • Root candidate: this ticket
  • Where: OsSdlc::Project, plugins/os-sdlc/lib/os_sdlc/project.rb
  • Session: c72ac6a8-cb89-4b97-904d-155babd2a7a2
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/c72ac6a8-cb89-4b97-904d-155babd2a7a2.jsonl
Map: #568 Blocked by #565 ## Context Responsibility audit of `project.rb` on 2026-09-13. Batch position 3 of 4. The `implementations.project_config` column has two decoders with different vocabularies and defaults. ## Observed `Project#to_json` defines the blob. `Runner::Implementation` reads it through `Project.from_json` and again with a hand-written `JSON.parse` on a string key. The second reader tolerates the column default `"{}"`. The first raises `ArgumentError: missing keyword: :name`, reached through `BriefFacts#project` and `GateCommands#project`. ``` plugins/os-sdlc/lib/os_sdlc/project.rb:142 to_h.except(:root, :map).to_json plugins/os-sdlc/lib/os_sdlc/project.rb:41 data = JSON.parse(json, symbolize_names: true) plugins/os-sdlc/lib/os_sdlc/runner/open_implementation.rb:39 project ? { project_config: project.to_json } : {} plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:69 def project(root) = Project.from_json(root, project_config) plugins/os-sdlc/lib/os_sdlc/runner/implementation.rb:74 def commit_on_complete? = JSON.parse(project_config || "{}")["commit_on_complete"] == true plugins/os-sdlc/lib/os_sdlc/runner/db.rb:260 t.String :project_config, text: true, null: false, default: "{}" ``` ## Reproduce ``` cd plugins/os-sdlc && ruby -Ilib -e 'require "os_sdlc"; OsSdlc::Project.from_json(Dir.pwd, "{}")' ``` ## Expected ```ruby def test_empty_project_config_yields_nil_project_not_argument_error implementation = build_implementation(project_config: "{}") assert_nil implementation.project(root) end def test_both_readers_agree_on_commit_on_complete # Implementation#commit_on_complete? and Implementation#project(root).commit_on_complete return the same value end ``` ## Illustration ```ruby # illustration, not spec class OsSdlc::Project class Config # lib/os_sdlc/project/config.rb TRAVELLING = ATTR_DEFAULTS.keys - %i[map] # the one key vocabulary def self.dump(project) def initialize(json) # "{}" and nil are one state here def empty? def commit_on_complete? def project(root) # nil when empty?, never ArgumentError end end ``` Callers that delegate: `OpenImplementation#project_config_kwargs`, `Implementation#project`, `Implementation#commit_on_complete?`, `BriefFacts#project`, `GateCommands#project`. ## Tasks - [ ] Failing tests: empty config yields nil, not ArgumentError; both readers agree on commit_on_complete - [ ] Build `OsSdlc::Project::Config` at `lib/os_sdlc/project/config.rb` - [ ] Delegate the five callers - [ ] `BriefFacts.new(implementation)` stays the whole constructor (ADR-0169) - [ ] Create the amending ADR with `/os-adr:create` when the ticket lands ## Acceptance criteria - Full suite green; one decoder for the blob - ADR case: amends ADR-0169 ## Origin - Trigger: `/os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/project.rb` on 2026-09-13; analysis at `.sdlc/tmp/responsibility-audit/2026-09-13-project/analysis.yaml` - Improvised this session: none - Chain: mixed responsibilities in `OsSdlc::Project` ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/project.rb`, ADR-0055/0074/0088/0169/0174) - Root candidate: this ticket - Where: `OsSdlc::Project`, `plugins/os-sdlc/lib/os_sdlc/project.rb` - Session: c72ac6a8-cb89-4b97-904d-155babd2a7a2 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/c72ac6a8-cb89-4b97-904d-155babd2a7a2.jsonl
Author
Owner

Work started via /os-sdlc:implement on branch ticket-566 (worktree). Map: #568.

Work started via /os-sdlc:implement on branch ticket-566 (worktree). Map: #568.
Author
Owner

Resolution

Done: OsSdlc::Project::Config at lib/os_sdlc/project/config.rb is the one decoder and encoder of implementations.project_config. nil and "{}" are one empty state; Config#project returns nil when empty. Project.from_json, Project#to_json, Implementation#project, and Implementation#commit_on_complete? delegate to it. BriefFacts.new(implementation) unchanged.

Evidence: Pipeline implementation 3 complete: 20 dispatches, 4 retries (behavior-verifier loop for the second increment, one test-reviewer reject, one post-refactor suite failure repaired). Suite 1210 runs 0 failures after the Config move. ADR-0176 amends ADR-0169. Branch ticket-566, merged via worktree finish. Test: tests/runner/implementation_empty_project_config_test.rb.

Follow-ups: none. Deviation: the pipeline nested Config inside project.rb; moved by hand to project/config.rb after implementation_complete (one class per file), suite and lint green.

## Resolution **Done:** OsSdlc::Project::Config at lib/os_sdlc/project/config.rb is the one decoder and encoder of implementations.project_config. nil and "{}" are one empty state; Config#project returns nil when empty. Project.from_json, Project#to_json, Implementation#project, and Implementation#commit_on_complete? delegate to it. BriefFacts.new(implementation) unchanged. **Evidence:** Pipeline implementation 3 complete: 20 dispatches, 4 retries (behavior-verifier loop for the second increment, one test-reviewer reject, one post-refactor suite failure repaired). Suite 1210 runs 0 failures after the Config move. ADR-0176 amends ADR-0169. Branch ticket-566, merged via worktree finish. Test: tests/runner/implementation_empty_project_config_test.rb. **Follow-ups:** none. Deviation: the pipeline nested Config inside project.rb; moved by hand to project/config.rb after implementation_complete (one class per file), suite and lint green.
jared closed this issue 2026-09-14 00:33:36 +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#566
No description provided.